/// minimum standand-alone cross-platform JSON process using variants // - this unit is a part of the freeware Synopse mORMot framework, // licensed under a MPL/GPL/LGPL tri-license; version 1.18 unit SynCrossPlatformJSON; { This file is part of Synopse mORMot framework. Synopse mORMot framework. Copyright (C) 2016 Arnaud Bouchez Synopse Informatique - http://synopse.info *** BEGIN LICENSE BLOCK ***** Version: MPL 1.1/GPL 2.0/LGPL 2.1 The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is Synopse mORMot framework. The Initial Developer of the Original Code is Arnaud Bouchez. Portions created by the Initial Developer are Copyright (C) 2016 the Initial Developer. All Rights Reserved. Contributor(s): - Witya Alternatively, the contents of this file may be used under the terms of either the GNU General Public License Version 2 or later (the "GPL"), or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), in which case the provisions of the GPL or the LGPL are applicable instead of those above. If you wish to allow use of your version of this file only under the terms of either the GPL or the LGPL, and not to allow others to use your version of this file under the terms of the MPL, indicate your decision by deleting the provisions above and replace them with the notice and other provisions required by the GPL or the LGPL. If you do not delete the provisions above, a recipient may use your version of this file under the terms of any one of the MPL, the GPL or the LGPL. ***** END LICENSE BLOCK ***** Version 1.18 - first public release, corresponding to mORMot Framework 1.18 - would compile with Delphi for any platform (including NextGen for mobiles), with FPC 2.7 or Kylix, and with SmartMobileStudio 2+ - FPC prior to 2.7.1 has some issues with working with variants: UTF-8 encoding is sometimes lost, and TInvokeableVariantType.SetProperty is broken } {$i SynCrossPlatform.inc} // define e.g. HASINLINE interface uses SysUtils, Classes, {$ifdef NEXTGEN} System.Generics.Collections, {$else} Contnrs, {$endif} Variants, TypInfo; type TStringDynArray = array of string; TVariantDynArray = array of variant; TIntegerDynArray = array of integer; /// this type is used to store BLOB content TByteDynArray = array of byte; PByteDynArray = ^TByteDynArray; {$ifndef UNICODE} {$ifdef FPC} NativeInt = PtrInt; NativeUInt = PtrUInt; {$else} NativeInt = integer; NativeUInt = cardinal; {$endif} RawByteString = AnsiString; {$endif} // this type will store UTF-8 encoded buffer (also on NextGen platform) {$ifdef NEXTGEN} TUTF8Buffer = TBytes; // TObjecTList is not defined in Mobile platforms TObjectList = TObjectList; {$else} TUTF8Buffer = UTF8String; {$endif} /// exception used during standand-alone cross-platform JSON process EJSONException = class(Exception); /// which kind of document the TJSONVariantData contains TJSONVariantKind = (jvUndefined, jvObject, jvArray); PJSONVariantData = ^TJSONVariantData; {$A-} /// stores any JSON object or array as variant // - this structure is not very optimized for speed or memory use, but is // simple and strong enough for our client-side purpose // - it is in fact already faster (and using less memory) than DBXJSON and // SuperObject / XSuperObject libraries - of course, mORMot's TDocVariant // is faster, as dwsJSON is in some cases, but those are not cross-platform {$ifdef UNICODE} TJSONVariantData = record private {$else} TJSONVariantData = object protected {$endif} VType: TVarType; _Align: byte; VKind: TJSONVariantKind; VCount: integer; function GetKind: TJSONVariantKind; function GetCount: integer; function GetVarData(const aName: string; var Dest: TVarData): boolean; function GetValue(const aName: string): variant; function GetValueCopy(const aName: string): variant; procedure SetValue(const aName: string; const aValue: variant); function GetItem(aIndex: integer): variant; procedure SetItem(aIndex: integer; const aItem: variant); public /// names of this jvObject Names: TStringDynArray; /// values of this jvObject or jvArray Values: TVariantDynArray; /// initialize the low-level memory structure procedure Init; overload; /// initialize the low-level memory structure with a given JSON content procedure Init(const JSON: string); overload; /// initialize the low-level memory structure with a given array of variant procedure InitFrom(const aValues: TVariantDynArray); overload; /// access to a nested TJSONVariantData item // - returns nil if aName was not found, or not a true TJSONVariantData item function Data(const aName: string): PJSONVariantData; {$ifdef HASINLINE}inline;{$endif} /// access to a nested TJSONVariantData item, creating it if necessary // - aPath can be specified with any depth, e.g. 'level1.level2.level3' // - if the item does not exist or is not a true TJSONVariantData, a new // one will be created, and returned as pointer function EnsureData(const aPath: string): PJSONVariantData; /// add a void TJSONVariantData to the jvArray and return a pointer to it function AddItem: PJSONVariantData; /// add a value to the jvArray // - raise a ESJONException if the instance is a jvObject procedure AddValue(const aValue: variant); /// add a name/value pair to the jvObject // - raise a ESJONException if the instance is a jvArray procedure AddNameValue(const aName: string; const aValue: variant); /// search for a name in this jvObject function NameIndex(const aName: string): integer; /// set a value of this jvObject to a given path // - aPath can be specified with any depth, e.g. 'level1.level2.level3' procedure SetPath(const aPath: string; const aValue: variant); /// fill this document from a JSON array or object function FromJSON(const JSON: string): boolean; /// convert this document into JSON array or object function ToJSON: string; /// fill the published properties of supplied class from this JSON object function ToObject(Instance: TObject): boolean; /// create an instance, and fill its published properties from this JSON object // - it should contain some "ClassName" properties, i.e. JSON should have // been created by ObjectToJSON(Instance,true) and the class should have // been registered with RegisterClassForJSON() function ToNewObject: TObject; /// kind of document this TJSONVariantData contains // - returns jvUndefined if this instance is not a TJSONVariant custom variant property Kind: TJSONVariantKind read GetKind; /// number of items in this jvObject or jvArray // - returns 0 if this instance is not a TJSONVariant custom variant property Count: integer read GetCount; /// access by name to a value of this jvObject // - value is returned as (varVariant or varByRef) for best speed // - will return UnAssigned if aName is not correct or this is not a jvObject property Value[const aName: string]: variant read GetValue write SetValue; default; /// access by name to a value of this jvObject // - value is returned as a true copy (not varByRef) so this property is // slower but safer than Value[], if the owning TJsonVariantData disappears // - will return UnAssigned if aName is not correct or this is not a jvObject property ValueCopy[const aName: string]: variant read GetValueCopy; /// access by index to a value of this jvArray // - will return UnAssigned if aIndex is not correct or this is not a jvArray property Item[aIndex: integer]: variant read GetItem write SetItem; end; {$A+} /// low-level class used to register TJSONVariantData as custom type // - allows late binding to values, e.g. // ! jsonvar.avalue := jsonvar.avalue+1; // - due to an issue with FPC implementation, you can only read properties, // not set them, so you should write: // ! TJSONVariantData(jsonvar)['avalue'] := jsonvar.avalue+1; TJSONVariant = class(TInvokeableVariantType) protected {$ifndef FPC} {$ifndef ISDELPHI6} function FixupIdent(const AText: string): string; override; {$endif} {$endif} public procedure Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); override; procedure Clear(var V: TVarData); override; function GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; override; {$ifdef FPC_VARIANTSETVAR} // see http://mantis.freepascal.org/view.php?id=26773 function SetProperty(var V: TVarData; const Name: string; const Value: TVarData): Boolean; override; {$else} function SetProperty(const V: TVarData; const Name: string; const Value: TVarData): Boolean; override; {$endif} procedure Cast(var Dest: TVarData; const Source: TVarData); override; procedure CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); override; end; /// handle a JSON result table, as returned by mORMot's server // - handle both expanded and non expanded layout // - will be used e.g. on client side for variant-based ORM data parsing TJSONTable = class protected fJSON: string; fFieldNames: TStringDynArray; fJSONExpanded: boolean; fJSONIndexFirstValue: integer; fJSONCurrentIndex: integer; fRowValues: TVariantDynArray; function Get(const FieldName: string): variant; public /// parse the supplied JSON content constructor Create(const aJSON: string); /// case-insensitive search for a field name function FieldIndex(const FieldName: string): integer; /// to be called in a loop to iterate through all data rows // - if returned true, Value[] contains the fields of this row function Step(SeekFirst: boolean=false): boolean; /// to be called in a loop to iterate through all data rows // - if returned true, RowValues contains this row as TJSONVariant function StepValue(var RowValues: variant; SeekFirst: boolean=false): boolean; /// after Step() returned true, can be used to retrieve a field value by name property Value[const FieldName: string]: variant read Get; default; /// after Step() returned true, can be used to retrieve a field value by index property RowValues: TVariantDynArray read fRowValues; /// the recognized field names property FieldNames: TStringDynArray read fFieldNames; /// the associated JSON content property JSON: string read fJSON; end; /// an abstract type used for RTTI type information TRTTITypeInfo = PPropInfo; /// an abstract type used for RTTI property information TRTTIPropInfo = PPropInfo; TRTTIPropInfoDynArray = array of TRTTIPropInfo; /// handle a JSON result table, as returned by mORMot's server // - handle both expanded and non expanded layout // - this class is able to use RTTI to fill all published properties of // a TObject TJSONTableObject = class(TJSONTable) protected fTypeInfo: pointer; fPropInfo: array of TRTTIPropInfo; procedure FillPropInfo(aTypeInfo: TRTTITypeInfo); virtual; procedure FillInstance(Instance: TObject); virtual; function GetPropInfo(aTypeInfo: TRTTITypeInfo; const PropName: string): TRTTIPropInfo; virtual; public /// to be called in a loop to iterate through all data rows // - if returned true, Object published properties will contain this row function StepObject(Instance: TObject; SeekFirst: boolean=false): boolean; virtual; end; /// used e.g. by TSynTest for each test case TPublishedMethod = record Name: string; Method: TMethod; end; /// as filled by GetPublishedMethods() TPublishedMethodDynArray = array of TPublishedMethod; /// create a TJSONVariant instance from a given JSON content // - typical usage may be: //! var doc: variant; //! json: string; //! begin //! doc := JSONVariant('{"test":1234,"name":"Joh\"n\r"}'); //! assert(doc.test=1234); // access via late binding //! assert(doc.name='Joh"n'#13); //! assert(doc.name2=null); // unknown properties returns null //! json := doc; // to convert a TJSONVariant to JSON, just assign to a string //! assert(json='{"test":1234,"name":"Joh\"n\r"}'); //! end; // - note that FPC does not allow to set values by late-binding function JSONVariant(const JSON: string): variant; overload; /// create a TJSONVariant TJSONVariant array from a supplied array of values function JSONVariant(const values: TVariantDynArray): variant; overload; /// create a TJSONVariant TJSONVariant array from a supplied array of values function JSONVariantFromConst(const constValues: array of variant): variant; /// access to a TJSONVariant instance members // - e.g. Kind, Count, Names[] or Values[] // - will raise an exception if the supplied variant is not a TJSONVariant // - this function is safer than TJSONVariant(JSONVariant) function JSONVariantData(const JSONVariant: variant): PJSONVariantData; /// access to a TJSONVariant instance members // - e.g. Kind, Count, Names[] or Values[] // - will return a read-only fake TJSONVariant with Kind=jvUndefined if the // supplied variant is not a TJSONVariant // - if ExpectedKind is jvArray of jvObject, it would return a fake TJSONVariant // with Kind=jvUndefined if the JSONVariant kind does not match - so you can write: // !var _a: integer; // ! _arr: PJSONVariantData; // !... // ! _arr := JSONVariantDataSafe(_variant,jvArray); // ! SetLength(result,_arr.Count); // ! for _a := 0 to _arr.Count-1 do // ! result[_a] := _arr.Values[_a]; // in the above code, _arr.Count will be 0 if _variant.Kind<>jvArray // - this function is safer than TJSONVariant(JSONVariant) function JSONVariantDataSafe(const JSONVariant: variant; ExpectedKind: TJSONVariantKind=jvUndefined): PJSONVariantData; var /// the custom variant type definition registered for TJSONVariant JSONVariantType: TInvokeableVariantType; /// compute the quoted JSON string corresponding to the supplied text function StringToJSON(const Text: string): string; /// compute the JSON representation of a floating-point value procedure DoubleToJSON(Value: double; var result: string); /// compute the ISO-8601 JSON text representation of a date/time value // - e.g. "YYYY-MM-DD" "Thh:mm:ss" or "YYYY-MM-DDThh:mm:ss" // - if Date is 0, will return "" function DateTimeToJSON(Value: TDateTime): string; /// compute the JSON representation of a variant value // - will work for simple types, or TJSONVariant object or array function ValueToJSON(const Value: variant): string; /// compute a variant from its JSON representation // - will work for simple types, or TJSONVariant object or array function JSONToValue(const JSON: string): variant; /// compute the ISO-8601 JSON text representation of the current date/time value // - e.g. "2015-06-27T20:59:29" function NowToIso8601: string; /// compute the unquoted ISO-8601 text representation of a date/time value // - e.g. 'YYYY-MM-DD' 'Thh:mm:ss' or 'YYYY-MM-DDThh:mm:ss' // - if Date is 0, will return '' function DateTimeToIso8601(Value: TDateTime): string; /// convert unquoted ISO-8601 text representation into a date/time value // - e.g. 'YYYY-MM-DD' 'Thh:mm:ss' or 'YYYY-MM-DDThh:mm:ss' function Iso8601ToDateTime(const Value: string): TDateTime; /// compute the JSON representation of an object published properties // - handle only simple types of properties, not nested class instances // - any TList/TObjectList/TCollection will be serialized as JSON array function ObjectToJSON(Instance: TObject; StoreClassName: boolean=false): string; /// fill an object published properties from the supplied JSON object // - handle only simple types of properties, not nested class instances function JSONToObject(Instance: TObject; const JSON: string): boolean; /// create a new object and fil its published properties from the supplied // JSON object, which should include "ClassName":"..." properties // - JSON should have been created with ObjectToJSON(Instance,true) and // the class should have been registered with RegisterClassForJSON() function JSONToNewObject(const JSON: string): pointer; /// register the class types to be created from its name // - used e.g. by JSONToNewObject() or TJSONVariantData.ToNewObject procedure RegisterClassForJSON(const Classes: array of TClass); /// create a class instance from its name // - the class should have been registered previously via RegisterClassForJSON() // - if the supplied class name is not found, will return nil function CreateClassForJSON(const ClassName: string): TObject; /// create a list of object published properties from the supplied JSON object // - handle only simple types of properties, not nested class instances function JSONToObjectList(ItemClass: TClass; const JSON: string): TObjectList; /// return a string corresponding to the type name, as stored in the RTTI // - e.g. 'TDateTime', 'TByteDynArray', 'TModTime', 'TCreateTime' function RTTIPropInfoTypeName(PropInfo: TRTTIPropInfo): string; /// retrieve the published properties type information about a given class procedure GetPropsInfo(TypeInfo: TRTTITypeInfo; var PropNames: TStringDynArray; var PropRTTI: TRTTIPropInfoDynArray); /// retrieve the value of a published property as variant function GetInstanceProp(Instance: TObject; PropInfo: TRTTIPropInfo): variant; /// set the value of a published property from a variant procedure SetInstanceProp(Instance: TObject; PropInfo: TRTTIPropInfo; const Value: variant); /// retrieve all the published methods of a given class, using RTTI procedure GetPublishedMethods(Instance: TObject; out Methods: TPublishedMethodDynArray); /// convert an "array of const" parameter value into its string representation function VarRecToValue(const V: TVarRec; out wasString: boolean): string; /// convert the supplied text as "text", as expected by SQL standard procedure DoubleQuoteStr(var text: string); /// decode a Base64-encoded string // - default withBase64Magic=TRUE will expect the string to start with our // JSON_BASE64_MAGIC marker function Base64JSONStringToBytes(const JSONString: string; var Bytes: TByteDynArray; withBase64Magic: boolean=true): boolean; /// Base-64 encode a BLOB into string // - default withBase64Magic=TRUE will include our JSON_BASE64_MAGIC marker function BytesToBase64JSONString(const Bytes: TByteDynArray; withBase64Magic: boolean=true): string; const /// special code to mark Base64 binary content in JSON string // - Unicode special char U+FFF0 is UTF-8 encoded as EF BF B0 bytes // - prior to Delphi 2009, it won't work as expected since U+FFF0 won't be // able to be converted into U+FFF0 {$ifdef UNICODE} JSON_BASE64_MAGIC: word = $fff0; {$else} JSON_BASE64_MAGIC: array[0..2] of byte = ($ef,$bf,$b0); {$endif} /// size, in platform chars, of our special code to mark Base64 binary // content in JSON string // - equals 1 since Delphi 2009 (UTF-16 encoded), or 3 for older versions // (UTF-8encoded) of the compiler compiler JSON_BASE64_MAGIC_LEN = sizeof(JSON_BASE64_MAGIC) div sizeof(char); {$ifndef ISSMS} /// read an UTF-8 (JSON) file into a native string // - file should be existing, otherwise an exception is raised function UTF8FileToString(const aFileName: TFileName): string; {$endif} /// this function is faster than str := str+chr ! procedure AppendChar(var str: string; chr: Char); {$ifdef HASINLINE}inline;{$endif} /// check that two ASCII-7 latin text do match function IdemPropName(const PropName1,PropName2: string): boolean; overload; {$ifdef HASINLINE}inline;{$endif} /// check that two ASCII-7 latin text do match // - first parameter is expected to be a shortstring low-level buffer - as such, // this overloaded function would work with NEXTGEN encoded RTTI function IdemPropName(PropName1: PByteArray; const PropName2: string): boolean; overload; {$ifdef HASINLINE}inline;{$endif} /// convert ASCII-7 latin text, encoded as a shortstring buffer, into a string // - as such, this function would work with NEXTGEN encoded RTTI function ShortStringToString(Buffer: PByteArray): string; /// check that two ASCII-7 latin text do match function StartWithPropName(const PropName1,PropName2: string): boolean; implementation function IdemPropName(const PropName1,PropName2: string): boolean; var L,i: integer; begin result := false; L := length(PropName2); if length(PropName1)<>L then exit; for i := 1 to L do if (ord(PropName1[i]) xor ord(PropName2[i])) and {$ifdef UNICODE}$ffdf{$else}$df{$endif}<>0 then exit; result := true; end; function ShortStringToString(Buffer: PByteArray): string; {$ifdef UNICODE} var i: integer; begin SetLength(result,Buffer^[0]); for i := 1 to Buffer^[0] do result[i] := chr(Buffer^[i]); end; {$else} begin SetString(result,PAnsiChar(@Buffer^[1]),Buffer^[0]); end; {$endif} function IdemPropName(PropName1: PByteArray; const PropName2: string): boolean; var L,i: integer; begin result := false; L := length(PropName2); if PropName1^[0]<>L then exit; for i := 1 to L do if (PropName1^[i] xor ord(PropName2[i])) and {$ifdef UNICODE}$ffdf{$else}$df{$endif}<>0 then exit; result := true; end; function StartWithPropName(const PropName1,PropName2: string): boolean; var L,i: integer; begin result := false; L := length(PropName2); if length(PropName1)0 then exit; result := true; end; {$ifndef ISSMS} // there is no file within HTML5 DOM {$ifdef FPC} // assume string is UTF-8 encoded (as with Lazarus/LCL) // note that when working with variants, FPC 2.7.1 sometimes clear the code page type UTF8ToString = RawByteString; {$else} {$ifndef UNICODE} function UTF8ToString(const utf8: TUTF8Buffer): string; begin result := UTF8ToAnsi(utf8); end; {$endif} {$endif} function UTF8FileToString(const aFileName: TFileName): string; var F: TFileStream; len: integer; utf8: TUTF8Buffer; begin F := TFileStream.Create(aFileName,fmOpenRead); try len := F.Size; SetLength(utf8,len); {$ifdef NEXTGEN} F.Read(utf8[0],len); result := TEncoding.UTF8.GetString(utf8); {$else} F.Read(utf8[1],len); result := UTF8ToString(utf8); {$endif} finally F.Free; end; end; {$endif} function JSONVariant(const JSON: string): variant; begin VarClear(result); TJSONVariantData(result).FromJSON(JSON); end; function JSONVariant(const values: TVariantDynArray): variant; begin VarClear(result); TJSONVariantData(result).Init; TJSONVariantData(result).VKind := jvArray; TJSONVariantData(result).VCount := length(values); TJSONVariantData(result).Values := values; end; function JSONVariantFromConst(const constValues: array of variant): variant; var i: integer; begin VarClear(result); with TJSONVariantData(result) do begin Init; VKind := jvArray; VCount := length(values); SetLength(Values,VCount); for i := 0 to VCount-1 do Values[i] := constValues[i]; end; end; function JSONVariantData(const JSONVariant: variant): PJSONVariantData; begin with TVarData(JSONVariant) do if VType=JSONVariantType.VarType then result := @JSONVariant else if VType=varByRef or varVariant then result := JSONVariantData(PVariant(VPointer)^) else raise EJSONException.CreateFmt('JSONVariantData.Data(%d<>JSONVariant)',[VType]); end; const // will be in code section of the exe, so will be read-only by design JSONVariantDataFake: TJSONVariantData = (); function JSONVariantDataSafe(const JSONVariant: variant; ExpectedKind: TJSONVariantKind=jvUndefined): PJSONVariantData; begin with TVarData(JSONVariant) do if VType=JSONVariantType.VarType then if (ExpectedKind=jvUndefined) or (TJSONVariantData(JSONVariant).VKind=ExpectedKind) then result := @JSONVariant else result := @JSONVariantDataFake else if VType=varByRef or varVariant then result := JSONVariantDataSafe(PVariant(VPointer)^) else result := @JSONVariantDataFake; end; procedure AppendChar(var str: string; chr: Char); {$ifdef ISSMS} // JavaScript immutable strings begin str := str+chr end; {$else} var len: Integer; begin len := length(str); SetLength(str,len+1); PChar(pointer(str))[len] := chr; // SetLength() made str unique end; {$endif} function StringToJSON(const Text: string): string; var len,j: integer; procedure DoEscape; var i: Integer; begin result := '"'+copy(Text,1,j-1); // here FPC 2.7.1 erases UTF-8 encoding for i := j to len do begin case Text[i] of #8: result := result+'\b'; #9: result := result+'\t'; #10: result := result+'\n'; #12: result := result+'\f'; #13: result := result+'\r'; '\': result := result+'\\'; '"': result := result+'\"'; else if Text[i]<' ' then result := result+'\u00'+IntToHex(ord(Text[i]),2) else AppendChar(result,Text[i]); // will be UTF-8 encoded later end; end; AppendChar(result,'"'); end; begin len := length(Text); for j := 1 to len do case Text[j] of #0..#31,'\','"': begin DoEscape; exit; end; end; // if we reached here, no character needs to be escaped in this string result := '"'+Text+'"'; // here FPC 2.7.1 erases UTF-8 encoding :( end; {$ifdef KYLIX} {$define NOFORMATSETTINGS} {$endif} {$ifdef ISDELPHI6} {$define NOFORMATSETTINGS} {$endif} {$ifdef NOFORMATSETTINGS} procedure DoubleToJSON(Value: double; var result: string); var decsep: Char; begin // warning: this is NOT thread-safe if you mix settings decsep := DecimalSeparator; result := FloatToStr(Value); DecimalSeparator := decsep; end; {$else} var SettingsUS: TFormatSettings {$ifdef FPC} = ( CurrencyFormat: 1; NegCurrFormat: 5; ThousandSeparator: ','; DecimalSeparator: '.'; CurrencyDecimals: 2; DateSeparator: '-'; TimeSeparator: ':'; ListSeparator: ','; CurrencyString: '$'; ShortDateFormat: 'd/m/y'; LongDateFormat: 'dd" "mmmm" "yyyy'; TimeAMString: 'AM'; TimePMString: 'PM'; ShortTimeFormat: 'hh:nn'; LongTimeFormat: 'hh:nn:ss'; ShortMonthNames: ('Jan','Feb','Mar','Apr','May','Jun', 'Jul','Aug','Sep','Oct','Nov','Dec'); LongMonthNames: ('January','February','March','April','May','June', 'July','August','September','October','November','December'); ShortDayNames: ('Sun','Mon','Tue','Wed','Thu','Fri','Sat'); LongDayNames: ('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'); TwoDigitYearCenturyWindow: 50;) {$endif}; procedure DoubleToJSON(Value: double; var result: string); begin result := FloatToStr(Value,SettingsUS); end; {$endif} function DateTimeToJSON(Value: TDateTime): string; begin // e.g. "YYYY-MM-DD" "Thh:mm:ss" or "YYYY-MM-DDThh:mm:ss" result := '"'+DateTimeToIso8601(Value)+'"'; end; function NowToIso8601: string; begin result := DateTimeToIso8601(Now); end; function DateTimeToIso8601(Value: TDateTime): string; begin // e.g. YYYY-MM-DD Thh:mm:ss or YYYY-MM-DDThh:mm:ss if Value=0 then result := '' else if frac(Value)=0 then result := FormatDateTime('yyyy"-"mm"-"dd',Value) else if trunc(Value)=0 then result := FormatDateTime('"T"hh":"nn":"ss',Value) else result := FormatDateTime('yyyy"-"mm"-"dd"T"hh":"nn":"ss',Value); end; function Iso8601ToDateTime(const Value: string): TDateTime; var Y,M,D, HH,MI,SS: cardinal; begin // YYYY-MM-DD Thh:mm:ss or YYYY-MM-DDThh:mm:ss // 1234567890 123456789 1234567890123456789 result := 0; case Length(Value) of 9: if (Value[1]='T') and (Value[4]=':') and (Value[7]=':') then begin HH := ord(Value[2])*10+ord(Value[3])-(48+480); MI := ord(Value[5])*10+ord(Value[6])-(48+480); SS := ord(Value[8])*10+ord(Value[9])-(48+480); if (HH<24) and (MI<60) and (SS<60) then result := EncodeTime(HH,MI,SS,0); end; 10: if (Value[5]=Value[8]) and (ord(Value[8]) in [ord('-'),ord('/')]) then begin Y := ord(Value[1])*1000+ord(Value[2])*100+ ord(Value[3])*10+ord(Value[4])-(48+480+4800+48000); M := ord(Value[6])*10+ord(Value[7])-(48+480); D := ord(Value[9])*10+ord(Value[10])-(48+480); if (Y<=9999) and ((M-1)<12) and ((D-1)<31) then result := EncodeDate(Y,M,D); end; 19: if (Value[5]=Value[8]) and (ord(Value[8]) in [ord('-'),ord('/')]) and (ord(Value[11]) in [ord(' '),ord('T')]) and (Value[14]=':') and (Value[17]=':') then begin Y := ord(Value[1])*1000+ord(Value[2])*100+ ord(Value[3])*10+ord(Value[4])-(48+480+4800+48000); M := ord(Value[6])*10+ord(Value[7])-(48+480); D := ord(Value[9])*10+ord(Value[10])-(48+480); HH := ord(Value[12])*10+ord(Value[13])-(48+480); MI := ord(Value[15])*10+ord(Value[16])-(48+480); SS := ord(Value[18])*10+ord(Value[19])-(48+480); if (Y<=9999) and ((M-1)<12) and ((D-1)<31) and (HH<24) and (MI<60) and (SS<60) then result := EncodeDate(Y,M,D)+EncodeTime(HH,MI,SS,0); end; end; end; function ValueToJSON(const Value: variant): string; var I64: Int64; begin if TVarData(Value).VType=JSONVariantType.VarType then result := TJSONVariantData(Value).ToJSON else if (TVarData(Value).VType=varByRef or varVariant) then result := ValueToJSON(PVariant(TVarData(Value).VPointer)^) else if TVarData(Value).VType<=varNull then result := 'null' else if VarIsOrdinal(Value) then begin I64 := Value; result := IntToStr(I64); end else if TVarData(Value).VType=varDate then result := DateTimeToJSON(TVarData(Value).VDouble) else if VarIsFloat(Value) then DoubleToJSON(Value,result) else if VarIsStr(Value) then result := StringToJSON(Value) else result := Value; end; function VarRecToValue(const V: TVarRec; out wasString: boolean): string; // http://smartmobilestudio.com/forums/topic/is-array-of-const-supported-in-sms begin wasString := not (V.VType in [vtBoolean,vtInteger,vtInt64,vtCurrency,vtExtended,vtVariant]); with V do case VType of {$ifndef NEXTGEN} vtString: result := string(VString^); vtAnsiString: result := string(AnsiString(VAnsiString)); vtChar: result := string(VChar); vtPChar: result := string(VPChar); vtWideString: result := string(WideString(VWideString)); {$endif} {$ifdef UNICODE} vtUnicodeString: result := string(VUnicodeString); {$endif} vtPWideChar: result := string(VPWideChar); vtWideChar: result := string(VWideChar); vtBoolean: if VBoolean then result := '1' else result := '0'; vtInteger: result := IntToStr(VInteger); vtInt64: result := IntToStr(VInt64^); vtCurrency: DoubleToJSON(VCurrency^,result); vtExtended: DoubleToJSON(VExtended^,result); vtObject: result := ObjectToJSON(VObject); vtVariant: if TVarData(VVariant^).VType<=varNull then result := 'null' else begin wasString := VarIsStr(VVariant^); result := VVariant^; end; else result := ''; end; end; procedure DoubleQuoteStr(var text: string); var i,j: integer; tmp: string; begin i := pos('"',text); if i=0 then begin text := '"'+text+'"'; exit; end; tmp := '"'+copy(text,1,i)+'"'; for j := i+1 to length(text) do if text[j]='"' then tmp := tmp+'""' else AppendChar(tmp,text[j]); text := tmp+'"'; end; { TJSONParser } type /// the JSON node types, as recognized by TJSONParser TJSONParserKind = ( kNone, kNull, kFalse, kTrue, kString, kInteger, kFloat, kObject, kArray); /// used to parse any JSON content TJSONParser = {$ifdef UNICODE}record{$else}object{$endif} JSON: string; Index: integer; JSONLength: integer; procedure Init(const aJSON: string; aIndex: integer); function GetNextChar: char; {$ifdef HASINLINE}inline;{$endif} function GetNextNonWhiteChar: char; {$ifdef HASINLINE}inline;{$endif} function CheckNextNonWhiteChar(aChar: char): boolean; {$ifdef HASINLINE}inline;{$endif} function GetNextString(out str: string): boolean; overload; function GetNextString: string; overload; {$ifdef HASINLINE}inline;{$endif} function GetNextJSON(out Value: variant): TJSONParserKind; function CheckNextIdent(const ExpectedIdent: string): Boolean; function GetNextAlphaPropName(out fieldName: string): boolean; function ParseJSONObject(var Data: TJSONVariantData): boolean; function ParseJSONArray(var Data: TJSONVariantData): boolean; procedure GetNextStringUnEscape(var str: string); end; procedure TJSONParser.Init(const aJSON: string; aIndex: integer); begin JSON := aJSON; JSONLength := length(JSON); Index := aIndex; end; function TJSONParser.GetNextChar: char; begin if Index<=JSONLength then begin result := JSON[Index]; inc(Index); end else result := #0; end; function TJSONParser.GetNextNonWhiteChar: char; begin if Index<=JSONLength then repeat if JSON[Index]>' ' then begin result := JSON[Index]; inc(Index); exit; end; inc(Index); until Index>JSONLength; result := #0; end; function TJSONParser.CheckNextNonWhiteChar(aChar: char): boolean; begin if Index<=JSONLength then repeat if JSON[Index]>' ' then begin result := JSON[Index]=aChar; if result then inc(Index); exit; end; inc(Index); until Index>JSONLength; result := false; end; procedure TJSONParser.GetNextStringUnEscape(var str: string); var c: char; u: string; unicode,err: integer; begin repeat c := GetNextChar; case c of #0: exit; '"': break; '\': begin c := GetNextChar; case c of #0: exit; 'b': AppendChar(str,#08); 't': AppendChar(str,#09); 'n': AppendChar(str,#$0a); 'f': AppendChar(str,#$0c); 'r': AppendChar(str,#$0d); 'u': begin u := Copy(JSON,Index,4); if length(u)<>4 then exit; inc(Index,4); val('$'+u,unicode,err); if err<>0 then exit; AppendChar(str,char(unicode)); end; else AppendChar(str,c); end; end; else AppendChar(str,c); end; until false; end; function TJSONParser.GetNextString(out str: string): boolean; var i: integer; begin for i := Index to JSONLength do case JSON[i] of '"': begin // end of string without escape -> direct copy str := copy(JSON,Index,i-Index); Index := i+1; result := true; exit; end; '\': begin // need unescaping str := copy(JSON,Index,i-Index); Index := i; GetNextStringUnEscape(str); result := true; exit; end; end; result := false; end; function TJSONParser.GetNextString: string; begin if not GetNextString(result) then result := ''; end; function TJSONParser.GetNextAlphaPropName(out fieldName: string): boolean; var i: integer; begin result := False; if (Index>=JSONLength) or not (Ord(JSON[Index]) in [Ord('A')..Ord('Z'),Ord('a')..Ord('z'),Ord('_'),Ord('$')]) then exit; // first char must be alphabetical for i := Index+1 to JSONLength do case Ord(JSON[i]) of Ord('0')..Ord('9'),Ord('A')..Ord('Z'),Ord('a')..Ord('z'),Ord('_'): ; // allow MongoDB extended syntax, e.g. {age:{$gt:18}} Ord(':'),Ord('='): begin // allow both age:18 and age=18 pairs fieldName := Copy(JSON,Index,i-Index); Index := i+1; result := true; exit; end; else exit; end; end; function TJSONParser.GetNextJSON(out Value: variant): TJSONParserKind; var str: string; i64: Int64; d: double; start,err: integer; begin result := kNone; case GetNextNonWhiteChar of 'n': if copy(JSON,Index,3)='ull' then begin inc(Index,3); result := kNull; Value := null; end; 'f': if copy(JSON,Index,4)='alse' then begin inc(Index,4); result := kFalse; Value := false; end; 't': if copy(JSON,Index,3)='rue' then begin inc(Index,3); result := kTrue; Value := true; end; '"': if GetNextString(str) then begin result := kString; Value := str; end; '{': if ParseJSONObject(TJSONVariantData(Value)) then result := kObject; '[': if ParseJSONArray(TJSONVariantData(Value)) then result := kArray; '-','0'..'9': begin start := Index-1; while true do case JSON[Index] of '-','+','0'..'9','.','E','e': inc(Index); else break; end; str := copy(JSON,start,Index-start); val(str,i64,err); if err=0 then begin Value := i64; result := kInteger; end else begin val(str,d,err); if err<>0 then exit; Value := d; result := kFloat; end; end; end; end; function TJSONParser.CheckNextIdent(const ExpectedIdent: string): Boolean; begin result := (GetNextNonWhiteChar='"') and (CompareText(GetNextString,ExpectedIdent)=0) and (GetNextNonWhiteChar=':'); end; function TJSONParser.ParseJSONArray(var Data: TJSONVariantData): boolean; var item: variant; begin result := false; Data.Init; if not CheckNextNonWhiteChar(']') then // '[]' -> void array repeat if GetNextJSON(item)=kNone then exit; Data.AddValue(item); case GetNextNonWhiteChar of ',': continue; ']': break; else exit; end; until false; SetLength(Data.Values,Data.VCount); Data.VKind := jvArray; result := true; end; function TJSONParser.ParseJSONObject(var Data: TJSONVariantData): boolean; var key: string; val: variant; begin result := false; Data.Init; if not CheckNextNonWhiteChar('}') then // '{}' -> void object repeat if CheckNextNonWhiteChar('"') then begin if (not GetNextString(key)) or (GetNextNonWhiteChar<>':') then exit; end else if not GetNextAlphaPropName(key) then exit; if GetNextJSON(val)=kNone then exit; // writeln(Copy(JSON,Index-10,30)); Data.AddNameValue(key,val); case GetNextNonWhiteChar of ',': continue; '}': break; else exit; end; until false; SetLength(Data.Names,Data.VCount); SetLength(Data.Values,Data.VCount); Data.VKind := jvObject; result := true; end; function JSONToValue(const JSON: string): variant; var Parser: TJSONParser; begin Parser.Init(JSON,1); Parser.GetNextJSON(result); end; { RTTI-oriented functions } const BASE64: array[0..63] of char = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var BASE64DECODE: array of ShortInt; function BytesToBase64JSONString(const Bytes: TByteDynArray; withBase64Magic: boolean): string; var i,len,x,c,j: cardinal; P: PChar; begin len := length(Bytes); if len=0 then begin result := ''; exit; end; if withBase64Magic then x := JSON_BASE64_MAGIC_LEN else x := 0; SetLength(result,((len+2)div 3)*4+x); P := pointer(result); if withBase64Magic then move(JSON_BASE64_MAGIC,P^,sizeof(JSON_BASE64_MAGIC)); j := 0; for i := 1 to len div 3 do begin c := Bytes[j] shl 16 or Bytes[j+1] shl 8 or Bytes[j+2]; inc(j,3); P[x] := BASE64[(c shr 18) and $3f]; P[x+1] := BASE64[(c shr 12) and $3f]; P[x+2] := BASE64[(c shr 6) and $3f]; P[x+3] := BASE64[c and $3f]; inc(x,4); end; case len mod 3 of 1: begin c := Bytes[j] shl 4; P[x] := BASE64[(c shr 6) and $3f]; P[x+1] := BASE64[c and $3f]; P[x+2] := '='; P[x+3] := '='; inc(x,4); end; 2: begin c := Bytes[j] shl 10 or Bytes[j+1] shl 2; P[x] := BASE64[(c shr 12) and $3f]; P[x+1] := BASE64[(c shr 6) and $3f]; P[x+2] := BASE64[c and $3f]; P[x+3] := '='; inc(x,4); end; end; assert(integer(x)=Length(Result)); end; function Base64One(c: Char): integer; {$ifdef HASINLINE}inline;{$endif} begin result := ord(c); if result>127 then result := -1 else result := BASE64DECODE[result]; end; function Base64JSONStringToBytes(const JSONString: string; var Bytes: TByteDynArray; withBase64Magic: boolean): boolean; var i,bits,value,x,magiclen,len: cardinal; begin result := JSONString=''; if result then exit; if withBase64Magic then if comparemem(pointer(JSONString),@JSON_BASE64_MAGIC,sizeof(JSON_BASE64_MAGIC)) then magiclen := JSON_BASE64_MAGIC_LEN else {$ifndef UNICODE} if JSONString[1]='?' then // handle UTF-8 decoding error on ANSI Delphi magiclen := 1 else {$endif} exit else magiclen := 0; // withBase64Magic=false x := length(JSONString); len := x-magiclen; if len and 3<>0 then exit; if len=0 then Bytes := nil else begin if BASE64DECODE=nil then begin SetLength(BASE64DECODE,128); for i := 0 to 127 do BASE64DECODE[i] := -1; for i := 0 to high(BASE64) do BASE64DECODE[ord(BASE64[i])] := i; end; len := (len shr 2)*3; if Base64One(JSONString[x])<0 then begin dec(len); if Base64One(JSONString[x-1])<0 then dec(len); end; SetLength(Bytes,len); bits := 0; value := 0; len := 0; for i := magiclen+1 to Length(JSONString) do begin x := ord(JSONString[i]); // inlined Base64One(JSONString[i]) if x>127 then break; x := cardinal(BASE64DECODE[x]); if integer(x)<0 then break; value := value*64+x; bits := bits+6; if bits>=8 then begin bits := bits-8; x := value shr bits; value := value and ((1 shl bits)-1); Bytes[len] := x; inc(len); end; end; end; result := len=cardinal(length(Bytes)); end; function RTTIPropInfoTypeName(PropInfo: TRTTIPropInfo): string; begin result := ShortStringToString(@PropInfo^.PropType^.Name); end; procedure GetPropsInfo(TypeInfo: TRTTITypeInfo; var PropNames: TStringDynArray; var PropRTTI: TRTTIPropInfoDynArray); var i,n: integer; List: PPropList; begin n := GetPropList(PTypeInfo(TypeInfo),List); SetLength(PropNames,n); SetLength(PropRTTI,n); for i := 0 to n-1 do begin PropRTTI[i] := List^[i]; PropNames[i] := ShortStringToString(@PropRTTI[i]^.Name); end; freemem(List); end; function IsDateTime(PropInfo: TRTTIPropInfo): boolean; {$ifdef HASINLINE}inline;{$endif} begin result := PropInfo^.PropType{$ifndef FPC}^{$endif}=TypeInfo(TDateTime); end; type // used to map a TPropInfo.GetProc/SetProc and retrieve its kind PropWrap = packed record FillBytes: array [0..SizeOf(Pointer)-2] of byte; /// = $ff for a field address, or =$fe for a virtual method Kind: byte; end; function IsBlob(PropInfo: TRTTIPropInfo): boolean; {$ifdef HASINLINE}inline;{$endif} begin // we only handle plain TByteDynArray properties without getter/setter {$ifdef FPC} result := (PropInfo^.PropType=TypeInfo(TByteDynArray)) and (PropInfo^.PropProcs and 3=ptField); {$else} result := (PropInfo^.PropType^=TypeInfo(TByteDynArray)) and (PropWrap(PropInfo^.GetProc).Kind=$FF); {$endif} end; function GetTByteDynArrayProp(Instance: TObject; PropInfo: TRTTIPropInfo): PByteDynArray; {$ifdef HASINLINE}inline;{$endif} begin result := Pointer(NativeUInt(Instance)+ (NativeUInt(PropInfo^.GetProc){$ifndef FPC} and $00FFFFFF{$endif})); end; function GetInstanceProp(Instance: TObject; PropInfo: TRTTIPropInfo): variant; var obj: TObject; begin VarClear(result); if (PropInfo=nil) or (Instance=nil) then exit; case PropInfo^.PropType^.Kind of tkInt64{$ifdef FPC}, tkQWord{$endif}: result := GetInt64Prop(Instance,PropInfo); tkEnumeration, tkInteger, tkSet: result := GetOrdProp(Instance,PropInfo); {$ifdef NEXTGEN} tkUString: result := GetStrProp(Instance,PropInfo); {$else} {$ifdef FPC}tkAString,{$endif}tkLString: result := GetStrProp(Instance,PropInfo); tkWString: result := GetWideStrProp(Instance,PropInfo); {$ifdef UNICODE} tkUString: result := GetUnicodeStrProp(Instance,PropInfo); {$endif UNICODE} {$endif NEXTGEN} tkFloat: if IsDateTime(PropInfo) then result := DateTimeToIso8601(GetFloatProp(Instance,PropInfo)) else result := GetFloatProp(Instance,PropInfo); tkVariant: result := GetVariantProp(Instance,PropInfo); tkClass: begin obj := TObject(NativeInt(GetOrdProp(Instance,PropInfo))); if obj=nil then result := null else TJSONVariantData(result).Init(ObjectToJSON(obj)); end; tkDynArray: if IsBlob(PropInfo) then result := BytesToBase64JSONString(GetTByteDynArrayProp(Instance,PropInfo)^); end; end; procedure SetInstanceProp(Instance: TObject; PropInfo: TRTTIPropInfo; const Value: variant); var blob: PByteDynArray; obj: TObject; begin if (PropInfo<>nil) and (Instance<>nil) then case PropInfo^.PropType^.Kind of tkInt64{$ifdef FPC}, tkQWord{$endif}: if TVarData(Value).VType=varInt64 then SetInt64Prop(Instance,PropInfo,TVarData(Value).VInt64) else SetOrdProp(Instance,PropInfo,Value); tkEnumeration, tkInteger, tkSet: SetOrdProp(Instance,PropInfo,Value); {$ifdef NEXTGEN} tkUString: if TVarData(Value).VType<=varNull then SetStrProp(Instance,PropInfo,'') else SetStrProp(Instance,PropInfo,Value); {$else} {$ifdef FPC}tkAString,{$endif} tkLString: if TVarData(Value).VType<=varNull then SetStrProp(Instance,PropInfo,'') else SetStrProp(Instance,PropInfo,Value); tkWString: if TVarData(Value).VType<=varNull then SetWideStrProp(Instance,PropInfo,'') else SetWideStrProp(Instance,PropInfo,Value); {$ifdef UNICODE} tkUString: if TVarData(Value).VType<=varNull then SetUnicodeStrProp(Instance,PropInfo,'') else SetUnicodeStrProp(Instance,PropInfo,Value); {$endif UNICODE} {$endif NEXTGEN} tkFloat: if IsDateTime(PropInfo) and VarIsStr(Value) then SetFloatProp(Instance,PropInfo,Iso8601ToDateTime(Value)) else SetFloatProp(Instance,PropInfo,Value); tkVariant: SetVariantProp(Instance,PropInfo,Value); tkDynArray: if IsBlob(PropInfo) then begin blob := GetTByteDynArrayProp(Instance,PropInfo); if (TVarData(Value).VType<=varNull) or not Base64JSONStringToBytes(Value,blob^) then Finalize(blob^); end; tkClass: begin obj := TObject(NativeInt(GetOrdProp(Instance,PropInfo))); if TVarData(Value).VType>varNull then if obj=nil then begin obj := JSONVariantData(Value)^.ToNewObject; if obj<>nil then SetOrdProp(Instance,PropInfo,NativeInt(obj)); end else JSONVariantData(Value)^.ToObject(obj); end; end; end; function JSONToObjectList(ItemClass: TClass; const JSON: string): TObjectList; var doc: TJSONVariantData; item: TObject; i: integer; begin doc.Init(JSON); if (doc.VKind<>jvArray) or (ItemClass=nil) then result := nil else begin result := TObjectList.Create; for i := 0 to doc.Count-1 do begin item := ItemClass.Create; if not JSONVariantData(doc.Values[i])^.ToObject(item) then begin FreeAndNil(result); exit; end; result.Add(item); end; end; end; function JSONToObject(Instance: TObject; const JSON: string): boolean; var doc: TJSONVariantData; begin if Instance=nil then result := false else begin doc.Init(JSON); result := doc.ToObject(Instance); end; end; function JSONToNewObject(const JSON: string): pointer; var doc: TJSONVariantData; begin doc.Init(JSON); result := doc.ToNewObject; end; var RegisteredClass: array of record ClassName: string; ClassType: TClass; end; function FindClassForJSON(const ClassName: string): integer; begin for result := 0 to high(RegisteredClass) do if IdemPropName(RegisteredClass[result].ClassName,ClassName) then exit; result := -1; end; function CreateClassForJSON(const ClassName: string): TObject; var i: integer; begin i := FindClassForJSON(ClassName); if i<0 then result := nil else result := RegisteredClass[i].ClassType.Create; end; procedure RegisterClassForJSON(const Classes: array of TClass); var c,i: integer; name: string; begin for c := 0 to high(Classes) do begin name := string(Classes[c].ClassName); i := FindClassForJSON(name); if i>=0 then continue; i := length(RegisteredClass); SetLength(RegisteredClass,i+1); RegisteredClass[i].ClassName := Name; RegisteredClass[i].ClassType := Classes[c]; end; end; function ObjectToJSON(Instance: TObject; StoreClassName: boolean): string; var TypeInfo: PTypeInfo; PropCount, i: integer; PropList: PPropList; PropInfo: PPropInfo; begin if Instance=nil then begin result := 'null'; exit; end; {$ifndef NEXTGEN} if Instance.InheritsFrom(TList) then begin if TList(Instance).Count=0 then result := '[]' else begin result := '['; for i := 0 to TList(Instance).Count-1 do result := result+ObjectToJSON(TObject( TList(Instance).List{$ifdef FPC}^{$endif}[i]),StoreClassName)+','; result[length(result)] := ']'; end; exit; end; {$endif} if Instance.InheritsFrom(TStrings) then begin if TStrings(Instance).Count=0 then result := '[]' else begin result := '['; for i := 0 to TStrings(Instance).Count-1 do result := result+StringToJSON(TStrings(Instance).Strings[i])+','; result[length(result)] := ']'; end; exit; end; if Instance.InheritsFrom(TCollection) then begin if TCollection(Instance).Count=0 then result := '[]' else begin result := '['; for i := 0 to TCollection(Instance).Count-1 do result := result+ObjectToJSON(TCollection(Instance).Items[i],StoreClassName)+','; result[length(result)] := ']'; end; exit; end; TypeInfo := Instance.ClassInfo; if TypeInfo=nil then begin result := 'null'; exit; end; PropCount := GetPropList(TypeInfo,PropList); if PropCount>0 then try if StoreClassName then result := '{"ClassName":"'+string(Instance.ClassName)+'",' else result := '{'; for i := 0 to PropCount-1 do begin PropInfo := PropList^[i]; result := result+StringToJSON(ShortStringToString(@PropInfo^.Name))+':'+ ValueToJSON(GetInstanceProp(Instance,PropInfo))+','; end; result[length(result)] := '}'; finally FreeMem(PropList); end else result := 'null'; end; procedure GetPublishedMethods(Instance: TObject; out Methods: TPublishedMethodDynArray); var n: integer; procedure AddParentsFirst(C: TClass); type TMethodInfo = packed record {$ifdef FPC} Name: PShortString; Addr: Pointer; {$else} Len: Word; Addr: Pointer; Name: Byte; {$endif} end; var M: ^TMethodInfo; Method: TMethod; i,MCount: integer; begin if C=nil then exit; AddParentsFirst(C.ClassParent); // put children methods afterward M := PPointer(NativeInt(C)+vmtMethodTable)^; if M=nil then exit; Method.Data := Instance; MCount := {$ifdef FPC}PCardinal{$else}PWord{$endif}(M)^; inc({$ifdef FPC}PCardinal{$else}PWord{$endif}(M)); for i := 1 to MCount do begin Method.Code := M^.Addr; if n>=length(Methods) then SetLength(Methods,n+32); Methods[n].Name := {$ifdef FPC}M^.Name^{$else}ShortStringToString(@M^.Name){$endif}; Methods[n].Method := Method; inc(n); {$ifdef FPC} inc(M); {$else} inc(PByte(M),M^.Len); {$endif} end; end; begin if Instance=nil then exit; n := 0; AddParentsFirst(Instance.ClassType); SetLength(Methods,n); end; { TJSONVariantData } procedure TJSONVariantData.Init; begin VType := JSONVariantType.VarType; {$ifdef UNICODE} // makes compiler happy _Align := 0; {$endif} VKind := jvUndefined; VCount := 0; SetLength(Names, 0); SetLength(Values, 0); pointer(Names) := nil; pointer(Values) := nil; end; procedure TJSONVariantData.Init(const JSON: string); begin Init; FromJSON(JSON); if VType=varNull then VKind := jvObject else if VType<>JSONVariantType.VarType then Init; // we expect a true JSON array or object here end; procedure TJSONVariantData.InitFrom(const aValues: TVariantDynArray); begin Init; VKind := jvArray; Values := aValues; end; procedure TJSONVariantData.AddNameValue(const aName: string; const aValue: variant); begin if VKind=jvUndefined then VKind := jvObject else if VKind<>jvObject then raise EJSONException.CreateFmt('AddNameValue(%s) over array',[aName]); if VCount<=length(Values) then begin SetLength(Values,VCount+VCount shr 3+32); SetLength(Names,VCount+VCount shr 3+32); end; Values[VCount] := aValue; Names[VCount] := aName; inc(VCount); end; procedure TJSONVariantData.AddValue(const aValue: variant); begin if VKind=jvUndefined then VKind := jvArray else if VKind<>jvArray then raise EJSONException.Create('AddValue() over object'); if VCount<=length(Values) then SetLength(Values,VCount+VCount shr 3+32); Values[VCount] := aValue; inc(VCount); end; function TJSONVariantData.FromJSON(const JSON: string): boolean; var Parser: TJSONParser; begin Parser.Init(JSON,1); result := Parser.GetNextJSON(variant(self)) in [kObject,kArray]; end; function TJSONVariantData.Data(const aName: string): PJSONVariantData; var i: integer; begin i := NameIndex(aName); if (i<0) or (TVarData(Values[i]).VType<>JSONVariantType.VarType) then result := nil else result := @Values[i]; end; function TJSONVariantData.GetKind: TJSONVariantKind; begin if (@self=nil) or (VType<>JSONVariantType.VarType) then result := jvUndefined else result := VKind; end; function TJSONVariantData.GetCount: integer; begin if (@self=nil) or (VType<>JSONVariantType.VarType) then result := 0 else result := VCount; end; function TJSONVariantData.GetValue(const aName: string): variant; begin VarClear(result); if (@self<>nil) and (VType=JSONVariantType.VarType) and (VKind=jvObject) then GetVarData(aName,TVarData(result)); end; function TJSONVariantData.GetValueCopy(const aName: string): variant; var i: cardinal; begin VarClear(result); if (@self<>nil) and (VType=JSONVariantType.VarType) and (VKind=jvObject) then begin i := cardinal(NameIndex(aName)); if inil) and (VType=JSONVariantType.VarType) and (VKind=jvArray) then if cardinal(aIndex)nil) and (VType=JSONVariantType.VarType) and (VKind=jvArray) then if cardinal(aIndex)nil) and (VType=JSONVariantType.VarType) and (Names<>nil) then for result := 0 to VCount-1 do if Names[result]=aName then exit; result := -1; end; procedure TJSONVariantData.SetPath(const aPath: string; const aValue: variant); var i: integer; begin for i := length(aPath) downto 1 do if aPath[i]='.' then begin EnsureData(copy(aPath,1,i-1))^.SetValue(copy(aPath,i+1,maxInt),aValue); exit; end; SetValue(aPath,aValue); end; function TJSONVariantData.EnsureData(const aPath: string): PJSONVariantData; var i: integer; new: TJSONVariantData; begin // recursive value set i := Pos('.',aPath); if i=0 then begin i := NameIndex(aPath); if i<0 then begin // not existing: create new new.Init; AddNameValue(aPath,variant(new)); result := @Values[VCount-1]; end else begin if TVarData(Values[i]).VType<>JSONVariantType.VarType then begin VarClear(Values[i]); TJSONVariantData(Values[i]).Init; // create as TJSONVariantData end; result := @Values[i]; end; end else result := EnsureData(copy(aPath,1,i-1))^.EnsureData(copy(aPath,i+1,maxInt)); end; function TJSONVariantData.AddItem: PJSONVariantData; var new: TJSONVariantData; begin new.Init; AddValue(variant(new)); result := @Values[VCount-1]; end; procedure TJSONVariantData.SetValue(const aName: string; const aValue: variant); var i: integer; begin if @self=nil then raise EJSONException.Create('Unexpected Value[] access'); if aName='' then raise EJSONException.Create('Unexpected Value['''']'); i := NameIndex(aName); if i<0 then AddNameValue(aName,aValue) else Values[i] := aValue; end; function TJSONVariantData.ToJSON: string; var i: integer; begin case VKind of jvObject: if VCount=0 then result := '{}' else begin result := '{'; for i := 0 to VCount-1 do result := result+StringToJSON(Names[i])+':'+ValueToJSON(Values[i])+','; result[length(result)] := '}'; end; jvArray: if VCount=0 then result := '[]' else begin result := '['; for i := 0 to VCount-1 do result := result+ValueToJSON(Values[i])+','; result[length(result)] := ']'; end; else result := 'null'; end; end; function TJSONVariantData.ToNewObject: TObject; var ndx,i: Integer; begin result := nil; if (Kind<>jvObject) or (Count=0) then exit; ndx := NameIndex('ClassName'); if ndx<0 then exit; result := CreateClassForJSON(Values[ndx]); if result=nil then exit; // class name has not been registered for i := 0 to Count-1 do if i<>ndx then SetInstanceProp(result,GetPropInfo(result,Names[i]),Values[i]); end; function TJSONVariantData.ToObject(Instance: TObject): boolean; var i: integer; aItem: TCollectionItem; begin result := false; if Instance=nil then exit; case Kind of jvObject: for i := 0 to Count-1 do SetInstanceProp(Instance, GetPropInfo(Instance,Names[i]),Values[i]); jvArray: if Instance.InheritsFrom(TCollection) then begin TCollection(Instance).Clear; for i := 0 to Count-1 do begin aItem := TCollection(Instance).Add; if not JSONVariantData(Values[i])^.ToObject(aItem) then exit; end; end else if Instance.InheritsFrom(TStrings) then try TStrings(Instance).BeginUpdate; TStrings(Instance).Clear; for i := 0 to Count-1 do TStrings(Instance).Add(Values[i]); finally TStrings(Instance).EndUpdate; end else exit; else exit; end; result := true; end; { TJSONVariant } procedure TJSONVariant.Cast(var Dest: TVarData; const Source: TVarData); begin CastTo(Dest,Source,VarType); end; procedure TJSONVariant.CastTo(var Dest: TVarData; const Source: TVarData; const AVarType: TVarType); begin if Source.VType<>VarType then RaiseCastError; variant(Dest) := TJSONVariantData(Source).ToJSON; end; procedure TJSONVariant.Clear(var V: TVarData); begin V.VType := varEmpty; Finalize(TJSONVariantData(V).Names); Finalize(TJSONVariantData(V).Values); end; procedure TJSONVariant.Copy(var Dest: TVarData; const Source: TVarData; const Indirect: Boolean); begin if Indirect then SimplisticCopy(Dest,Source,true) else begin VarClear(variant(Dest)); TJSONVariantData(Dest).Init; TJSONVariantData(Dest) := TJSONVariantData(Source); end; end; {$ifndef FPC} {$ifndef ISDELPHI6} function TJSONVariant.FixupIdent(const AText: string): string; begin // we expect the names to be case-sensitive result := AText; end; {$endif} {$endif} function TJSONVariant.GetProperty(var Dest: TVarData; const V: TVarData; const Name: string): Boolean; begin if not TJSONVariantData(V).GetVarData(Name,Dest) then Dest.VType := varNull; result := true; end; {$ifdef FPC_VARIANTSETVAR} function TJSONVariant.SetProperty(var V: TVarData; const Name: string; const Value: TVarData): Boolean; {$else} function TJSONVariant.SetProperty(const V: TVarData; const Name: string; const Value: TVarData): Boolean; {$endif} begin {$ifdef FPC} {$ifndef FPC_VARIANTSETVAR} raise EJSONException.Create('Setting TJSONVariant via late-binding does not'+ ' work with FPC - see http://mantis.freepascal.org/view.php?id=26773 -'+ ' use latest SVN or JSONVariantDataSafe(jsonvar)^[''prop''] := ... instead'); {$endif} {$endif} TJSONVariantData(V).SetValue(Name,variant(Value)); result := true; end; { TJSONTable } constructor TJSONTable.Create(const aJSON: string); var f,firstValue: integer; EndOfField: char; fieldCount, fieldName, dummy: variant; Parser: TJSONParser; begin Parser.Init(aJSON,1); fJSON := aJSON; EndOfField := #0; if (Parser.GetNextNonWhiteChar='{') and Parser.CheckNextIdent('fieldCount') and (Parser.GetNextJSON(fieldCount)=kInteger) and (Parser.GetNextNonWhiteChar=',') and Parser.CheckNextIdent('values') and (Parser.GetNextNonWhiteChar='[') then begin // non expanded format: {"fieldCount":2,"values":["ID","Int",1,0,2,0,3,...] SetLength(fFieldNames,integer(fieldCount)); for f := 0 to high(fFieldNames) do begin if Parser.GetNextJSON(fieldName)<>kString then exit; fFieldNames[f] := fieldName; EndOfField := Parser.GetNextNonWhiteChar; if EndOfField<>',' then if (EndOfField<>']') or (f<>High(FieldNames)) then exit end; if EndOfField=',' then fJSONIndexFirstValue := Parser.Index; end else begin // expanded format: [{"ID":1,"Int":0},{"ID":2,"Int":0},{"ID":3,...] Parser.Index := 1; if (Parser.GetNextNonWhiteChar='[') and (Parser.GetNextNonWhiteChar='{') then begin firstValue := Parser.Index; f := 0; repeat if (Parser.GetNextJSON(fieldName)<>kString) or (Parser.GetNextNonWhiteChar<>':') then exit; if Parser.GetNextJSON(dummy)=kNone then exit; SetLength(fFieldNames,f+1); fFieldNames[f] := fieldName; inc(f); EndOfField := Parser.GetNextNonWhiteChar; if EndOfField<>',' then if EndOfField='}' then break else exit; until false; fJSONIndexFirstValue := firstValue; fJSONExpanded := true; end; end; SetLength(fRowValues,length(fFieldNames)); end; function TJSONTable.FieldIndex(const FieldName: string): integer; begin for result := 0 to high(fFieldNames) do if CompareText(fFieldNames[result],FieldName)=0 then exit; result := -1; end; function TJSONTable.Get(const FieldName: string): variant; var ndx: integer; begin ndx := FieldIndex(FieldName); if ndx<0 then result := null else result := fRowValues[ndx]; end; function TJSONTable.Step(SeekFirst: boolean): boolean; var f: integer; EndOfField: char; Parser: TJSONParser; begin result := false; if SeekFirst or (fJSONCurrentIndex=0) then fJSONCurrentIndex := fJSONIndexFirstValue; if fJSONCurrentIndex<=0 then exit; Parser.Init(fJSON,fJSONCurrentIndex); fJSONCurrentIndex := -1; // indicates end of content in case of exit below EndOfField := #0; for f := 0 to high(fRowValues) do begin if fJSONExpanded and not Parser.CheckNextIdent(fFieldNames[f]) then exit; if Parser.GetNextJSON(fRowValues[f])=kNone then exit; EndOfField := Parser.GetNextNonWhiteChar; if EndOfField<>',' then if f<>High(fRowValues) then exit else if ((EndOfField=']') and (not fJSONExpanded)) or ((EndOfField='}') and fJSONExpanded) then break else exit; end; if fJSONExpanded then begin if EndOfField<>'}' then exit; EndOfField := Parser.GetNextNonWhiteChar; if (EndOfField=',') and (Parser.GetNextNonWhiteChar<>'{') then exit; end; if EndOfField=',' then fJSONCurrentIndex := Parser.Index; // indicates next Step() has data result := true; end; function TJSONTable.StepValue(var RowValues: variant; SeekFirst: boolean): boolean; begin result := Step(SeekFirst); if not result then exit; if TVarData(RowValues).VType<>JSONVariantType.VarType then begin VarClear(RowValues); TJSONVariantData(RowValues).Init; end; TJSONVariantData(RowValues).VKind := jvObject; TJSONVariantData(RowValues).VCount := Length(fFieldNames); TJSONVariantData(RowValues).Names := fFieldNames; TJSONVariantData(RowValues).Values := fRowValues; end; { TJSONTableObject } function TJSONTableObject.StepObject(Instance: TObject; SeekFirst: boolean=false): boolean; begin if (Instance=nil) then result := false else result := Step(SeekFirst); if result then FillInstance(Instance); end; procedure TJSONTableObject.FillInstance(Instance: TObject); var i: integer; begin if fTypeInfo<>Instance.ClassInfo then FillPropInfo(Instance.ClassInfo); for i := 0 to Length(fPropInfo)-1 do SetInstanceProp(Instance,fPropInfo[i],fRowValues[i]); end; function TJSONTableObject.GetPropInfo(aTypeInfo: TRTTITypeInfo; const PropName: string): TRTTIPropInfo; begin result := TypInfo.GetPropInfo(PTypeInfo(aTypeInfo),PropName); end; procedure TJSONTableObject.FillPropInfo(aTypeInfo: TRTTITypeInfo); var i: integer; begin fTypeInfo := aTypeInfo; SetLength(fPropInfo,Length(fFieldNames)); for i := 0 to length(FieldNames)-1 do fPropInfo[i] := GetPropInfo(aTypeInfo,fFieldNames[i]); end; initialization JSONVariantType := TJSONVariant.Create; {$ifndef FPC} {$ifndef NOFORMATSETTINGS} {$ifdef ISDELPHIXE} SettingsUS := TFormatSettings.Create('en_US'); {$else} GetLocaleFormatSettings($0409,SettingsUS); {$endif} {$endif} {$endif} end.