Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
2176 lines (1988 sloc) 68.6 KB
/// 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) 2019 Arnaud Bouchez
Synopse Informatique - https://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) 2019
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,
{$endif}
{$ifndef NEXTGEN}
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;
{$ifdef FPC}
NativeInt = PtrInt;
NativeUInt = PtrUInt;
{$else}
{$ifndef ISDELPHI2010} // Delphi 2009 NativeUInt is buggy
NativeInt = integer;
NativeUInt = cardinal;
{$endif}
{$ifndef UNICODE}
RawByteString = AnsiString;
{$endif}
{$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<TObject>;
{$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 USEOBJECTINSTEADOFRECORD}
TJSONVariantData = object
protected
{$else}
TJSONVariantData = record
private
{$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
// - you should call Clear before calling overloaded Init several times
procedure Init; overload;
/// initialize the low-level memory structure with a given JSON content
// - you should call Clear before calling overloaded Init several times
procedure Init(const JSON: string); overload;
/// initialize the low-level memory structure with a given array of variant
// - you should call Clear before calling overloaded Init several times
procedure InitFrom(const aValues: TVariantDynArray); overload;
/// delete all internal stored data
// - basically the same as Finalize(aJsonVariantData) + aJsonVariantData.Init
// - you should call this method before calling overloaded Init several times
procedure Clear;
/// 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; StoreClassName: boolean = False): 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)<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;
{$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 // str := str+chr would have created a temporary string for chr
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 TVarData(Value).VType=varBoolean then
if TVarData(Value).VBoolean then
result := 'true' else
result := 'false' else
if TVarData(Value).VType=varDate then
result := DateTimeToJSON(TVarData(Value).VDouble) else
if VarIsOrdinal(Value) then begin
I64 := Value;
result := IntToStr(I64);
end 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^);
{$ifdef FPC}
vtQWord: result := IntToStr(VQWord^);
{$endif}
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
{$ifdef USEOBJECTINSTEADOFRECORD}
TJSONParser = object
{$else}
TJSONParser = record
{$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; StoreClassName: boolean): 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, StoreClassName));
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,StoreClassName))+',';
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;
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;
VCount := Length(aValues);
end;
procedure TJSONVariantData.Clear;
begin
Names := nil;
Values := nil;
Init;
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 i<cardinal(length(Values)) then
result := Values[i];
end;
end;
function TJSONVariantData.GetItem(aIndex: integer): variant;
begin
VarClear(result);
if (@self<>nil) and (VType=JSONVariantType.VarType) and (VKind=jvArray) then
if cardinal(aIndex)<cardinal(VCount) then
result := Values[aIndex];
end;
procedure TJSONVariantData.SetItem(aIndex: integer; const aItem: variant);
begin
if (@self<>nil) and (VType=JSONVariantType.VarType) and (VKind=jvArray) then
if cardinal(aIndex)<cardinal(VCount) then
Values[aIndex] := aItem;
end;
function TJSONVariantData.GetVarData(const aName: string;
var Dest: TVarData): boolean;
var i: cardinal;
begin
i := cardinal(NameIndex(aName));
if i<cardinal(length(Values)) then begin
Dest.VType := varVariant or varByRef;
Dest.VPointer := @Values[i];
result := true;
end else
result := false;
end;
function TJSONVariantData.NameIndex(const aName: string): integer;
begin
if (@self<>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.
You can’t perform that action at this time.