{ This file is part of the Free Component Library Implementation of TJSONConfig class Copyright (c) 2007 Michael Van Canneyt michael@freepascal.org See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} { TJSONConfig enables applications to use JSON files for storing their configuration data } {$IFDEF FPC} {$MODE objfpc} {$H+} {$ENDIF} unit jsonConf; interface uses SysUtils, Classes, fpjson, jsonscanner, jsonparser; Const DefaultJSONOptions = [joUTF8,joComments]; type EJSONConfigError = class(Exception); (* ******************************************************************** "APath" is the path and name of a value: A JSON configuration file is hierachical. "/" is the path delimiter, the part after the last "/" is the name of the value. The path components will be mapped to nested JSON objects, with the name equal to the part. In practice this means that "/my/path/value" will be written as: { "my" : { "path" : { "value" : Value } } } ******************************************************************** *) { TJSONConfig } TJSONConfig = class(TComponent) private FFilename: String; FFormatIndentSize: Integer; FFormatoptions: TFormatOptions; FFormatted: Boolean; FJSONOptions: TJSONOptions; FKey: TJSONObject; procedure DoSetFilename(const AFilename: String; ForceReload: Boolean); procedure SetFilename(const AFilename: String); procedure SetJSONOptions(AValue: TJSONOptions); Function StripSlash(Const P : UnicodeString) : UnicodeString; protected FJSON: TJSONObject; FModified: Boolean; Procedure LoadFromFile(Const AFileName : String); Procedure LoadFromStream(S : TStream); virtual; procedure Loaded; override; function FindNodeForValue(const APath: UnicodeString; aExpectedType: TJSONDataClass; out AParent: TJSONObject; out ElName: UnicodeString): TJSONData; function FindPath(Const APath: UnicodeString; AllowCreate : Boolean) : TJSONObject; function FindObject(Const APath: UnicodeString; AllowCreate : Boolean) : TJSONObject; function FindObject(Const APath: UnicodeString; AllowCreate : Boolean;Out ElName : UnicodeString) : TJSONObject; function FindElement(Const APath: UnicodeString; CreateParent : Boolean; AllowObject : Boolean = False) : TJSONData; function FindElement(Const APath: UnicodeString; CreateParent : Boolean; out AParent : TJSONObject; Out ElName : UnicodeString; AllowObject : Boolean = False) : TJSONData; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; Procedure Reload; procedure Clear; procedure Flush; // Writes the JSON file procedure OpenKey(const aPath: UnicodeString; AllowCreate : Boolean); procedure CloseKey; procedure ResetKey; Procedure EnumSubKeys(Const APath : UnicodeString; List : TStrings); Procedure EnumValues(Const APath : UnicodeString; List : TStrings); function GetValue(const APath: UnicodeString; const ADefault: UnicodeString): UnicodeString; overload; function GetValue(const APath: RawByteString; const ADefault: RawByteString): UnicodeString; overload; function GetValue(const APath: UnicodeString; ADefault: Integer): Integer; overload; function GetValue(const APath: RawByteString; ADefault: Integer): Integer; overload; function GetValue(const APath: UnicodeString; ADefault: Int64): Int64; overload; function GetValue(const APath: RawByteString; ADefault: Int64): Int64; overload; function GetValue(const APath: UnicodeString; ADefault: Boolean): Boolean; overload; function GetValue(const APath: RawByteString; ADefault: Boolean): Boolean; overload; function GetValue(const APath: UnicodeString; ADefault: Double): Double; overload; function GetValue(const APath: RawByteString; ADefault: Double): Double; overload; Function GetValue(const APath: UnicodeString; AValue: TStrings; Const ADefault: String) : Boolean; overload; Function GetValue(const APath: RawByteString; AValue: TStrings; Const ADefault: String) : Boolean; overload; Function GetValue(const APath: UnicodeString; AValue: TStrings; Const ADefault: TStrings): Boolean; overload; procedure SetValue(const APath: UnicodeString; const AValue: UnicodeString); overload; procedure SetValue(const APath: RawByteString; const AValue: RawByteString); overload; procedure SetValue(const APath: UnicodeString; AValue: Integer); overload; procedure SetValue(const APath: UnicodeString; AValue: Int64); overload; procedure SetValue(const APath: UnicodeString; AValue: Boolean); overload; procedure SetValue(const APath: UnicodeString; AValue: Double); overload; procedure SetValue(const APath: UnicodeString; AValue: TStrings; AsObject : Boolean = False); overload; procedure SetDeleteValue(const APath: UnicodeString; const AValue, DefValue: UnicodeString); overload; procedure SetDeleteValue(const APath: UnicodeString; AValue, DefValue: Integer); overload; procedure SetDeleteValue(const APath: UnicodeString; AValue, DefValue: Int64); overload; procedure SetDeleteValue(const APath: UnicodeString; AValue, DefValue: Boolean); overload; procedure DeletePath(const APath: UnicodeString); procedure DeleteValue(const APath: UnicodeString); property Modified: Boolean read FModified; published Property Filename: String read FFilename write SetFilename; Property Formatted : Boolean Read FFormatted Write FFormatted; Property FormatOptions : TFormatOptions Read FFormatoptions Write FFormatOptions Default DefaultFormat; Property FormatIndentsize : Integer Read FFormatIndentSize Write FFormatIndentSize Default DefaultIndentSize; Property JSONOptions : TJSONOptions Read FJSONOptions Write SetJSONOptions Default DefaultJSONOptions; end; // =================================================================== implementation Resourcestring SErrInvalidJSONFile = '"%s" is not a valid JSON configuration file.'; SErrCouldNotOpenKey = 'Could not open key "%s".'; SErrCannotNotReplaceKey = 'A (sub)key with name "%s" already exists.'; constructor TJSONConfig.Create(AOwner: TComponent); begin inherited Create(AOwner); FJSON:=TJSONObject.Create; FKey:=FJSON; FFormatOptions:=DefaultFormat; FFormatIndentsize:=DefaultIndentSize; FJSONOptions:=DefaultJSONOptions; end; destructor TJSONConfig.Destroy; begin if Assigned(FJSON) then begin Flush; FreeANdNil(FJSON); end; inherited Destroy; end; procedure TJSONConfig.Clear; begin FJSON.Clear; FKey:=FJSON; end; procedure TJSONConfig.Flush; Var F : TFileStream; S : TJSONStringType; begin if Modified then begin F:=TFileStream.Create(FileName,fmCreate); Try if Formatted then S:=FJSON.FormatJSON(Formatoptions,FormatIndentSize) else S:=FJSON.AsJSON; if S>'' then F.WriteBuffer(S[1],Length(S)); Finally F.Free; end; FModified := False; end; end; function TJSONConfig.FindObject(const APath: UnicodeString; AllowCreate: Boolean ): TJSONObject; Var Dummy : UnicodeString; begin Result:=FindObject(APath,AllowCreate,Dummy); end; function TJSONConfig.FindObject(const APath: UnicodeString; AllowCreate: Boolean; out ElName: UnicodeString): TJSONObject; Var S,El : UnicodeString; P,I : Integer; T : TJSonObject; begin // Writeln('Looking for : ', APath); S:=APath; If Pos('/',S)=1 then Result:=FJSON else Result:=FKey; Repeat P:=Pos('/',S); If (P<>0) then begin // Only real paths, ignore double slash If (P<>1) then begin El:=Copy(S,1,P-1); If (Result.Count=0) then I:=-1 else I:=Result.IndexOfName(UTF8Encode(El)); If (I=-1) then // No element with this name. begin If AllowCreate then begin // Create new node. T:=Result; Result:=TJSonObject.Create; T.Add(UTF8Encode(El),Result); end else Result:=Nil end else // Node found, check if it is an object begin if (Result.Items[i].JSONtype=jtObject) then Result:=Result.Objects[UTF8Encode(el)] else begin // Writeln(el,' type wrong'); If AllowCreate then begin // Writeln('Creating ',el); Result.Delete(I); T:=Result; Result:=TJSonObject.Create; T.Add(UTF8Encode(El),Result); end else Result:=Nil end; end; end; Delete(S,1,P); end; Until (P=0) or (Result=Nil); ElName:=S; end; function TJSONConfig.FindElement(const APath: UnicodeString; CreateParent: Boolean; AllowObject : Boolean = False): TJSONData; Var O : TJSONObject; ElName : UnicodeString; begin Result:=FindElement(APath,CreateParent,O,ElName,AllowObject); end; function TJSONConfig.FindElement(const APath: UnicodeString; CreateParent: Boolean; out AParent: TJSONObject; out ElName: UnicodeString; AllowObject : Boolean = False): TJSONData; Var I : Integer; begin Result:=Nil; Aparent:=FindObject(APath,CreateParent,ElName); If Assigned(Aparent) then begin // Writeln('Found parent, looking for element:',elName); I:=AParent.IndexOfName(UTF8Encode(ElName)); // Writeln('Element index is',I); If (I<>-1) And ((AParent.items[I].JSONType<>jtObject) or AllowObject) then Result:=AParent.Items[i]; end; // Writeln('Find ',aPath,' in "',FJSON.AsJSOn,'" : ',Elname,' : ',Result<>NIl); end; function TJSONConfig.GetValue(const APath: RawByteString; const ADefault: RawByteString): UnicodeString; begin Result:=GetValue(UTF8Decode(aPath),UTF8Decode(ADefault)); end; function TJSONConfig.GetValue(const APath: UnicodeString; const ADefault: UnicodeString): UnicodeString; var El : TJSONData; begin El:=FindElement(StripSlash(APath),False); If Assigned(El) then Result:=El.AsUnicodeString else Result:=ADefault; end; function TJSONConfig.GetValue(const APath: RawByteString; ADefault: Integer): Integer; begin Result:=GetValue(UTF8Decode(aPath),ADefault); end; function TJSONConfig.GetValue(const APath: UnicodeString; ADefault: Integer): Integer; var El : TJSONData; begin El:=FindElement(StripSlash(APath),False); If Not Assigned(el) then Result:=ADefault else if (el is TJSONNumber) then Result:=El.AsInteger else Result:=StrToIntDef(El.AsString,ADefault); end; function TJSONConfig.GetValue(const APath: RawByteString; ADefault: Int64): Int64; begin Result:=GetValue(UTF8Decode(aPath),ADefault); end; function TJSONConfig.GetValue(const APath: UnicodeString; ADefault: Int64): Int64; var El : TJSONData; begin El:=FindElement(StripSlash(APath),False); If Not Assigned(el) then Result:=ADefault else if (el is TJSONNumber) then Result:=El.AsInt64 else Result:=StrToInt64Def(El.AsString,ADefault); end; function TJSONConfig.GetValue(const APath: RawByteString; ADefault: Boolean): Boolean; begin Result:=GetValue(UTF8Decode(aPath),ADefault); end; function TJSONConfig.GetValue(const APath: UnicodeString; ADefault: Boolean): Boolean; var El : TJSONData; begin El:=FindElement(StripSlash(APath),False); If Not Assigned(el) then Result:=ADefault else if (el is TJSONBoolean) then Result:=El.AsBoolean else Result:=StrToBoolDef(El.AsString,ADefault); end; function TJSONConfig.GetValue(const APath: RawByteString; ADefault: Double): Double; begin Result:=GetValue(UTF8Decode(aPath),ADefault); end; function TJSONConfig.GetValue(const APath: UnicodeString; ADefault: Double): Double; var El : TJSONData; begin El:=FindElement(StripSlash(APath),False); If Not Assigned(el) then Result:=ADefault else if (el is TJSONNumber) then Result:=El.AsFloat else Result:=StrToFloatDef(El.AsString,ADefault); end; function TJSONConfig.GetValue(const APath: RawByteString; AValue: TStrings; const ADefault: String): Boolean; begin Result:=GetValue(UTF8Decode(aPath),AValue, ADefault); end; function TJSONConfig.GetValue(const APath: UnicodeString; AValue: TStrings; const ADefault: String): Boolean; var El : TJSONData; D : TJSONEnum; begin AValue.Clear; El:=FindElement(StripSlash(APath),False,True); Result:=Assigned(el); If Not Result then begin AValue.Text:=ADefault; exit; end; Case El.JSONType of jtArray: For D in El do if D.Value.JSONType in ActualValueJSONTypes then AValue.Add(D.Value.AsString); jtObject: For D in El do if D.Value.JSONType in ActualValueJSONTypes then AValue.Add(D.Key+'='+D.Value.AsString); else AValue.Text:=EL.AsString end; end; function TJSONConfig.GetValue(const APath: UnicodeString; AValue: TStrings; const ADefault: TStrings): Boolean; begin Result:=GetValue(APath,AValue,''); If Not Result then AValue.Assign(ADefault); end; procedure TJSONConfig.SetValue(const APath: UnicodeString; const AValue: UnicodeString); var El : TJSONData; ElName : UnicodeString; O : TJSONObject; begin El:=FindNodeForValue(aPath,TJSONString,O,elName); If Not Assigned(el) then begin El:=TJSONString.Create(AValue); O.Add(UTF8Encode(ElName),El); end else El.AsUnicodeString:=AValue; FModified:=True; end; procedure TJSONConfig.SetValue(const APath: RawByteString; const AValue: RawByteString); begin SetValue(UTF8Decode(APath),UTF8Decode(AValue)); end; procedure TJSONConfig.SetDeleteValue(const APath: UnicodeString; const AValue, DefValue: UnicodeString); begin if AValue = DefValue then DeleteValue(APath) else SetValue(APath, AValue); end; procedure TJSONConfig.SetValue(const APath: UnicodeString; AValue: Integer); var El : TJSONData; ElName : UnicodeString; O : TJSONObject; begin El:=FindNodeForValue(aPath,TJSONIntegerNumber,O,elName); If Not Assigned(el) then begin El:=TJSONIntegerNumber.Create(AValue); O.Add(UTF8Encode(ElName),El); end else El.AsInteger:=AValue; FModified:=True; end; procedure TJSONConfig.SetValue(const APath: UnicodeString; AValue: Int64); var El : TJSONData; ElName : UnicodeString; O : TJSONObject; begin El:=FindNodeForValue(aPath,TJSONInt64Number,O,elName); If Not Assigned(el) then begin El:=TJSONInt64Number.Create(AValue); O.Add(UTF8Encode(ElName),El); end else El.AsInt64:=AValue; FModified:=True; end; procedure TJSONConfig.SetDeleteValue(const APath: UnicodeString; AValue, DefValue: Integer); begin if AValue = DefValue then DeleteValue(APath) else SetValue(APath, AValue); end; procedure TJSONConfig.SetDeleteValue(const APath: UnicodeString; AValue, DefValue: Int64); begin if AValue = DefValue then DeleteValue(APath) else SetValue(APath, AValue); end; procedure TJSONConfig.SetValue(const APath: UnicodeString; AValue: Boolean); var El : TJSONData; ElName : UnicodeString; O : TJSONObject; begin El:=FindNodeForValue(aPath,TJSONBoolean,O,elName); If Not Assigned(el) then begin El:=TJSONBoolean.Create(AValue); O.Add(UTF8Encode(ElName),El); end else El.AsBoolean:=AValue; FModified:=True; end; procedure TJSONConfig.SetValue(const APath: UnicodeString; AValue: Double); var El : TJSONData; ElName : UnicodeString; O : TJSONObject; begin El:=FindNodeForValue(aPath,TJSONFloatNumber,O,elName); If Not Assigned(el) then begin El:=TJSONFloatNumber.Create(AValue); O.Add(UTF8Encode(ElName),El); end else El.AsFloat:=AValue; FModified:=True; end; procedure TJSONConfig.SetValue(const APath: UnicodeString; AValue: TStrings; AsObject : Boolean = False); var El : TJSONData; ElName : UnicodeString; O : TJSONObject; I : integer; A : TJSONArray; N,V : String; begin if AsObject then El:=FindNodeForValue(aPath,TJSONObject,O,elName) else El:=FindNodeForValue(aPath,TJSONArray,O,elName); If Not Assigned(el) then begin if AsObject then El:=TJSONObject.Create else El:=TJSONArray.Create; O.Add(UTF8Encode(ElName),El); end; if Not AsObject then begin A:=El as TJSONArray; A.Clear; For N in Avalue do A.Add(N); end else begin O:=El as TJSONObject; For I:=0 to AValue.Count-1 do begin AValue.GetNameValue(I,N,V); O.Add(N,V); end; end; FModified:=True; end; procedure TJSONConfig.SetDeleteValue(const APath: UnicodeString; AValue, DefValue: Boolean); begin if AValue = DefValue then DeleteValue(APath) else SetValue(APath,AValue); end; procedure TJSONConfig.DeletePath(const APath: UnicodeString); Var P : UnicodeString; L : integer; Node : TJSONObject; ElName : UnicodeString; begin P:=StripSlash(APath); L:=Length(P); If (L>0) then begin Node := FindObject(P,False,ElName); If Assigned(Node) then begin L:=Node.IndexOfName(UTF8Encode(ElName)); If (L<>-1) then Node.Delete(L); end; end; FModified:=True; end; procedure TJSONConfig.DeleteValue(const APath: UnicodeString); begin DeletePath(APath); end; procedure TJSONConfig.Reload; begin if Length(Filename) > 0 then DoSetFilename(Filename,True); end; procedure TJSONConfig.Loaded; begin inherited Loaded; Reload; end; function TJSONConfig.FindNodeForValue(const APath: UnicodeString; aExpectedType : TJSONDataClass; out AParent: TJSONObject; out ElName: UnicodeString): TJSONData; var I : Integer; begin Result:=FindElement(StripSlash(APath),True,aParent,ElName,True); if Assigned(Result) and Not Result.InheritsFrom(aExpectedType) then begin I:=aParent.IndexOfName(UTF8Encode(elName)); aParent.Delete(i); Result:=Nil; end; end; function TJSONConfig.FindPath(const APath: UnicodeString; AllowCreate: Boolean ): TJSONObject; Var P : UnicodeString; L : Integer; begin P:=APath; L:=Length(P); If (L=0) or (P[L]<>'/') then P:=P+'/'; Result:=FindObject(P,AllowCreate); end; procedure TJSONConfig.DoSetFilename(const AFilename: String; ForceReload: Boolean); begin if (not ForceReload) and (FFilename = AFilename) then exit; FFilename := AFilename; if csLoading in ComponentState then exit; Flush; If Not FileExists(AFileName) then Clear else LoadFromFile(AFileName); end; procedure TJSONConfig.SetFilename(const AFilename: String); begin DoSetFilename(AFilename, False); end; procedure TJSONConfig.SetJSONOptions(AValue: TJSONOptions); begin if FJSONOptions=AValue then Exit; FJSONOptions:=AValue; if csLoading in ComponentState then exit; if (FFileName<>'') then Reload; end; function TJSONConfig.StripSlash(const P: UnicodeString): UnicodeString; Var L : Integer; begin L:=Length(P); If (L>0) and (P[l]='/') then Result:=Copy(P,1,L-1) else Result:=P; end; procedure TJSONConfig.LoadFromFile(const AFileName: String); Var F : TFileStream; begin F:=TFileStream.Create(AFileName,fmopenRead or fmShareDenyWrite); try LoadFromStream(F); finally F.Free; end; end; procedure TJSONConfig.LoadFromStream(S: TStream); Var P : TJSONParser; J : TJSONData; begin P:=TJSONParser.Create(S,FJSONOptions); try J:=P.Parse; If (J is TJSONObject) then begin FreeAndNil(FJSON); FJSON:=J as TJSONObject; FKey:=FJSON; end else begin FreeAndNil(J); Raise EJSONConfigError.CreateFmt(SErrInvalidJSONFile,[FileName]); end; finally P.Free; end; end; procedure TJSONConfig.CloseKey; begin ResetKey; end; procedure TJSONConfig.OpenKey(const aPath: UnicodeString; AllowCreate: Boolean); Var P : UnicodeString; L : Integer; begin P:=APath; L:=Length(P); If (L=0) then FKey:=FJSON else begin if (P[L]<>'/') then P:=P+'/'; FKey:=FindObject(P,AllowCreate); If (FKey=Nil) Then Raise EJSONConfigError.CreateFmt(SErrCouldNotOpenKey,[APath]); end; end; procedure TJSONConfig.ResetKey; begin FKey:=FJSON; end; procedure TJSONConfig.EnumSubKeys(const APath: UnicodeString; List: TStrings); Var AKey : TJSONObject; I : Integer; begin AKey:=FindPath(APath,False); If Assigned(AKey) then begin For I:=0 to AKey.Count-1 do If AKey.Items[i] is TJSONObject then List.Add(AKey.Names[i]); end; end; procedure TJSONConfig.EnumValues(const APath: UnicodeString; List: TStrings); Var AKey : TJSONObject; I : Integer; begin AKey:=FindPath(APath,False); If Assigned(AKey) then begin For I:=0 to AKey.Count-1 do If Not (AKey.Items[i] is TJSONObject) then List.Add(AKey.Names[i]); end; end; end.