/// Framework Core Low-Level JSON Processing // - this unit is a part of the Open Source Synopse mORMot framework 2, // licensed under a MPL/GPL/LGPL three license - see LICENSE.md unit mormot.core.json; { ***************************************************************************** JSON functions shared by all framework units - Low-Level JSON Processing Functions - TJsonWriter class with proper JSON escaping and WriteObject() support - JSON-aware TSynNameValue TSynPersistentStoreJson - JSON-aware TSynDictionary Storage - JSON Unserialization for any kind of Values - JSON Serialization Wrapper Functions - Abstract Classes with Auto-Create-Fields ***************************************************************************** } interface {$I mormot.defines.inc} uses classes, contnrs, sysutils, {$ifdef ISDELPHI} typinfo, // for proper Delphi inlining {$endif ISDELPHI} mormot.core.base, mormot.core.os, mormot.core.unicode, mormot.core.text, mormot.core.datetime, mormot.core.rtti, mormot.core.buffers, mormot.core.data; { ********** Low-Level JSON Processing Functions } type /// exception raised by this unit, in relation to raw JSON process EJsonException = class(ESynException); /// kind of first character used from JSON_TOKENS[] for efficient JSON parsing TJsonToken = ( jtNone, jtDoubleQuote, jtFirstDigit, jtNullFirstChar, jtTrueFirstChar, jtFalseFirstChar, jtObjectStart, jtArrayStart, jtObjectStop, jtArrayStop, jtAssign, jtComma, jtSingleQuote, jtEqual, jtIdentifierFirstChar, jtSlash, jtEndOfBuffer); /// defines a lookup table used for branch-less first char JSON parsing TJsonTokens = array[AnsiChar] of TJsonToken; /// points to a lookup table used for branch-less first char JSON parsing PJsonTokens = ^TJsonTokens; /// kind of character used from JSON_CHARS[] for efficient JSON parsing // - using such a set compiles into TEST [MEM], IMM so is more efficient // than a regular set of AnsiChar which generates much slower BT [MEM], IMM // - the same 256-byte memory will also be reused from L1 CPU cache // during the parsing of complex JSON input // - TTestCoreProcess.JSONBenchmark shows around 900MB/s on my i5 notebook TJsonChar = set of ( jcJsonIdentifierFirstChar, jcJsonIdentifier, jcEndOfJsonFieldOr0, jcEndOfJsonFieldNotName, jcEndOfJsonValueField, jcJsonStringMarker, jcDigitFirstChar, jcDigitFloatChar); /// defines a lookup table used for branch-less JSON parsing TJsonCharSet = array[AnsiChar] of TJsonChar; /// points to a lookup table used for branch-less JSON parsing PJsonCharSet = ^TJsonCharSet; const /// JSON_ESCAPE[] lookup value: indicates no escape needed JSON_ESCAPE_NONE = 0; /// JSON_ESCAPE[] lookup value: indicates #0 (end of string) JSON_ESCAPE_ENDINGZERO = 1; /// JSON_ESCAPE[] lookup value: should be escaped as \u00xx JSON_ESCAPE_UNICODEHEX = 2; /// JSON_UNESCAPE[] lookup value: indicates #0 or unexpected control char JSON_UNESCAPE_UNEXPECTED = #0; /// JSON_UNESCAPE[] lookup value: indicates '\u0123' UTF-16 pattern JSON_UNESCAPE_UTF16 = #1; var /// 256-byte lookup table for fast branchless initial character JSON parsing JSON_TOKENS: TJsonTokens; /// 256-byte lookup table for fast branchless JSON parsing // - to be used e.g. as: // ! if jvJsonIdentifier in JSON_CHARS[P^] then ... JSON_CHARS: TJsonCharSet; /// 256-byte lookup table for fast branchless JSON text escaping // - 0 = JSON_ESCAPE_NONE indicates no escape needed // - 1 = JSON_ESCAPE_ENDINGZERO indicates #0 (end of string) // - 2 = JSON_ESCAPE_UNICODEHEX should be escaped as \u00xx // - b,t,n,f,r,\," as escaped character for #8,#9,#10,#12,#13,\," JSON_ESCAPE: array[byte] of byte; /// 256-byte lookup table for fast branchless JSON text un-escaping // - #0 = JSON_UNESCAPE_UNEXPECTED for unexpected #0 or control char // - #1 = JSON_UNESCAPE_UTF16 for '\u0123' UTF-16 pattern // - #8,#9,#10,#12,#13 as unescaped char from b,t,n,f,r // - other characters are litterals and should be written as such JSON_UNESCAPE: array[AnsiChar] of AnsiChar; /// how many initial chars of a JSON array are parsed for intial capacity // - used e.g. by _JL_DynArray() and TDocVariantData.InitJsonInPlace() // - 64KB was found out empirically as a good value - but you can tune it JSON_PREFETCH: integer = 65536; /// returns TRUE if the given text buffers would be escaped when written as JSON // - e.g. if contains " or \ characters, as defined by // http://www.ietf.org/rfc/rfc4627.txt function NeedsJsonEscape(const Text: RawUtf8): boolean; overload; /// returns TRUE if the given text buffers would be escaped when written as JSON // - e.g. if contains " or \ characters, as defined by // http://www.ietf.org/rfc/rfc4627.txt function NeedsJsonEscape(P: PUtf8Char): boolean; overload; /// returns TRUE if the given text buffers would be escaped when written as JSON // - e.g. if contains " or \ characters, as defined by // http://www.ietf.org/rfc/rfc4627.txt function NeedsJsonEscape(P: PUtf8Char; PLen: integer): boolean; overload; {$ifdef HASINLINE}inline;{$endif} /// UTF-8 decode one or two \u#### JSON escaped codepoints into Dest // - P^ should point at 'u1234' just after \u1234 // - return ending P position, maybe after another \u#### UTF-16 surrogate char function JsonUnicodeEscapeToUtf8(var D: PUtf8Char; P: PUtf8Char): PUtf8Char; // {$ifdef HASINLINE}inline;{$endif} /// ensure all UTF-8 Unicode glyphs are escaped as \u#### UTF-16 JSON // - this will work at raw UTF-8 text level: if your input is true JSON, // consider using JsonReformat(s, jsonEscapeUnicode) instead function JsonUnicodeEscape(const s: RawUtf8): RawUtf8; /// ensure all \u#### UTF-16 JSON are decoded into plain UTF-8 content // - this will work at raw UTF-8 text level: if your input is true JSON, // consider using JsonReformat(s, jsonNoEscapeUnicode) instead function JsonUnicodeUnEscape(const s: RawUtf8): RawUtf8; /// encode one \u#### JSON escaped UTF-16 codepoint into Dest procedure Utf16ToJsonUnicodeEscape(var B: PUtf8Char; c: PtrUint; tab: PByteToWord); {$ifdef HASINLINE} inline; {$endif} /// test if the supplied buffer is a "string" value or a numerical value // (floating point or integer), according to the characters within // - this version will recognize null/false/true as strings // - e.g. IsString('0')=false, IsString('abc')=true, IsString('null')=true function IsString(P: PUtf8Char): boolean; /// test if the supplied buffer is a "string" value or a numerical value // (floating or integer), according to the JSON encoding schema // - this version will NOT recognize JSON null/false/true as strings // - e.g. IsStringJson('0')=false, IsStringJson('abc')=true, // but IsStringJson('null')=false // - will follow the JSON definition of number, i.e. '0123' is a string (i.e. // '0' is excluded at the begining of a number) and '123' is not a string function IsStringJson(P: PUtf8Char): boolean; /// test if the supplied text buffer seems to be a correct (extended) JSON value // - will allow comments and extended MongoDB JSON syntax unless Strict=true // - numbers, escaped strings and commas are wild guessed, for performance function IsValidJson(P: PUtf8Char; len: PtrInt; strict: boolean = false): boolean; overload; /// test if the supplied text seems to be a correct (extended) JSON value // - will allow comments and extended MongoDB JSON syntax unless Strict=true // - numbers, escaped strings and commas are wild guessed, for performance function IsValidJson(const s: RawUtf8; strict: boolean = false): boolean; overload; /// test if the supplied #0 ended buffer is a correct (extended) JSON value // - will allow comments and extended MongoDB JSON syntax unless Strict=true // - numbers, escaped strings and commas are wild guessed, for performance function IsValidJsonBuffer(P: PUtf8Char; strict: boolean = false): boolean; /// returns the JSON type of a supplied #0 ended buffer // - will move to the first non-space char, then returns its JSON_TOKENS[] value // - for valid JSON, is likely to return jtDoubleQuote, jtFirstDigit, // jtNullFirstChar, jtTrueFirstChar, jtFalseFirstChar, jtObjectStart or jtArrayStart function GetFirstJsonToken(P: PUtf8Char): TJsonToken; /// validate the supplied #0 ended buffer and returns its JSON type // - on parsing error, returns P=nil and jtNone // - will move P to the next JSON item, and return the JSON token kind, e.g. // jtArrayStart, jtObjectStart, jtDoubleQuote or jtFirstDigit // - will allow comments and extended MongoDB JSON syntax unless Strict=true // - optionally return the number of nested items for jtArrayStart/jtObjectStart function GetNextJsonToken(var P: PUtf8Char; strict: boolean = false; DocCount: PInteger = nil): TJsonToken; /// simple method to go after the next ',' character procedure IgnoreComma(var P: PUtf8Char); {$ifdef HASINLINE}inline;{$endif} /// returns TRUE if the given text buffer contains simple characters as // recognized by JSON extended syntax // - follow GetJsonPropName and GotoNextJsonObjectOrArray expectations function JsonPropNameValid(P: PUtf8Char): boolean; {$ifdef HASINLINE}inline;{$endif} type /// efficient JSON value parser / in-place decoder // - as used by JsonDecode() and all internal JSON functions {$ifdef USERECORDWITHMETHODS} TGetJsonField = record {$else} TGetJsonField = object {$endif USERECORDWITHMETHODS} public /// input/output JSON parsing buffer address Json: PUtf8Char; /// in-place output parsed JSON value, unescaped and #0 terminated // - see associated WasString to find out its actual type Value: PUtf8Char; /// in-place output parsed JSON value length ValueLen: integer; /// set if the value was actually a JSON string // - "strings" are decoded as 'strings', with WasString=true, properly JSON // unescaped (e.g. any \u0123 pattern would be converted into UTF-8 content) // - numbers are decoded as text, e.g. '1.234', with WasString=false // - null is decoded as Value=nil and WasString=false // - true/false are decoded as 'true'/'false' with WasString=false WasString: boolean; /// the ',' ':' or '}' separator just after the Value // - may have been overwritten with a #0 termination in the input buffer EndOfObject: AnsiChar; /// decode a JSON field value in-place from an UTF-8 encoded text buffer // - warning: will decode in the Json buffer memory itself (no memory copy // nor allocation), for faster process - so take care that it is not shared // - Value/ValueLen/WasString is set with the parsed value // - EndOfObject is set to the JSON ending char (',' ':' or '}' e.g.) // - Json points to the next field to be decoded, or nil on parsing error procedure GetJsonField; /// decode a JSON 64-bit integer value from an UTF-8 encoded text buffer function GetJsonInt64: Int64; {$ifdef HASINLINE} inline; {$endif} /// decode a JSON field value in-place into a RawUtf8 string procedure GetJsonValue(var Text: RawUtf8); /// decode a JSON content from an UTF-8 encoded buffer // - GetJsonField will only handle JSON "strings" or numbers - if // HandleValuesAsObjectOrArray is TRUE, this function will process JSON { // objects } or [ arrays ] and add a #0 at the end of it // - warning: will decode in the Json buffer memory itself (no memory // allocation or copy), for faster process - so take care that it is not shared // - Value/ValueLen/WasString is set with the parsed JSON value // - EndOfObject is set to the JSON ending char (',' ':' or '}' e.g.) // - Json points to the next value to be decoded, or nil on parsing error procedure GetJsonFieldOrObjectOrArray( HandleValuesAsObjectOrArray: boolean = true; NormalizeBoolean: boolean = true); end; {$ifndef PUREMORMOT2} /// decode a JSON field value in-place from an UTF-8 encoded text buffer // - compatibility wrapper around TGetJsonField.GetJsonField method function GetJsonField(P: PUtf8Char; out PDest: PUtf8Char; WasString: PBoolean = nil; EndOfObject: PUtf8Char = nil; Len: PInteger = nil): PUtf8Char; {$ifdef HASINLINE} inline; {$endif} /// decode a JSON content from an UTF-8 encoded buffer // - compatibility wrapper around TGetJsonField.GetJsonFieldOrObjectOrArray function GetJsonFieldOrObjectOrArray(var Json: PUtf8Char; WasString: PBoolean = nil; EndOfObject: PUtf8Char = nil; HandleValuesAsObjectOrArray: boolean = false; NormalizeBoolean: boolean = true; Len: PInteger = nil): PUtf8Char; {$ifdef HASINLINE} inline; {$endif} {$endif PUREMORMOT2} /// decode a JSON field name in an UTF-8 encoded buffer // - this function decodes in the P^ buffer memory itself (no memory allocation // or copy), for faster process - so take care that P^ is not shared // - it will return the property name, with an ending #0, and "..." content // properly unescaped unless NoJsonUnescape is set to true // - returns nil on error // - this function will handle strict JSON property name (i.e. a "string"), but // also MongoDB extended syntax, e.g. {age:{$gt:18}} or {'people.age':{$gt:18}} // see @http://docs.mongodb.org/manual/reference/mongodb-extended-json function GetJsonPropName(var Json: PUtf8Char; Len: PInteger = nil; NoJsonUnescape: boolean = false): PUtf8Char; /// decode a JSON field name in an UTF-8 encoded ShortString variable // - this function would left the P^ buffer memory untouched, so may be safer // than the overloaded GetJsonPropName() function in some cases // - it will return the property name as a local UTF-8 encoded ShortString, // or PropName='' on error // - this function won't unescape the property name, as strict JSON (i.e. a "st\"ring") // - but it will handle MongoDB syntax, e.g. {age:{$gt:18}} or {'people.age':{$gt:18}} // see @http://docs.mongodb.org/manual/reference/mongodb-extended-json procedure GetJsonPropNameShort(var P: PUtf8Char; out PropName: ShortString); /// decode a JSON object or array from an UTF-8 encoded buffer // - as called by GetJsonFieldOrObjectOrArray() for HandleValuesAsObjectOrArray // - return the position of the next JSON item (with EndOfObject and optionally // Len^ properly set) or nil on parsing error function GetJsonObjectOrArray(P: PUtf8Char; EndOfObject: PUtf8Char; Len: PInteger = nil): PUtf8Char; /// retrieve the next JSON item as a RawJson undecoded variable // - P buffer can be either any JSON item, i.e. a string, a number or even a // JSON array (ending with ]) or a JSON object (ending with }) // - EndOfObject (if not nil) is set to the JSON value end char (',' ':' or '}') // - input buffer is not modified in-place, since result is directly copied procedure GetJsonItemAsRawJson(var P: PUtf8Char; var result: RawJson; EndOfObject: PAnsiChar = nil); /// retrieve the next JSON item as a RawUtf8 decoded buffer // - P buffer can be either any JSON item, i.e. a string, a number or even a // JSON array (ending with ]) or a JSON object (ending with }) // - EndOfObject (if not nil) is set to the JSON value end char (',' ':' or '}') // - just calls TGetJsonField, and create a new RawUtf8 from the returned value, // after proper string unescape (with WasString^=true) // - warning: input buffer is modified in-place during output value parsing function GetJsonItemAsRawUtf8(var P: PUtf8Char; var output: RawUtf8; WasString: PBoolean = nil; EndOfObject: PUtf8Char = nil): boolean; /// get the next character after a quoted buffer // - the first character in P^ must be " // - it will return the latest " position, ignoring \" within // - caller should check that return PUtf8Char is indeed a " function GotoEndOfJsonString(P: PUtf8Char): PUtf8Char; /// reach position just after the current JSON string in the supplied UTF-8 buffer // - will first ensure that P^='"' then process like GotoEndJsonItem() function GotoEndJsonItemString(P: PUtf8Char): PUtf8Char; /// reach position just after the current JSON item in the supplied UTF-8 buffer // - buffer can be either any JSON item, i.e. a string, a number or even a // JSON array (ending with ]) or a JSON object (ending with }) // - returns nil if the specified buffer is not valid JSON content // - returns the position in buffer just after the item excluding the separator // character - i.e. result^ may be ',','}',']' // - will allow comments and extended MongoDB JSON syntax - use // GotoEndJsonItemStrict() if you expect a more standard JSON parsing function GotoEndJsonItem(P: PUtf8Char; PMax: PUtf8Char = nil): PUtf8Char; /// reach position just after the current JSON item in the supplied UTF-8 buffer // - in respect to GotoEndJsonItem(), this function will validate for strict // JSON simple values, i.e. real numbers or only true/false/null constants, // and refuse commens or MongoDB extended syntax like {age:{$gt:18}} // - numbers and escaped strings are not fully validated, just their charset function GotoEndJsonItemStrict(P: PUtf8Char; PMax: PUtf8Char = nil): PUtf8Char; /// reach the position of the next JSON item(s) in the supplied UTF-8 buffer // - buffer can be either any JSON item, i.e. a string, a number or even a // JSON array (ending with ]) or a JSON object (ending with }) // - returns nil if the specified number of items is not available in buffer // - returns the position in buffer after the item including the separator // character (optionally in EndOfObject) - i.e. result will be at the start of // the next object, and EndOfObject may be ',','}',']' function GotoNextJsonItem(P: PUtf8Char; NumberOfItemsToJump: cardinal = 1; EndOfObject: PAnsiChar = nil; PMax: PUtf8Char = nil; Strict: boolean = false): PUtf8Char; overload; /// reach the position of the next JSON item in the supplied UTF-8 buffer // - similar to the GotoNextJsonItem() with NumberOfItemsToJump=1 function GotoNextJsonItem(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char; overload; {don't inline to reduce the stack size of the caller function} /// search the EndOfObject of a JSON buffer, just like TGetJsonField does function ParseEndOfObject(P: PUtf8Char; out EndOfObject: AnsiChar): PUtf8Char; {$ifdef HASINLINE}inline;{$endif} /// compute the number of elements of a JSON array // - this will handle any kind of arrays, including those with nested // JSON objects or arrays // - incoming P^ should point to the first char AFTER the initial '[' (which // may be a closing ']') // - returns 0 if the supplied input is invalid, or the number of identified // items in the JSON array buffer // - if PMax is set, will abort after this position, and return the current // counted number of items as negative, which could be used as initial allocation // before the loop - typical use in this case is e.g. // ! cap := abs(JsonArrayCount(P, P + JSON_PREFETCH)); // - some performance numbers on a Core i5-13400: // $ JsonArrayCount(P) in 10.95ms i.e. 14.3M/s, 1.7 GB/s // $ JsonArrayCount(P,PMax) in 11.05ms i.e. 14.1M/s, 1.7 GB/s function JsonArrayCount(P: PUtf8Char; PMax: PUtf8Char = nil; Strict: boolean = false): integer; /// go to the #nth item of a JSON array // - implemented via a fast SAX-like approach: the input buffer is not changed, // nor no memory buffer allocated neither content copied // - returns nil if the supplied index is out of range // - returns a pointer to the index-nth item in the JSON array (first index=0) // - this will handle any kind of arrays, including those with nested // JSON objects or arrays // - incoming P^ should point to the first initial '[' char function JsonArrayItem(P: PUtf8Char; Index: integer): PUtf8Char; /// retrieve the positions of all elements of a JSON array // - this will handle any kind of arrays, including those with nested // JSON objects or arrays // - warning: incoming P^ should point to the first char AFTER the initial '[' // (which may be a closing ']') - calling e.g. NextNotSpaceCharIs() // - returns false if the supplied input is invalid // - returns true on success, with Values[] pointing to each unescaped value, // may be a JSON string, object, array of constant function JsonArrayDecode(P: PUtf8Char; out Values: TPUtf8CharDynArray): boolean; /// compute the number of fields in a JSON object // - this will handle any kind of objects, including those with nested JSON // documents, and also comments or MongoDB extended syntax (unless Strict=true) // - warning: incoming P^ should point to the first char AFTER the initial '{' // (which may be a closing '}') // - will abort if P reaches PMax (if not nil), and return the current counted // number of items as negative, which could be used as initial allocation before // a parsing loop - typical use in this case is e.g. // ! cap := abs(JsonObjectPropCount(P, P + JSON_PREFETCH)); function JsonObjectPropCount(P: PUtf8Char; PMax: PUtf8Char = nil; Strict: boolean = false): PtrInt; /// go to a named property of a JSON object // - implemented via a fast SAX-like approach: the input buffer is not changed, // nor no memory buffer allocated neither content copied // - PropName is search case-insensitively as 'propertyname' or 'property*' // - returns nil if the supplied property name does not exist // - returns a pointer to the matching item value in the JSON object // - this will handle any kind of objects, including those with nested // JSON objects or arrays // - incoming P^ should point to the first initial '{' char function JsonObjectItem(P: PUtf8Char; const PropName: RawUtf8; PropNameFound: PRawUtf8 = nil): PUtf8Char; overload; /// go to a buffer-named property of a JSON object // - as called by overloaded JsonObjectItem() function JsonObjectItem(P: PUtf8Char; PropName: PUtf8Char; PropNameLen: PtrInt; PropNameFound: PRawUtf8): PUtf8Char; overload; /// go to a property of a JSON object, by its full path, e.g. 'parent.child' // - implemented via a fast SAX-like approach: the input buffer is not changed, // nor no memory buffer allocated neither content copied // - PropPath is search case-insensitively as 'parent.child' or 'parent.ch*' // - returns nil if the supplied property path does not exist // - returns a pointer to the matching item value in the JSON object // - this will handle any kind of objects, including those with nested // JSON objects or arrays // - incoming P^ should point to the first initial '{' char function JsonObjectByPath(JsonObject, PropPath: PUtf8Char): PUtf8Char; /// return all matching properties of a JSON object // - here the PropPath could be a comma-separated list of case-insensitive full // paths, e.g. 'Prop1,Prop2' or 'Obj1.Obj2.Prop*,Obj1.Prop1' // - returns '' if no property did match // - returns a JSON object of all matching properties // - this will handle any kind of objects, including those with nested // JSON objects or arrays // - incoming P^ should point to the first initial '{' char function JsonObjectsByPath(JsonObject, PropPath: PUtf8Char): RawUtf8; /// convert one JSON object into two JSON arrays of keys and values // - i.e. makes the following transformation: // $ {key1:value1,key2,value2...} -> [key1,key2...] + [value1,value2...] // - this function won't allocate any memory during its process, nor // modify the JSON input buffer // - is the reverse of the TJsonWriter.AddJsonArraysAsJsonObject() method // - used e.g. by TSynDictionary.LoadFromJson // - returns the number of items parsed and stored into keys/values, -1 on // error parsing the input JSON buffer function JsonObjectAsJsonArrays(Json: PUtf8Char; out keys, values: RawUtf8): integer; /// remove comments and trailing commas from a text buffer before passing // it to a JSON parser // - handle two types of comments: starting from // till end of line // or /* ..... */ blocks anywhere in the text content // - trailing commas is replaced by ' ', so resulting JSON is valid for parsers // what not allows trailing commas (browsers for example) // - may be used to prepare configuration files before loading; // for example we store server configuration in file config.json and // put some comments in this file then code for loading is: // !var // ! cfg: RawUtf8; // ! ... // ! cfg := StringFromFile(Executable.ProgramFilePath + 'config.json'); // ! RemoveCommentsFromJson(@cfg[1]); // ! pLastChar := JsonToObject(obj, pointer(cfg), isvalid); procedure RemoveCommentsFromJson(P: PUtf8Char); overload; /// remove comments from a text buffer before passing it to JSON parser // - won't remove the comments in-place, but allocate a new string function RemoveCommentsFromJson(const s: RawUtf8): RawUtf8; overload; /// helper to retrieve the bit mapped integer value of a set from its JSON text // - Names and MaxValue should be retrieved from RTTI // - if supplied P^ is a JSON integer number, will read it directly // - if P^ maps some ["item1","item2"] content, would fill all matching bits // - if P^ contains ['*'], would fill all bits // - returns P=nil if reached prematurely the end of content, or returns // the value separator (e.g. , or }) in EndOfObject (like GetJsonField) function GetSetNameValue(Names: PShortString; MinValue, MaxValue: integer; var P: PUtf8Char; out EndOfObject: AnsiChar): QWord; overload; /// helper to retrieve the bit mapped integer value of a set from its JSON text // - overloaded function using the RTTI function GetSetNameValue(Info: PRttiInfo; var P: PUtf8Char; out EndOfObject: AnsiChar): QWord; overload; /// retrieve a pointer to JSON string field content, without unescaping it // - returns either ':' for name field, or } , for value field // - returns nil on JSON content error // - this function won't touch the JSON buffer, so you can call it before // using in-place escape process via JsonDecode() or TGetJsonField function JsonRetrieveStringField(P: PUtf8Char; out Field: PUtf8Char; out FieldLen: integer; ExpectNameField: boolean): PUtf8Char; {$ifdef HASINLINE}inline;{$endif} /// retrieve a class Rtti, as saved by ObjectToJson(...,[...,woStoreClassName,...]); // - JSON input should be either 'null', either '{"ClassName":"TMyClass",...}' // - calls IdemPropName/JsonRetrieveStringField so input buffer won't be // modified, but caller should ignore this "ClassName" property later on // - the corresponding class shall have been previously registered by // Rtti.RegisterClass(), in order to retrieve the class type from it name - // or, at least, by the RTL Classes.RegisterClass() function, if AndGlobalFindClass // parameter is left to default true so that RTL Classes.FindClass() is called function JsonRetrieveObjectRttiCustom(var Json: PUtf8Char; AndGlobalFindClass: boolean): TRttiCustom; /// encode a JSON object UTF-8 buffer into URI parameters // - you can specify property names to ignore during the object decoding // - you can omit the leading query delimiter ('?') by setting IncludeQueryDelimiter=false // - warning: the ParametersJson input buffer will be modified in-place function UrlEncodeJsonObject(const UriName: RawUtf8; ParametersJson: PUtf8Char; const PropNamesToIgnore: array of RawUtf8; IncludeQueryDelimiter: boolean = true): RawUtf8; overload; /// encode a JSON object UTF-8 buffer into URI parameters // - you can specify property names to ignore during the object decoding // - you can omit the leading query delimiter ('?') by setting IncludeQueryDelimiter=false // - overloaded function which will make a copy of the input JSON before parsing function UrlEncodeJsonObject(const UriName, ParametersJson: RawUtf8; const PropNamesToIgnore: array of RawUtf8; IncludeQueryDelimiter: boolean = true): RawUtf8; overload; /// formats and indents a JSON array or document to the specified layout // - just a wrapper around TJsonWriter.AddJsonReformat() method // - WARNING: the JSON buffer is decoded in-place, so P^ WILL BE modified procedure JsonBufferReformat(P: PUtf8Char; out result: RawUtf8; Format: TTextWriterJsonFormat = jsonHumanReadable); /// formats and indents a JSON array or document to the specified layout // - just a wrapper around TJsonWriter.AddJsonReformat, making a private // of the supplied JSON buffer (so that JSON content would stay untouched) function JsonReformat(const Json: RawUtf8; Format: TTextWriterJsonFormat = jsonHumanReadable): RawUtf8; /// formats and indents a JSON array or document as a file // - just a wrapper around TJsonWriter.AddJsonReformat() method // - WARNING: the JSON buffer is decoded in-place, so P^ WILL BE modified function JsonBufferReformatToFile(P: PUtf8Char; const Dest: TFileName; Format: TTextWriterJsonFormat = jsonHumanReadable): boolean; /// formats and indents a JSON array or document as a file // - just a wrapper around TJsonWriter.AddJsonReformat, making a private // of the supplied JSON buffer (so that JSON content would stay untouched) function JsonReformatToFile(const Json: RawUtf8; const Dest: TFileName; Format: TTextWriterJsonFormat = jsonHumanReadable): boolean; /// convert UTF-8 content into a JSON string // - with proper escaping of the content, and surounding " characters procedure QuotedStrJson(const aText: RawUtf8; var result: RawUtf8; const aPrefix: RawUtf8 = ''; const aSuffix: RawUtf8 = ''); overload; {$ifdef HASINLINE}inline;{$endif} /// convert UTF-8 buffer into a JSON string // - with proper escaping of the content, and surounding " characters procedure QuotedStrJson(P: PUtf8Char; PLen: PtrInt; var result: RawUtf8; const aPrefix: RawUtf8 = ''; const aSuffix: RawUtf8 = ''); overload; /// convert UTF-8 content into a JSON string // - with proper escaping of the content, and surounding " characters function QuotedStrJson(const aText: RawUtf8): RawUtf8; overload; {$ifdef HASINLINE}inline;{$endif} const FIELDCOUNT_PATTERN: PUtf8Char = '{"fieldCount":'; // PatternLen = 14 chars ROWCOUNT_PATTERN: PUtf8Char = ',"rowCount":'; // PatternLen = 12 chars VALUES_PATTERN: PUtf8Char = ',"values":['; // PatternLen = 11 chars /// quickly check if an UTF-8 buffer start with the supplied Pattern // - PatternLen is at least 8 bytes long, typically FIELDCOUNT_PATTERN, // ROWCOUNT_PATTERN or VALUES_PATTERN constants // - defined here for TDocVariantData.InitArrayFromResults function Expect(var P: PUtf8Char; Pattern: PUtf8Char; PatternLen: PtrInt): boolean; {$ifdef HASINLINE}inline;{$endif} /// parse JSON content in not-expanded format // - i.e. stored as // $ {"fieldCount":2,"values":["f1","f2","1v1",1v2,"2v1",2v2...],"rowCount":20} // - search and extract "fieldCount" and "rowCount" field information // - defined here for TDocVariantData.InitArrayFromResults function IsNotExpandedBuffer(var P: PUtf8Char; PEnd: PUtf8Char; var FieldCount, RowCount: PtrInt): boolean; /// efficient retrieval of the number of rows in non-expanded layout // - search for "rowCount": at the end of the JSON buffer function NotExpandedBufferRowCountPos(P, PEnd: PUtf8Char): PUtf8Char; /// low-level prepare GetFieldCountExpanded() parsing returning '{' or ']' function GotoFieldCountExpanded(P: PUtf8Char): PUtf8Char; /// low-level parsing of the first expanded JSON object to guess fields count function GetFieldCountExpanded(P: PUtf8Char): integer; /// fast Format() function replacement, handling % and ? parameters // - call rather FormatSql() and FormatJson() wrappers instead // - resulting string has no length limit and uses fast concatenation // - any supplied TObject instance will be written as their class name procedure FormatParams(const Format: RawUtf8; const Args, Params: array of const; JsonFormat: boolean; var Result: RawUtf8); /// fast Format() function replacement, handling % but also ? inlined parameters // - will include Args[] for every % in Format // - will include Params[] for every ? in Format, as "inlined" ORM or DB values, // e.g. :(1234): for numbers, and :('quoted '' string'): for text // - note that, due to a Delphi compiler limitation, cardinal values should be // type-casted to Int64() (otherwise the integer mapped value will be converted) // - is a wrapper around FormatParams(Format, Args, Params, false, result); function FormatSql(const Format: RawUtf8; const Args, Params: array of const): RawUtf8; /// fast Format() function replacement, handling % but also ? parameters as JSON // - will include Args[] for every % in Format // - will include Params[] for every ? in Format, as their JSON value, with // proper JSON double quotes and escaping for strings // - note that, due to a Delphi compiler limitation, cardinal values should be // type-casted to Int64() (otherwise the integer mapped value will be converted) // - is a wrapper around FormatParams(Format, Args, Params, true, result); function FormatJson(const Format: RawUtf8; const Args, Params: array of const): RawUtf8; {$ifndef PUREMORMOT2} // rather call FormatSql() and FormatJson() functions function FormatUtf8(const Format: RawUtf8; const Args, Params: array of const; JsonFormat: boolean = false): RawUtf8; overload; {$endif PUREMORMOT2} { ********** TJsonWriter class with proper JSON escaping and WriteObject() support } type /// JSON-capable TTextWriter/TTextDateWriter inherited class // - in addition to TTextWriter/TTextDateWriter, will handle JSON // serialization of any kind of value, including records, classes or arrays TJsonWriter = class(TTextDateWriter) protected // used by AddCRAndIndent for enums, sets and T*ObjArray comment of values fBlockComment: RawUtf8; // used by WriteObjectAsString/AddDynArrayJsonAsString methods fInternalJsonWriter: TJsonWriter; procedure InternalAddFixedAnsi(Source: PAnsiChar; SourceChars: cardinal; AnsiToWide: PWordArray; Escape: TTextWriterKind); // called after TRttiCustomProp.GetValueDirect/GetValueGetter procedure AddRttiVarData(const Value: TRttiVarData; Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions); public /// release all internal structures destructor Destroy; override; /// gives access to a temporary TJsonWriter // - returned instance is owned by this TJsonWriter, and voided // - may be used to escape some JSON espaced value (i.e. escape it twice), // in conjunction with AddJsonEscape(Source: TJsonWriter) function GetTempJsonWriter: TJsonWriter; /// append '[' or '{' with proper indentation procedure BlockBegin(Starter: AnsiChar; Options: TTextWriterWriteObjectOptions); {$ifdef HASINLINE}inline;{$endif} /// append ',' with proper indentation // - warning: this will break CancelLastComma, since CRLF+tabs are added procedure BlockAfterItem(Options: TTextWriterWriteObjectOptions); {$ifdef HASINLINE}inline;{$endif} /// append ']' or '}' with proper indentation procedure BlockEnd(Stopper: AnsiChar; Options: TTextWriterWriteObjectOptions); {$ifdef HASINLINE}inline;{$endif} /// used internally by WriteObject() when serializing a published property // - will call AddCRAndIndent then append "PropName": procedure WriteObjectPropNameHumanReadable(PropName: PUtf8Char; PropNameLen: PtrInt); /// used internally by WriteObject() when serializing a published property // - will call AddCRAndIndent then append "PropName": procedure WriteObjectPropNameShort(const PropName: ShortString; Options: TTextWriterWriteObjectOptions); {$ifdef HASINLINE}inline;{$endif} /// same as WriteObject(), but will double all internal " and bound with " // - this implementation will avoid most memory allocations procedure WriteObjectAsString(Value: TObject; Options: TTextWriterWriteObjectOptions = [woDontStoreDefault]); /// same as AddDynArrayJson(), but will double all internal " and bound with " // - this implementation will avoid most memory allocations procedure AddDynArrayJsonAsString(aTypeInfo: PRttiInfo; var aValue; WriteOptions: TTextWriterWriteObjectOptions = []); /// append a JSON field name, followed by an escaped UTF-8 JSON String and // a comma (',') procedure AddPropJsonString(const PropName: ShortString; const Text: RawUtf8); /// append CR+LF (#13#10) chars and #9 indentation // - will also flush any fBlockComment procedure AddCRAndIndent; override; /// write some #0 ended UTF-8 text, according to the specified format // - if Escape is a constant, consider calling directly AddNoJsonEscape, // AddJsonEscape or AddOnSameLine methods procedure Add(P: PUtf8Char; Escape: TTextWriterKind); override; /// write some #0 ended UTF-8 text, according to the specified format // - if Escape is a constant, consider calling directly AddNoJsonEscape, // AddJsonEscape or AddOnSameLine methods procedure Add(P: PUtf8Char; Len: PtrInt; Escape: TTextWriterKind); override; /// write some #0 ended Unicode text as UTF-8, according to the specified format // - if Escape is a constant, consider calling directly AddNoJsonEscapeW, // AddJsonEscapeW or AddOnSameLineW methods procedure AddW(P: PWord; Len: PtrInt; Escape: TTextWriterKind); {$ifdef HASINLINE}inline;{$endif} /// append some UTF-8 encoded chars to the buffer, from the main AnsiString type // - use the current system code page for AnsiString parameter procedure AddAnsiString(const s: AnsiString; Escape: TTextWriterKind); overload; /// append some UTF-8 encoded chars to the buffer, from any AnsiString value // - if CodePage is left to its default value of -1, it will assume // CurrentAnsiConvert.CodePage prior to Delphi 2009, but newer UNICODE // versions of Delphi will retrieve the code page from string // - if CodePage is defined to a >= 0 value, the encoding will take place procedure AddAnyAnsiString(const s: RawByteString; Escape: TTextWriterKind; CodePage: integer = -1); /// append some UTF-8 encoded chars to the buffer, from any Ansi buffer // - the codepage should be specified, e.g. CP_UTF8, CP_RAWBYTESTRING, // CP_WINANSI, or any version supported by the Operating System // - if codepage is 0, the current CurrentAnsiConvert.CodePage would be used // - will use TSynAnsiConvert to perform the conversion to UTF-8 procedure AddAnyAnsiBuffer(P: PAnsiChar; Len: PtrInt; Escape: TTextWriterKind; CodePage: integer); /// write some data Base64 encoded // - if withMagic is TRUE, will write as '"\uFFF0base64encodedbinary"' procedure WrBase64(P: PAnsiChar; Len: PtrUInt; withMagic: boolean); override; /// write some binary-saved data with Base64 encoding // - if withMagic is TRUE, will write as '"\uFFF0base64encodedbinary"' // - is a wrapper around BinarySave() and WrBase64() procedure BinarySaveBase64(Data: pointer; Info: PRttiInfo; Kinds: TRttiKinds; withMagic: boolean; withCrc: boolean = false); /// append some values at once // - text values (e.g. RawUtf8) will be escaped as JSON by default procedure Add(const Values: array of const); overload; /// append some values at once with custom escaping procedure Add(const Values: array of const; Escape: TTextWriterKind); overload; /// append an array of RawUtf8 as CSV of JSON strings procedure AddCsvUtf8(const Values: array of RawUtf8); /// append an array of const as CSV of JSON values procedure AddCsvConst(const Values: array of const); /// append a quoted string as JSON, with in-place decoding // - if QuotedString does not start with ' or ", it will written directly // (i.e. expects to be a number, or null/true/false constants) // - as used e.g. by TJsonObjectDecoder.EncodeAsJson method and // JsonEncodeNameSQLValue() function procedure AddQuotedStringAsJson(const QuotedString: RawUtf8); /// append strings or integers with a specified format // - this overriden version will properly handle JSON escape // - % = #37 marks a string, integer, floating-point, or class parameter // to be appended as text (e.g. class name) // - note that due to a limitation of the "array of const" format, cardinal // values should be type-casted to Int64() - otherwise the integer mapped // value will be transmitted, therefore wrongly procedure Add(const Format: RawUtf8; const Values: array of const; Escape: TTextWriterKind = twNone; WriteObjectOptions: TTextWriterWriteObjectOptions = [woFullExpand]); override; /// append a variant content as number or string // - this overriden version will properly handle JSON escape // - properly handle Value as a TRttiVarData from TRttiProp.GetValue procedure AddVariant(const Value: variant; Escape: TTextWriterKind = twJsonEscape; WriteOptions: TTextWriterWriteObjectOptions = []); override; /// append complex types as JSON content using raw TypeInfo() // - handle rkClass as WriteObject, rkEnumeration/rkSet with proper options, // rkRecord, rkDynArray or rkVariant using proper JSON serialization // - other types will append 'null' procedure AddTypedJson(Value, TypeInfo: pointer; WriteOptions: TTextWriterWriteObjectOptions = []); override; /// serialize as JSON the given object procedure WriteObject(Value: TObject; WriteOptions: TTextWriterWriteObjectOptions = [woDontStoreDefault]); override; /// append complex types as JSON content using TRttiCustom // - called e.g. by TJsonWriter.AddVariant() for varAny / TRttiVarData procedure AddRttiCustomJson(Value: pointer; RttiCustom: TObject; Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions); /// append a JSON value, array or document, in a specified format // - this overriden version will properly handle JSON escape function AddJsonReformat(Json: PUtf8Char; Format: TTextWriterJsonFormat; EndOfObject: PUtf8Char): PUtf8Char; override; /// append a JSON value, array or document as simple XML content // - you can use JsonBufferToXML() and JsonToXML() functions as wrappers // - this method is called recursively to handle all kind of JSON values // - WARNING: the JSON buffer is decoded in-place, so will be changed // - returns the end of the current JSON converted level, or nil if the // supplied content was not correct JSON function AddJsonToXML(Json: PUtf8Char; ArrayName: PUtf8Char = nil; EndOfObject: PUtf8Char = nil): PUtf8Char; /// append a record content as UTF-8 encoded JSON or custom serialization // - default serialization will use Base64 encoded binary stream, or // a custom serialization, in case of a previous registration via // TRttiJson.RegisterCustomSerializer() class method - from a dynamic array // handling this kind of records, or directly from TypeInfo() of the record // - by default, custom serializers defined via RegisterCustomSerializer() // would write enumerates and sets as integer numbers, unless // twoEnumSetsAsTextInRecord or twoEnumSetsAsBooleanInRecord is set in // the instance CustomOptions // - returns the element size function AddRecordJson(Value: pointer; RecordInfo: PRttiInfo; WriteOptions: TTextWriterWriteObjectOptions = []): PtrInt; /// append a void record content as UTF-8 encoded JSON or custom serialization // - this method will first create a void record (i.e. filled with #0 bytes) // then save its content with default or custom serialization procedure AddVoidRecordJson(RecordInfo: PRttiInfo; WriteOptions: TTextWriterWriteObjectOptions = []); /// append a dynamic array content as UTF-8 encoded JSON array // - typical content could be // ! '[1,2,3,4]' or '["\uFFF0base64encodedbinary"]' procedure AddDynArrayJson(var DynArray: TDynArray; WriteOptions: TTextWriterWriteObjectOptions = []); overload; /// append a dynamic array content as UTF-8 encoded JSON array // - expect a dynamic array TDynArrayHashed wrapper as incoming parameter procedure AddDynArrayJson(var DynArray: TDynArrayHashed; WriteOptions: TTextWriterWriteObjectOptions = []); overload; {$ifdef HASINLINE}inline;{$endif} /// append a dynamic array content as UTF-8 encoded JSON array // - returns the array element size function AddDynArrayJson(Value: pointer; Info: TRttiCustom; WriteOptions: TTextWriterWriteObjectOptions = []): PtrInt; overload; /// append UTF-8 content as text // - Text CodePage will be used (if possible) - assume RawUtf8 otherwise // - will properly handle JSON escape between two " double quotes procedure AddText(const Text: RawByteString; Escape: TTextWriterKind = twJsonEscape); /// append UTF-16 content as text // - P should be a #0 terminated PWideChar buffer // - will properly handle JSON escape between two " double quotes procedure AddTextW(P: PWord; Escape: TTextWriterKind = twJsonEscape); /// append some UTF-8 encoded chars to the buffer // - escapes chars according to the JSON RFC // - if Len is 0, writing will stop at #0 (default Len = 0 is slightly faster // than specifying Len>0 if you are sure P is zero-ended - e.g. from RawUtf8) procedure AddJsonEscape(P: Pointer; Len: PtrInt = 0); overload; /// append some Unicode encoded chars to the buffer // - if Len is 0, Len is calculated from zero-ended widechar // - escapes chars according to the JSON RFC procedure AddJsonEscapeW(P: PWord; Len: PtrInt = 0); /// append some UTF-8 encoded chars to the buffer, from a RTL string type // - faster than AddJsonEscape(pointer(StringToUtf8(string)) // - escapes chars according to the JSON RFC procedure AddJsonEscapeString(const s: string); {$ifdef HASINLINE}inline;{$endif} /// append some UTF-8 encoded chars to the buffer, from the main AnsiString type // - escapes chars according to the JSON RFC procedure AddJsonEscapeAnsiString(const s: AnsiString); /// append an open array constant value to the buffer // - "" will be added if necessary // - escapes chars according to the JSON RFC // - very fast (avoid most temporary storage) procedure AddJsonEscape(const V: TVarRec); overload; /// append a UTF-8 JSON string, JSON escaped between double quotes // - "" will always be added, before calling AddJsonEscape() procedure AddJsonString(const Text: RawUtf8); /// flush a supplied TJsonWriter, and write pending data as JSON escaped text // - may be used with InternalJsonWriter, as a faster alternative to // ! AddJsonEscape(Pointer(fInternalJsonWriter.Text),0); procedure AddJsonEscape(Source: TJsonWriter); overload; /// flush a supplied TJsonWriter, and write pending data as JSON escaped text // - may be used with InternalJsonWriter, as a faster alternative to // ! AddNoJsonEscapeUtf8(Source.Text); procedure AddNoJsonEscape(Source: TJsonWriter); overload; /// append a UTF-8 already encoded JSON buffer forcing Unicode escape // - don't escapes chars according to the JSON RFC but convert any 8-bit // UTF-8 values as their UTF-16 \u#### escaped content // - i.e. generate a pure ASCII output with no UTF-8 encoding involved // - used for jsonEscapeUnicode to follow python default json.dumps() layout procedure AddNoJsonEscapeForcedUnicode(P: PUtf8Char; Len: PtrInt); /// append a UTF-8 encoded JSON buffer without any \u#### Unicode escape // - i.e. \u#### patterns will be converted into pure UTF-8 output // - as used for jsonNoEscapeUnicode transformation procedure AddNoJsonEscapeForcedNoUnicode(P: PUtf8Char; Len: PtrInt); /// append an open array constant value to the buffer // - "" won't be added for string values // - string values may be escaped, depending on the supplied parameter // - very fast (avoid most temporary storage) procedure Add(const V: TVarRec; Escape: TTextWriterKind = twNone; WriteObjectOptions: TTextWriterWriteObjectOptions = [woFullExpand]); overload; /// encode the supplied data as an UTF-8 valid JSON object content // - data must be supplied two by two, as Name,Value pairs, e.g. // ! aWriter.AddJsonEscape(['name','John','year',1972]); // will append to the buffer: // ! '{"name":"John","year":1972}' // - or you can specify nested arrays or objects with '['..']' or '{'..'}': // ! aWriter.AddJsonEscape(['doc','{','name','John','ab','[','a','b']','}','id',123]); // will append to the buffer: // ! '{"doc":{"name":"John","abc":["a","b"]},"id":123}' // - note that, due to a Delphi compiler limitation, cardinal values should be // type-casted to Int64() (otherwise the integer mapped value will be converted) // - you can pass nil as parameter for a null JSON value procedure AddJsonEscape( const NameValuePairs: array of const); overload; /// encode the supplied (extended) JSON content, with parameters, // as an UTF-8 valid JSON object content // - in addition to the JSON RFC specification strict mode, this method will // handle some BSON-like extensions, e.g. unquoted field names: // ! aWriter.AddJson('{id:?,%:{name:?,birthyear:?}}',['doc'],[10,'John',1982]); // - you can use nested _Obj() / _Arr() instances // ! aWriter.AddJson('{%:{$in:[?,?]}}',['type'],['food','snack']); // ! aWriter.AddJson('{type:{$in:?}}',[],[_Arr(['food','snack'])]); // ! // which are the same as: // ! aWriter.AddShort('{"type":{"$in":["food","snack"]}}'); // - if the mormot.db.nosql.bson unit is used in the application, the MongoDB // Shell syntax will also be recognized to create TBsonVariant, like // ! new Date() ObjectId() MinKey MaxKey // // see @http://docs.mongodb.org/manual/reference/mongodb-extended-json // ! aWriter.AddJson('{name:?,field:/%/i}',['acme.*corp'],['John'])) // ! // will write // ! '{"name":"John","field":{"$regex":"acme.*corp","$options":"i"}}' // - will call internally _JsonFastFmt() to create a temporary TDocVariant // with all its features - so is slightly slower than other AddJson* methods procedure AddJson(const Format: RawUtf8; const Args, Params: array of const); /// append two JSON arrays of keys and values as one JSON object // - i.e. makes the following transformation: // $ [key1,key2...] + [value1,value2...] -> {key1:value1,key2,value2...} // - this method won't allocate any memory during its process, nor // modify the keys and values input buffers // - is the reverse of the JsonObjectAsJsonArrays() function // - used e.g. by TSynDictionary.SaveToJson procedure AddJsonArraysAsJsonObject(keys, values: PUtf8Char); end; { ************ JSON-aware TSynNameValue TSynPersistentStoreJson } type /// store one Name/Value pair, as used by TSynNameValue class TSynNameValueItem = record /// the name of the Name/Value pair // - this property is hashed by TSynNameValue for fast retrieval Name: RawUtf8; /// the value of the Name/Value pair Value: RawUtf8; /// any associated Pointer or numerical value Tag: PtrInt; end; /// Name/Value pairs storage, as used by TSynNameValue class TSynNameValueItemDynArray = array of TSynNameValueItem; /// event handler used to convert on the fly some UTF-8 text content TOnSynNameValueConvertRawUtf8 = function( const text: RawUtf8): RawUtf8 of object; /// callback event used by TSynNameValue TOnSynNameValueNotify = procedure( const Item: TSynNameValueItem; Index: PtrInt) of object; /// pseudo-class used to store Name/Value RawUtf8 pairs // - use internally a TDynArrayHashed instance for fast retrieval // - is therefore faster than TRawUtf8List // - is defined as an object, not as a class: you can use this in any // class, without the need to destroy the content // - Delphi "object" is buggy on stack -> also defined as record with methods {$ifdef USERECORDWITHMETHODS} TSynNameValue = record private {$else} TSynNameValue = object protected {$endif USERECORDWITHMETHODS} fOnAdd: TOnSynNameValueNotify; function GetBlobData: RawByteString; procedure SetBlobData(const aValue: RawByteString); function GetStr(const aName: RawUtf8): RawUtf8; {$ifdef HASINLINE}inline;{$endif} function GetInt(const aName: RawUtf8): Int64; {$ifdef HASINLINE}inline;{$endif} public /// the internal Name/Value storage List: TSynNameValueItemDynArray; /// the number of Name/Value pairs Count: integer; /// low-level access to the internal storage hasher DynArray: TDynArrayHashed; /// initialize the storage // - will also reset the internal List[] and the internal hash array procedure Init(aCaseSensitive: boolean); /// add an element to the array // - if aName already exists, its associated Value will be updated procedure Add(const aName, aValue: RawUtf8; aTag: PtrInt = 0); /// reset content, then add all name=value pairs from a supplied .ini file // section content // - will first call Init(false) to initialize the internal array // - Section can be retrieved e.g. via FindSectionFirstLine() procedure InitFromIniSection(Section: PUtf8Char; const OnTheFlyConvert: TOnSynNameValueConvertRawUtf8 = nil; const OnAdd: TOnSynNameValueNotify = nil); /// reset content, then add all name=value; CSV pairs // - will first call Init(false) to initialize the internal array // - if ItemSep=#10, then any kind of line feed (CRLF or LF) will be handled procedure InitFromCsv(Csv: PUtf8Char; NameValueSep: AnsiChar = '='; ItemSep: AnsiChar = #10); /// reset content, then add all fields from an JSON object // - will first call Init() to initialize the internal array // - then parse the incoming JSON object, storing all its field values // as RawUtf8, and returning TRUE if the supplied content is correct // - warning: the supplied JSON buffer will be decoded and modified in-place function InitFromJson(Json: PUtf8Char; aCaseSensitive: boolean = false): boolean; /// reset content, then add all name, value pairs // - will first call Init(false) to initialize the internal array procedure InitFromNamesValues(const Names, Values: array of RawUtf8); /// search for a Name, return the index in List // - using fast O(1) hash algoritm function Find(const aName: RawUtf8): PtrInt; /// search for the first chars of a Name, return the index in List // - using O(n) calls of IdemPChar() function // - here aUpperName should be already uppercase, as expected by IdemPChar() function FindStart(const aUpperName: RawUtf8): PtrInt; /// search for a Value, return the index in List // - using O(n) brute force algoritm with case-sensitive aValue search function FindByValue(const aValue: RawUtf8): PtrInt; /// search for a Name, and delete its entry in the List if it exists function Delete(const aName: RawUtf8): boolean; /// search for a Value, and delete its entry in the List if it exists // - returns the number of deleted entries // - you may search for more than one match, by setting a >1 Limit value function DeleteByValue(const aValue: RawUtf8; Limit: integer = 1): integer; /// search for a Name, return the associated Value as a UTF-8 string function Value(const aName: RawUtf8; const aDefaultValue: RawUtf8 = ''): RawUtf8; /// search for a Name, return the associated Value as integer function ValueInt(const aName: RawUtf8; const aDefaultValue: Int64 = 0): Int64; /// search for a Name, return the associated Value as boolean // - returns true only if the value is exactly '1' / 'true' function ValueBool(const aName: RawUtf8): boolean; {$ifdef HASINLINE}inline;{$endif} /// search for a Name, return the associated Value as an enumerate // - returns true and set aEnum if aName was found, and associated value // matched an aEnumTypeInfo item // - returns false if no match was found function ValueEnum(const aName: RawUtf8; aEnumTypeInfo: PRttiInfo; out aEnum; aEnumDefault: PtrUInt = 0): boolean; overload; /// returns all values, as CSV or INI content function AsCsv(const KeySeparator: RawUtf8 = '='; const ValueSeparator: RawUtf8 = #13#10; const IgnoreKey: RawUtf8 = ''): RawUtf8; /// returns all values as a JSON object of string fields function AsJson: RawUtf8; /// fill the supplied two arrays of RawUtf8 with the stored values procedure AsNameValues(out Names, Values: TRawUtf8DynArray); /// search for a Name, return the associated Value as variant // - returns null if the name was not found function ValueVariantOrNull(const aName: RawUtf8): variant; /// compute a TDocVariant document from the stored values // - output variant will be reset and filled as a TDocVariant instance, // ready to be serialized as a JSON object // - if there is no value stored (i.e. Count=0), set null procedure AsDocVariant(out DocVariant: variant; ExtendedJson: boolean = false; ValueAsString: boolean = true; AllowVarDouble: boolean = false); overload; /// compute a TDocVariant document from the stored values function AsDocVariant(ExtendedJson: boolean = false; ValueAsString: boolean = true): variant; overload; {$ifdef HASINLINE}inline;{$endif} /// merge the stored values into a TDocVariant document // - existing properties would be updated, then new values will be added to // the supplied TDocVariant instance, ready to be serialized as a JSON object // - if ValueAsString is TRUE, values would be stored as string // - if ValueAsString is FALSE, numerical values would be identified by // IsString() and stored as such in the resulting TDocVariant // - if you let ChangedProps point to a TDocVariantData, it would contain // an object with the stored values, just like AsDocVariant // - returns the number of updated values in the TDocVariant, 0 if // no value was changed function MergeDocVariant(var DocVariant: variant; ValueAsString: boolean; ChangedProps: PVariant = nil; ExtendedJson: boolean = false; AllowVarDouble: boolean = false): integer; /// returns true if the Init() method has been called function Initialized: boolean; /// can be used to set all data from one BLOB memory buffer procedure SetBlobDataPtr(aValue, aValueMax: pointer); /// can be used to set or retrieve all stored data as one BLOB content property BlobData: RawByteString read GetBlobData write SetBlobData; /// event triggerred after an item has just been added to the list property OnAfterAdd: TOnSynNameValueNotify read fOnAdd write fOnAdd; /// search for a Name, return the associated Value as a UTF-8 string // - returns '' if aName is not found in the stored keys property Str[const aName: RawUtf8]: RawUtf8 read GetStr; default; /// search for a Name, return the associated Value as integer // - returns 0 if aName is not found, or not a valid Int64 in the stored keys property Int[const aName: RawUtf8]: Int64 read GetInt; /// search for a Name, return the associated Value as boolean // - returns true if aName stores '1' / 'true' as associated value property Bool[const aName: RawUtf8]: boolean read ValueBool; end; /// a reference pointer to a Name/Value RawUtf8 pairs storage PSynNameValue = ^TSynNameValue; /// implement a cache of some key/value pairs, e.g. to improve reading speed // - used e.g. by TSqlDataBase for caching the SELECT statements results in an // internal JSON format (which is faster than a query to the SQLite3 engine) // - internally make use of an efficient hashing algorithm for fast response // (i.e. TSynNameValue will use the TDynArrayHashed wrapper mechanism) TSynCache = class(TSynPersistent) protected fNameValue: TSynNameValue; fRamUsed: cardinal; fMaxRamUsed: cardinal; fTimeoutSeconds: cardinal; fTimeoutTix: cardinal; fSafe: TRWLock; // writes should be reentrant for Reset procedure ResetIfNeeded; // call Reset after TimeoutSeconds public /// initialize the internal storage // - aMaxCacheRamUsed can set the maximum RAM to be used for values, in bytes // (default is 16 MB), after which the cache is flushed // - by default, key search is done case-insensitively, but you can specify // another option here // - by default, there is no timeout period, but you may specify a number of // seconds of inactivity (i.e. no Add call) after which the cache is flushed constructor Create(aMaxCacheRamUsed: cardinal = 16 shl 20; aCaseSensitive: boolean = false; aTimeoutSeconds: cardinal = 0); reintroduce; /// find a Key in the cache entries // - return '' if nothing found: you may call Add() just after to insert // the expected value in the cache // - return the associated Value otherwise, and the associated integer tag // if aResultTag address is supplied // - this method is thread-safe, using the Safe R/W locker of this instance function Find(const aKey: RawUtf8; aResultTag: PPtrInt = nil): RawUtf8; /// add a Key/Value pair in the cache entries // - returns true if aKey was not existing yet, and aValue has been stored // - returns false if aKey did already exist in the internal cache, and // its entry has been updated with the supplied aValue/aTag // - this method is thread-safe, using the Safe R/W locker of this instance function AddOrUpdate(const aKey, aValue: RawUtf8; aTag: PtrInt): boolean; /// called after a write access to the database to flush the cache // - set Count to 0 // - release all cache memory // - returns TRUE if was flushed, i.e. if there was something in cache // - this method is thread-safe, using the Safe R/W locker of this instance function Reset: boolean; /// access to the internal R/W locker, for thread-safe process // - Find/AddOrUpdate methods are protected by this R/W lock property Safe: TRWLock read fSafe; published /// number of entries in the cache property Count: integer read fNameValue.Count; /// the current global size of Values in RAM cache, in bytes property RamUsed: cardinal read fRamUsed; /// the maximum RAM to be used for values, in bytes // - the cache is flushed when ValueSize reaches this limit // - default is 16 MB (16 shl 20) property MaxRamUsed: cardinal read fMaxRamUsed; /// after how many seconds betwen Add() calls the cache should be flushed // - equals 0 by default, meaning no time out property TimeoutSeconds: cardinal read fTimeoutSeconds; end; type /// implement binary persistence and JSON serialization (not deserialization) TSynPersistentStoreJson = class(TSynPersistentStore) protected // append "name" -> inherited should add properties to the JSON object procedure AddJson(W: TJsonWriter); virtual; public /// serialize this instance as a JSON object function SaveToJson(reformat: TTextWriterJsonFormat = jsonCompact): RawUtf8; end; { *********** JSON-aware TSynDictionary Storage } type /// exception raised during TSynDictionary process ESynDictionary = class(ESynException); // internal flag, used only by TSynDictionary.InArray protected method TSynDictionaryInArray = ( iaFind, iaFindAndDelete, iaFindAndUpdate, iaFindAndAddIfNotExisting, iaAdd, iaAddForced); /// event called by TSynDictionary.ForEach methods to iterate over stored items // - if the implementation method returns TRUE, will continue the loop // - if the implementation method returns FALSE, will stop values browsing // - aOpaque is a custom value specified at ForEach() method call TOnSynDictionary = function(const aKey; var aValue; aIndex, aCount: integer; aOpaque: pointer): boolean of object; /// event called by TSynDictionary.DeleteDeprecated // - called just before deletion: return false to by-pass this item TOnSynDictionaryCanDelete = function(const aKey, aValue; aIndex: integer): boolean of object; /// thread-safe dictionary to store some values from associated keys // - will maintain a dynamic array of values, associated with a hash table // for the keys, so that setting or retrieving values would be O(1) // - thread-safe by default, since most methods are protected by a TSynLocker; // use the ThreadUse option to tune thread-safety (e.g. disable or use a TRWLock) // - TDynArray is a wrapper which does not store anything, whereas this class // actually stores both keys and values, and provide convenient methods to // access the stored data, including JSON serialization and binary storage // - consider IKeyValue<> from mormot.core.collections.pas, for more robust // generics-based code where TKey/TValue are propagated to all methods TSynDictionary = class(TSynLocked) protected fKeys: TDynArrayHashed; fValues: TDynArray; fTimeOut: TCardinalDynArray; fTimeOuts: TDynArray; fCompressAlgo: TAlgoCompress; fOnCanDelete: TOnSynDictionaryCanDelete; function InternalAddUpdate(aKey, aValue: pointer; aUpdate: boolean): PtrInt; function InArray(const aKey, aArrayValue; aAction: TSynDictionaryInArray; aCompare: TDynArraySortCompare): boolean; procedure SetTimeouts; function ComputeNextTimeOut: cardinal; {$ifdef HASINLINE} inline; {$endif} function GetCapacity: integer; procedure SetCapacity(const Value: integer); function GetTimeOutSeconds: cardinal; {$ifdef HASINLINE} inline; {$endif} procedure SetTimeOutSeconds(Value: cardinal); function GetThreadUse: TSynLockerUse; {$ifdef HASINLINE} inline; {$endif} procedure SetThreadUse(const Value: TSynLockerUse); {$ifdef HASINLINE} inline; {$endif} public /// initialize the dictionary storage, specifying dynamic array keys/values // - aKeyTypeInfo should be a dynamic array TypeInfo() RTTI pointer, which // would store the keys within this TSynDictionary instance // - aValueTypeInfo should be a dynamic array TypeInfo() RTTI pointer, which // would store the values within this TSynDictionary instance // - by default, string keys would be searched following exact case, unless // aKeyCaseInsensitive is TRUE // - you can set an optional timeout period, in seconds - you should call // DeleteDeprecated periodically to search for deprecated items constructor Create(aKeyTypeInfo, aValueTypeInfo: PRttiInfo; aKeyCaseInsensitive: boolean = false; aTimeoutSeconds: cardinal = 0; aCompressAlgo: TAlgoCompress = nil; aHasher: THasher = nil); reintroduce; virtual; {$ifdef HASGENERICS} /// initialize the dictionary storage, specifying keys/values as generic types // - just a convenient wrapper around TSynDictionary.Create() // - consider IKeyValue<> from mormot.core.collections.pas, for more robust // generics-based code where TKey/TValue are propagated to all methods class function New(aKeyCaseInsensitive: boolean = false; aTimeoutSeconds: cardinal = 0; aCompressAlgo: TAlgoCompress = nil; aHasher: THasher = nil): TSynDictionary; static; {$ifdef FPC} inline; {$endif} {$endif HASGENERICS} /// finalize the storage // - would release all internal stored values destructor Destroy; override; /// try to add a value associated with a primary key // - returns the index of the inserted item, -1 if aKey is already existing // - this method is thread-safe, since it will lock the instance function Add(const aKey, aValue): PtrInt; /// store a value associated with a primary key // - returns the index of the matching item // - if aKey does not exist, a new entry is added // - if aKey does exist, the existing entry is overriden with aValue // - this method is thread-safe, since it will lock the instance function AddOrUpdate(const aKey, aValue): PtrInt; /// clear the value associated via aKey // - does not delete the entry, but reset its value // - returns the index of the matching item, -1 if aKey was not found // - this method is thread-safe, since it will lock the instance function Clear(const aKey): PtrInt; /// delete all key/value stored in the current instance procedure DeleteAll; /// delete a key/value association from its supplied aKey // - this would delete the entry, i.e. matching key and value pair // - returns the index of the deleted item, -1 if aKey was not found // - this method is thread-safe, since it will lock the instance function Delete(const aKey): PtrInt; /// delete a key/value association from its internal index // - this method is not thread-safe: you should use fSafe.Lock/Unlock // e.g. then Find/FindValue to retrieve the index value function DeleteAt(aIndex: PtrInt): boolean; /// search and delete all deprecated items according to TimeoutSeconds // - returns how many items have been deleted // - you can call this method very often: it will ensure that the // search process will take place at most once every second // - this method is thread-safe, but blocking during the process function DeleteDeprecated(tix64: Int64 = 0): integer; /// search of a primary key within the internal hashed dictionary // - returns the index of the matching item, -1 if aKey was not found // - if you want to access the value, you should use fSafe.Lock/Unlock: // consider using Exists or FindAndCopy thread-safe methods instead // - aUpdateTimeOut will update the associated timeout value of the entry function Find(const aKey; aUpdateTimeOut: boolean = false): PtrInt; /// search of a primary key within the internal hashed dictionary // - returns a pointer to the matching item, nil if aKey was not found // - if you want to access the value, you should use fSafe.Lock/Unlock: // consider using Exists or FindAndCopy thread-safe methods instead // - aUpdateTimeOut will update the associated timeout value of the entry function FindValue(const aKey; aUpdateTimeOut: boolean = false; aIndex: PPtrInt = nil): pointer; /// search of a primary key within the internal hashed dictionary // - returns a pointer to the matching or already existing value item // - if you want to access the value, you should use fSafe.Lock/Unlock: // consider using Exists or FindAndCopy thread-safe methods instead // - will update the associated timeout value of the entry, if applying function FindValueOrAdd(const aKey; var added: boolean; aIndex: PPtrInt = nil): pointer; /// search of a stored value by its primary key, and return a local copy // - so this method is thread-safe // - returns TRUE if aKey was found, FALSE if no match exists // - will update the associated timeout value of the entry, unless // aUpdateTimeOut is set to false function FindAndCopy(const aKey; var aValue; aUpdateTimeOut: boolean = true): boolean; /// search of a stored value by its primary key, then delete and return it // - returns TRUE if aKey was found, fill aValue with its content, // and delete the entry in the internal storage // - so this method is thread-safe // - returns FALSE if no match exists function FindAndExtract(const aKey; var aValue): boolean; /// search of a stored value, and return seconds since its timeout was set // - returns -1 if aKey was not found // - this method is thread-safe function FindAndGetElapsedSeconds(const aKey): integer; /// search of a stored value, and delete it if its timeout was set too long ago // - returns true if aKey was found and deleted // - returns false if aKey was not found or the entry not deprecated // - this method is thread-safe function FindAndDeleteDeprecated(const aKey; aSeconds: integer): boolean; /// search for a primary key presence // - returns TRUE if aKey was found, FALSE if no match exists // - this method is thread-safe function Exists(const aKey): boolean; /// search for a value presence // - returns TRUE if aValue was found, FALSE if no match exists // - this method is thread-safe, but will use O(n) slow browsing function ExistsValue(const aValue; aCompare: TDynArraySortCompare = nil): boolean; /// apply a specified event over all items stored in this dictionnary // - would browse the list in the adding order // - returns the number of times OnEach has been called // - this method is thread-safe - if the callback modifies the data, set // MayModify=true and call fSafe.Lock/UnLock when writing function ForEach(const OnEach: TOnSynDictionary; Opaque: pointer = nil; MayModify: boolean = true): integer; overload; /// apply a specified event over matching items stored in this dictionnary // - would browse the list in the adding order, comparing each key and/or // value item with the supplied comparison functions and aKey/aValue content // - returns the number of times OnMatch has been called, i.e. how many times // KeyCompare(aKey,Keys[#])=0 or ValueCompare(aValue,Values[#])=0 // - this method is thread-safe - if the callback modifies the data, set // MayModify=true and call fSafe.Lock/UnLock when writing function ForEach(const OnMatch: TOnSynDictionary; KeyCompare, ValueCompare: TDynArraySortCompare; const aKey, aValue; Opaque: pointer = nil; MayModify: boolean = true): integer; overload; /// touch the entry timeout field so that it won't be deprecated sooner // - this method is not thread-safe, and is expected to be executed e.g. // from a ForEach() TOnSynDictionary callback procedure SetTimeoutAtIndex(aIndex: PtrInt); /// search aArrayValue item in a dynamic-array value associated via aKey // - expect the stored value to be a dynamic array itself // - would search for aKey as primary key, then use TDynArray.Find // to delete any aArrayValue match in the associated dynamic array // - returns FALSE if Values is not a tkDynArray, or if aKey or aArrayValue // were not found // - this method is thread-safe, since it will lock the instance function FindInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare = nil): boolean; /// search of a stored key by its associated key, and return a key local copy // - won't use any hashed index but RTTI TDynArray.IndexOf search over // over fValues() so is much slower than FindAndCopy() for huge arrays // - will update the associated timeout value of the entry, unless // aUpdateTimeOut is set to false // - this method is thread-safe // - returns TRUE if aValue was found, FALSE if no match exists function FindKeyFromValue(const aValue; out aKey; aUpdateTimeOut: boolean = true): boolean; /// add aArrayValue item within a dynamic-array value associated via aKey // - expect the stored value to be a dynamic array itself // - would search for aKey as primary key, then use TDynArray.Add // to add aArrayValue to the associated dynamic array // - returns FALSE if Values is not a tkDynArray, or if aKey was not found // - this method is thread-safe, since it will lock the instance function AddInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare = nil): boolean; /// add aArrayValue item within a dynamic-array value associated via aKey // - expect the stored value to be a dynamic array itself // - would search for aKey as primary key, create the entry if not found, // then use TDynArray.Add to add aArrayValue to the associated dynamic array // - returns FALSE if Values is not a tkDynArray // - this method is thread-safe, since it will lock the instance function AddInArrayForced(const aKey, aArrayValue; aCompare: TDynArraySortCompare = nil): boolean; /// add once aArrayValue within a dynamic-array value associated via aKey // - expect the stored value to be a dynamic array itself // - would search for aKey as primary key, then use // TDynArray.FindAndAddIfNotExisting to add once aArrayValue to the // associated dynamic array // - returns FALSE if Values is not a tkDynArray, or if aKey was not found // - this method is thread-safe, since it will lock the instance function AddOnceInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare = nil): boolean; /// clear aArrayValue item of a dynamic-array value associated via aKey // - expect the stored value to be a dynamic array itself // - would search for aKey as primary key, then use TDynArray.FindAndDelete // to delete any aArrayValue match in the associated dynamic array // - returns FALSE if Values is not a tkDynArray, or if aKey or aArrayValue // were not found // - this method is thread-safe, since it will lock the instance function DeleteInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare = nil): boolean; /// replace aArrayValue item of a dynamic-array value associated via aKey // - expect the stored value to be a dynamic array itself // - would search for aKey as primary key, then use TDynArray.FindAndUpdate // to delete any aArrayValue match in the associated dynamic array // - returns FALSE if Values is not a tkDynArray, or if aKey or aArrayValue were // not found // - this method is thread-safe, since it will lock the instance function UpdateInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare = nil): boolean; /// make a copy of the stored values // - this method is thread-safe, since it will lock the instance during copy // - resulting length(Dest) will match the exact values count // - T*ObjArray will be reallocated and copied by content (using a temporary // JSON serialization), unless ObjArrayByRef is true and pointers are copied procedure CopyValues(out Dest; ObjArrayByRef: boolean = false); /// serialize the content as a "key":value JSON object procedure SaveToJson(W: TJsonWriter; EnumSetsAsText: boolean = false); overload; /// serialize the content as a "key":value JSON object function SaveToJson(EnumSetsAsText: boolean = false): RawUtf8; overload; /// serialize the Values[] as a JSON array function SaveValuesToJson(EnumSetsAsText: boolean = false; ReFormat: TTextWriterJsonFormat = jsonCompact): RawUtf8; /// unserialize the content from "key":value JSON object // - if the JSON input may not be correct (i.e. if not coming from SaveToJson), // you may set EnsureNoKeyCollision=TRUE for a slow but safe keys validation function LoadFromJson(const Json: RawUtf8; CustomVariantOptions: PDocVariantOptions = nil): boolean; overload; /// unserialize the content from "key":value JSON object // - note that input JSON buffer is not modified in place: no need to create // a temporary copy if the buffer is about to be re-used function LoadFromJson(Json: PUtf8Char; CustomVariantOptions: PDocVariantOptions = nil): boolean; overload; /// save the content as SynLZ-compressed raw binary data // - warning: this format is tied to the values low-level RTTI, so if you // change the value/key type definitions, LoadFromBinary() would fail function SaveToBinary(NoCompression: boolean = false; Algo: TAlgoCompress = nil): RawByteString; /// load the content from SynLZ-compressed raw binary data // - as previously saved by SaveToBinary method function LoadFromBinary(const binary: RawByteString): boolean; /// can be assigned to OnCanDeleteDeprecated to check // TSynPersistentLock(aValue).Safe.IsLocked before actual deletion class function OnCanDeleteSynPersistentLock( const aKey, aValue; aIndex: PtrInt): boolean; {$ifndef PUREMORMOT2} class function OnCanDeleteSynPersistentLocked( const aKey, aValue; aIndex: PtrInt): boolean; {$endif PUREMORMOT2} /// returns how many items are currently stored in this dictionary // - this method is NOT thread-safe so should be protected by fSafe.Lock/UnLock function Count: integer; {$ifdef HASINLINE}inline;{$endif} /// direct access to the primary key identifiers // - if you want to access the keys, you should use fSafe.Lock/Unlock property Keys: TDynArrayHashed read fKeys; /// direct access to the associated stored values // - if you want to access the values, you should use fSafe.Lock/Unlock property Values: TDynArray read fValues; /// defines how many items are currently stored in Keys/Values internal arrays // - if you set a maximum size of this store (even a rough size), Add() are // likely to be up to twice faster than letting the table grow by chunks property Capacity: integer read GetCapacity write SetCapacity; /// direct low-level access to the internal access tick (GetTickCount64 shr 10) // - may be nil if TimeOutSeconds=0 property TimeOut: TCardinalDynArray read fTimeOut; /// returns the aTimeOutSeconds parameter value, as specified to Create() // - warning: setting a new timeout will clear all previous content property TimeOutSeconds: cardinal read GetTimeOutSeconds write SetTimeOutSeconds; /// the compression algorithm used for binary serialization property CompressAlgo: TAlgoCompress read fCompressAlgo write fCompressAlgo; /// callback to by-pass DeleteDeprecated deletion by returning false // - can be assigned e.g. to OnCanDeleteSynPersistentLock if Value is a // TSynPersistentLock instance, to avoid any potential access violation property OnCanDeleteDeprecated: TOnSynDictionaryCanDelete read fOnCanDelete write fOnCanDelete; /// can tune TSynDictionary threading process depending on your use case // - will redirect to the internal Safe TSynLocker instance // - warning: to be set only before any process is done // - advice: any performance impact should always be monitored, not guessed property ThreadUse: TSynLockerUse read GetThreadUse write SetThreadUse; end; const // TSynDictionary.fSafe.Padding[DIC_*] place holders - defined here for inlining DIC_KEYCOUNT = 0; // Keys.Count integer DIC_KEY = 1; // Key.Value pointer DIC_VALUECOUNT = 2; // Values.Count integer DIC_VALUE = 3; // Values.Value pointer DIC_TIMECOUNT = 4; // Timeouts.Count integer DIC_TIMESEC = 5; // Timeouts Seconds integer DIC_TIMETIX = 6; // last GetTickCount64 shr 10 integer { ********** Low-level JSON Serialization for any kind of Values } type /// internal stack-allocated structure for nested JSON serialization // - defined here for low-level use within TRttiJsonSave functions {$ifdef USERECORDWITHMETHODS} TJsonSaveContext = record {$else} TJsonSaveContext = object {$endif USERECORDWITHMETHODS} public /// the associated stream writer for the JSON output W: TJsonWriter; /// serialization options as specified for this process // - as used by AddShort/Add64/AddDateTime methods Options: TTextWriterWriteObjectOptions; /// the RTTI information of the current serialized type Info: TRttiCustom; /// the RTTI information of the current serialized property // - is likely to be nil outside of properties serialization Prop: PRttiCustomProp; /// initialize this low-level JSON serialization context procedure Init(WR: TJsonWriter; WriteOptions: TTextWriterWriteObjectOptions; Rtti: TRttiCustom); {$ifdef HASINLINE}inline;{$endif} /// some basic function to append a shorstring JSON value according to Options procedure AddShort(PS: PShortString); /// some basic function to append an Int64 JSON value according to Options procedure Add64(Value: PInt64; UnSigned: boolean); {$ifdef HASINLINE}inline;{$endif} /// some basic function to append a TDateTime JSON value according to Options procedure AddDateTime(Value: PDateTime; WithMS: boolean); /// some basic function to append a "name":boolean JSON pair value procedure AddShortBoolean(PS: PShortString; Value: boolean); end; /// internal function handler for JSON persistence of any TRttiParserType value // - i.e. the kind of functions called via PT_JSONSAVE[] lookup table TRttiJsonSave = procedure(Data: pointer; const Ctxt: TJsonSaveContext); { ********** Low-level JSON Unserialization for any kind of Values } type /// store one name/value pair of raw UTF-8 content, from a JSON buffer // - used e.g. by JsonDecode() overloaded function or UrlEncodeJsonObject() // to returns names/values TNameValuePUtf8Char = record /// pointer and length to the actual UTF-8 name text Name: TValuePUtf8Char; /// pointer and length to the actual UTF-8 value text Value: TValuePUtf8Char; end; /// used e.g. by JsonDecode() overloaded function to returns name/value pairs TNameValuePUtf8CharDynArray = array of TNameValuePUtf8Char; /// decode the supplied UTF-8 JSON content for the supplied names // - data will be set in Values, according to the Names supplied e.g. // ! JsonDecode(JSON,['name','year'],@Values) -> Values[0].Value='John'; Values[1].Value='1972'; // - if any supplied name wasn't found its corresponding Values[] will be nil // - this procedure will decode the JSON content in-memory, i.e. the PUtf8Char // array is created inside JSON, which is therefore modified: make a private // copy first if you want to reuse the JSON content // - if HandleValuesAsObjectOrArray is TRUE, then this procedure will handle // JSON arrays or objects // - support enhanced JSON syntax, e.g. '{name:'"John",year:1972}' is decoded // just like '{"name":'"John","year":1972}' procedure JsonDecode(var Json: RawUtf8; const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean = false); overload; /// decode the supplied UTF-8 JSON content for the supplied names // - an overloaded function when the JSON is supplied as a RawJson variable procedure JsonDecode(var Json: RawJson; const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean = false); overload; /// decode the supplied UTF-8 JSON object for the supplied field names // - data will be set in Values, according to the Names supplied e.g. // ! JsonDecode(P,['name','year'],Values) -> Values[0]^='John'; Values[1]^='1972'; // - if any supplied name wasn't found its corresponding Values[] will be nil // - this procedure will decode the JSON content in-memory, i.e. the PUtf8Char // array is created inside P, which is therefore modified: make a private // copy first if you want to reuse the JSON content // - if HandleValuesAsObjectOrArray is TRUE, then this procedure will handle // JSON arrays or objects // - if ValuesLen is set, ValuesLen[] will contain the length of each Values[] // - returns a pointer to the next content item in the JSON buffer function JsonDecode(P: PUtf8Char; const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean = false): PUtf8Char; overload; /// decode the supplied UTF-8 JSON object for the supplied field names // - overloaded function expecting the names supplied as a constant array // - slightly faster than the one using "const Names: array of RawUtf8" function JsonDecode(P: PUtf8Char; Names: PPUtf8CharArray; NamesCount: integer; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean = false): PUtf8Char; overload; /// decode the supplied UTF-8 JSON object into an array of name/value pairs // - this procedure will decode the JSON content in-memory, i.e. the PUtf8Char // array is created inside JSON, which is therefore modified: make a private // copy first if you want to reuse the JSON content // - the supplied JSON buffer should stay available until Name/Value pointers // from returned Values[] are accessed // - if HandleValuesAsObjectOrArray is TRUE, then this procedure will handle // JSON arrays or objects // - support enhanced JSON syntax, e.g. '{name:'"John",year:1972}' is decoded // just like '{"name":'"John","year":1972}' function JsonDecode(P: PUtf8Char; out Values: TNameValuePUtf8CharDynArray; HandleValuesAsObjectOrArray: boolean = false): PUtf8Char; overload; /// decode the supplied UTF-8 JSON object for the one supplied field name // - this function will decode the JSON content in-memory, so will unescape it // in-place: it must be called only once with the same JSON data function JsonDecode(var Json: RawUtf8; const aName: RawUtf8 = 'result'; WasString: PBoolean = nil; HandleValuesAsObjectOrArray: boolean = false): RawUtf8; overload; {$ifdef HASINLINE} inline; {$endif} /// decode the supplied UTF-8 JSON object for the one supplied field name // - this function will decode and modify the input JSON buffer in-place function JsonDecode(Json: PUtf8Char; const aName: RawUtf8; WasString: PBoolean; HandleValuesAsObjectOrArray: boolean): RawUtf8; overload; type /// available options for JSON parsing process // - by default, parsing will fail if a JSON field name is not part of the // object published properties, unless jpoIgnoreUnknownProperty is defined - // this option will also ignore read-only properties (i.e. with only a getter) // - by default, function will check that the supplied JSON value will // be a JSON string when the property is a string, unless jpoIgnoreStringType // is defined and JSON numbers are accepted and stored as text // - by default any unexpected value for enumerations will be marked as // invalid, unless jpoIgnoreUnknownEnum is defined, so that in such case the // ordinal 0 value is left, and loading continues // - by default, only simple kind of variant types (string/numbers) are // handled: set jpoHandleCustomVariants if you want to handle any custom - // in this case , it will handle direct JSON [array] of {object}: but if you // also define jpoHandleCustomVariantsWithinString, it will also try to // un-escape a JSON string first, i.e. handle "[array]" or "{object}" content // (may be used e.g. when JSON has been retrieved from a database TEXT column) // - by default, a temporary instance will be created if a published field // has a setter, and the instance is expected to be released later by the // owner class: set jpoSetterExpectsToFreeTempInstance to let JsonParser // (and TPropInfo.ClassFromJson) release it when the setter returns, and // jpoSetterNoCreate to avoid the published field instance creation // - set jpoAllowInt64Hex to let Int64/QWord fields accept hexadecimal string // (as generated e.g. via the woInt64AsHex option) // - by default, double values won't be stored as variant values, unless // jpoAllowDouble is set - see also dvoAllowDoubleValue in TDocVariantOptions // - jpoObjectListClassNameGlobalFindClass would also search for "ClassName": // TObjectList serialized field with the global Classes.FindClass() function // - null will release any class instance, unless jpoNullDontReleaseObjectInstance // is set which will leave the instance untouched // - values will be left untouched before parsing, unless jpoClearValues // is defined, to void existing record fields or class published properties TJsonParserOption = ( jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoIgnoreUnknownEnum, jpoHandleCustomVariants, jpoHandleCustomVariantsWithinString, jpoSetterExpectsToFreeTempInstance, jpoSetterNoCreate, jpoAllowInt64Hex, jpoAllowDouble, jpoObjectListClassNameGlobalFindClass, jpoNullDontReleaseObjectInstance, jpoClearValues); /// set of options for JsonParser() parsing process TJsonParserOptions = set of TJsonParserOption; /// efficient execution context of the JSON parser // - defined here for low-level use of TRttiJsonLoad functions // - inherit from TGetJsonField to include ParseNext/ParseNextAny unserialized // Value/ValueLen and flags, and Json as current position in the JSON input {$ifdef USERECORDWITHMETHODS} TJsonParserContext = record public Get: TGetJsonField; function Json: PUtf8Char; {$ifdef HASINLINE} inline; {$endif} function Value: PUtf8Char; {$ifdef HASINLINE} inline; {$endif} function ValueLen: PtrInt; {$ifdef HASINLINE} inline; {$endif} function WasString: boolean; {$ifdef HASINLINE} inline; {$endif} function EndOfObject: AnsiChar; {$ifdef HASINLINE} inline; {$endif} {$else} TJsonParserContext = object(TGetJsonField) {$endif USERECORDWITHMETHODS} public /// true if the last parsing succeeded Valid: boolean; /// customize parsing Options: TJsonParserOptions; /// how TDocVariant should be created CustomVariant: PDocVariantOptions; /// contains the current value RTTI Info: TRttiCustom; /// contains the current property value RTTI Prop: PRttiCustomProp; /// force the item class when reading a TObjectList without "ClassName":... ObjectListItem: TRttiCustom; /// optional RawUtf8 values interning Interning: TRawUtf8Interning; /// TDocVariant initialization options DVO: TDocVariantOptions; /// initialize this unserialization context procedure InitParser(P: PUtf8Char; Rtti: TRttiCustom; O: TJsonParserOptions; CV: PDocVariantOptions; ObjectListItemClass: TClass; RawUtf8Interning: TRawUtf8Interning); /// call inherited GetJsonField() to retrieve the next JSON value // - on success, return true and set Value/ValueLen and WasString fields function ParseNext: boolean; {$ifdef HASINLINE}inline;{$endif} /// call inherited GetJsonFieldOrObjectOrArray() to retrieve the next JSON value // - on success, return true and set Value/ValueLen and WasString fields function ParseNextAny: boolean; {$ifdef HASINLINE}inline;{$endif} /// retrieve the next JSON value as UTF-8 text function ParseUtf8: RawUtf8; /// retrieve the next JSON value as RTL string text function ParseString: string; /// retrieve the next JSON value as integer function ParseInteger: Int64; /// set the EndOfObject field of a JSON buffer, just like GetJsonField() does // - to be called whan a JSON object or JSON array has been manually parsed procedure ParseEndOfObject; {$ifdef HASINLINE}inline;{$endif} /// parse a 'null' value from JSON buffer function ParseNull: boolean; {$ifdef HASINLINE}inline;{$endif} /// parse initial '[' token from JSON buffer // - once all the nested values have been read, call ParseEndOfObject function ParseArray: boolean; /// parse initial '{' token from JSON buffer // - once all the nested values have been read, call ParseEndOfObject function ParseObject: boolean; overload; /// wrapper around JsonDecode() to easily get JSON object values function ParseObject(const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean = false): boolean; overload; /// parse a JSON object from the buffer into a // - if ObjectListItem was not defined, expect the JSON input to start as // '{"ClassName":"TMyClass",...}' function ParseNewObject: TObject; {$ifdef HASINLINE}inline;{$endif} /// parse a property value, properly calling any setter procedure ParsePropComplex(Data: pointer); end; PJsonParserContext = ^TJsonParserContext; /// internal function handler for JSON reading of any TRttiParserType value TRttiJsonLoad = procedure(Data: pointer; var Ctxt: TJsonParserContext); var /// default options for the JSON parser // - as supplied to LoadJson() with Tolerant=false // - defined as var, not as const, to allow process-wide override JSONPARSER_DEFAULTOPTIONS: TJsonParserOptions = []; /// some open-minded options for the JSON parser // - as supplied to LoadJson() with Tolerant=true // - won't block JSON unserialization due to some minor unexpected values // - used e.g. by TObjArraySerializer.CustomReader and // TInterfacedObjectFake.FakeCall/TServiceMethodExecute.ExecuteJson methods // - defined as var, not as const, to allow process-wide override JSONPARSER_TOLERANTOPTIONS: TJsonParserOptions = [jpoHandleCustomVariants, jpoIgnoreUnknownEnum, jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoAllowInt64Hex]; /// access default (false) or tolerant (true) JSON parser options // - to be used as JSONPARSER_DEFAULTORTOLERANTOPTIONS[tolerant] JSONPARSER_DEFAULTORTOLERANTOPTIONS: array[boolean] of TJsonParserOptions = ( [], [jpoHandleCustomVariants, jpoIgnoreUnknownEnum, jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoAllowInt64Hex]); // backward compatibility types redirections {$ifndef PUREMORMOT2} type TJsonToObjectOption = TJsonParserOption; TJsonToObjectOptions = TJsonParserOptions; const j2oSQLRawBlobAsBase64 = woRawBlobAsBase64; j2oIgnoreUnknownProperty = jpoIgnoreUnknownProperty; j2oIgnoreStringType = jpoIgnoreStringType; j2oIgnoreUnknownEnum = jpoIgnoreUnknownEnum; j2oHandleCustomVariants = jpoHandleCustomVariants; j2oHandleCustomVariantsWithinString = jpoHandleCustomVariantsWithinString; j2oSetterExpectsToFreeTempInstance = jpoSetterExpectsToFreeTempInstance; j2oSetterNoCreate = jpoSetterNoCreate; j2oAllowInt64Hex = jpoAllowInt64Hex; const JSONTOOBJECT_TOLERANTOPTIONS: TJsonParserOptions = [jpoHandleCustomVariants, jpoIgnoreUnknownEnum, jpoIgnoreUnknownProperty, jpoIgnoreStringType, jpoAllowInt64Hex]; {$endif PUREMORMOT2} { ********** Custom JSON Serialization } type /// the callback signature used by TRttiJson for serializing JSON data // - Data^ should be written into W, with the supplied Options TOnRttiJsonWrite = procedure(W: TJsonWriter; Data: pointer; Options: TTextWriterWriteObjectOptions) of object; /// the callback signature used by TRttiJson for unserializing JSON data // - set Context.Valid=true if Context.JSON has been parsed into Data^ TOnRttiJsonRead = procedure(var Context: TJsonParserContext; Data: pointer) of object; /// the callback signature used by TRttiJson for serializing JSON classes // - Instance should be written into W, with the supplied Options // - is in fact a convenient alias to the TOnRttiJsonWrite callback TOnClassJsonWrite = procedure(W: TJsonWriter; Instance: TObject; Options: TTextWriterWriteObjectOptions) of object; /// the callback signature used by TRttiJson for unserializing JSON classes // - set Context.Valid=true if Context.JSON has been parsed into Instance // - is in fact a convenient alias to the TOnRttiJsonRead callback TOnClassJsonRead = procedure(var Context: TJsonParserContext; Instance: TObject) of object; /// JSON-aware TRttiCustom class - used for global RttiCustom: TRttiCustomList TRttiJson = class(TRttiCustom) protected fCompare: array[{CaseInsens:}boolean] of TRttiCompare; // for ValueCompare fIncludeReadOptions: TJsonParserOptions; fIncludeWriteOptions: TTextWriterWriteObjectOptions; // overriden for proper JSON process - set fJsonSave and fJsonLoad function SetParserType(aParser: TRttiParserType; aParserComplex: TRttiParserComplexType): TRttiCustom; override; procedure SetValueClass(aClass: TClass; aInfo: PRttiInfo); override; public /// simple wrapper around TRttiJsonSave(fJsonSave) procedure RawSaveJson(Data: pointer; const Ctxt: TJsonSaveContext); {$ifdef HASINLINE}inline;{$endif} /// simple wrapper around TRttiJsonLoad(fJsonLoad) procedure RawLoadJson(Data: pointer; var Ctxt: TJsonParserContext); {$ifdef HASINLINE}inline;{$endif} /// create and parse a new TObject instance of this rkClass function ParseNewInstance(var Context: TJsonParserContext): TObject; /// compare two stored values of this type function ValueCompare(Data, Other: pointer; CaseInsensitive: boolean): integer; override; /// fill a variant with a stored value of this type // - complex values can be returned as TDocVariant after JSON conversion, // using e.g. @JSON_[mFast] as optional Options parameter function ValueToVariant(Data: pointer; out Dest: TVarData; Options: pointer{PDocVariantOptions} = nil): PtrInt; override; /// unserialize some JSON input into Data^ // - as used by LoadJson() and similar high-level functions procedure ValueLoadJson(Data: pointer; var Json: PUtf8Char; EndOfObject: PUtf8Char; ParserOptions: TJsonParserOptions; CustomVariantOptions: PDocVariantOptions; ObjectListItemClass: TClass; Interning: TRawUtf8Interning); /// how many iterations could be done one a given value // - returns -1 if the value is not iterable, or length(DynArray) or // TRawUtf8List.Count or TList.Count or TSynList.Count // - note that TStrings values are not supported, because they require a // temporary string variable for their getter function ValueIterateCount(Data: pointer): integer; override; /// iterate over one sub-item of a given value // - returns nil if the value is not iterable or Index is out of range // - returns a pointer to the value, rkClass/rkLString kinds being already // resolved (as the TList/TSynList/TRawUtf8List items are returned), // so you can directly trans-type the result to TObject() or RawUtf8() // - ResultRtti holds the type of the resolved result pointer // - note that TStrings values are not supported, because they require a // temporary string variable for their getter method function ValueIterate(Data: pointer; Index: PtrUInt; out ResultRtti: TRttiCustom): pointer; override; /// lookup a value by a path name e.g. 'one.two.three' nested values // - for a record/class, will search for a property name // - for a TDocVariant/TBsonVariant, calls TSynInvokeableVariantType.IntGet // - for an enumeration or set, will return true/false about the enum name // - for a string, Data^ will be compared to the name function ValueByPath(var Data: pointer; Path: PUtf8Char; var Temp: TVarData; PathDelim: AnsiChar = '.'): TRttiCustom; override; /// efficient search of TRttiJson from a given RTTI TypeInfo() // - to be used instead of Rtti.Find() to return directly the TRttiJson instance class function Find(Info: PRttiInfo): TRttiJson; {$ifdef HASINLINE}inline;{$endif} /// register a custom callback for JSON serialization of a given TypeInfo() // - for a dynamic array, will customize the item serialization callbacks // - replace deprecated TTextWriter.RegisterCustomJSONSerializer() method class function RegisterCustomSerializer(Info: PRttiInfo; const Reader: TOnRttiJsonRead; const Writer: TOnRttiJsonWrite): TRttiJson; /// unregister any custom callback for JSON serialization of a given TypeInfo() // - will also work after RegisterFromText() class function UnRegisterCustomSerializer(Info: PRttiInfo): TRttiJson; /// register a custom callback for JSON serialization of a given class // - replace deprecated TTextWriter.RegisterCustomJSONSerializer() method class function RegisterCustomSerializerClass(ObjectClass: TClass; const Reader: TOnClassJsonRead; const Writer: TOnClassJsonWrite): TRttiJson; /// unregister any custom callback for JSON serialization of a given class class function UnRegisterCustomSerializerClass(ObjectClass: TClass): TRttiJson; /// register TypeInfo() custom JSON serialization for a given dynamic // array or record // - to be used instead of homonomous Rtti.RegisterFromText() to supply // an additional set of serialization/unserialization JSON options class function RegisterFromText(DynArrayOrRecord: PRttiInfo; const RttiDefinition: RawUtf8; IncludeReadOptions: TJsonParserOptions; IncludeWriteOptions: TTextWriterWriteObjectOptions): TRttiJson; /// define an additional set of unserialization JSON options // - is included for this type to the supplied TJsonParserOptions property IncludeReadOptions: TJsonParserOptions read fIncludeReadOptions write fIncludeReadOptions; /// define an additional set of serialization JSON options // - is included for this type to the supplied TTextWriterWriteObjectOptions property IncludeWriteOptions: TTextWriterWriteObjectOptions read fIncludeWriteOptions write fIncludeWriteOptions; end; { ********** JSON Serialization Wrapper Functions } /// encode the supplied data as an UTF-8 valid JSON object content // - data must be supplied two by two, as Name,Value pairs, e.g. // ! JsonEncode(['name','John','year',1972]) = '{"name":"John","year":1972}' // - or you can specify nested arrays or objects with '['..']' or '{'..'}': // ! J := JsonEncode(['doc','{','name','John','abc','[','a','b','c',']','}','id',123]); // ! assert(J='{"doc":{"name":"John","abc":["a","b","c"]},"id":123}'); // - note that, due to a Delphi compiler limitation, cardinal values should be // type-casted to Int64() (otherwise the integer mapped value will be converted) // - you can pass nil as parameter for a null JSON value function JsonEncode(const NameValuePairs: array of const): RawUtf8; overload; /// encode the supplied (extended) JSON content, with parameters, // as an UTF-8 valid JSON object content // - in addition to the JSON RFC specification strict mode, this method will // handle some BSON-like extensions, e.g. unquoted field names: // ! aJson := JsonEncode('{id:?,%:{name:?,birthyear:?}}',['doc'],[10,'John',1982]); // - you can use nested _Obj() / _Arr() instances // ! aJson := JsonEncode('{%:{$in:[?,?]}}',['type'],['food','snack']); // ! aJson := JsonEncode('{type:{$in:?}}',[],[_Arr(['food','snack'])]); // ! // will both return // ! '{"type":{"$in":["food","snack"]}}') // - if the mormot.db.nosql.bson unit is used in the application, the MongoDB // Shell syntax will also be recognized to create TBsonVariant, like // ! new Date() ObjectId() MinKey MaxKey // // see @http://docs.mongodb.org/manual/reference/mongodb-extended-json // ! aJson := JsonEncode('{name:?,field:/%/i}',['acme.*corp'],['John'])) // ! // will return // ! '{"name":"John","field":{"$regex":"acme.*corp","$options":"i"}}' // - will call internally _JsonFastFmt() to create a temporary TDocVariant with // all its features - so is slightly slower than other JsonEncode* functions function JsonEncode(const Format: RawUtf8; const Args, Params: array of const): RawUtf8; overload; /// encode the supplied RawUtf8 array data as an UTF-8 valid JSON array content function JsonEncodeArrayUtf8( const Values: array of RawUtf8): RawUtf8; overload; /// encode the supplied integer array data as a valid JSON array function JsonEncodeArrayInteger( const Values: array of integer): RawUtf8; overload; /// encode the supplied floating-point array data as a valid JSON array function JsonEncodeArrayDouble( const Values: array of double): RawUtf8; overload; /// encode the supplied array data as a valid JSON array content // - if WithoutBraces is TRUE, no [ ] will be generated // - note that, due to a Delphi compiler limitation, cardinal values should be // type-casted to Int64() (otherwise the integer mapped value will be converted) function JsonEncodeArrayOfConst(const Values: array of const; WithoutBraces: boolean = false): RawUtf8; overload; /// encode the supplied array data as a valid JSON array content // - if WithoutBraces is TRUE, no [ ] will be generated // - note that, due to a Delphi compiler limitation, cardinal values should be // type-casted to Int64() (otherwise the integer mapped value will be converted) procedure JsonEncodeArrayOfConst(const Values: array of const; WithoutBraces: boolean; var result: RawUtf8); overload; /// encode as JSON {"name":value} object, from a potential SQL quoted value // - will unquote the SQLValue using TJsonWriter.AddQuotedStringAsJson() procedure JsonEncodeNameSQLValue(const Name, SQLValue: RawUtf8; var result: RawUtf8); var /// the options used by TObjArraySerializer, TInterfacedObjectFake and // TServiceMethodExecute when serializing values as JSON // - used as DEFAULT_WRITEOPTIONS[DontStoreVoidJson] // - you can modify this global variable to customize the whole process DEFAULT_WRITEOPTIONS: array[boolean] of TTextWriterWriteObjectOptions = ( [woDontStoreDefault, woRawBlobAsBase64], [woDontStoreDefault, woDontStoreVoid, woRawBlobAsBase64]); /// the options used by TSynJsonFileSettings.SaveIfNeeded // - you can modify this global variable to customize the whole process SETTINGS_WRITEOPTIONS: TTextWriterWriteObjectOptions = [woHumanReadable, woStoreStoredFalse, woHumanReadableFullSetsAsStar, woHumanReadableEnumSetAsComment, woInt64AsHex]; /// the options used by TServiceFactoryServer.OnLogRestExecuteMethod // - you can modify this global variable to customize the whole process SERVICELOG_WRITEOPTIONS: TTextWriterWriteObjectOptions = [woDontStoreDefault, woDontStoreVoid, woHideSensitivePersonalInformation]; /// serialize most kind of content as JSON, using its RTTI // - is just a wrapper around TJsonWriter.AddTypedJson() // - so would handle tkClass, tkEnumeration, tkSet, tkRecord, tkDynArray, // tkVariant kind of content - other kinds would return 'null' // - you can override serialization options if needed procedure SaveJson(const Value; TypeInfo: PRttiInfo; Options: TTextWriterOptions; var result: RawUtf8; ObjectOptions: TTextWriterWriteObjectOptions = []); overload; /// serialize most kind of content as JSON, using its RTTI // - is just a wrapper around TJsonWriter.AddTypedJson() function SaveJson(const Value; TypeInfo: PRttiInfo; EnumSetsAsText: boolean): RawUtf8; overload; {$ifdef HASINLINE}inline;{$endif} /// serialize most kind of content as JSON, using its RTTI function SaveJson(const Value; TypeInfo: PRttiInfo): RawUtf8; overload; /// serialize most kind of content as JSON, using its RTTI and a type name // - could be used if you know the type name and not the TypeInfo() // - will use Rtti.RegisterTypeFromName() so the type should be known, i.e. be // a simple type, or have been alredy registered // - returns '' if TypeName is not recognized function SaveJson(const Value; const TypeName: RawUtf8; Options: TTextWriterOptions = []): RawUtf8; overload; {$ifdef FPC} /// special global function exported to Lazarus for runtime evaluation, within // latest trunk fpdebug, of any variable as JSON, using mORMot RTTI // - the "JsonForDebug" function name is recognized by recent fpdebug, and // called and try to serialize a variable as JSON in Lazarus debug windows - see // https://wiki.freepascal.org/IDE_Window:_Ide_Options_-_Backend_Value_Converter // - this function will recognize 1) all type names registered to mORMot RTTI // (using Rtti.Register*() methods), 2) T* class types guessing from their VMT, // 3) I* types recognized as interface, and their associated "as TObject" class // instance will be serialized procedure JsonForDebug(Value: pointer; var TypeName: RawUtf8; out JsonResultText: RawUtf8); {$endif FPC} /// save record into its JSON serialization as saved by TJsonWriter.AddRecordJson // - will use default Base64 encoding over RecordSave() binary - or custom true // JSON format (as set by Rtti.RegisterFromText/TRttiJson.RegisterCustomSerializer // or via enhanced RTTI), if available (following EnumSetsAsText optional // parameter for nested enumerates and sets) function RecordSaveJson(const Rec; TypeInfo: PRttiInfo; EnumSetsAsText: boolean = false): RawUtf8; {$ifdef HASINLINE}inline;{$endif} /// serialize a dynamic array content as JSON // - Value shall be set to the source dynamic array field // - is just a wrapper around TJsonWriter.AddDynArrayJson(), creating // a temporary TDynArray wrapper on the stack // - to be used e.g. for custom record JSON serialization, within a // TDynArrayJsonCustomWriter callback or Rtti.RegisterFromText() // (following EnumSetsAsText optional parameter for nested enumerates and sets) function DynArraySaveJson(const Value; TypeInfo: PRttiInfo; EnumSetsAsText: boolean = false): RawUtf8; /// serialize a dynamic array content, supplied as raw binary buffer, as JSON // - Value shall be set to the source dynamic array field // - is just a wrapper around TJsonWriter.AddDynArrayJson(), creating // a temporary TDynArray wrapper on the stack // - to be used e.g. for custom record JSON serialization, within a // TDynArrayJsonCustomWriter callback or Rtti.RegisterFromText() function DynArrayBlobSaveJson(TypeInfo: PRttiInfo; BlobValue: pointer; BlobLen: PtrInt): RawUtf8; /// wrapper to serialize a T*ObjArray dynamic array as JSON // - for proper serialization on Delphi 7-2009, use Rtti.RegisterObjArray() function ObjArrayToJson(const aObjArray; aOptions: TTextWriterWriteObjectOptions = [woDontStoreDefault]): RawUtf8; /// will serialize set of TObject into its UTF-8 JSON representation // - follows ObjectToJson()/TJsonWriter.WriterObject() functions output // - if Names is not supplied, the corresponding class names would be used function ObjectsToJson(const Names: array of RawUtf8; const Values: array of TObject; Options: TTextWriterWriteObjectOptions = [woDontStoreDefault]): RawUtf8; /// persist a class instance into a JSON file // - returns TRUE on success, false on error (e.g. the file name is invalid // or the file is existing and could not be overwritten) // - see ObjectToJson() as defined in momrot.core.text.pas function ObjectToJsonFile(Value: TObject; const JsonFile: TFileName; Options: TTextWriterWriteObjectOptions = [woHumanReadable]): boolean; /// get any (potentially nested) object property by path // - complex values (e.g. dynamic array properties) will be returned as // TDocVariant after JSON conversion function GetValueObject(Instance: TObject; const Path: RawUtf8; out Value: variant): boolean; /// unserialize most kind of content as JSON, using its RTTI, as saved by // TJsonWriter.AddRecordJson / RecordSaveJson // - same implementation than GetDataFromJson() global low-level function // - returns nil on error, or the end of buffer on success // - warning: the JSON buffer will be modified in-place during process - use // LoadJson() instead or a make temporary copy if you need to access it later function LoadJsonInPlace(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil; Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): PUtf8Char; /// unserialize most kind of content as JSON, using its RTTI, as saved by // TJsonWriter.AddRecordJson / RecordSaveJson // - this overloaded function will make a private copy before parsing it, // so is safe with a read/only or shared string - but slightly slower function LoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil; Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): boolean; /// fill a record content from a JSON serialization as saved by // TJsonWriter.AddRecordJson / RecordSaveJson // - will use default Base64 encoding over RecordSave() binary - or custom // JSON format (as set by Rtti.RegisterFromText/TRttiJson.RegisterCustomSerializer // or via enhanced RTTI), if available // - returns nil on error, or the end of buffer on success // - warning: the JSON buffer will be modified in-place during process - use // a temporary copy if you need to access it later or if the string comes from // a constant (refcount=-1) - see e.g. the overloaded RecordLoadJson() function RecordLoadJson(var Rec; Json: PUtf8Char; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil; Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): PUtf8Char; overload; /// fill a record content from a JSON serialization as saved by // TJsonWriter.AddRecordJson / RecordSaveJson // - this overloaded function will make a private copy before parsing it, // so is safe with a read/only or shared string - but slightly slower function RecordLoadJson(var Rec; const Json: RawUtf8; TypeInfo: PRttiInfo; CustomVariantOptions: PDocVariantOptions = nil; Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): boolean; overload; /// fill a dynamic array content from a JSON serialization as saved by // TJsonWriter.AddDynArrayJson with or without twoNonExpandedArrays layout // - Value shall be set to the target dynamic array field // - return a pointer at the end of the data read from JSON, nil in case // of an invalid input buffer // - could be used e.g. for custom record JSON unserialization, within a // TDynArrayJsonCustomReader callback // - warning: the JSON buffer will be modified in-place during process - use // a temporary copy if you need to access it later or if the string comes from // a constant (refcount=-1) - see e.g. the overloaded DynArrayLoadJson() // - some numbers on a Core i5-13500, extracted from our regression tests: // $ DynArrayLoadJson exp in 32.86ms i.e. 4.7M rows/s, 596.5 MB/s // $ DynArrayLoadJson non exp in 22.46ms i.e. 6.9M rows/s, 383.7 MB/s function DynArrayLoadJson(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char = nil; CustomVariantOptions: PDocVariantOptions = nil; Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): PUtf8Char; overload; /// fill a dynamic array content from a JSON serialization as saved by // TJsonWriter.AddDynArrayJson, which won't be modified // - this overloaded function will make a private copy before parsing it, // so is safe with a read/only or shared string - but slightly slower function DynArrayLoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo; CustomVariantOptions: PDocVariantOptions = nil; Tolerant: boolean = true; Interning: TRawUtf8Interning = nil): boolean; overload; /// read an object properties, as saved by ObjectToJson function // - ObjectInstance must be an existing TObject instance // - the data inside From^ is modified in-place (unescaped and transformed): // calling JsonToObject(pointer(JSONRawUtf8)) will change the JSONRawUtf8 // variable content, which may not be what you expect - consider using the // ObjectLoadJson() function instead // - handle integer, Int64, enumerate (including boolean), set, floating point, // TDateTime, TCollection, TStrings, TRawUtf8List, variant, and string properties // (excluding ShortString, but including WideString and UnicodeString under // Delphi 2009+) // - TList won't be handled since it may leak memory when calling TList.Clear // - won't handle TObjectList (even if ObjectToJson is able to serialize // them) since has no way of knowing the object type to add (TCollection.Add // is missing), unless: 1. you set the TObjectListItemClass property as expected, // and provide a TObjectList object, or 2. woStoreClassName option has been // used at ObjectToJson() call and the corresponding classes have been previously // registered by Rtti.RegisterClass() // - will clear any previous TCollection objects, and convert any null JSON // basic type into nil - e.g. if From='null', will call FreeAndNil(Value) // - you can add some custom (un)serializers for ANY class, via mormot.core.json // TRttiJson.RegisterCustomSerializer() class method // - set Valid=TRUE on success, Valid=FALSE on error, and the main function // will point in From at the syntax error place (e.g. on any unknown property name) // - caller should explicitly perform a SetDefaultValuesObject(Value) if // the default values are expected to be set before JSON parsing function JsonToObject(var ObjectInstance; From: PUtf8Char; out Valid: boolean; TObjectListItemClass: TClass = nil; Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): PUtf8Char; /// parse the supplied JSON with some tolerance about Settings format // - will make a TSynTempBuffer copy for parsing, and un-comment it // - returns true if the supplied JSON was successfully retrieved // - returns false on error function JsonSettingsToObject(const JsonContent: RawUtf8; Instance: TObject): boolean; /// read an object properties, as saved by ObjectToJson function // - ObjectInstance must be an existing TObject instance // - this overloaded version will make a private copy of the supplied JSON // content (via TSynTempBuffer), to ensure the original buffer won't be modified // during process, before calling safely JsonToObject() // - will return TRUE on success, or FALSE if the supplied JSON was invalid function ObjectLoadJson(var ObjectInstance; const Json: RawUtf8; TObjectListItemClass: TClass = nil; Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): boolean; /// create a new object instance, as saved by ObjectToJson(...,[...,woStoreClassName,...]); // - JSON input should be either 'null', either '{"ClassName":"TMyClass",...}' // - woStoreClassName option shall have been used at ObjectToJson() call // - and the corresponding class shall have been previously registered by // Rtti.RegisterClass() to retrieve the class type from it name // - the data inside From^ is modified in-place (unescaped and transformed): // don't call JsonToObject(pointer(JSONRawUtf8)) but makes a temporary copy of // the JSON text buffer before calling this function, if want to reuse it later function JsonToNewObject(var From: PUtf8Char; var Valid: boolean; Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): TObject; /// read an TObject published property, as saved by ObjectToJson() function // - will use direct in-memory reference to the object, or call the corresponding // setter method (if any), creating a temporary instance // - unserialize the JSON input buffer via a call to JsonToObject() // - by default, a temporary instance will be created if a published field // has a setter, and the instance is expected to be released later by the // owner class: you can set the j2oSetterExpectsToFreeTempInstance option // to let this method release it when the setter returns function PropertyFromJson(Prop: PRttiCustomProp; Instance: TObject; From: PUtf8Char; var Valid: boolean; Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): PUtf8Char; /// decode a specified parameter compatible with URI encoding into its original // object contents // - ObjectInstance must be an existing TObject instance // - will call internally JsonToObject() function to unserialize its content // - UrlDecodeExtended('price=20.45&where=LastName%3D%27M%C3%B4net%27','PRICE=',P,@Next) // will return Next^='where=...' and P=20.45 // - if Upper is not found, Value is not modified, and result is FALSE // - if Upper is found, Value is modified with the supplied content, and result is TRUE function UrlDecodeObject(U: PUtf8Char; Upper: PAnsiChar; var ObjectInstance; Next: PPUtf8Char = nil; Options: TJsonParserOptions = []): boolean; /// fill the object properties from a JSON file content // - ObjectInstance must be an existing TObject instance // - this function will call RemoveCommentsFromJson() before process function JsonFileToObject(const JsonFile: TFileName; var ObjectInstance; TObjectListItemClass: TClass = nil; Options: TJsonParserOptions = []; Interning: TRawUtf8Interning = nil): boolean; const /// standard header for an UTF-8 encoded XML file XMLUTF8_HEADER = ''#13#10; /// standard namespace for a generic XML File XMLUTF8_NAMESPACE = ''; /// convert a JSON array or document into a simple XML content // - just a wrapper around TJsonWriter.AddJsonToXML, with an optional // header before the XML converted data (e.g. XMLUTF8_HEADER), and an optional // name space content node which will nest the generated XML data (e.g. // '') - the // corresponding ending token will be appended after (e.g. '') // - WARNING: the JSON buffer is decoded in-place, so P^ WILL BE modified procedure JsonBufferToXML(P: PUtf8Char; const Header, NameSpace: RawUtf8; out result: RawUtf8); /// convert a JSON array or document into a simple XML content // - just a wrapper around TJsonWriter.AddJsonToXML, making a private copy // of the supplied JSON buffer using TSynTempBuffer (so that JSON content // would stay untouched) // - the optional header is added at the beginning of the resulting string // - an optional name space content node could be added around the generated XML, // e.g. '' function JsonToXML(const Json: RawUtf8; const Header: RawUtf8 = XMLUTF8_HEADER; const NameSpace: RawUtf8 = ''): RawUtf8; { ********************* Abstract Classes with Auto-Create-Fields } /// should be called by T*AutoCreateFields constructors // - will also register this class type, if needed, so RegisterClass() is // redundant to this method function AutoCreateFields(ObjectInstance: TObject): TRttiJson; {$ifdef HASINLINE}inline;{$endif} /// should be called by T*AutoCreateFields destructors // - constructor should have called AutoCreateFields() procedure AutoDestroyFields(ObjectInstance: TObject; Info: TRttiJson = nil); {$ifdef HASINLINE}inline;{$endif} /// internal function called by AutoCreateFields() when inlined // - do not call this internal function, but always AutoCreateFields() function DoRegisterAutoCreateFields(ObjectInstance: TObject): TRttiJson; type /// abstract TPersistent class, which will instantiate all its nested class // published properties, then release them (and any T*ObjArray) when freed // - TSynAutoCreateFields is to be preferred in most cases, thanks to its // lower overhead // - note that non published (e.g. public) properties won't be instantiated, // serialized, nor released - but may contain weak references to other classes // - please take care that you will not create any endless recursion: you should // ensure that at one level, nested published properties won't have any class // instance refering to its owner (there is no weak reference - remember!) // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before TPersistentAutoCreateFields = class(TPersistentWithCustomCreate) public /// this overriden constructor will instantiate all its nested // class or T*ObjArray published properties constructor Create; override; /// finalize the instance, and its class or T*ObjArray published properties destructor Destroy; override; end; /// our own empowered TPersistentAutoCreateFields-like parent class // - this class is a perfect parent to store any data by value, e.g. DDD Value // Objects, Entities or Aggregates // - is defined as an abstract class able with a virtual constructor, RTTI // for published properties, and automatic memory management of all nested // class published properties: any class defined as a published property will // be owned by this instance - i.e. with strong reference // - will also release any T*ObjArray dynamic array storage of persistents, // previously registered via Rtti.RegisterObjArray() for Delphi 7-2009 // - nested published classes (or T*ObjArray) don't need to inherit from // TSynAutoCreateFields: they may be from any TPersistent/TSynPersistent type // - note that non published (e.g. public) properties won't be instantiated, // serialized, nor released - but may contain weak references to other classes // - please take care that you will not create any endless recursion: you should // ensure that at one level, nested published properties won't have any class // instance refering to its owner (there is no weak reference - remember!) // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before // - TPersistent/TPersistentAutoCreateFields have an unexpected speed overhead // due a giant lock introduced to manage property name fixup resolution // (which we won't use outside the UI) - this class is definitively faster TSynAutoCreateFields = class(TSynPersistent) public /// this overriden constructor will instantiate all its nested // class or T*ObjArray published properties constructor Create; override; /// finalize the instance, and its class or T*ObjArray published properties destructor Destroy; override; end; /// meta-class definition of TSynAutoCreateFields TSynAutoCreateFieldsClass = class of TSynAutoCreateFields; /// adding locking methods to a TSynAutoCreateFields with virtual constructor TSynAutoCreateFieldsLocked = class(TSynPersistentLock) public /// initialize the object instance, its associated lock, and its nested // class or T*ObjArray published properties constructor Create; override; /// release the instance (including the locking resource) and nested classes destructor Destroy; override; end; /// meta-class definition of TSynAutoCreateFieldsLocked TSynAutoCreateFieldsLockedClass = class of TSynAutoCreateFieldsLocked; /// abstract TInterfacedObject class, which will instantiate all its nested // class published properties, then release them when freed // - will handle automatic memory management of all nested class and T*ObjArray // published properties: any class or T*ObjArray defined as a published // property will be owned by this instance - i.e. with strong reference // - non published properties (e.g. public) won't be instantiated, so may // store weak class references // - could be used for gathering of TCollectionItem properties, e.g. for // Domain objects in DDD, especially for list of value objects, with some // additional methods defined by an Interface // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before TInterfacedObjectAutoCreateFields = class(TInterfacedObjectWithCustomCreate) public /// this overriden constructor will instantiate all its nested // class or T*ObjArray published properties constructor Create; override; /// finalize the instance, and release its published properties destructor Destroy; override; end; /// meta-class definition of TInterfacedObjectAutoCreateFields TInterfacedObjectAutoCreateFieldsClass = class of TInterfacedObjectAutoCreateFields; /// abstract interface parent with common methods for JSON serialization // - to implement this, you can inherit from TInterfacedSerializable // or TInterfacedSerializableAutoCreateFields ISerializable = interface ['{EA7F298D-06D7-4ADF-9F75-6598B75338B3}'] // methods used as getter/setter for the Json property function GetJson: RawUtf8; procedure SetJson(const value: RawUtf8); /// serialize this instance into a JSON array/object specific format function ToJson(format: TTextWriterJsonFormat; options: TTextWriterWriteObjectOptions = []): RawUtf8; overload; /// convert this instance into JSON array/object as RTL string function ToString(format: TTextWriterJsonFormat = jsonCompact; options: TTextWriterWriteObjectOptions = []): string; /// raw unserialization of a JSON content into this instance procedure FromJson(var Context: TJsonParserContext); /// raw serialization of this instance into a JSON writer procedure ToJson(W: TJsonWriter; options: TTextWriterWriteObjectOptions); overload; /// unserialize/serialize this IDocList/IDocDict from/into a JSON array/object // - use ToString if you want the result as RTL string property Json: RawUtf8 read GetJson write SetJson; end; {$M+} /// abstract class parent with ISerializable methods for JSON serialization // - you need to override Create, ToJson and FromJson abstract methods TInterfacedSerializable = class(TInterfacedObject, ISerializable) protected // methods used as getter/setter for the Json property function GetJson: RawUtf8; procedure SetJson(const value: RawUtf8); virtual; // used internally for proper ISerializable instances serialization class function SerializableInterface: TRttiCustom; {$ifdef HASINLINE} inline; {$endif} class procedure JS(W: TJsonWriter; data: pointer; options: TTextWriterWriteObjectOptions); class procedure JL(var context: TJsonParserContext; data: pointer); public /// factory of one class implementing a ISerializable interface // - this abstract method must be overriden constructor Create(options: PDocVariantOptions); reintroduce; virtual; abstract; /// raw serialization of this instance into a JSON writer // - this abstract method must be overriden procedure ToJson(W: TJsonWriter; options: TTextWriterWriteObjectOptions); overload; virtual; abstract; /// raw unserialization of a JSON content into this instance // - this abstract method must be overriden procedure FromJson(var context: TJsonParserContext); virtual; abstract; public /// register this class to implement a given ISerializer sub-interface class function RegisterToRtti(InterfaceInfo: PRttiInfo): TRttiJson; /// return the associated ISerializer sub-interface TGuid // - as registered by RegisterToRtti() class method class function Guid: PGuid; /// create a new instance as the associated ISerializer sub-interface // - as registered by RegisterToRtti() class method class procedure NewInterface(out Obj); /// serialize this instance into a JSON array/object specific format function ToJson(format: TTextWriterJsonFormat; options: TTextWriterWriteObjectOptions): RawUtf8; overload; virtual; /// convert this instance into JSON array/object as RTL string function ToString(format: TTextWriterJsonFormat; options: TTextWriterWriteObjectOptions): string; reintroduce; virtual; /// unserialize/serialize this instance from/into a JSON array/object // - use ToString if you want the result as RTL string property Json: RawUtf8 read GetJson write SetJson; end; {$M-} /// meta-class of the TInterfacedSerializable type TInterfacedSerializableClass = class of TInterfacedSerializable; /// points to a TInterfacedSerializable class instance PInterfacedSerializable = ^TInterfacedSerializable; /// abstract ISerializable class parent with auto-create published fields // - you should inherit this class, associated with an interface inheriting // from ISerializable (and propably with a method returning self to access the // properties), then call once the RegisterToRtti() class function // - could be used e.g. to implement a DDD/KDD Aggregate object with both // ref-counted data and methods, ready to be serialized over SOA TInterfacedSerializableAutoCreateFields = class(TInterfacedSerializable) protected fRttiJson: TRttiJson; public /// instantiate all nested class or T*ObjArray published properties constructor Create(options: PDocVariantOptions = nil); override; /// finalize the instance, and release its published properties destructor Destroy; override; /// raw JSON serialization of the published properties of this instance procedure ToJson(W: TJsonWriter; options: TTextWriterWriteObjectOptions); override; /// raw JSON unserialization into the published properties of this instance procedure FromJson(var context: TJsonParserContext); override; /// low-level access to the RTTI information associated with this class property RttiJson: TRttiJson read fRttiJson; end; /// abstract TCollectionItem class, which will instantiate all its nested class // published properties, then release them (and any T*ObjArray) when freed // - could be used for gathering of TCollectionItem properties, e.g. for // Domain objects in DDD, especially for list of value objects // - consider using T*ObjArray dynamic array published properties in your // value types instead of TCollection storage: T*ObjArray have a lower overhead // and are easier to work with, once Rtti.RegisterObjArray is called on Delphi // 7-2009 to register the T*ObjArray type (not needed on FPC and Delphi 2010+) // - note that non published (e.g. public) properties won't be instantiated, // serialized, nor released - but may contain weak references to other classes // - please take care that you will not create any endless recursion: you should // ensure that at one level, nested published properties won't have any class // instance refering to its owner (there is no weak reference - remember!) // - since the destructor will release all nested properties, you should // never store a reference to any of those nested instances if this owner // may be freed before TCollectionItemAutoCreateFields = class(TCollectionItem) public /// this overriden constructor will instantiate all its nested // class or T*ObjArray published properties constructor Create(Collection: TCollection); override; /// finalize the instance, and release its published properties destructor Destroy; override; end; /// customize TSynJsonFileSettings process // - fsoDisableSaveIfNeeded will disable SaveIfNeeded method process // - fsoReadIni will disable JSON loading, and expect INI file format // - fsoWriteIni will force SaveIfNeeded to use the INI layout TSynJsonFileSettingsOption = ( fsoDisableSaveIfNeeded, fsoReadIni, fsoWriteIni); TSynJsonFileSettingsOptions = set of TSynJsonFileSettingsOption; /// abstract parent class able to store settings as JSON file // - would fallback and try to read as INI file if no valid JSON is found TSynJsonFileSettings = class(TSynAutoCreateFields) protected fInitialJsonContent, fSectionName: RawUtf8; fFileName: TFileName; fLoadedAsIni: boolean; fSettingsOptions: TSynJsonFileSettingsOptions; // could be overriden to validate the content coherency and/or clean fields function AfterLoad: boolean; virtual; public /// read existing settings from a JSON content // - if the input is no JSON object, then a .INI structure is tried function LoadFromJson(const aJson: RawUtf8; const aSectionName: RawUtf8 = 'Main'): boolean; /// read existing settings from a JSON or INI file file function LoadFromFile(const aFileName: TFileName; const aSectionName: RawUtf8 = 'Main'): boolean; virtual; /// just a wrapper around ExtractFilePath(FileName); function FolderName: TFileName; /// persist the settings as a JSON file, named from LoadFromFile() parameter // - will use the INI format if it was used at loading, or fsoWriteIni is set procedure SaveIfNeeded; virtual; /// optional persistence file name, as set by LoadFromFile() property FileName: TFileName read fFileName write fFileName; /// allow to customize the storing process property SettingsOptions: TSynJsonFileSettingsOptions read fSettingsOptions write fSettingsOptions; end; /// meta-class definition of TSynJsonFileSettings TSynJsonFileSettingsClass = class of TSynJsonFileSettings; implementation uses mormot.core.variants; { ********** Low-Level JSON Processing Functions } function NeedsJsonEscape(P: PUtf8Char; PLen: integer): boolean; var tab: PByteArray; begin result := true; tab := @JSON_ESCAPE; if PLen > 0 then repeat if tab[ord(P^)] <> JSON_ESCAPE_NONE then exit; inc(P); dec(PLen); until PLen = 0; result := false; end; function NeedsJsonEscape(const Text: RawUtf8): boolean; begin result := NeedsJsonEscape(pointer(Text), length(Text)); end; function NeedsJsonEscape(P: PUtf8Char): boolean; var tab: PByteArray; esc: byte; begin result := false; if P = nil then exit; tab := @JSON_ESCAPE; repeat esc := tab[ord(P^)]; if esc = JSON_ESCAPE_NONE then inc(P) else if esc = JSON_ESCAPE_ENDINGZERO then exit else break; until false; result := true; end; function JsonUnicodeEscapeToUtf8(var D: PUtf8Char; P: PUtf8Char): PUtf8Char; var c, s: cardinal; begin // P^ points at 'u1234' just after \u0123 c := HexToWideChar(P + 1); if c <= $7f then if c >= 32 then D^ := AnsiChar(c) else if c = 0 then D^ := '?' // \u0000 is an invalid value (at least in our framework) else begin PInt64(D)^ := PInt64(P - 1)^; // control chars should always be escaped inc(D, 5); end else if c < $7ff then begin D[0] := AnsiChar($C0 or (c shr 6)); D[1] := AnsiChar($80 or (c and $3F)); inc(D); end else if (c >= UTF16_HISURROGATE_MIN) and // decode from two UTF-16 surrogates (c <= UTF16_LOSURROGATE_MAX) then if PWord(P + 5)^ = ord('\') + ord('u') shl 8 then begin s := HexToWideChar(P + 7); if s = 0 then D^ := '?' // invalid surrogate else begin case c of // inlined Utf16CharToUtf8() UTF16_HISURROGATE_MIN..UTF16_HISURROGATE_MAX: c := ((c - UTF16_SURROGATE_OFFSET) shl 10) or (s xor UTF16_LOSURROGATE_MIN); UTF16_LOSURROGATE_MIN..UTF16_LOSURROGATE_MAX: c := ((s - UTF16_SURROGATE_OFFSET) shl 10) or (c xor UTF16_LOSURROGATE_MIN); end; inc(D, Ucs4ToUtf8(c, D)); result := P + 11; exit; end; end else D^ := '?' // the first \u#### expects a following \u#### surrogate else begin D[0] := AnsiChar($E0 or (c shr 12)); D[1] := AnsiChar($80 or ((c shr 6) and $3F)); D[2] := AnsiChar($80 or (c and $3F)); inc(D,2); end; inc(D); result := P + 5; end; procedure JsonDoUniEscape(const s: RawUtf8; var result: RawUtf8; esc: boolean); var tmp: TTextWriterStackBuffer; begin with TJsonWriter.CreateOwnedStream(tmp) do try if esc then AddNoJsonEscapeForcedUnicode(pointer(s), length(s)) else AddNoJsonEscapeForcedNoUnicode(pointer(s), length(s)); SetText(result); finally Free; end; end; function JsonUnicodeEscape(const s: RawUtf8): RawUtf8; begin JsonDoUniEscape(s, result, true); end; function JsonUnicodeUnEscape(const s: RawUtf8): RawUtf8; begin JsonDoUniEscape(s, result, false); end; procedure Utf16ToJsonUnicodeEscape(var B: PUtf8Char; c: PtrUInt; tab: PByteToWord); var P: PUtf8Char; begin P := B; PWord(P + 1)^ := ord('\') + ord('u') shl 8; PWord(P + 3)^ := tab[c shr 8]; PWord(P + 5)^ := tab[c and $ff]; inc(B, 6); end; function IsString(P: PUtf8Char): boolean; // test if P^ is a "string" value begin if P = nil then begin result := false; exit; end; while (P^ <= ' ') and (P^ <> #0) do inc(P); if (P[0] in ['0'..'9']) or // is first char numeric? ((P[0] in ['-', '+']) and (P[1] in ['0'..'9'])) then begin // check if P^ is a true numerical value repeat inc(P); until not (P^ in ['0'..'9']); // check digits if P^ = '.' then repeat inc(P); until not (P^ in ['0'..'9']); // check fractional digits if ((P^ = 'e') or (P^ = 'E')) and (P[1] in ['0'..'9', '+', '-']) then begin inc(P); if P^ = '+' then inc(P) else if P^ = '-' then inc(P); while (P^ >= '0') and (P^ <= '9') do inc(P); end; while (P^ <= ' ') and (P^ <> #0) do inc(P); result := (P^ <> #0); exit; end else result := true; // don't begin with a numerical value -> must be a string end; function IsStringJson(P: PUtf8Char): boolean; // test if P^ is a "string" value var c4: integer; c: AnsiChar; tab: PJsonCharSet; begin if P = nil then begin result := false; exit; end; while (P^ <= ' ') and (P^ <> #0) do inc(P); tab := @JSON_CHARS; c4 := PInteger(P)^; if (((c4 = NULL_LOW) or (c4 = TRUE_LOW)) and (jcEndOfJsonValueField in tab[P[4]])) or ((c4 = FALSE_LOW) and (P[4] = 'e') and (jcEndOfJsonValueField in tab[P[5]])) then begin result := false; // constants are no string exit; end; c := P^; if c = '-' then begin inc(P); c := P^; end; if ((c >= '1') and (c <= '9')) or // is first char numeric? ((c = '0') and ((P[1] < '0') or (P[1] > '9'))) then // '012' not JSON begin // check if c is a true numerical value repeat inc(P); until (P^ < '0') or (P^ > '9'); // check digits if P^ = '.' then repeat inc(P); until (P^ < '0') or (P^ > '9'); // check fractional digits if ((P^ = 'e') or (P^ = 'E')) and (jcDigitFirstChar in tab[P[1]]) then begin inc(P); c := P^; if c = '+' then inc(P) else if c = '-' then inc(P); while (P^ >= '0') and (P^ <= '9') do inc(P); end; while (P^ <= ' ') and (P^ <> #0) do inc(P); result := (P^ <> #0); exit; end else result := true; // don't begin with a numerical value -> must be a string end; type TJsonGotoEndParserState = ( stObjectName, stObjectValue, stValue); /// state machine for fast (900MB/s) parsing of (extended) JSON input {$ifdef USERECORDWITHMETHODS} TJsonGotoEndParser = record {$else} TJsonGotoEndParser = object {$endif USERECORDWITHMETHODS} public {$ifdef CPUX86} JsonSet: PJsonCharSet; // not enough registers in i386 mode {$endif CPUX86} State: TJsonGotoEndParserState; ExpectStandard: boolean; StackCount: integer; JsonFirst: PJsonTokens; Max: PUtf8Char; // checking Max after each comma is good enough RootCount: integer; // 500 nested documents seem enough in practice (SQlite3 uses 1000) Stack: array[0..500] of TJsonGotoEndParserState; procedure Init(Strict: boolean; PMax: PUtf8Char); {$ifdef HASINLINE} inline; {$endif} procedure InitCount(Strict: boolean; PMax: PUtf8Char; First: TJsonGotoEndParserState); // reusable method able to jump over any JSON value (up to Max) function GotoEnd(P: PUtf8Char): PUtf8Char; overload; function GotoEnd(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char; overload; {$ifdef HASINLINE} inline; {$endif} end; procedure TJsonGotoEndParser.Init(Strict: boolean; PMax: PUtf8Char); begin {$ifdef CPUX86} JsonSet := @JSON_CHARS; {$endif CPUX86} State := stValue; ExpectStandard := Strict; StackCount := 0; JsonFirst := @JSON_TOKENS; Max := PMax; end; // RootCount is not initialized by default unless InitCount() is called procedure TJsonGotoEndParser.InitCount(Strict: boolean; PMax: PUtf8Char; First: TJsonGotoEndParserState); begin Init(Strict, PMax); RootCount := 0; Stack[0] := stValue; // emulate parsing of the opening [ or { inc(StackCount); State := First; end; function TJsonGotoEndParser.GotoEnd(P: PUtf8Char): PUtf8Char; var n: PtrInt; {$ifndef CPUX86} JsonSet: PJsonCharSet; // will use a register for this lookup table {$endif CPUX86} label prop, stop, assign; begin result := nil; // to notify unexpected end if P = nil then exit; {$ifndef CPUX86} JsonSet := @JSON_CHARS; {$endif CPUX86} repeat {$ifdef FPC} while (P^ <= ' ') and (P^ <> #0) do inc(P); {$else} if P^ in [#1..' '] then repeat inc(P) until not (P^ in [#1..' ']); {$endif FPC} case JsonFirst[P^] of // FPC and Delphi will use a jump table :) jtNone: exit;// unexpected character in JSON input jtDoubleQuote: // '"' begin repeat // inlined GotoEndOfJsonString2() inc(P); if not (jcJsonStringMarker in JsonSet[P^]) then // [#0, '"', '\'] continue; // very fast parsing of most UTF-8 chars if P^ = '"' then break else if P^ = #0 then exit; // unexpected end of string/buffer inc(P); // P^ was '\' -> ignore \# ou \u0123 if P^ = #0 then exit; // buffer overflow detected as \#0 until false; inc(P); if (StackCount <> 0) or (State = stObjectName) then continue; break; end; jtFirstDigit: // '-', '0'..'9' begin if (State = stObjectName) and ExpectStandard then exit; // '0123' excluded by JSON, but not here repeat inc(P); until not (jcDigitFloatChar in JsonSet[P^]); // not ['-', '+', '0'..'9', '.', 'E', 'e'] if (StackCount <> 0) or (State = stObjectName) then continue; break; end; jtNullFirstChar: // 'n' if (PInteger(P)^ = NULL_LOW) and (jcEndOfJsonValueField in JsonSet[P[4]]) then inc(P, 3) else goto prop; jtTrueFirstChar: // 't' if (PInteger(P)^ = TRUE_LOW) and (jcEndOfJsonValueField in JsonSet[P[4]]) then inc(P, 3) else goto prop; jtFalseFirstChar: // 'f' if (PInteger(P + 1)^ = FALSE_LOW2) and (jcEndOfJsonValueField in JsonSet[P[5]]) then inc(P, 4) else goto prop; jtObjectStart: // { begin n := StackCount; if (State = stObjectName) or (n > high(Stack)) then exit; // too many nested documents Stack[n] := State; inc(StackCount); State := stObjectName; inc(P); continue; end; jtArrayStart: // [ begin n := StackCount; if (State = stObjectName) or (n > high(Stack)) then exit; // too many nested documents Stack[n] := State; inc(StackCount); State := stValue; inc(P); continue; end; jtObjectStop: // } begin if State = stValue then exit; stop: n := StackCount; if n = 0 then exit; // invalid input dec(n); inc(RootCount, ord(n = 0)); StackCount := n; State := Stack[n]; end; jtArrayStop: // ] if State <> stValue then exit else goto stop; jtAssign: // : begin assign: if State <> stObjectName then exit; State := stObjectValue; inc(P); continue; end; jtComma: // , begin if State = stObjectName then exit; dec(State, ord(State = stObjectValue)); // branchless update inc(P); inc(RootCount, ord(StackCount = 1)); if (Max = nil) or // checking Max after each comma is good enough (P < Max) then continue; // reached end of allowed - but valid - input if RootCount = 0 then dec(RootCount) // first item may be huge -> at least -1 else RootCount := -RootCount; exit; end; jtSingleQuote: // '''' as single-quoted identifier or value if ExpectStandard then exit else repeat inc(P); if P^ <= ' ' then exit; until P^ = ''''; jtEqual: // = if ExpectStandard then exit else goto assign; jtIdentifierFirstChar: // ['_', 'a'..'z', 'A'..'Z', '$'] begin prop: if ExpectStandard then exit; repeat repeat inc(P); until not (jcJsonIdentifier in JsonSet[P^]); // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']'] while (P^ <= ' ') and (P^ <> #0) do inc(P); until not (jcJsonIdentifierFirstChar in JsonSet[P^]); // new date(... while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ = '(' then begin // handle e.g. "born":isodate("1969-12-31") repeat inc(P); until (P^ > ' ') or (P^ = #0); if P^ = '"' then begin repeat inc(P); until jcJsonStringMarker in JsonSet[P^]; // [#0, '"', '\'] if P^ <> '"' then exit; inc(P); end; while (P^ <> ')') and (P^ <> #0) do inc(P); if P^ <> #0 then inc(P); end else if State <> stObjectName then exit; // identifier values are functions like isodate() objectid() continue; end; jtSlash: // '/' extended /regex/i or /*comment*/ or //comment begin if ExpectStandard then exit; inc(P); if P^ = #0 then exit else if P^ = '*' then // ignore /* comment */ begin repeat inc(P); if P^ = #0 then exit; until PWord(P)^ = ord('*') + ord('/') shl 8; inc(P, 2); continue; end else if P^ = '/' then // ignore // comment begin P := GotoNextLine(P + 1); if P = nil then exit; continue; end else begin repeat // extended /regex/i syntax inc(P); if P^ = #0 then exit; until P^ = '/'; while not (jcEndOfJsonFieldNotName in JsonSet[P[1]]) do inc(P); end; end; jtEndOfBuffer: // #0 if StackCount <> 0 then exit // unclosed array or object else break; // return #0 else exit; // paranoid (every and each TJsonToken should be handled above) end; // if we are here we know this was an identifier or value inc(P); if (StackCount = 0) and (State <> stObjectName) then break; until false; while (P^ <= ' ') and (P^ <> #0) do inc(P); result := P; // points to the next meaningful char end; function TJsonGotoEndParser.GotoEnd(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char; var c: AnsiChar; begin result := GotoEnd(P); if result = nil then exit; c := result^; // return last jcEndOfJsonFieldOr0 EndOfObject := c; if c <> #0 then inc(result); end; function IsValidJson(const s: RawUtf8; strict: boolean): boolean; begin result := IsValidJson(pointer(s), length(s), strict); end; function IsValidJson(P: PUtf8Char; len: PtrInt; strict: boolean): boolean; var B: PUtf8Char; parser: TJsonGotoEndParser; begin result := false; if (P = nil) or (len <= 0) then exit; B := P; {%H-}parser.Init(strict, P + len); P := parser.GotoEnd(P); result := (P <> nil) and (P - B = len); end; function GetFirstJsonToken(P: PUtf8Char): TJsonToken; begin if P <> nil then result := JSON_TOKENS[GotoNextNotSpace(P)^] else result := jtNone; end; function GetNextJsonToken(var P: PUtf8Char; strict: boolean; DocCount: PInteger): TJsonToken; var parser: TJsonGotoEndParser; begin result := jtNone; if DocCount <> nil then DocCount^ := 0; if P = nil then exit; P := GotoNextNotSpace(P); result := JSON_TOKENS[P^]; if result in [jtNone, jtEndOfBuffer, jtAssign, jtEqual, jtComma] then begin P := nil; result := jtNone; exit; end; {%H-}parser.Init(strict, nil); parser.RootCount := 0; P := parser.GotoEnd(P); if P = nil then result := jtNone else if DocCount <> nil then DocCount^ := parser.RootCount; end; function IsValidJsonBuffer(P: PUtf8Char; strict: boolean): boolean; var parser: TJsonGotoEndParser; begin {%H-}parser.Init(strict, nil); result := parser.GotoEnd(P) <> nil; end; procedure IgnoreComma(var P: PUtf8Char); begin if P <> nil then begin while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ = ',' then inc(P); end; end; function JsonPropNameValid(P: PUtf8Char): boolean; var tab: PJsonCharSet; begin tab := @JSON_CHARS; if (P <> nil) and (jcJsonIdentifierFirstChar in tab[P^]) then begin // ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$'] repeat inc(P); until not (jcJsonIdentifier in tab[P^]); // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']'] result := P^ = #0; end else result := false; end; {$ifndef PUREMORMOT2} function GetJsonField(P: PUtf8Char; out PDest: PUtf8Char; WasString: PBoolean; EndOfObject: PUtf8Char; Len: PInteger): PUtf8Char; var info: TGetJsonField; begin info.Json := P; info.GetJsonField; PDest := info.Json; if WasString <> nil then WasString^ := info.WasString; if EndOfObject <> nil then EndOfObject^ := info.EndOfObject; if Len <> nil then Len^ := info.ValueLen; result := info.Value; end; function GetJsonFieldOrObjectOrArray(var Json: PUtf8Char; WasString: PBoolean; EndOfObject: PUtf8Char; HandleValuesAsObjectOrArray, NormalizeBoolean: boolean; Len: PInteger): PUtf8Char; var info: TGetJsonField; begin info.Json := Json; info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray, NormalizeBoolean); Json := info.Json; if WasString <> nil then WasString^ := info.WasString; if EndOfObject <> nil then EndOfObject^ := info.EndOfObject; if Len <> nil then Len^ := info.ValueLen; result := info.Value; end; {$endif PUREMORMOT2} { TGetJsonField } procedure TGetJsonField.GetJsonValue(var Text: RawUtf8); begin GetJsonField; FastSetString(Text, Value, ValueLen); end; function TGetJsonField.GetJsonInt64: Int64; begin GetJsonField; result := GetInt64(Value); end; procedure TGetJsonField.GetJsonField; var P, D: PUtf8Char; c4, surrogate, extra: PtrUInt; c: AnsiChar; {$ifdef CPUX86NOTPIC} tab: TJsonCharSet absolute JSON_CHARS; // not enough registers {$else} tab: PJsonCharSet; {$endif CPUX86NOTPIC} begin // see http://www.ietf.org/rfc/rfc4627.txt P := Json; Json := nil; // Json=nil indicates error or unexpected end (#0) Value := nil; ValueLen := 0; // ensure returns ValueLen=0 on invalid input (Json=nil) WasString := false; // not a string by default if P = nil then exit; while P^ <= ' ' do begin if P^ = #0 then exit; inc(P); end; {$ifndef CPUX86NOTPIC} tab := @JSON_CHARS; {$endif CPUX86NOTPIC} case JSON_TOKENS[P^] of jtFirstDigit: // '-', '0'..'9' begin // numerical value Value := P; if P^ = '0' then if (P[1] >= '0') and (P[1] <= '9') then // 0123 excluded by JSON! exit; repeat // loop all '-', '+', '0'..'9', '.', 'E', 'e' inc(P); until not (jcDigitFloatChar in tab[P^]); if P^ = #0 then exit; // a JSON number value should be followed by , } or ] ValueLen := P - Value; if (P^ <= ' ') and (P^ <> #0) then begin P^ := #0; // force numerical field with no trailing ' ' inc(P); end; end; jtDoubleQuote: // '"' begin // " -> unescape P^ into D^ inc(P); Value := P; // points to the unescaped JSON string WasString := true; while not (jcJsonStringMarker in tab[P^]) do // not [#0, '"', '\'] inc(P); // very fast parsing of most UTF-8 chars within "string" D := P; if P^ <> '"' then repeat // escape needed -> in-place unescape from P^ into D^ c := P^; if not (jcJsonStringMarker in tab[c]) then begin inc(P); D^ := c; inc(D); continue; // very fast parsing of most UTF-8 chars within "string" end; // P^ is either #0, '"' or '\' if c = '"' then // end of string break; if c = #0 then // premature ending (leaving Json=nil) exit; // unescape JSON text: process char after \ inc(P); // P^ was '\' here c := JSON_UNESCAPE[P^]; if c > JSON_UNESCAPE_UTF16 then begin inc(P); D^ := c; inc(D); continue; // direct un-escape of most \x values end else if c = JSON_UNESCAPE_UNEXPECTED then exit; // avoid \#0 potential buffer overflow issue or control char // JSON_UNESCAPE_UTF16: decode '\u0123' UTF-16 into UTF-8 // (inlined JsonUnicodeEscapeToUtf8() to optimize GetJsonField) c4 := (ConvertHexToBin[ord(P[1])] shl 12) or (ConvertHexToBin[ord(P[2])] shl 8) or (ConvertHexToBin[ord(P[3])] shl 4) or ConvertHexToBin[ord(P[4])]; // optimistic conversion (no check) inc(P, 5); case c4 of 0: // \u0000 is an invalid value (at least in our framework) begin D^ := '?'; inc(D); end; 1..$7f: begin D^ := AnsiChar(c4); inc(D); end; $80..$7ff: begin D[0] := AnsiChar($C0 or (c4 shr 6)); D[1] := AnsiChar($80 or (c4 and $3F)); inc(D, 2); end; UTF16_HISURROGATE_MIN..UTF16_LOSURROGATE_MAX: if PWord(P)^ = ord('\') + ord('u') shl 8 then begin inc(P); surrogate := (ConvertHexToBin[ord(P[1])] shl 12) or (ConvertHexToBin[ord(P[2])] shl 8) or (ConvertHexToBin[ord(P[3])] shl 4) or ConvertHexToBin[ord(P[4])]; case c4 of // inlined Utf16CharToUtf8() UTF16_HISURROGATE_MIN..UTF16_HISURROGATE_MAX: c4 := ((c4 - UTF16_SURROGATE_OFFSET) shl 10) or (surrogate xor UTF16_LOSURROGATE_MIN); UTF16_LOSURROGATE_MIN..UTF16_LOSURROGATE_MAX: c4 := ((surrogate - UTF16_SURROGATE_OFFSET) shl 10) or (c4 xor UTF16_LOSURROGATE_MIN); end; if c4 <= $7ff then c := #2 else if c4 <= $ffff then c := #3 else if c4 <= $1FFFFF then c := #4 else if c4 <= $3FFFFFF then c := #5 else c := #6; extra := ord(c) - 1; repeat D[extra] := AnsiChar((c4 and $3f) or $80); c4 := c4 shr 6; dec(extra); until extra = 0; D^ := AnsiChar(byte(c4) or UTF8_TABLE.FirstByte[ord(c)]); inc(D, ord(c)); inc(P, 5); end else begin // unexpected surrogate without its pair D^ := '?'; inc(D); end; else begin D[0] := AnsiChar($E0 or (c4 shr 12)); D[1] := AnsiChar($80 or ((c4 shr 6) and $3F)); D[2] := AnsiChar($80 or (c4 and $3F)); inc(D, 3); end; end; until false; // here P^='"' inc(P); D^ := #0; // make zero-terminated ValueLen := D - Value; end; jtSingleQuote: // extended/non-standard 'text' single quoted content begin inc(P); Value := P; // points to the unquoted string WasString := true; D := P; repeat c := P^; if c = #0 then exit else if c = '''' then if P[1] = '''' then inc(P) // unquote double quotes else break; D^ := c; inc(D); inc(P); until false; inc(P); D^ := #0; // make zero-terminated ValueLen := D - Value; end; jtNullFirstChar: // 'n' if (PInteger(P)^ = NULL_LOW) and (jcEndOfJsonValueField in tab[P[4]]) then // [#0, #9, #10, #13, ' ', ',', '}', ']'] // null -> returns nil and WasString=false inc(P, 4) else exit; jtFalseFirstChar: // 'f' if (PInteger(P + 1)^ = FALSE_LOW2) and (jcEndOfJsonValueField in tab[P[5]]) then // [#0, #9, #10, #13, ' ', ',', '}', ']'] begin // false -> returns 'false' and WasString=false Value := P; ValueLen := 5; inc(P, 5); end else exit; jtTrueFirstChar: // 't' if (PInteger(P)^ = TRUE_LOW) and (jcEndOfJsonValueField in tab[P[4]]) then // [#0, #9, #10, #13, ' ', ',', '}', ']'] begin // true -> returns 'true' and WasString=false Value := P; ValueLen := 4; inc(P, 4); end else exit; else // leave Json=nil on error (e.g. if a {...} or [...] was supplied) exit; end; while not (jcEndOfJsonFieldOr0 in tab[P^]) do // loop until #0 , ] } : delimiter inc(P); EndOfObject := P^; // ensure JSON value is zero-terminated, and continue after it if P^ <> #0 then begin P^ := #0; Json := P + 1; end else Json := P; end; function TryGotoEndOfComment(P: PUtf8Char): PUtf8Char; begin repeat result := P; // return input P^ = '/' if no comment was found inc(P); if P^ = '*' then // ignore /* comment */ begin repeat inc(P); if P^ = #0 then exit; until PWord(P)^ = ord('*') + ord('/') shl 8; result := GotoNextNotSpace(P + 2); end else if P^ = '/' then // ignore // comment begin P := GotoNextLine(P + 1); if P = nil then exit; result := GotoNextNotSpace(P); end else exit; until P^ <> '/'; // there may be other subsequent comments ;) end; procedure TGetJsonField.GetJsonFieldOrObjectOrArray( HandleValuesAsObjectOrArray, NormalizeBoolean: boolean); var P: PUtf8Char; parser: TJsonGotoEndParser; c: integer; begin P := Json; Value := nil; ValueLen := 0; if P = nil then exit; while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ = '/' then P := TryGotoEndOfComment(P); if HandleValuesAsObjectOrArray and (P^ in ['{', '[']) then begin WasString := false; Value := P; {%H-}parser.Init({strict=}false, nil); P := parser.GotoEnd(P); if P = nil then Value := nil else begin ValueLen := P - Value; while (P^ <= ' ') and (P^ <> #0) do inc(P); EndOfObject := P^; if P^ <> #0 then begin P^ := #0; // make zero-terminated as GetJsonField() inc(P); end; end; Json := P; end else begin Json := P; GetJsonField; if not WasString and NormalizeBoolean and (Value <> nil) then begin c := PInteger(Value)^; if c = TRUE_LOW then Value := pointer(SmallUInt32Utf8[1]) // normalize true -> 1 else if c = FALSE_LOW then Value := pointer(SmallUInt32Utf8[0]) // normalize false -> 0 else exit; ValueLen := 1; // result = '0' or '1' end; end; end; function GotoEndOfJsonString2(P: PUtf8Char; tab: PJsonCharSet): PUtf8Char; {$ifdef HASINLINE} inline; {$endif} begin // P[-1]='"' at function call repeat if not (jcJsonStringMarker in tab[P^]) then begin inc(P); // not [#0, '"', '\'] continue; // very fast parsing of most UTF-8 chars end; if (P^ = '"') or (P^ = #0) or (P[1] = #0) then // end of string/buffer, or buffer overflow detected as \#0 break; inc(P, 2); // P^ was '\' -> ignore \# ou \u0123 until false; result := P; // P^='"' at function return (if input was correct) end; function GotoEndOfJsonString(P: PUtf8Char): PUtf8Char; begin // P^='"' at function call and at successful function return result := GotoEndOfJsonString2(P + 1, @JSON_CHARS); end; function GotoEndJsonItemString(P: PUtf8Char): PUtf8Char; var tab: PJsonCharSet; begin // see TOrmTableJson.ParseAndConvert and TDocVariantData.InitArrayFromResults if P <> nil then repeat if P^ = '"' then begin inc(P); tab := @JSON_CHARS; repeat // inlined GotoEndOfJsonString2() if not (jcJsonStringMarker in tab[P^]) then begin inc(P); // not [#0, '"', '\'] continue; // very fast parsing of most UTF-8 chars end; if P^ = '"' then begin repeat inc(P); until not (P^ in [#1..' ']); result := P; exit; end else if (P^ = #0) or (P[1] = #0) then // end of string/buffer, or buffer overflow detected as \#0 break; inc(P, 2); // P^ was '\' -> ignore \# ou \u0123 until false; break; end else if P^ <= ' ' then begin if P^ = #0 then break; inc(P); continue; end; break; until false; result := nil; end; function GetJsonPropName(var Json: PUtf8Char; Len: PInteger; NoJsonUnescape: boolean): PUtf8Char; var P, Name: PUtf8Char; tab: PJsonCharSet; info: TGetJsonField; begin // should match GotoNextJsonObjectOrArray() and JsonPropNameValid() result := nil; // returns nil on invalid input P := Json; if P = nil then exit; while P^ <= ' ' do begin if P^ = #0 then begin Json := nil; // reached early end of input exit; end; inc(P); end; if P^ = '/' then P := TryGotoEndOfComment(P); Name := P + 1; tab := @JSON_CHARS; if P^ = '"' then begin // handle very efficiently the most common case of unescaped double quotes repeat inc(P); until jcJsonStringMarker in tab[P^]; // [#0, '"', '\'] if P^ <> '"' then // we need to handle a complex property name (seldom encoutered) if P^ = #0 then exit else if NoJsonUnescape then P := GotoEndOfJsonString2(P, tab) else begin // should be unescaped info.Json := Name - 1; info.GetJsonField; if (info.Value <> nil) and info.WasString and (info.EndOfObject = ':') then begin result := info.Value; if Len <> nil then Len^ := info.ValueLen; end; Json := info.Json; exit; end; end else if P^ = '''' then // single quotes won't handle nested quote character repeat inc(P); if P^ < ' ' then exit; until P^ = '''' else begin // e.g. '{age:{$gt:18}}' if not (jcJsonIdentifierFirstChar in tab[P^]) then exit; // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$'] repeat inc(P); until not (jcJsonIdentifier in tab[P^]); // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']'] if P^ = #0 then exit; dec(Name); if Len <> nil then Len^ := P - Name; info.EndOfObject := P^; P^ := #0; // Name should end with #0 if not (info.EndOfObject in [':', '=']) then // relaxed {age=10} syntax repeat inc(P); if P^ = #0 then exit; until P^ in [':', '=']; Json := P + 1; result := Name; exit; end; if Len <> nil then Len^ := P - Name; P^ := #0; // ensure Name is #0 terminated repeat inc(P); if P^ = #0 then exit; until P^ = ':'; Json := P + 1; result := Name; end; procedure GetJsonPropNameShort(var P: PUtf8Char; out PropName: ShortString); var Name: PAnsiChar; c: AnsiChar; tab: PJsonCharSet; label ok; begin // match GotoNextJsonObjectOrArray() and overloaded GetJsonPropName() PropName[0] := #0; if P = nil then exit; while P^ <= ' ' do begin if P^ = #0 then begin P := nil; exit; end; inc(P); end; Name := pointer(P); c := P^; if c = '/' then begin P := TryGotoEndOfComment(P); c := P^; end; if c = '"' then begin inc(Name); tab := @JSON_CHARS; repeat inc(P); until jcJsonStringMarker in tab[P^]; // end at [#0, '"', '\'] if P^ <> '"' then exit; ok: SetString(PropName, Name, P - Name); // note: won't unescape JSON strings repeat inc(P) until (P^ > ' ') or (P^ = #0); if P^ <> ':' then begin PropName[0] := #0; exit; end; inc(P); end else if c = '''' then begin // single quotes won't handle nested quote character inc(P); inc(Name); while P^ <> '''' do if P^ < ' ' then exit else inc(P); goto ok; end else begin // e.g. '{age:{$gt:18}}' tab := @JSON_CHARS; if not (jcJsonIdentifierFirstChar in tab[c]) then exit; // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$'] repeat inc(P); until not (jcJsonIdentifier in tab[P^]); // not ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']'] SetString(PropName, Name, P - Name); while (P^ <= ' ') and (P^ <> #0) do inc(P); if (P^ <> ':') and (P^ <> '=') then begin // allow both age:18 and age=18 pairs (very relaxed JSON syntax) PropName[0] := #0; exit; end; inc(P); end; end; function JsonRetrieveStringField(P: PUtf8Char; out Field: PUtf8Char; out FieldLen: integer; ExpectNameField: boolean): PUtf8Char; var tab: PJsonCharSet; begin result := nil; // retrieve string field if P = nil then exit; while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ <> '"' then exit; inc(P); Field := P; tab := @JSON_CHARS; while not (jcJsonStringMarker in tab[P^]) do // not [#0, '"', '\'] inc(P); // very fast parsing of most UTF-8 chars within "string" if P^ <> '"' then exit; // here P^ should be '"' FieldLen := P - Field; // check valid JSON delimiter repeat inc(P) until (P^ > ' ') or (P^ = #0); if ExpectNameField then begin if P^ <> ':' then exit; // invalid name field end else if not (P^ in ['}', ',']) then exit; // invalid value field result := P; // return either ':' for name field, or } , for value field end; function GlobalFindClass(classname: PUtf8Char; classnamelen: integer): TRttiCustom; var name: string; c: TClass; begin Utf8DecodeToString(classname, classnamelen, name); c := FindClass(name); if c = nil then result := nil else result := Rtti.RegisterClass(c); end; function JsonRetrieveObjectRttiCustom(var Json: PUtf8Char; AndGlobalFindClass: boolean): TRttiCustom; var tab: PNormTable; P, classname: PUtf8Char; classnamelen: integer; begin result := nil; P := GotoNextNotSpace(Json + 1); // at input, Json^ = '{' tab := @NormToUpperAnsi7; if IdemPChar(P, '"CLASSNAME":', tab) then inc(P, 12) else if IdemPChar(P, 'CLASSNAME:', tab) then inc(P, 10) else exit; // we expect woStoreClassName option to have been used P := JsonRetrieveStringField(P, classname, classnamelen, false); if P = nil then exit; // invalid (maybe too complex) Json string value Json := P; // Json^ is either } or , result := Rtti.FindName(classname, classnamelen, rkClass); if (result = nil) and AndGlobalFindClass then result := GlobalFindClass(classname, classnamelen); end; procedure GetJsonItemAsRawJson(var P: PUtf8Char; var result: RawJson; EndOfObject: PAnsiChar); var B: PUtf8Char; begin result := ''; if P = nil then exit; B := GotoNextNotSpace(P); P := GotoEndJsonItem(B); if P = nil then exit; FastSetString(RawUtf8(result), B, P - B); while (P^ <= ' ') and (P^ <> #0) do inc(P); if EndOfObject <> nil then EndOfObject^ := P^; if P^ <> #0 then //if P^=',' then repeat inc(P) until (P^ > ' ') or (P^ = #0); end; function GetJsonItemAsRawUtf8(var P: PUtf8Char; var output: RawUtf8; WasString: PBoolean; EndOfObject: PUtf8Char): boolean; var info: TGetJsonField; begin info.Json := P; info.GetJsonValue(output); P := info.Json; if WasString <> nil then WasString^ := info.WasString; if EndOfObject <> nil then EndOfObject^ := info.EndOfObject; result := info.Json <> nil; end; function GotoEndJsonItemStrict(P, PMax: PUtf8Char): PUtf8Char; var parser: TJsonGotoEndParser; begin {%H-}parser.Init({strict=}true, PMax); result := parser.GotoEnd(P); end; function GotoEndJsonItem(P, PMax: PUtf8Char): PUtf8Char; var parser: TJsonGotoEndParser; begin {%H-}parser.Init({strict=}false, PMax); result := parser.GotoEnd(P); end; function GotoNextJsonItem(P: PUtf8Char; NumberOfItemsToJump: cardinal; EndOfObject: PAnsiChar; PMax: PUtf8Char; Strict: boolean): PUtf8Char; var parser: TJsonGotoEndParser; begin {%H-}parser.Init(Strict, PMax); result := nil; // to notify unexpected end if NumberOfItemsToJump <> 0 then repeat P := parser.GotoEnd(P); if P = nil then exit; if EndOfObject <> nil then EndOfObject^ := P^; // return last jcEndOfJsonFieldOr0 if P^ <> #0 then inc(P); dec(NumberOfItemsToJump); until NumberOfItemsToJump = 0; result := P; end; function GotoNextJsonItem(P: PUtf8Char; var EndOfObject: AnsiChar): PUtf8Char; var parser: TJsonGotoEndParser; begin {%H-}parser.Init(false, nil); result := parser.GotoEnd(P, EndOfObject); end; function GetJsonObjectOrArray(P: PUtf8Char; EndOfObject: PUtf8Char; Len: PInteger): PUtf8Char; var parser: TJsonGotoEndParser; begin {%H-}parser.Init({strict=}false, nil); result := parser.GotoEnd(P); if result = nil then exit; if Len <> nil then Len^ := result - P; while (result^ <= ' ') and (result^ <> #0) do inc(result); if EndOfObject <> nil then EndOfObject^ := result^; if result^ <> #0 then begin result^ := #0; // make zero-terminated inc(result); end; end; function JsonArrayCount(P, PMax: PUtf8Char; Strict: boolean): integer; var parser: TJsonGotoEndParser; begin {%H-}parser.InitCount(Strict, PMax, stValue); if (parser.GotoEnd(P) = nil) and (parser.RootCount >= 0) then result := 0 // invalid input else result := parser.RootCount; // negative if PMax was reached end; function JsonArrayDecode(P: PUtf8Char; out Values: TPUtf8CharDynArray): boolean; var n, max: PtrInt; parser: TJsonGotoEndParser; begin result := false; max := 0; n := 0; {%H-}parser.Init({strict=}false, nil); P := GotoNextNotSpace(P); if P^ <> ']' then repeat if max = n then begin max := NextGrow(max); SetLength(Values, max); end; Values[n] := P; inc(n); P := parser.GotoEnd(P); if P = nil then exit; // invalid content, or #0 reached if P^ <> ',' then break; inc(P); until false; if P^ = ']' then begin if n <> 0 then DynArrayFakeLength(Values{%H-}, n); result := true; end else Values := nil; end; function JsonArrayItem(P: PUtf8Char; Index: integer): PUtf8Char; var parser: TJsonGotoEndParser; begin if P <> nil then begin P := GotoNextNotSpace(P); if P^ = '[' then begin {%H-}parser.Init({strict=}false, nil); P := GotoNextNotSpace(P + 1); if P^ <> ']' then repeat if Index <= 0 then begin result := P; exit; end; P := parser.GotoEnd(P); if (P = nil) or (P^ <> ',') then break; // invalid content or #0 reached inc(P); dec(Index); until false; end; end; result := nil; end; function JsonObjectPropCount(P, PMax: PUtf8Char; Strict: boolean): PtrInt; var parser: TJsonGotoEndParser; begin // is very efficiently inlined on FPC {%H-}parser.InitCount(Strict, PMax, stObjectName); P := parser.GotoEnd(P); result := parser.RootCount; if P = nil then // <0 means aborted when PMax or #0 was reached if result >= 0 then result := 0; // the JSON input was invalid end; function JsonObjectItem(P: PUtf8Char; const PropName: RawUtf8; PropNameFound: PRawUtf8): PUtf8Char; begin result := JsonObjectItem(P, pointer(PropName), length(PropName), PropNameFound); end; function JsonObjectItem(P: PUtf8Char; PropName: PUtf8Char; PropNameLen: PtrInt; PropNameFound: PRawUtf8): PUtf8Char; var name: ShortString; // no memory allocation nor P^ modification PropNameUpper: array[byte] of AnsiChar; parser: TJsonGotoEndParser; begin if P <> nil then begin P := GotoNextNotSpace(P); if PropNameLen > 0 then begin if PropName[PropNameLen - 1] = '*' then begin UpperCopy255Buf(PropNameUpper{%H-}, PropName, PropNameLen - 1)^ := #0; PropNameLen := 0; // mark 'PropName*' search end; if P^ = '{' then P := GotoNextNotSpace(P + 1); if P^ <> '}' then repeat GetJsonPropNameShort(P, name); if (name[0] = #0) or (name[0] > #200) then break; while (P^ <= ' ') and (P^ <> #0) do inc(P); if PropNameLen = 0 then // 'PropName*' begin name[ord(name[0]) + 1] := #0; // make ASCIIZ if IdemPChar(@name[1], PropNameUpper) then begin if PropNameFound <> nil then FastSetString(PropNameFound^, @name[1], ord(name[0])); result := P; exit; end; end else if IdemPropName(name, PropName, PropNameLen) then begin result := P; exit; end; {%H-}parser.Init({strict=}false, nil); P := parser.GotoEnd(P); if (P = nil) or (P^ <> ',') then break; // invalid content, or #0 reached inc(P); until false; end; end; result := nil; end; function JsonObjectByPath(JsonObject, PropPath: PUtf8Char): PUtf8Char; var objName: ShortString; begin result := nil; if (JsonObject = nil) or (PropPath = nil) then exit; repeat GetNextItemShortString(PropPath, @objName, '.'); if objName[0] = #0 then exit; JsonObject := JsonObjectItem(JsonObject, @objName[1], ord(objName[0]), nil); if JsonObject = nil then exit; until PropPath = nil; // found full name scope result := JsonObject; end; function JsonObjectsByPath(JsonObject, PropPath: PUtf8Char): RawUtf8; var itemName, objName, propNameFound, objPath: RawUtf8; start, ending, obj: PUtf8Char; WR: TTextWriter; temp: TTextWriterStackBuffer; procedure AddFromStart(const name: RawUtf8); begin start := GotoNextNotSpace(start); ending := GotoEndJsonItem(start); if ending = nil then exit; if WR = nil then begin WR := TTextWriter.CreateOwnedStream(temp); WR.AddDirect('{'); end else WR.AddComma; WR.AddFieldName(name); while (ending > start) and (ending[-1] <= ' ') do dec(ending); // trim right WR.AddNoJsonEscape(start, ending - start); end; begin result := ''; if (JsonObject = nil) or (PropPath = nil) then exit; WR := nil; try repeat GetNextItem(PropPath, ',', itemName); if itemName = '' then break; if itemName[length(itemName)] <> '*' then // single property lookup begin start := JsonObjectByPath(JsonObject, pointer(itemName)); if start <> nil then AddFromStart(itemName); end else // 'propname*' may append several properties begin objPath := ''; obj := pointer(itemName); repeat GetNextItem(obj, '.', objName); if objName = '' then exit; propNameFound := ''; JsonObject := JsonObjectItem(JsonObject, objName, @propNameFound); if JsonObject = nil then exit; if obj = nil then begin // found full name scope start := JsonObject; repeat AddFromStart(objPath + propNameFound); ending := GotoNextNotSpace(ending); if ending^ <> ',' then break; propNameFound := ''; start := JsonObjectItem( GotoNextNotSpace(ending + 1), objName, @propNameFound); until start = nil; break; end else objPath := objPath + objName + '.'; until false; end; until PropPath = nil; if WR <> nil then begin WR.AddDirect('}'); WR.SetText(result); end; finally WR.Free; end; end; function JsonObjectAsJsonArrays(Json: PUtf8Char; out keys, values: RawUtf8): integer; var wk, wv: TTextWriter; kb, ke, vb, ve: PUtf8Char; temp1, temp2: TTextWriterStackBuffer; parser: TJsonGotoEndParser; n: integer; begin result := -1; if (Json = nil) or (Json^ <> '{') then exit; parser.Init({strict=}false, nil); n := 0; wk := TTextWriter.CreateOwnedStream(temp1); wv := TTextWriter.CreateOwnedStream(temp2); try wk.AddDirect('['); wv.AddDirect('['); kb := Json + 1; repeat ke := parser.GotoEnd(kb); if (ke = nil) or (ke^ <> ':') then exit; // invalid input content vb := ke + 1; ve := parser.GotoEnd(vb); if (ve = nil) or not (ve^ in [',', '}']) then exit; wk.AddNoJsonEscape(kb, ke - kb); wk.AddComma; wv.AddNoJsonEscape(vb, ve - vb); wv.AddComma; kb := ve + 1; inc(n); until ve^ = '}'; wk.CancelLastComma(']'); wk.SetText(keys); wv.CancelLastComma(']'); wv.SetText(values); result := n; // success finally wv.Free; wk.Free; end; end; function DoRemoveComment(P: PUtf8Char): PUtf8Char; {$ifdef HASINLINE} inline; {$endif} begin result := P + 1; case result^ of '/': begin // this is // comment - replace by ' ' dec(result); repeat result^ := ' '; inc(result) until result^ in [#0, #10, #13]; if result^ <> #0 then inc(result); end; '*': begin // this is /* comment - replace by ' ' but keep CRLF result[-1] := ' '; repeat if not (result^ in [#10, #13]) then result^ := ' '; // keep CRLF for line numbering (e.g. for error) inc(result); if PWord(result)^ = ord('*') + ord('/') shl 8 then begin PWord(result)^ := $2020; inc(result, 2); break; end; until result^ = #0; end; end; end; procedure RemoveCommentsFromJson(P: PUtf8Char); var PComma: PUtf8Char; begin // replace comments by ' ' characters which will be ignored by parser if P <> nil then while P^ <> #0 do begin case P^ of '"': begin P := GotoEndOfJsonString2(P + 1, @JSON_CHARS); if P^ <> '"' then exit; inc(P); end; '/': P := DoRemoveComment(P); ',': begin // replace trailing comma by space for strict JSON parsers PComma := P; repeat inc(P) until (P^ > ' ') or (P^ = #0); if P^ = '/' then P := DoRemoveComment(P); while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ in ['}', ']'] then PComma^ := ' '; // see https://github.com/synopse/mORMot/pull/349 end; else inc(P); end; end; end; function RemoveCommentsFromJson(const s: RawUtf8): RawUtf8; begin if PosExChar('/', s) = 0 then result := s else begin FastSetString(result, pointer(s), length(s)); RemoveCommentsFromJson(pointer(s)); // remove in-place end; end; function ParseEndOfObject(P: PUtf8Char; out EndOfObject: AnsiChar): PUtf8Char; var tab: PJsonCharSet; begin if P <> nil then begin tab := @JSON_CHARS; // mimics GetJsonField() while not (jcEndOfJsonFieldOr0 in tab[P^]) do inc(P); // not #0 , ] } : EndOfObject := P^; if P^ <> #0 then repeat inc(P); // ignore trailing , ] } and any successive spaces until (P^ > ' ') or (P^ = #0); end; result := P; end; function GetSetNameValue(Names: PShortString; MinValue, MaxValue: integer; var P: PUtf8Char; out EndOfObject: AnsiChar): QWord; var info: TGetJsonField; tmp: shortstring; begin result := 0; if (P = nil) or (Names = nil) or (MinValue < 0) or (MaxValue < 0) then exit; while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ = '[' then begin // stored as JSON array repeat inc(P) until (P^ > ' ') or (P^ = #0); if P^ = ']' then inc(P) else begin info.Json := P; repeat info.GetJsonField; if (info.Value = nil) or not info.WasString then begin P := nil; // invalid input (expects a JSON array of strings) exit; end; SetNamesValue(Names, MinValue, MaxValue, info.Value, info.ValueLen, result); until info.EndOfObject = ']'; P := info.Json; if P = nil then exit; // avoid GPF below if already reached the input end end; P := ParseEndOfObject(P, EndOfObject); // mimics GetJsonField() if EndOfObject = #0 then P := nil; // as in mORMot 1 end else begin info.Json := P; info.GetJsonField; P := info.Json; if info.WasString then // stored as CSV text (e.g. from a .INI file) while info.Value <> nil do begin GetNextItemShortString(info.Value, @tmp); SetNamesValue(Names, MinValue, MaxValue, @tmp[1], ord(tmp[0]), result); end else // stored as a 64-bit unsigned integer SetQWord(info.Value, result); EndOfObject := info.EndOfObject; end; end; function GetSetNameValue(Info: PRttiInfo; var P: PUtf8Char; out EndOfObject: AnsiChar): QWord; var Names: PShortString; MinValue, MaxValue: integer; begin if (Info <> nil) and (Info^.Kind = rkSet) and (Info^.SetEnumType(Names, MinValue, MaxValue) <> nil) then result := GetSetNameValue(Names, MinValue, MaxValue, P, EndOfObject) else result := 0; end; function UrlEncodeJsonObject(const UriName: RawUtf8; ParametersJson: PUtf8Char; const PropNamesToIgnore: array of RawUtf8; IncludeQueryDelimiter: boolean): RawUtf8; var i, j: PtrInt; sep: AnsiChar; Params: TNameValuePUtf8CharDynArray; temp: TTextWriterStackBuffer; begin if ParametersJson = nil then result := UriName else with TTextWriter.CreateOwnedStream(temp) do try AddString(UriName); if (JsonDecode(ParametersJson, Params, true) <> nil) and (Params <> nil) then begin sep := '?'; for i := 0 to length(Params) - 1 do with Params[i] do begin for j := 0 to high(PropNamesToIgnore) do if IdemPropNameU(PropNamesToIgnore[j], Name.Text, Name.Len) then begin Name.Len := 0; break; end; if Name.Len = 0 then continue; // was within PropNamesToIgnore[] if IncludeQueryDelimiter then Add(sep); AddShort(Name.Text, Name.Len); Add('='); AddString(UrlEncode(Value.Text)); sep := '&'; IncludeQueryDelimiter := true; end; end; SetText(result); finally Free; end; end; function UrlEncodeJsonObject(const UriName, ParametersJson: RawUtf8; const PropNamesToIgnore: array of RawUtf8; IncludeQueryDelimiter: boolean): RawUtf8; var temp: TSynTempBuffer; begin temp.Init(ParametersJson); try result := UrlEncodeJsonObject( UriName, temp.buf, PropNamesToIgnore, IncludeQueryDelimiter); finally temp.Done; end; end; procedure QuotedStrJson(P: PUtf8Char; PLen: PtrInt; var result: RawUtf8; const aPrefix, aSuffix: RawUtf8); var temp: TTextWriterStackBuffer; Lp, Ls: PtrInt; D: PUtf8Char; begin if ((P = nil) or (PLen <= 0)) and (aPrefix = '') and (aSuffix = '') then result := '""' else if (pointer(result) = pointer(P)) or NeedsJsonEscape(P, PLen) then // use TJsonWriter.AddJsonEscape() for proper JSON escape with TJsonWriter.CreateOwnedStream(temp) do try AddString(aPrefix); AddDirect('"'); AddJsonEscape(P, PLen); AddDirect('"'); AddString(aSuffix); SetText(result); exit; finally Free; end else begin // direct allocation if no JSON escape is needed Lp := length(aPrefix); Ls := length(aSuffix); FastSetString(result, PLen + Lp + Ls + 2); D := pointer(result); // we checked dest result <> source P above if Lp > 0 then begin MoveFast(pointer(aPrefix)^, D^, Lp); inc(D, Lp); end; D^ := '"'; MoveFast(P^, D[1], PLen); inc(D, PLen); D[1] := '"'; if Ls > 0 then MoveFast(pointer(aSuffix)^, D[2], Ls); end; end; procedure QuotedStrJson(const aText: RawUtf8; var result: RawUtf8; const aPrefix, aSuffix: RawUtf8); begin QuotedStrJson(pointer(aText), Length(aText), result, aPrefix, aSuffix); end; function QuotedStrJson(const aText: RawUtf8): RawUtf8; begin QuotedStrJson(pointer(aText), Length(aText), result, '', ''); end; procedure JsonBufferReformat(P: PUtf8Char; out result: RawUtf8; Format: TTextWriterJsonFormat); var temp: array[word] of byte; // 64KB buffer begin if P <> nil then with TJsonWriter.CreateOwnedStream(@temp, SizeOf(temp)) do try AddJsonReformat(P, Format, nil); SetText(result); finally Free; end; end; function JsonReformat(const Json: RawUtf8; Format: TTextWriterJsonFormat): RawUtf8; var tmp: TSynTempBuffer; begin tmp.Init(Json); try JsonBufferReformat(tmp.buf, result, Format); finally tmp.Done; end; end; function JsonBufferReformatToFile(P: PUtf8Char; const Dest: TFileName; Format: TTextWriterJsonFormat): boolean; var F: TStream; temp: array[word] of word; // 128KB begin try F := TFileStreamEx.Create(Dest, fmCreate); try with TJsonWriter.Create(F, @temp, SizeOf(temp)) do try AddJsonReformat(P, Format, nil); FlushFinal; finally Free; end; result := true; finally F.Free; end; except on Exception do result := false; end; end; function JsonReformatToFile(const Json: RawUtf8; const Dest: TFileName; Format: TTextWriterJsonFormat): boolean; var tmp: TSynTempBuffer; begin tmp.Init(Json); try result := JsonBufferReformatToFile(tmp.buf, Dest, Format); finally tmp.Done; end; end; function Expect(var P: PUtf8Char; Pattern: PUtf8Char; PatternLen: PtrInt): boolean; var i: PtrInt; J: PUtf8Char; begin result := false; J := P; if J = nil then exit; while (J^ <= ' ') and (J^ <> #0) do inc(J); if PPtrInt(J)^ = PPtrInt(Pattern)^ then // PatternLen is at least 8 bytes long begin for i := SizeOf(PtrInt) to PatternLen - 1 do if J[i] <> Pattern[i] then exit; P := J + PatternLen; result := true; end; end; function IsNotExpandedBuffer(var P: PUtf8Char; PEnd: PUtf8Char; var FieldCount, RowCount: PtrInt): boolean; var RowCountPos: PUtf8Char; begin if not Expect(P, FIELDCOUNT_PATTERN, 14) then begin result := false; exit; end; FieldCount := GetNextItemCardinal(P, #0); if Expect(P, ROWCOUNT_PATTERN, 12) then RowCount := GetNextItemCardinal(P, #0) // initial "rowCount":xxxx else begin if PEnd = nil then PEnd := P + mormot.core.base.StrLen(P); // late search of ending RowCountPos := NotExpandedBufferRowCountPos(P, PEnd); if RowCountPos = nil then RowCount := -1 // no "rowCount":xxxx else RowCount := GetCardinal(RowCountPos); // trailing "rowCount":xxxx end; result := (FieldCount <> 0) and Expect(P, VALUES_PATTERN, 11); if result and (RowCount < 0) then begin RowCount := JsonArrayCount(P, PEnd) div FieldCount; // 900MB/s browse if RowCount <= 0 then RowCount := -1; // bad format -> no data end; end; function NotExpandedBufferRowCountPos(P, PEnd: PUtf8Char): PUtf8Char; var i: PtrInt; begin // search for "rowCount": at the end of the JSON buffer result := nil; if (PEnd <> nil) and (PEnd - P > 24) then for i := 1 to 24 do case PEnd[-i] of ']', ',': exit; ':': begin if CompareMemFixed(PEnd - i - 11, pointer(ROWCOUNT_PATTERN), 11) then result := PEnd - i + 1; exit; end; end; end; function GotoFieldCountExpanded(P: PUtf8Char): PUtf8Char; begin result := nil; while P^ <> '[' do if P^ = #0 then exit else inc(P); // need an array of objects repeat inc(P); if P^ = #0 then exit; until P^ in ['{', ']']; // go to object beginning result := P; end; function GetFieldCountExpanded(P: PUtf8Char): integer; var EndOfObject: AnsiChar; parser: TJsonGotoEndParser; begin result := 0; {%H-}parser.Init(false, nil); repeat P := parser.GotoEnd(P, EndOfObject{%H-}); // ignore Name P := parser.GotoEnd(P, EndOfObject); // ignore Value if P = nil then begin // unexpected end result := 0; exit; end; inc(result); if EndOfObject = '}' then break; // end of object until false; end; procedure FormatParams(const Format: RawUtf8; const Args, Params: array of const; JsonFormat: boolean; var Result: RawUtf8); var A, P: PtrInt; F, FDeb: PUtf8Char; isParam: AnsiChar; tmp: TTempUtf8; wasString: boolean; temp: TTextWriterStackBuffer; begin if (Format = '') or ((high(Args) < 0) and (high(Params) < 0)) then // no formatting to process, but may be a const // -> make unique since e.g. _JsonFmt() will parse it in-place FastSetString(Result, pointer(Format), length(Format)) else if high(Params) < 0 then // faster function with no ? FormatUtf8(Format, Args, Result) else if Format = '%' then // optimize raw conversion VarRecToUtf8(Args[0], Result) else // handle any number of parameters with minimal memory allocations with TJsonWriter.CreateOwnedStream(temp) do try A := 0; P := 0; F := pointer(Format); while F^ <> #0 do begin if (F^ <> '%') and (F^ <> '?') then begin // handle plain text between % ? markers FDeb := F; repeat inc(F); until F^ in [#0, '%', '?']; AddNoJsonEscape(FDeb, F - FDeb); if F^ = #0 then break; end; isParam := F^; inc(F); // jump '%' or '?' if (isParam = '%') and (A <= high(Args)) then begin // handle % substitution if Args[A].VType = vtObject then AddShort(ClassNameShort(Args[A].VObject)^) else Add(Args[A]); inc(A); end else if (isParam = '?') and (P <= high(Params)) then begin // handle ? substitution as JSON or SQL if JsonFormat then AddJsonEscape(Params[P]) // does the JSON magic including "quotes" else begin Add(':', '('); // markup for SQL parameter binding VarRecToTempUtf8(Params[P], tmp, @wasString); if wasString then AddQuotedStr(tmp.Text, tmp.Len, '''') // SQL quote else AddShort(tmp.Text, tmp.Len); // numbers if tmp.TempRawUtf8 <> nil then RawUtf8(tmp.TempRawUtf8) := ''; // release temp memory Add(')', ':'); end; inc(P); end else begin // no more available Args or Params -> add all remaining text AddNoJsonEscape(F, length(Format) - (F - pointer(Format))); break; end; end; SetText(Result); finally Free; end; end; function FormatSql(const Format: RawUtf8; const Args, Params: array of const): RawUtf8; begin FormatParams(Format, Args, Params, {json=}false, result); end; function FormatJson(const Format: RawUtf8; const Args, Params: array of const): RawUtf8; begin FormatParams(Format, Args, Params, {json=}true, result); end; {$ifndef PUREMORMOT2} function FormatUtf8(const Format: RawUtf8; const Args, Params: array of const; JsonFormat: boolean): RawUtf8; begin FormatParams(Format, Args, Params, JsonFormat, result); end; {$endif PUREMORMOT2} { ********** Low-Level JSON Serialization for all TRttiParserType } // some methods defined here for proper inlining procedure TJsonWriter.BlockAfterItem(Options: TTextWriterWriteObjectOptions); begin B[1] := ','; inc(B); if woHumanReadable in Options then AddCRAndIndent; end; procedure TJsonWriter.BlockBegin(Starter: AnsiChar; Options: TTextWriterWriteObjectOptions); begin if woHumanReadable in Options then begin AddCRAndIndent; inc(fHumanReadableLevel); end; Add(Starter); end; procedure TJsonWriter.BlockEnd(Stopper: AnsiChar; Options: TTextWriterWriteObjectOptions); begin if woHumanReadable in Options then begin dec(fHumanReadableLevel); AddCRAndIndent; end; B[1] := Stopper; inc(B); end; { TJsonSaveContext } procedure TJsonSaveContext.Init(WR: TJsonWriter; WriteOptions: TTextWriterWriteObjectOptions; Rtti: TRttiCustom); begin W := WR; if Rtti <> nil then WriteOptions := WriteOptions + TRttiJson(Rtti).fIncludeWriteOptions; Options := WriteOptions; Info := Rtti; Prop := nil; end; procedure TJsonSaveContext.AddShort(PS: PShortString); begin W.Add('"'); if twoTrimLeftEnumSets in W.CustomOptions then W.AddTrimLeftLowerCase(PS) else W.AddShort(PS^); W.AddDirect('"'); end; procedure TJsonSaveContext.Add64(Value: PInt64; UnSigned: boolean); begin if woInt64AsHex in Options then if Value^ = 0 then W.Add('"', '"') else W.AddBinToHexDisplayLower(Value, SizeOf(Value^), '"') else if UnSigned then W.AddQ(PQWord(Value)^) else W.Add(Value^); end; procedure TJsonSaveContext.AddDateTime(Value: PDateTime; WithMS: boolean); var d: double; begin if woDateTimeWithMagic in Options then W.AddShorter(JSON_SQLDATE_MAGIC_QUOTE_STR) else W.Add('"'); d := unaligned(Value^); W.AddDateTime(d, WithMS); if woDateTimeWithZSuffix in Options then if not (twoDateTimeWithZ in W.CustomOptions) then // if not already done if frac(d) = 0 then // FireFox can't decode short form "2017-01-01Z" W.AddShort('T00:00:00Z') // the same pattern for date and dateTime else W.Add('Z'); W.AddDirect('"'); end; procedure TJsonSaveContext.AddShortBoolean(PS: PShortString; Value: boolean); begin AddShort(PS); W.Add(':'); W.Add(Value); end; procedure _JS_Null(Data: PBoolean; const Ctxt: TJsonSaveContext); var W: TJsonWriter; begin W := Ctxt.W; W.AddNull; end; procedure _JS_Boolean(Data: PBoolean; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add(Data^); end; procedure _JS_Byte(Data: PByte; const Ctxt: TJsonSaveContext); var W: TJsonWriter; begin W := Ctxt.W; W.AddU(Data^); end; procedure _JS_SmallInt(Data: PSmallInt; const Ctxt: TJsonSaveContext); var W: TJsonWriter; begin W := Ctxt.W; W.Add(Data^); end; procedure _JS_ShortInt(Data: PShortInt; const Ctxt: TJsonSaveContext); var W: TJsonWriter; begin W := Ctxt.W; W.Add(Data^); end; procedure _JS_Cardinal(Data: PCardinal; const Ctxt: TJsonSaveContext); var W: TJsonWriter; begin W := Ctxt.W; W.AddU(Data^); end; procedure _JS_Currency(Data: PInt64; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddCurr64(Data); end; procedure _JS_Double(Data: PDouble; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddDouble(unaligned(Data^)); end; procedure _JS_Extended(Data: PSynExtended; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddDouble({$ifndef TSYNEXTENDED80}unaligned{$endif}(Data^)); end; procedure _JS_Int64(Data: PInt64; const Ctxt: TJsonSaveContext); begin Ctxt.Add64(Data, {unsigned=}false); end; procedure _JS_Integer(Data: PInteger; const Ctxt: TJsonSaveContext); var W: TJsonWriter; begin W := Ctxt.W; W.Add(Data^); end; procedure _JS_QWord(Data: PInt64; const Ctxt: TJsonSaveContext); begin Ctxt.Add64(Data, {unsigned=}true); end; procedure _JS_RawByteString(Data: PRawByteString; const Ctxt: TJsonSaveContext); begin if (Data^ = '') or ((rcfIsRawBlob in Ctxt.Info.Cache.Flags) and not (woRawBlobAsBase64 in Ctxt.Options)) then Ctxt.W.AddNull else begin Ctxt.W.Add('"'); // no magic trailer as with mORMot 1 Ctxt.W.WrBase64(pointer(Data^), length(Data^), {withmagic=}false); Ctxt.W.AddDirect('"'); end; end; procedure _JS_RawJson(Data: PRawJson; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddRawJson(Data^); end; procedure _JS_RawUtf8(Data: PAnsiChar; const Ctxt: TJsonSaveContext); var cp: cardinal; begin Ctxt.W.Add('"'); Data := PPointer(Data)^; if Data <> nil then begin cp := Ctxt.Info.Cache.CodePage; if cp = CP_UTF8 then Ctxt.W.AddJsonEscape(Data, {len=}0) else Ctxt.W.AddAnyAnsiBuffer(Data, PStrLen(Data - _STRLEN)^, twJsonEscape, cp); end; Ctxt.W.AddDirect('"'); end; procedure _JS_Ansi(Data: PAnsiChar; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add('"'); Data := PPointer(Data)^; if Data <> nil then with PStrRec(Data - SizeOf(TStrRec))^ do // will handle any AnsiString, WinAnsiString or other CP Ctxt.W.AddAnyAnsiBuffer(Data, length, twJsonEscape, {$ifdef HASCODEPAGE} codePage {$else} Ctxt.Info.Cache.CodePage {$endif}); Ctxt.W.AddDirect('"'); end; procedure _JS_Single(Data: PSingle; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddSingle(Data^); end; procedure _JS_Unicode(Data: PPWord; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add('"'); Ctxt.W.AddJsonEscapeW(Data^); Ctxt.W.AddDirect('"'); end; procedure _JS_Char(Data: PAnsiChar; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add('"'); if Data^ <> #0 then // #0 will be serialized as "" Ctxt.W.AddJsonEscape(Data, 1); Ctxt.W.AddDirect('"'); end; procedure _JS_WideChar(Data: PWord; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add('"'); if Data^ <> 0 then Ctxt.W.AddJsonEscapeW(Data, 1); Ctxt.W.AddDirect('"'); end; procedure _JS_DateTime(Data: PDateTime; const Ctxt: TJsonSaveContext); begin Ctxt.AddDateTime(Data, {withms=}false); end; procedure _JS_DateTimeMS(Data: PDateTime; const Ctxt: TJsonSaveContext); begin Ctxt.AddDateTime(Data, {withms=}true); end; procedure _JS_GUID(Data: PGUID; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add(Data, '"'); end; procedure _JS_Hash(Data: pointer; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddBinToHexDisplayLower(Data, Ctxt.Info.Size, '"'); end; procedure _JS_Binary(Data: pointer; const Ctxt: TJsonSaveContext); begin if IsZeroSmall(Data, Ctxt.Info.BinarySize) then Ctxt.W.Add('"', '"') // serialize "" for 0 value else Ctxt.W.AddBinToHexDisplayLower(Data, Ctxt.Info.BinarySize, '"'); end; procedure _JS_TimeLog(Data: PInt64; const Ctxt: TJsonSaveContext); begin if woTimeLogAsText in Ctxt.Options then Ctxt.W.AddTimeLog(Data, '"') else Ctxt.Add64(Data, true); end; procedure _JS_UnixTime(Data: PInt64; const Ctxt: TJsonSaveContext); begin if woTimeLogAsText in Ctxt.Options then Ctxt.W.AddUnixTime(Data, '"') else Ctxt.Add64(Data, true); end; procedure _JS_UnixMSTime(Data: PInt64; const Ctxt: TJsonSaveContext); begin if woTimeLogAsText in Ctxt.Options then Ctxt.W.AddUnixMSTime(Data, {withms=}true, '"') else Ctxt.Add64(Data, true); end; procedure _JS_WinAnsi(Data: PWinAnsiString; const Ctxt: TJsonSaveContext); begin Ctxt.W.Add('"'); Ctxt.W.AddAnyAnsiBuffer(pointer(Data^), length(Data^), twJsonEscape, CP_WINANSI); Ctxt.W.AddDirect('"'); end; procedure _JS_Word(Data: PWord; const Ctxt: TJsonSaveContext); begin Ctxt.W.AddU(Data^); end; procedure _JS_Interface(Data: PInterface; const Ctxt: TJsonSaveContext); begin {$ifdef HASINTERFACEASTOBJECT} // interfaces can be saved/serialized as their own object instance, // but not restored/unserialized in _JL_Interface() if Data^ <> nil then Ctxt.W.WriteObject(Data^ as TObject) else {$endif HASINTERFACEASTOBJECT} Ctxt.W.AddNull; end; procedure _JS_PUtf8Char(Data: PPUtf8Char; const Ctxt: TJsonSaveContext); begin // PUtf8Char can be saved/serialized as their own UTF-8 content, // but not restored/unserialized in _JL_PUtf8Char() Ctxt.W.Add('"'); if Data^ <> nil then Ctxt.W.AddJsonEscape(Data^, {len=}0); Ctxt.W.AddDirect('"'); end; procedure _JS_ID(Data: PInt64; const Ctxt: TJsonSaveContext); var _str: ShortString; begin Ctxt.W.Add(Data^); if woIDAsIDstr in Ctxt.Options then begin Ctxt.W.BlockAfterItem(Ctxt.Options); if (Ctxt.Prop <> nil) and (Ctxt.Prop^.Name <> '') then begin Ansi7StringToShortString(Ctxt.Prop^.Name, _str); AppendShort('_str', _str); Ctxt.W.WriteObjectPropNameShort(_str, Ctxt.Options); end else Ctxt.W.WriteObjectPropNameShort('ID_str', Ctxt.Options); Ctxt.W.Add('"'); Ctxt.W.Add(Data^); Ctxt.W.AddDirect('"'); end; end; procedure _JS_Enumeration(Data: PByte; const Ctxt: TJsonSaveContext); var o: TTextWriterOptions; PS: PShortString; begin o := Ctxt.W.CustomOptions; if (Ctxt.Options * [woFullExpand, woHumanReadable, woEnumSetsAsText] <> []) or (o * [twoEnumSetsAsBooleanInRecord, twoEnumSetsAsTextInRecord] <> []) then begin PS := Ctxt.Info.Cache.EnumInfo^.GetEnumNameOrd(Data^); if twoEnumSetsAsBooleanInRecord in o then Ctxt.AddShortBoolean(PS, true) else Ctxt.AddShort(PS); if woHumanReadableEnumSetAsComment in Ctxt.Options then Ctxt.Info.Cache.EnumInfo^.GetEnumNameAll(Ctxt.W.fBlockComment, '', true); end else Ctxt.W.AddU(Data^); end; procedure _JS_Set(Data: PCardinal; const Ctxt: TJsonSaveContext); var PS: PShortString; i: cardinal; v: QWord; o: TTextWriterOptions; begin o := Ctxt.W.CustomOptions; if twoEnumSetsAsBooleanInRecord in o then begin // { "set1": true/false, .... } with proper indentation PS := Ctxt.Info.Cache.EnumList; Ctxt.W.BlockBegin('{', Ctxt.Options); i := 0; repeat if i >= Ctxt.Info.Cache.EnumMin then Ctxt.AddShortBoolean(PS, GetBitPtr(Data, i)); if i = Ctxt.Info.Cache.EnumMax then break; inc(i); Ctxt.W.BlockAfterItem(Ctxt.Options); inc(PByte(PS), PByte(PS)^ + 1); // next until false; Ctxt.W.BlockEnd('}', Ctxt.Options); end else if (Ctxt.Options * [woFullExpand, woHumanReadable, woEnumSetsAsText] <> []) or (twoEnumSetsAsTextInRecord in o) then begin // [ "set1", "set4", .... } on same line Ctxt.W.Add('['); if ((twoFullSetsAsStar in o) or (woHumanReadableFullSetsAsStar in Ctxt.Options)) and GetAllBits(Data^, Ctxt.Info.Cache.EnumMax + 1) then Ctxt.W.AddShorter('"*"') else begin PS := Ctxt.Info.Cache.EnumList; for i := 0 to Ctxt.Info.Cache.EnumMax do begin if (i >= Ctxt.Info.Cache.EnumMin) and GetBitPtr(Data, i) then begin Ctxt.AddShort(PS); Ctxt.W.AddComma; end; inc(PByte(PS), PByte(PS)^ + 1); // next end; Ctxt.W.CancelLastComma; end; Ctxt.W.AddDirect(']'); if woHumanReadableEnumSetAsComment in Ctxt.Options then Ctxt.Info.Cache.EnumInfo^.GetEnumNameAll( Ctxt.W.fBlockComment, '"*" or a set of ', true); end else begin // standard serialization as unsigned integer (up to 64 items) v := 0; MoveFast(Data^, v, Ctxt.Info.Size); Ctxt.W.AddQ(v); end; end; procedure _JS_Array(Data: PAnsiChar; const Ctxt: TJsonSaveContext); var n: integer; jsonsave: TRttiJsonSave; c: TJsonSaveContext; begin {%H-}c.Init(Ctxt.W, Ctxt.Options, Ctxt.Info.ArrayRtti); c.W.BlockBegin('[', c.Options); jsonsave := c.Info.JsonSave; // e.g. PT_JSONSAVE/PTC_JSONSAVE if Assigned(jsonsave) then begin // efficient JSON serialization n := Ctxt.Info.Cache.ItemCount; repeat jsonsave(Data, c); dec(n); if n = 0 then break; c.W.BlockAfterItem(c.Options); inc(Data, c.Info.Cache.Size); until false; end else // fallback to raw RTTI binary serialization with Base64 encoding c.W.BinarySaveBase64(Data, Ctxt.Info.Info, [rkArray], {withMagic=}true, {withcrc=}false); c.W.BlockEnd(']', c.Options); end; procedure _JS_DynArray_Custom(Data: pointer; const Ctxt: TJsonSaveContext); begin // TRttiJson.RegisterCustomSerializer() custom callback for each item TOnRttiJsonWrite(TRttiJson(Ctxt.Info).fJsonWriter)( Ctxt.W, Data, Ctxt.Options); end; procedure _JS_OneProp(var c: TJsonSaveContext; p: PRttiCustomProp; Data: PAnsiChar); {$ifdef HASINLINE} inline; {$endif} begin if (woHideSensitivePersonalInformation in c.Options) and (rcfSpi in p^.Value.Flags) then c.W.AddShorter('"***"') else if p^.OffsetGet >= 0 then begin // direct value write (record field or plain class property) c.Info := p^.Value; c.Prop := p; TRttiJsonSave(c.Info.JsonSave)(Data + p^.OffsetGet, c); end else // need to call a getter method p^.AddValueJson(c.W, Data, c.Options); end; type TCCHook = class(TObjectWithCustomCreate); // to access its protected methods procedure _JS_NonExpanded(var c: TJsonSaveContext; Data: PAnsiChar; n: integer); var v: PAnsiChar; item: TRttiCustom; p: PRttiCustomProp; f: integer; begin // {"fieldCount":2,"rowCount":20,"values":["f1","f2","1v1",1v2,"2v1",2v2...]} item := c.Info; c.W.BlockBegin('{', c.Options); c.W.AddShort('"fieldCount":'); c.W.AddU(item.Props.CountNonVoid); c.W.AddShort(',"rowCount":'); c.W.AddU(n); c.W.AddShort(',"values":['); c.W.AddString(item.Props.NamesAsJsonArray); // pre-computed - with trailing , if n <> 0 then repeat if item.Kind = rkClass then v := PPointer(Data)^ else v := Data; p := pointer(item.Props.List); f := item.Props.Count; repeat if p^.Name <> '' then begin if not (rcfHookWriteProperty in item.Flags) or not TCCHook(v).RttiWritePropertyValue(c.W, p, c.Options) then _JS_OneProp(c, p, v); c.W.AddComma; // no c.W.BlockAfterItem() if non-expanded end; inc(p); dec(f); until f = 0; inc(Data, item.Cache.Size); dec(n); until n = 0; c.W.CancelLastComma(']'); c.W.BlockEnd('}', c.Options); end; procedure _JS_DynArray(Data: PPointer; const Ctxt: TJsonSaveContext); var n, s: PtrInt; jsonsave: TRttiJsonSave; P: PAnsiChar; c: TJsonSaveContext; begin {%H-}c.Init(Ctxt.W, Ctxt.Options, Ctxt.Info.ArrayRtti); if (twoNonExpandedArrays in c.W.CustomOptions) and (c.Info <> nil) and (c.Info.Props.CountNonVoid > 0) and (Data^ <> nil) then begin // non-expanded format efficient serialization n := PDALen(PAnsiChar(Data^) - _DALEN)^ + _DAOFF; // length(Data) if n <> 1 then // expanded is fine for a single object array begin _JS_NonExpanded(c, Data^, n); exit; end; end; c.W.BlockBegin('[', c.Options); if Data^ <> nil then begin if TRttiJson(Ctxt.Info).fJsonWriter.Code <> nil then begin c.Info := Ctxt.Info; jsonsave := @_JS_DynArray_Custom; // redirect to custom callback end else if c.Info = nil then jsonsave := nil else jsonsave := c.Info.JsonSave; // e.g. PT_JSONSAVE/PTC_JSONSAVE if Assigned(jsonsave) then begin // efficient JSON serialization P := Data^; n := PDALen(P - _DALEN)^ + _DAOFF; // length(Data) s := Ctxt.Info.Cache.ItemSize; // c.Info may be nil repeat jsonsave(P, c); dec(n); if n = 0 then break; c.W.BlockAfterItem(c.Options); inc(P, s); until false; end else // fallback to raw RTTI binary serialization with Base64 encoding c.W.BinarySaveBase64(Data, Ctxt.Info.Info, [rkDynArray], {withMagic=}true, {withcrc=}false); end else if (woHumanReadableEnumSetAsComment in Ctxt.Options) and (c.Info <> nil) and (rcfHasNestedProperties in c.Info.Flags) then // void dynarray should include record/T*ObjArray fields as comment c.Info.Props.AsText(c.W.fBlockComment, true, 'array of {', '}'); c.W.BlockEnd(']', c.Options); end; procedure _JS_Variant(Data: PVarData; const Ctxt: TJsonSaveContext); forward; /// use pointer to allow any kind of Data^ type in _JS_*() functions // - typecast to TRttiJsonSave for proper function call const VARIANT_JSONSAVE: array[varEmpty .. varOleUInt] of pointer = ( {0} @_JS_Null, @_JS_Null, @_JS_SmallInt, @_JS_Integer, @_JS_Single, {5} @_JS_Double, @_JS_Currency, @_JS_DateTime, nil, nil, {10} nil, @_JS_Boolean, nil, nil, nil, {15} nil, @_JS_ShortInt, @_JS_Byte, @_JS_Word, @_JS_Cardinal, {20} @_JS_Int64, @_JS_QWord, @_JS_Integer, @_JS_Cardinal); // rkRecord and rkClass are handled in TRttiJson.SetParserType PT_JSONSAVE: array[TRttiParserType] of pointer = ( nil, @_JS_Array, @_JS_Boolean, @_JS_Byte, @_JS_Cardinal, @_JS_Currency, @_JS_Double, @_JS_Extended, @_JS_Int64, @_JS_Integer, @_JS_QWord, @_JS_RawByteString, @_JS_RawJson, @_JS_RawUtf8, nil, @_JS_Single, {$ifdef UNICODE} @_JS_Unicode {$else} @_JS_Ansi {$endif}, @_JS_Unicode, @_JS_DateTime, @_JS_DateTimeMS, @_JS_GUID, @_JS_Hash, @_JS_Hash, @_JS_Hash, nil, @_JS_TimeLog, @_JS_Unicode, @_JS_UnixTime, @_JS_UnixMSTime, @_JS_Variant, @_JS_Unicode, @_JS_WinAnsi, @_JS_Word, @_JS_Enumeration, @_JS_Set, nil, @_JS_DynArray, @_JS_Interface, @_JS_PUtf8Char, nil); PTC_JSONSAVE: array[TRttiParserComplexType] of pointer = ( nil, nil, nil, nil, @_JS_ID, @_JS_ID, @_JS_QWord, @_JS_QWord, @_JS_QWord); procedure _JS_Variant(Data: PVarData; const Ctxt: TJsonSaveContext); var vt: cardinal; cv: TSynInvokeableVariantType; save: TRttiJsonSave; begin repeat vt := Data^.VType; if vt <> varVariantByRef then break; Data := Data^.VPointer; until false; if vt <= high(VARIANT_JSONSAVE) then begin save := VARIANT_JSONSAVE[vt]; if Assigned(save) then begin save(@Data^.VAny, Ctxt); exit; end; end; case vt of // most common strings varString: {$ifdef HASCODEPAGE} _JS_Ansi(@Data^.VAny, Ctxt); {$else} // old Delphi can't use Ctxt.Info.Cache.CodePage Ctxt.W.AddText(RawByteString(Data^.VString), twJsonEscape); {$endif HASCODEPAGE} {$ifdef HASVARUSTRING} varUString, {$endif} varOleStr: _JS_Unicode(@Data^.VAny, Ctxt); else begin cv := FindSynVariantType(vt); // our custom types if cv <> nil then cv.ToJson(Ctxt.W, Data) else // unsupported or seldom used Ctxt.W.AddVariant(PVariant(Data)^, twJsonEscape, Ctxt.Options); end; end; end; procedure AppendExceptionLocation(w: TJsonWriter; e: ESynException); begin // call TDebugFile.FindLocationShort if mormot.core.log is used w.Add('"'); w.AddShort(GetExecutableLocation(e.RaisedAt)); w.Add('"'); end; // serialization of properties for both records and classes procedure _JS_RttiCustom(Data: PAnsiChar; const Ctxt: TJsonSaveContext); var nfo: TRttiJson; p: PRttiCustomProp; t: TClass; n: integer; flags: set of (isNotFirst, noStored, noDefault, noHook, noVoid, isHumanReadable); c: TJsonSaveContext; // dedicated context used for fields/properties begin c.W := Ctxt.W; c.Options := Ctxt.Options; nfo := TRttiJson(Ctxt.Info); if nfo.Kind = rkClass then begin if Data <> nil then Data := PPointer(Data)^; // class instances are accessed by reference if Data = nil then begin c.W.AddNull; // append 'null' for nil class instance exit; end; t := PClass(Data)^; // actual class of this instance if t <> nfo.ValueClass then nfo := TRttiJson(Rtti.RegisterClass(t)); // work on proper inherited class flags := []; if (woStoreStoredFalse in c.Options) or (rcfDisableStored in nfo.Flags) then include(flags, noStored); if not (woDontStoreDefault in c.Options) then include(flags, noDefault); if not (rcfHookWriteProperty in nfo.Flags) then include(flags, noHook); end else begin exclude(c.Options, woFullExpand); // not available for null or records flags := [noStored, noDefault, noHook]; end; if nfo.fJsonWriter.Code <> nil then // TRttiJson.RegisterCustomSerializer() begin // e.g. TOrm.RttiJsonWrite TOnRttiJsonWrite(nfo.fJsonWriter)(c.W, Data, c.Options); exit; end; if not (rcfHookWrite in nfo.Flags) or not TCCHook(Data).RttiBeforeWriteObject(c.W, c.Options) then begin // regular JSON serialization using nested fields/properties if not ((woDontStoreVoid in c.Options) or (twoIgnoreDefaultInRecord in c.W.CustomOptions)) then include(flags, noVoid); if woHumanReadable in c.Options then begin include(flags, isHumanReadable); c.W.BlockBegin('{', c.Options) end else c.W.Add('{'); c.Prop := pointer(nfo.Props.List); n := nfo.Props.Count; if (nfo.Kind = rkClass) and (c.Options * [woFullExpand, woStoreClassName, woStorePointer, woDontStoreInherited] <> []) then begin if woFullExpand in c.Options then begin c.W.AddInstanceName(TObject(Data), ':'); c.W.BlockBegin('{', c.Options); end; if woStoreClassName in c.Options then begin c.W.WriteObjectPropNameShort('ClassName', c.Options); c.W.AddDirect('"'); c.W.AddShort(ClassNameShort(PClass(Data)^)^); c.W.AddDirect('"'); if (c.Prop <> nil) or (woStorePointer in c.Options) then c.W.BlockAfterItem(c.Options); end; if woStorePointer in c.Options then begin c.W.WriteObjectPropNameShort('Address', c.Options); if nfo.ValueRtlClass = vcESynException then AppendExceptionLocation(c.W, ESynException(Data)) else c.W.AddPointer(PtrUInt(Data), '"'); if c.Prop <> nil then c.W.BlockAfterItem(c.Options); end; if woDontStoreInherited in c.Options then with nfo.Props do if NotInheritedIndex <> 0 then begin // List[NotInheritedIndex]..List[Count-1] is the last class level inc(c.Prop, NotInheritedIndex); dec(n, NotInheritedIndex); end; end; if n > 0 then begin // this is the main loop serializing Info.Props[] p := c.Prop; repeat if // handle Props.NameChange() set to Name='' to ignore this field (p^.Name <> '') and // handle woStoreStoredFalse flag and "stored" attribute in code ((p^.Stored = rpsTrue) or // most common case (noStored in flags) or ((p^.Stored = rpsGetter) and (p^.Prop.IsStoredGetter(pointer(Data))))) and // handle woDontStoreDefault flag over "default" attribute in code ((noDefault in flags) or (p^.OrdinalDefault = NO_DEFAULT) or not p^.ValueIsDefault(Data)) and // detect 0 numeric values and empty strings ((noVoid in flags) or not p^.ValueIsVoid(Data)) then begin // if we reached here, we should serialize this property if isNotFirst in flags then // append ',' and proper indentation if a field was just appended c.W.BlockAfterItem(c.Options); if isHumanReadable in flags then c.W.WriteObjectPropNameHumanReadable(pointer(p^.Name), length(p^.Name)) else c.W.AddProp(pointer(p^.Name), length(p^.Name)); if (noHook in flags) or not TCCHook(Data).RttiWritePropertyValue(c.W, p, c.Options) then _JS_OneProp(c, p, Data); include(flags, isNotFirst); end; dec(n); if n = 0 then break; inc(p); until false; end; if rcfHookWrite in nfo.Flags then TCCHook(Data).RttiAfterWriteObject(c.W, c.Options); if isHumanReadable in flags then c.W.BlockEnd('}', c.Options) else c.W.AddDirect('}'); if woFullExpand in c.Options then c.W.BlockEnd('}', c.Options); end; end; // most known RTL classes custom serialization procedure _JS_Objects(W: TJsonWriter; Value: PObject; Count: integer; Options: TTextWriterWriteObjectOptions); var ctxt: TJsonSaveContext; save: TRttiJsonSave; c, v: pointer; // reuse ctxt.Info if classes are the same (very likely) begin c := nil; save := nil; {%H-}ctxt.Init(W, Options, nil); W.BlockBegin('[', Options); if Count > 0 then repeat v := Value^; if v = nil then W.AddNull else begin v := PPointer(v)^; // check Value class if v <> c then begin // need to retrieve the RTTI c := v; ctxt.Info := Rtti.RegisterClass(TClass(v)); save := ctxt.Info.JsonSave; end; // this is where each object is serialized save(pointer(Value), ctxt); end; dec(Count); if Count = 0 then break; W.BlockAfterItem(Options); inc(Value); until false; W.BlockEnd(']', Options); end; procedure _JS_TList(Data: PList; const Ctxt: TJsonSaveContext); begin if Data^ = nil then Ctxt.W.AddNull else _JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, Ctxt.Options); end; procedure _JS_TObjectList(Data: PObjectList; const Ctxt: TJsonSaveContext); var o: TTextWriterWriteObjectOptions; begin if Data^ = nil then begin Ctxt.W.AddNull; exit; end; o := Ctxt.Options; if not (woObjectListWontStoreClassName in o) then include(o, woStoreClassName); _JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, o); end; procedure _JS_TCollection(Data: PCollection; const Ctxt: TJsonSaveContext); var item: TCollectionItem; i, last: PtrInt; c: TJsonSaveContext; // reuse same context for all collection items begin if Data^ = nil then begin Ctxt.W.AddNull; exit; end; // can't use AddObjects() since we don't have access to the TCollection list {%H-}c.Init(Ctxt.W, Ctxt.Options, Rtti.RegisterClass(Data^.ItemClass)); c.W.BlockBegin('[', c.Options); i := 0; last := Data^.Count - 1; if last >= 0 then repeat item := Data^.Items[i]; TRttiJsonSave(c.Info.JsonSave)(@item, c); if i = last then break; c.W.BlockAfterItem(c.Options); inc(i); until false; c.W.BlockEnd(']', c.Options); end; procedure _JS_TStrings(Data: PStrings; const Ctxt: TJsonSaveContext); var i, last: PtrInt; begin if Data^ = nil then begin Ctxt.W.AddNull; exit; end; Ctxt.W.BlockBegin('[', Ctxt.Options); i := 0; last := Data^.Count - 1; if last >= 0 then repeat Ctxt.W.Add('"'); Ctxt.W.AddJsonEscapeString(Data^.Strings[i]); Ctxt.W.AddDirect('"'); if i = last then break; Ctxt.W.BlockAfterItem(Ctxt.Options); inc(i); until false; Ctxt.W.BlockEnd(']', Ctxt.Options); end; procedure _JS_TRawUtf8List(Data: PRawUtf8List; const Ctxt: TJsonSaveContext); var i, last: PtrInt; u: PPUtf8CharArray; begin if Data^ = nil then begin Ctxt.W.AddNull; exit; end; Ctxt.W.BlockBegin('[', Ctxt.Options); i := 0; u := Data^.TextPtr; last := Data^.Count - 1; if last >= 0 then repeat Ctxt.W.Add('"'); Ctxt.W.AddJsonEscape(u[i]); Ctxt.W.AddDirect('"'); if i = last then break; Ctxt.W.BlockAfterItem(Ctxt.Options); inc(i); until false; Ctxt.W.BlockEnd(']', Ctxt.Options); end; procedure _JS_TSynList(Data: PSynList; const Ctxt: TJsonSaveContext); begin if Data^ = nil then Ctxt.W.AddNull else _JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, Ctxt.Options); end; procedure _JS_TSynObjectList(Data: PSynObjectList; const Ctxt: TJsonSaveContext); var o: TTextWriterWriteObjectOptions; begin if Data^ = nil then begin Ctxt.W.AddNull; exit; end; o := Ctxt.Options; if not (woObjectListWontStoreClassName in o) then include(o, woStoreClassName); _JS_Objects(Ctxt.W, pointer(Data^.List), Data^.Count, o); end; { ********** TJsonWriter class with proper JSON escaping and WriteObject() support } { TJsonWriter } procedure TJsonWriter.WriteObjectPropNameHumanReadable( PropName: PUtf8Char; PropNameLen: PtrInt); begin AddCRAndIndent; // won't do anything if has already been done AddProp(PropName, PropNameLen); // handle twoForceJsonExtended Add(' '); end; procedure TJsonWriter.WriteObjectPropNameShort(const PropName: ShortString; Options: TTextWriterWriteObjectOptions); begin if woHumanReadable in Options then WriteObjectPropNameHumanReadable(@PropName[1], ord(PropName[0])) else AddProp(@PropName[1], ord(PropName[0])); end; procedure TJsonWriter.WriteObjectAsString(Value: TObject; Options: TTextWriterWriteObjectOptions); var W: TJsonWriter; begin Add('"'); W := GetTempJsonWriter; W.WriteObject(Value, Options); AddJsonEscape(W); Add('"'); end; procedure TJsonWriter.AddDynArrayJsonAsString(aTypeInfo: PRttiInfo; var aValue; WriteOptions: TTextWriterWriteObjectOptions); var temp: TDynArray; W: TJsonWriter; begin Add('"'); temp.Init(aTypeInfo, aValue); W := GetTempJsonWriter; W.AddDynArrayJson(temp, WriteOptions); AddJsonEscape(W); Add('"'); end; procedure TJsonWriter.AddCRAndIndent; begin if fBlockComment <> '' then begin AddShorter(' // '); AddString(fBlockComment); fBlockComment := ''; end; inherited AddCRAndIndent; end; procedure TJsonWriter.AddPropJsonString(const PropName: ShortString; const Text: RawUtf8); begin AddProp(@PropName[1], ord(PropName[0])); AddJsonString(Text); // " + AddJsonEscape(Text) + " AddComma; end; procedure TJsonWriter.InternalAddFixedAnsi(Source: PAnsiChar; SourceChars: cardinal; AnsiToWide: PWordArray; Escape: TTextWriterKind); var c: cardinal; esc: byte; begin if SourceChars > 0 then repeat case Escape of // twJsonEscape or twOnSameLine only occur on c <= $7f twNone: repeat if B >= BEnd then FlushToStream; c := byte(Source^); inc(Source); if c > $7F then break; if c = 0 then exit; inc(B); B^ := AnsiChar(c); dec(SourceChars); if SourceChars = 0 then exit; until false; twJsonEscape: repeat if B >= BEnd then FlushToStream; c := byte(Source^); inc(Source); if c > $7F then break; if c = 0 then exit; esc := JSON_ESCAPE[c]; // c<>0 -> esc<>JSON_ESCAPE_ENDINGZERO if esc = JSON_ESCAPE_NONE then begin // no escape needed inc(B); B^ := AnsiChar(c); end else if esc = JSON_ESCAPE_UNICODEHEX then begin // characters below ' ', #7 e.g. -> \u0007 AddShorter('\u00'); AddByteToHex(c); end else Add('\', AnsiChar(esc)); // escaped as \ + b,t,n,f,r,\," dec(SourceChars); if SourceChars = 0 then exit; until false; else //twOnSameLine: repeat if B >= BEnd then FlushToStream; c := byte(Source^); inc(Source); if c > $7F then break; if c = 0 then exit; inc(B); if c < 32 then B^ := ' ' // on same line else B^ := AnsiChar(c); dec(SourceChars); if SourceChars = 0 then exit; until false; end; // handle c > $7F (no surrogate is expected in TSynAnsiFixedWidth charsets) c := AnsiToWide[c]; // convert FixedAnsi char into Unicode char if c > $7ff then begin B[1] := AnsiChar($E0 or (c shr 12)); B[2] := AnsiChar($80 or ((c shr 6) and $3F)); B[3] := AnsiChar($80 or (c and $3F)); inc(B, 3); end else begin B[1] := AnsiChar($C0 or (c shr 6)); B[2] := AnsiChar($80 or (c and $3F)); inc(B, 2); end; dec(SourceChars); until SourceChars = 0; end; destructor TJsonWriter.Destroy; begin inherited Destroy; fInternalJsonWriter.Free; end; function TJsonWriter.GetTempJsonWriter: TJsonWriter; begin if fInternalJsonWriter = nil then fInternalJsonWriter := TJsonWriter.CreateOwnedStream(4096, {noshare=}true) else fInternalJsonWriter.CancelAllAsNew; result := fInternalJsonWriter; end; procedure TJsonWriter.Add(P: PUtf8Char; Escape: TTextWriterKind); begin if P <> nil then case Escape of twNone: AddNoJsonEscape(P, StrLen(P)); twJsonEscape: AddJsonEscape(P); twOnSameLine: AddOnSameLine(P); end; end; procedure TJsonWriter.Add(P: PUtf8Char; Len: PtrInt; Escape: TTextWriterKind); begin if P <> nil then case Escape of twNone: AddNoJsonEscape(P, Len); twJsonEscape: AddJsonEscape(P, Len); twOnSameLine: AddOnSameLine(P, Len); end; end; procedure TJsonWriter.AddW(P: PWord; Len: PtrInt; Escape: TTextWriterKind); begin if P <> nil then case Escape of twNone: AddNoJsonEscapeW(P, Len); twJsonEscape: AddJsonEscapeW(P, Len); twOnSameLine: AddOnSameLineW(P, Len); end; end; procedure TJsonWriter.AddAnsiString(const s: AnsiString; Escape: TTextWriterKind); begin AddAnyAnsiBuffer(pointer(s), length(s), Escape, 0); end; procedure TJsonWriter.AddAnyAnsiString(const s: RawByteString; Escape: TTextWriterKind; CodePage: integer); var L: integer; begin L := length(s); if L = 0 then exit; if (L > 2) and (PInteger(s)^ and $ffffff = JSON_BASE64_MAGIC_C) then begin AddNoJsonEscape(pointer(s), L); // was marked as a BLOB content exit; end; if CodePage < 0 then {$ifdef HASCODEPAGE} CodePage := GetCodePage(s); {$else} CodePage := CP_ACP; // TSynAnsiConvert.Engine(0)=CurrentAnsiConvert {$endif HASCODEPAGE} AddAnyAnsiBuffer(pointer(s), L, Escape, CodePage); end; procedure EngineAppendUtf8(W: TJsonWriter; Engine: TSynAnsiConvert; P: PAnsiChar; Len: PtrInt; Escape: TTextWriterKind); var tmp: TSynTempBuffer; begin // explicit conversion using a temporary UTF-16 buffer on stack Engine.AnsiBufferToUnicode(tmp.Init(Len * 3), P, Len); // includes ending #0 W.AddW(tmp.buf, 0, Escape); tmp.Done; end; procedure TJsonWriter.AddAnyAnsiBuffer(P: PAnsiChar; Len: PtrInt; Escape: TTextWriterKind; CodePage: integer); var B: PUtf8Char; engine: TSynAnsiConvert; label utf8; begin if (P = nil) or (Len <= 0) then exit; if CodePage = CP_ACP then // CP_UTF8 is very likely on POSIX or LCL CodePage := Unicode_CodePage; // = CurrentAnsiConvert.CodePage case CodePage of CP_UTF8: // direct write of RawUtf8 content begin if Escape = twJsonEscape then Len := 0; // faster with no Len utf8: Add(PUtf8Char(P), Len, Escape); end; CP_RAWBYTESTRING: // direct write of RawByteString content as UTF-8 goto utf8; CP_UTF16: // direct write of UTF-16 content AddW(PWord(P), 0, Escape); CP_RAWBLOB: // RawBlob written with Base64 encoding begin AddShorter(JSON_BASE64_MAGIC_S); // \uFFF0 WrBase64(P, Len, {withMagic=}false); end; else begin // first handle trailing 7-bit ASCII chars, by quad B := pointer(P); if Len >= 4 then repeat if PCardinal(P)^ and $80808080 <> 0 then break; // break on first non ASCII quad inc(P, 4); dec(Len, 4); until Len < 4; if (Len > 0) and (P^ <= #127) then repeat inc(P); dec(Len); until (Len = 0) or (P^ > #127); if P <> pointer(B) then Add(B, P - B, Escape); if Len <= 0 then exit; // rely on explicit conversion for all remaining ASCII characters engine := TSynAnsiConvert.Engine(CodePage); if PClass(engine)^ = TSynAnsiFixedWidth then InternalAddFixedAnsi(P, Len, pointer(TSynAnsiFixedWidth(engine).AnsiToWide), Escape) else EngineAppendUtf8(self, engine, P, Len, Escape); end; end; end; procedure TJsonWriter.WrBase64(P: PAnsiChar; Len: PtrUInt; withMagic: boolean); var trailing, main, n: PtrUInt; begin if P = nil then Len := 0; if withMagic then if Len <= 0 then begin AddNull; // JSON null is better than "" for BLOBs exit; end else AddShorter(JSON_BASE64_MAGIC_QUOTE_S); // "\uFFF0 if Len > 0 then begin n := Len div 3; trailing := Len - n * 3; dec(Len, trailing); if BEnd - B > integer(n + 1) shl 2 then begin // will fit in available space in Buf -> fast in-buffer Base64 encoding n := Base64EncodeMain(@B[1], P, Len); // may use AVX2 on FPC x86_64 inc(B, n * 4); inc(P, n * 3); end else begin // bigger than available space in Buf -> do it per chunk FlushToStream; while Len > 0 do begin n := ((fTempBufSize - 4) shr 2) * 3; if Len < n then n := Len; main := Base64EncodeMain(PAnsiChar(fTempBuf), P, n); n := main * 4; if n < cardinal(fTempBufSize) - 4 then inc(B, n) else WriteToStream(fTempBuf, n); n := main * 3; inc(P, n); dec(Len, n); end; end; if trailing > 0 then begin Base64EncodeTrailing(@B[1], P, trailing); inc(B, 4); end; end; if withMagic then Add('"'); end; procedure TJsonWriter.BinarySaveBase64(Data: pointer; Info: PRttiInfo; Kinds: TRttiKinds; withMagic, withCrc: boolean); var temp: TSynTempBuffer; begin BinarySave(Data, temp, Info, Kinds, withCrc); WrBase64(temp.buf, temp.len, withMagic); temp.Done; end; procedure TJsonWriter.Add(const Format: RawUtf8; const Values: array of const; Escape: TTextWriterKind; WriteObjectOptions: TTextWriterWriteObjectOptions); var ValuesIndex: integer; S, F: PUtf8Char; begin if Format = '' then exit; if (Format = '%') and (high(Values) >= 0) then begin Add(Values[0], Escape); exit; end; ValuesIndex := 0; F := pointer(Format); repeat S := F; repeat if (F^ = #0) or (F^ = '%') then break; inc(F); until false; AddNoJsonEscape(S, F - S); if F^ = #0 then exit; // add next value as text instead of F^='%' placeholder if ValuesIndex <= high(Values) then // missing value will display nothing Add(Values[ValuesIndex], Escape, WriteObjectOptions); inc(F); inc(ValuesIndex); until false; end; procedure TJsonWriter.AddCsvUtf8(const Values: array of RawUtf8); var i: PtrInt; begin if length(Values) = 0 then exit; for i := 0 to high(Values) do begin Add('"'); AddJsonEscape(pointer(Values[i])); AddDirect('"', ','); end; CancelLastComma; end; procedure TJsonWriter.AddCsvConst(const Values: array of const); var i: PtrInt; begin if length(Values) = 0 then exit; for i := 0 to high(Values) do begin AddJsonEscape(Values[i]); AddComma; end; CancelLastComma; end; procedure TJsonWriter.Add(const Values: array of const); var i: PtrInt; begin for i := 0 to high(Values) do AddJsonEscape(Values[i]); end; procedure TJsonWriter.Add(const Values: array of const; Escape: TTextWriterKind); var i: PtrInt; begin for i := 0 to high(Values) do Add(Values[i], Escape); end; procedure TJsonWriter.AddQuotedStringAsJson(const QuotedString: RawUtf8); var L: integer; P, B: PUtf8Char; quote: AnsiChar; begin L := length(QuotedString); if L = 0 then exit; quote := QuotedString[1]; if (quote in ['''', '"']) and (QuotedString[L] = quote) then begin Add('"'); P := pointer(QuotedString); inc(P); repeat B := P; while P[0] <> quote do inc(P); if P[1] <> quote then break; // end quote inc(P); AddJsonEscape(B, P - B); inc(P); // ignore double quote until false; if P - B <> 0 then AddJsonEscape(B, P - B); Add('"'); end else // was not a quoted string AddNoJsonEscape(pointer(QuotedString), length(QuotedString)); end; procedure TJsonWriter.AddVariant(const Value: variant; Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions); var ctxt: TJsonSaveContext; cv: TSynInvokeableVariantType; v: PVarData; vt: cardinal; save: TRttiJsonSave; begin v := @Value; repeat vt := v^.VType; if vt <> varVariantByRef then break; v := v^.VPointer; until false; if vt <= high(VARIANT_JSONSAVE) then begin ctxt.W := self; ctxt.Options := WriteOptions; // other fields are just ignored save := VARIANT_JSONSAVE[vt]; if Assigned(save) then begin save(@v^.VAny, ctxt); exit; end; end; if vt = varString then AddText(RawByteString(v^.VString), Escape) else case vt of varOleStr {$ifdef HASVARUSTRING}, varUString{$endif}: AddTextW(v^.VAny, Escape); varAny: // rkEnumeration,rkSet,rkDynArray,rkClass,rkInterface,rkRecord,rkObject // from TRttiCustomProp.GetValueDirect/GetValueGetter AddRttiVarData(PRttiVarData(v)^, Escape, WriteOptions); varVariantByRef: AddVariant(PVariant(v^.VPointer)^, Escape, WriteOptions); varStringByRef: AddText(PRawByteString(v^.VAny)^, Escape); {$ifdef HASVARUSTRING} varUStringByRef, {$endif} varOleStrByRef: AddTextW(PPointer(v^.VAny)^, Escape) else begin cv := FindSynVariantType(vt); // our custom types if cv <> nil then cv.ToJson(self, v) else if not CustomVariantToJson(self, v, Escape) then // other custom raise EJsonException.CreateUtf8('%.AddVariant VType=%', [self, vt]); end; end; end; procedure TJsonWriter.AddTypedJson(Value, TypeInfo: pointer; WriteOptions: TTextWriterWriteObjectOptions); var ctxt: TJsonSaveContext; save: TRttiJsonSave; begin {%H-}ctxt.Init(self, WriteOptions, Rtti.RegisterType(TypeInfo)); if ctxt.Info <> nil then begin save := ctxt.Info.JsonSave; if Assigned(save) then save(Value, ctxt) else BinarySaveBase64(Value, TypeInfo, rkRecordTypes, {withMagic=}true); end else AddNull; // paranoid check end; procedure TJsonWriter.WriteObject(Value: TObject; WriteOptions: TTextWriterWriteObjectOptions); var ctxt: TJsonSaveContext; save: TRttiJsonSave; begin if Value <> nil then begin // Rtti.RegisterClass() may create fake RTTI if {$M+} was not used {%H-}ctxt.Init(self, WriteOptions, Rtti.RegisterClass(PClass(Value)^)); save := ctxt.Info.JsonSave; if Assigned(save) then begin save(@Value, ctxt); exit; end; end; AddNull; end; procedure TJsonWriter.AddRttiCustomJson(Value: pointer; RttiCustom: TObject; Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions); var ctxt: TJsonSaveContext; save: TRttiJsonSave; begin {%H-}ctxt.Init(self, WriteOptions, TRttiCustom(RttiCustom)); save := ctxt.Info.JsonSave; if Assigned(save) then save(Value, ctxt) else BinarySaveBase64(Value, ctxt.Info.Info, rkAllTypes, {magic=}Escape <> twOnSameLine); end; procedure TJsonWriter.AddRttiVarData(const Value: TRttiVarData; Escape: TTextWriterKind; WriteOptions: TTextWriterWriteObjectOptions); var V64: Int64; begin if Value.PropValueIsInstance then begin // from TRttiCustomProp.GetValueGetter if rcfGetOrdProp in Value.Prop.Value.Cache.Flags then begin // rkEnumeration,rkSet,rkDynArray,rkClass,rkInterface V64 := Value.Prop.Prop.GetOrdProp(Value.PropValue); AddRttiCustomJson(@V64, Value.Prop.Value, Escape, WriteOptions); end else // rkRecord,rkObject have no getter methods raise EJsonException.CreateUtf8('%.AddRttiVarData: unsupported % (%)', [self, Value.Prop.Value.Name, ToText(Value.Prop.Value.Kind)^]); end else // from TRttiCustomProp.GetValueDirect AddRttiCustomJson(Value.PropValue, Value.Prop.Value, Escape, WriteOptions); end; procedure TJsonWriter.AddText(const Text: RawByteString; Escape: TTextWriterKind); begin if Escape = twJsonEscape then Add('"'); {$ifdef HASCODEPAGE} AddAnyAnsiString(Text, Escape); {$else} Add(pointer(Text), length(Text), Escape); {$endif HASCODEPAGE} if Escape <> twJsonEscape then exit; B[1] := '"'; inc(B); end; procedure TJsonWriter.AddTextW(P: PWord; Escape: TTextWriterKind); begin if Escape = twJsonEscape then Add('"'); AddW(P, 0, Escape); if Escape <> twJsonEscape then exit; B[1] := '"'; inc(B); end; function TJsonWriter.AddJsonReformat(Json: PUtf8Char; Format: TTextWriterJsonFormat; EndOfObject: PUtf8Char): PUtf8Char; var objEnd: AnsiChar; Name, Value: PUtf8Char; NameLen: integer; ValueLen: PtrInt; tab: PJsonCharSet; begin result := nil; if Json = nil then exit; while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); case Json^ of '[': begin // array repeat inc(Json) until (Json^ = #0) or (Json^ > ' '); if Json^ = ']' then begin Add('['); inc(Json); end else begin if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddCRAndIndent; inc(fHumanReadableLevel); Add('['); repeat if Json = nil then exit; if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddCRAndIndent; Json := AddJsonReformat(Json, Format, @objEnd); if objEnd = ']' then break; AddDirect(objEnd); until false; dec(fHumanReadableLevel); if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddCRAndIndent; end; AddDirect(']'); end; '{': begin // object repeat inc(Json) until (Json^ = #0) or (Json^ > ' '); Add('{'); inc(fHumanReadableLevel); if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddCRAndIndent; if Json^ = '}' then repeat inc(Json) until (Json^ = #0) or (Json^ > ' ') else repeat // processs property name Name := GetJsonPropName(Json, @NameLen, {nounescape=}true); if Name = nil then exit; if (Format in [jsonUnquotedPropName, jsonUnquotedPropNameCompact]) and JsonPropNameValid(Name) then AddNoJsonEscape(Name, NameLen) else begin AddDirect('"'); if Format < jsonEscapeUnicode then AddNoJsonEscape(Name, NameLen) else if Format = jsonNoEscapeUnicode then AddNoJsonEscapeForcedNoUnicode(Name, NameLen) else AddNoJsonEscapeForcedUnicode(Name, NameLen); AddDirect('"'); end; if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddDirect(':', ' ') else AddDirect(':'); // recurcisvely process value while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); Json := AddJsonReformat(Json, Format, @objEnd); if objEnd = '}' then break; Add(objEnd); if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddCRAndIndent; until false; dec(fHumanReadableLevel); if Format in [jsonHumanReadable, jsonUnquotedPropName] then AddCRAndIndent; AddDirect('}'); end; '"': begin // string Value := Json; Json := GotoEndOfJsonString2(Json + 1, @JSON_CHARS); if Json^ <> '"' then exit; inc(Json); if Format < jsonEscapeUnicode then AddNoJsonEscape(Value, Json - Value) else if Format = jsonNoEscapeUnicode then AddNoJsonEscapeForcedNoUnicode(Value, Json - Value) else AddNoJsonEscapeForcedUnicode(Value, Json - Value); end; else begin // numeric value or true/false/null constant or MongoDB extended tab := @JSON_CHARS; if jcEndOfJsonFieldOr0 in tab[Json^] then exit; // #0 , ] } : Value := Json; ValueLen := 0; repeat inc(ValueLen); until jcEndOfJsonFieldOr0 in tab[Json[ValueLen]]; inc(Json, ValueLen); while (ValueLen > 0) and (Value[ValueLen - 1] <= ' ') do dec(ValueLen); AddShort(Value, ValueLen); end; end; if Json = nil then exit; while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); if EndOfObject <> nil then EndOfObject^ := Json^; if Json^ <> #0 then repeat inc(Json) until (Json^ = #0) or (Json^ > ' '); result := Json; end; function TJsonWriter.AddJsonToXML(Json: PUtf8Char; ArrayName, EndOfObject: PUtf8Char): PUtf8Char; var info: TGetJsonField; Name: PUtf8Char; n, c: integer; begin result := nil; if Json = nil then exit; while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); if Json^ = '/' then Json := TryGotoEndOfComment(Json); case Json^ of '[': begin repeat inc(Json); until (Json^ = #0) or (Json^ > ' '); if Json^ = ']' then Json := GotoNextNotSpace(Json + 1) else begin n := 0; repeat if Json = nil then exit; Add('<'); if ArrayName = nil then Add(n) else AddXmlEscape(ArrayName); Add('>'); Json := AddJsonToXML(Json, nil, @info.EndOfObject); Add('<', '/'); if ArrayName = nil then Add(n) else AddXmlEscape(ArrayName); Add('>'); inc(n); until info.EndOfObject = ']'; end; end; '{': begin repeat inc(Json); until (Json^ = #0) or (Json^ > ' '); if Json^ = '}' then Json := GotoNextNotSpace(Json + 1) else begin repeat Name := GetJsonPropName(Json); if Name = nil then exit; while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); if Json^ = '[' then // arrays are written as list of items, without root Json := AddJsonToXML(Json, Name, @info.EndOfObject) else begin Add('<'); AddXmlEscape(Name); Add('>'); Json := AddJsonToXML(Json, Name, @info.EndOfObject); Add('<', '/'); AddXmlEscape(Name); Add('>'); end; until info.EndOfObject = '}'; end; end; else begin // unescape the JSON content and write as UTF-8 escaped XML info.Json := Json; info.GetJsonField; if info.Value = nil then AddNull else begin c := PInteger(info.Value)^ and $ffffff; if (c = JSON_BASE64_MAGIC_C) or (c = JSON_SQLDATE_MAGIC_C) then inc(info.Value, 3); // ignore the Magic codepoint encoded as UTF-8 AddXmlEscape(info.Value); end; if EndOfObject <> nil then EndOfObject^ := info.EndOfObject; result := info.Json; exit; end; end; if Json <> nil then begin while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); if EndOfObject <> nil then EndOfObject^ := Json^; if Json^ <> #0 then repeat inc(Json); until (Json^ = #0) or (Json^ > ' '); end; result := Json; end; procedure TJsonWriter.AddJsonEscape(P: Pointer; Len: PtrInt); var i, start: PtrInt; {$ifdef CPUX86NOTPIC} tab: TNormTableByte absolute JSON_ESCAPE; {$else} tab: PByteArray; {$endif CPUX86NOTPIC} label noesc; begin if P = nil then exit; if Len = 0 then dec(Len); // -1 = no end = AddJsonEscape(P, 0) i := 0; {$ifndef CPUX86NOTPIC} tab := @JSON_ESCAPE; {$endif CPUX86NOTPIC} if tab[PByteArray(P)[i]] = JSON_ESCAPE_NONE then begin noesc: start := i; if Len < 0 then // fastest loop is with AddJsonEscape(P, 0) repeat inc(i); until tab[PByteArray(P)[i]] <> JSON_ESCAPE_NONE else repeat inc(i); until (i >= Len) or (tab[PByteArray(P)[i]] <> JSON_ESCAPE_NONE); inc(PByte(P), start); dec(i, start); if Len >= 0 then dec(Len, start); if BEnd - B <= i then AddNoJsonEscapeBig(P, i) else begin MoveFast(P^, B[1], i); inc(B, i); end; if (Len >= 0) and (i >= Len) then exit; end; repeat if B >= BEnd then FlushToStream; case tab[PByteArray(P)[i]] of // better codegen with no temp var JSON_ESCAPE_NONE: goto noesc; JSON_ESCAPE_ENDINGZERO: // #0 exit; JSON_ESCAPE_UNICODEHEX: begin // characters below ' ', #7 e.g. -> // 'u0007' PCardinal(B + 1)^ := ord('\') + ord('u') shl 8 + ord('0') shl 16 + ord('0') shl 24; inc(B, 4); PWord(B + 1)^ := TwoDigitsHexWB[PByteArray(P)[i]]; end; else // escaped as \ + b,t,n,f,r,\," PWord(B + 1)^ := (integer(tab[PByteArray(P)[i]]) shl 8) or ord('\'); end; inc(i); inc(B, 2); until (Len >= 0) and (i >= Len); end; procedure TJsonWriter.AddJsonEscapeString(const s: string); begin if s <> '' then {$ifdef UNICODE} AddJsonEscapeW(pointer(s), Length(s)); {$else} AddAnyAnsiString(s, twJsonEscape, 0); {$endif UNICODE} end; procedure TJsonWriter.AddJsonEscapeAnsiString(const s: AnsiString); begin AddAnyAnsiString(s, twJsonEscape, 0); end; procedure TJsonWriter.AddJsonEscapeW(P: PWord; Len: PtrInt); var i, c, s: PtrInt; esc: byte; tab: PByteArray; begin if P = nil then exit; if Len = 0 then Len := MaxInt; i := 0; while i < Len do begin s := i; tab := @JSON_ESCAPE; repeat c := PWordArray(P)[i]; if (c <= 127) and (tab[c] <> JSON_ESCAPE_NONE) then break; inc(i); until i >= Len; if i <> s then AddNoJsonEscapeW(@PWordArray(P)[s], i - s); if i >= Len then exit; c := PWordArray(P)[i]; if c = 0 then exit; esc := tab[c]; if esc = JSON_ESCAPE_ENDINGZERO then // #0 exit else if esc = JSON_ESCAPE_UNICODEHEX then begin // characters below ' ', #7 e.g. -> \u0007 AddShorter('\u00'); AddByteToHex(c); end else Add('\', AnsiChar(esc)); // escaped as \ + b,t,n,f,r,\," inc(i); end; end; procedure TJsonWriter.AddJsonEscape(const V: TVarRec); begin with V do case VType of vtPointer: AddNull; vtString, vtAnsiString, {$ifdef HASVARUSTRING}vtUnicodeString, {$endif} vtPChar, vtChar, vtWideChar, vtWideString, vtClass: begin Add('"'); case VType of vtString: if (VString <> nil) and (VString^[0] <> #0) then AddJsonEscape(@VString^[1], ord(VString^[0])); vtAnsiString: AddJsonEscape(VAnsiString); {$ifdef HASVARUSTRING} vtUnicodeString: AddJsonEscapeW(pointer(UnicodeString(VUnicodeString)), length(UnicodeString(VUnicodeString))); {$endif HASVARUSTRING} vtPChar: AddJsonEscape(VPChar); vtChar: AddJsonEscape(@VChar, 1); vtWideChar: AddJsonEscapeW(@VWideChar, 1); vtWideString: AddJsonEscapeW(VWideString); vtClass: AddClassName(VClass); end; AddDirect('"'); end; vtBoolean: Add(VBoolean); // 'true'/'false' vtInteger: Add(VInteger); vtInt64: Add(VInt64^); {$ifdef FPC} vtQWord: AddQ(V.VQWord^); {$endif FPC} vtExtended: AddDouble(VExtended^); vtCurrency: AddCurr64(VInt64); vtObject: WriteObject(VObject); vtVariant: AddVariant(VVariant^, twJsonEscape); end; end; procedure TJsonWriter.AddJsonEscape(Source: TJsonWriter); begin if Source.fTotalFileSize = 0 then AddJsonEscape(Source.fTempBuf, Source.B - Source.fTempBuf + 1) else AddJsonEscape(Pointer(Source.Text)); end; procedure TJsonWriter.AddNoJsonEscape(Source: TJsonWriter); begin if Source.fTotalFileSize = 0 then AddNoJsonEscape(Source.fTempBuf, Source.B - Source.fTempBuf + 1) else AddNoJsonEscapeUtf8(Source.Text); end; procedure TJsonWriter.AddNoJsonEscapeForcedUnicode(P: PUtf8Char; Len: PtrInt); var S, P2: PUtf8Char; c: cardinal; tab: PByteToWord; label nxt; begin if Len > 0 then repeat // handle 7-bit ASCII chars, by quad if possible S := P; if Len >= 4 then repeat if PCardinal(S)^ and $80808080 <> 0 then break; // break on first non ASCII quad inc(S, 4); dec(Len, 4); until Len < 4; if (Len > 0) and (S^ <= #127) then // some 1..3 trailing ASCII chars repeat inc(S); dec(Len); until (Len = 0) or (S^ > #127); P2 := P; P := S; dec(S, PtrUInt(P2)); if S <> nil then AddNoJsonEscape(P2, PtrUInt(S)); nxt:if Len = 0 then exit; // some characters needs UTF-16 \u#### Unicode encoding if B >= BEnd then FlushToStream; P2 := P; c := UTF8_TABLE.GetHighUtf8Ucs4(P); dec(Len, P - P2); if (Len < 0) or (c = 0) then break; tab := @TwoDigitsHexWBLower; if c <= $ffff then Utf16ToJsonUnicodeEscape(B, c, tab) else begin dec(c, $10000); // store as UTF-16 surrogates Utf16ToJsonUnicodeEscape(B, (c shr 10) or UTF16_HISURROGATE_MIN, tab); Utf16ToJsonUnicodeEscape(B, (c and $3FF) or UTF16_LOSURROGATE_MIN, tab); end; if P^ > #127 then goto nxt; until false; end; procedure TJsonWriter.AddNoJsonEscapeForcedNoUnicode(P: PUtf8Char; Len: PtrInt); var P2: PUtf8Char; begin if Len > 0 then repeat P2 := P; repeat if P^ <> '\' then // quickly search for \### escape marker begin inc(P); dec(Len); if Len = 0 then break; continue; end; if P[1] = 'u' then // found a \u#### pattern break; inc(P, 2); // ignore this \# two-chars escape block dec(Len, 2); if Len = 0 then break; if Len < 0 then exit; until false; if P <> P2 then AddNoJsonEscape(P2, P - P2); if Len <= 0 then exit; // some characters needs UTF-16 \u#### Unicode decoding if B >= BEnd then FlushToStream; P2 := P; inc(P); // P^ should point at 'u1234' just after \u1234 inc(B); P := JsonUnicodeEscapeToUtf8(B, P); // decode up to two UTF-16 surrogates dec(B); dec(Len, P - P2); until Len <= 0; end; procedure TJsonWriter.AddJsonString(const Text: RawUtf8); begin Add('"'); AddJsonEscape(pointer(Text)); B[1] := '"'; inc(B); end; procedure TJsonWriter.Add(const V: TVarRec; Escape: TTextWriterKind; WriteObjectOptions: TTextWriterWriteObjectOptions); begin with V do case VType of vtInteger: Add(VInteger); vtBoolean: if VBoolean then // normalize Add('1') else Add('0'); vtChar: Add(@VChar, 1, Escape); vtExtended: AddDouble(VExtended^); vtCurrency: AddCurr64(VInt64); vtInt64: Add(VInt64^); {$ifdef FPC} vtQWord: AddQ(VQWord^); {$endif FPC} vtVariant: AddVariant(VVariant^, Escape); vtString: if (VString <> nil) and (VString^[0] <> #0) then Add(@VString^[1], ord(VString^[0]), Escape); vtInterface, vtPointer: AddPointer(PtrUInt(VPointer)); vtPChar: Add(PUtf8Char(VPChar), Escape); vtObject: WriteObject(VObject, WriteObjectOptions); vtClass: AddClassName(VClass); vtWideChar: AddW(@VWideChar, 1, Escape); vtPWideChar: AddW(pointer(VPWideChar), StrLenW(VPWideChar), Escape); vtAnsiString: if VAnsiString <> nil then // expect RawUtf8 Add(VAnsiString, length(RawUtf8(VAnsiString)), Escape); vtWideString: if VWideString <> nil then AddW(VWideString, length(WideString(VWideString)), Escape); {$ifdef HASVARUSTRING} vtUnicodeString: if VUnicodeString <> nil then // convert to UTF-8 AddW(VUnicodeString, length(UnicodeString(VUnicodeString)), Escape); {$endif HASVARUSTRING} end; end; procedure TJsonWriter.AddJson(const Format: RawUtf8; const Args, Params: array of const); var temp: variant; begin _JsonFmt(Format, Args, Params, JSON_FAST, temp); AddVariant(temp, twJsonEscape); end; procedure TJsonWriter.AddJsonArraysAsJsonObject(keys, values: PUtf8Char); var k, v: PUtf8Char; parser: TJsonGotoEndParser; begin if (keys = nil) or (keys[0] <> '[') or (values = nil) or (values[0] <> '[') or (keys[1] = ']') or (values[1] = ']') then begin AddNull; exit; end; inc(keys); // jump initial [ inc(values); Add('{'); {%H-}parser.Init({strict=}false, nil); repeat k := parser.GotoEnd(keys); v := parser.GotoEnd(values); if (k = nil) or (v = nil) then break; // invalid JSON input AddNoJsonEscape(keys, k - keys); AddDirect(':'); AddNoJsonEscape(values, v - values); AddComma; if (k^ <> ',') or (v^ <> ',') then break; // reached the end of the input JSON arrays keys := k + 1; values := v + 1; until false; CancelLastComma('}'); end; procedure TJsonWriter.AddJsonEscape(const NameValuePairs: array of const); var a: integer; procedure WriteValue; begin case VarRecAsChar(NameValuePairs[a]) of ord('['): begin Add('['); while a < high(NameValuePairs) do begin inc(a); if VarRecAsChar(NameValuePairs[a]) = ord(']') then break; WriteValue; end; CancelLastComma(']'); end; ord('{'): begin Add('{'); while a < high(NameValuePairs) do begin inc(a); if VarRecAsChar(NameValuePairs[a]) = ord('}') then break; AddJsonEscape(NameValuePairs[a]); Add(':'); inc(a); WriteValue; end; CancelLastComma('}'); end else AddJsonEscape(NameValuePairs[a]); end; AddComma; end; begin Add('{'); a := 0; while a < high(NameValuePairs) do begin AddJsonEscape(NameValuePairs[a]); inc(a); AddDirect(':'); WriteValue; inc(a); end; CancelLastComma('}'); end; function TJsonWriter.AddRecordJson(Value: pointer; RecordInfo: PRttiInfo; WriteOptions: TTextWriterWriteObjectOptions): PtrInt; var ctxt: TJsonSaveContext; begin {%H-}ctxt.Init(self, WriteOptions, Rtti.RegisterType(RecordInfo)); if rcfHasNestedProperties in ctxt.Info.Flags then // we know the fields from text definition TRttiJsonSave(ctxt.Info.JsonSave)(Value, ctxt) else // fallback to binary serialization, trailing crc32c and Base64 encoding BinarySaveBase64(Value, RecordInfo, rkRecordTypes, {magic=}true); result := ctxt.Info.Size; end; procedure TJsonWriter.AddVoidRecordJson(RecordInfo: PRttiInfo; WriteOptions: TTextWriterWriteObjectOptions); var tmp: TSynTempBuffer; begin tmp.InitZero(RecordInfo.RecordSize); AddRecordJson(tmp.buf, RecordInfo, WriteOptions); tmp.Done; end; procedure TJsonWriter.AddDynArrayJson(var DynArray: TDynArray; WriteOptions: TTextWriterWriteObjectOptions); var ctxt: TJsonSaveContext; len, backup: PtrInt; hacklen: PDALen; begin len := DynArray.Count; if len = 0 then Add('[', ']') else begin {%H-}ctxt.Init(self, WriteOptions, DynArray.Info); hacklen := PDALen(PAnsiChar(DynArray.Value^) - _DALEN); backup := hacklen^; hacklen^ := len - _DAOFF; // may use ExternalCount -> ovewrite length(Array) _JS_DynArray(DynArray.Value, ctxt); hacklen^ := backup; // restore original length/capacity end; end; procedure TJsonWriter.AddDynArrayJson(var DynArray: TDynArrayHashed; WriteOptions: TTextWriterWriteObjectOptions); begin // needed if UNDIRECTDYNARRAY is defined (Delphi 2009+) AddDynArrayJson(PDynArray(@DynArray)^, WriteOptions); end; function TJsonWriter.AddDynArrayJson(Value: pointer; Info: TRttiCustom; WriteOptions: TTextWriterWriteObjectOptions): PtrInt; var temp: TDynArray; begin if Info.Kind <> rkDynArray then raise EDynArray.CreateUtf8('%.AddDynArrayJson: % is %, expected rkDynArray', [self, Info.Name, ToText(Info.Kind)^]); temp.InitRtti(Info, Value^); AddDynArrayJson(temp, WriteOptions); result := temp.Info.Cache.ItemSize; end; { ********** Low-Level JSON UnSerialization for all TRttiParserType } { TJsonParserContext } procedure TJsonParserContext.InitParser(P: PUtf8Char; Rtti: TRttiCustom; O: TJsonParserOptions; CV: PDocVariantOptions; ObjectListItemClass: TClass; RawUtf8Interning: TRawUtf8Interning); begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; Valid := true; Interning := RawUtf8Interning; if Rtti <> nil then O := O + TRttiJson(Rtti).fIncludeReadOptions; Options := O; if CV <> nil then begin DVO := CV^; CustomVariant := @DVO; end else if jpoHandleCustomVariants in O then begin if jpoAllowDouble in o then DVO := JSON_FAST_FLOAT else DVO := JSON_FAST; CustomVariant := @DVO; end else CustomVariant := nil; if jpoHandleCustomVariantsWithinString in O then include(DVO, dvoJsonObjectParseWithinString); Info := Rtti; Prop := nil; if ObjectListItemClass = nil then ObjectListItem := nil else ObjectListItem := mormot.core.rtti.Rtti.RegisterClass(ObjectListItemClass); end; {$ifdef USERECORDWITHMETHODS} function TJsonParserContext.Json: PUtf8Char; begin result := Get.Json; end; function TJsonParserContext.Value: PUtf8Char; begin result := Get.Value; end; function TJsonParserContext.ValueLen: PtrInt; begin result := Get.ValueLen; end; function TJsonParserContext.WasString: boolean; begin result := Get.WasString; end; function TJsonParserContext.EndOfObject: AnsiChar; begin result := Get.EndOfObject; end; {$endif USERECORDWITHMETHODS} function TJsonParserContext.ParseNext: boolean; begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonField; result := Json <> nil; Valid := result; end; function TJsonParserContext.ParseNextAny: boolean; begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonFieldOrObjectOrArray; result := Json <> nil; Valid := result; end; function TJsonParserContext.ParseUtf8: RawUtf8; begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonField; Valid := Json <> nil; Interning.Unique(result, Value, ValueLen) end; function TJsonParserContext.ParseString: string; begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}GetJsonField; Valid := Json <> nil; Utf8DecodeToString(Value, ValueLen, result); end; function TJsonParserContext.ParseInteger: Int64; begin if ParseNext then SetInt64(Value, result{%H-}) else result := 0; end; procedure TJsonParserContext.ParseEndOfObject; var P: PUtf8Char; begin if Valid then begin P := Json; if P^ <> #0 then P := mormot.core.json.ParseEndOfObject( P, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; Valid := P <> nil; end; end; function TJsonParserContext.ParseNull: boolean; var P: PUtf8Char; begin result := false; if Valid then if Json <> nil then begin P := GotoNextNotSpace(Json); {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; if PCardinal(P)^ = NULL_LOW then begin P := mormot.core.json.ParseEndOfObject( P + 4, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); if P <> nil then begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; result := true; end else Valid := false; end; end else result := true; // nil -> null end; function TJsonParserContext.ParseArray: boolean; var P: PUtf8Char; begin result := false; // no need to parse P := GotoNextNotSpace(Json); {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; if P^ = '[' then begin P := GotoNextNotSpace(P + 1); // ignore trailing [ if P^ = ']' then begin // void but valid array P := mormot.core.json.ParseEndOfObject( P + 1, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); Valid := P <> nil; {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; end else begin // we have a non void [...] array -> caller should parse it result := true; {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; end; end else Valid := ParseNull; // only not [...] value allowed is null end; function TJsonParserContext.ParseObject: boolean; var P: PUtf8Char; begin result := false; // no need to parse P := GotoNextNotSpace(Json); {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; if P^ = '{' then begin P := GotoNextNotSpace(P + 1); // ignore trailing { if P^ = '}' then begin // void but valid array P := mormot.core.json.ParseEndOfObject( P + 1, {$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); Valid := P <> nil; {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; end else begin // we have a non void {...} array -> caller should parse it result := true; {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P; end; end else Valid := ParseNull; // only not {...} value allowed is null end; function TJsonParserContext.ParseNewObject: TObject; begin if ObjectListItem = nil then begin Info := JsonRetrieveObjectRttiCustom({$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, jpoObjectListClassNameGlobalFindClass in Options); if (Info <> nil) and (Json^ = ',') then Json^ := '{' // to parse other properties as a regular Json object else begin Valid := false; result := nil; exit; end; end; result := TRttiJson(Info).ParseNewInstance(self); end; function TJsonParserContext.ParseObject(const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean): boolean; begin {$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := JsonDecode( Json, Names, Values, HandleValuesAsObjectOrArray); if Json = nil then Valid := false else ParseEndOfObject; result := Valid; end; procedure _JL_Boolean(Data: PBoolean; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := GetBoolean(Ctxt.Value); end; procedure _JL_Byte(Data: PByte; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := GetCardinal(Ctxt.Value); end; procedure _JL_Cardinal(Data: PCardinal; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := GetCardinal(Ctxt.Value); end; procedure _JL_Integer(Data: PInteger; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := GetInteger(Ctxt.Value); end; procedure _JL_Currency(Data: PInt64; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := StrToCurr64(Ctxt.Value); end; procedure _JL_Double(Data: PDouble; var Ctxt: TJsonParserContext); var err: integer; begin if Ctxt.ParseNext then begin unaligned(Data^) := GetExtended(Ctxt.Value, err); Ctxt.Valid := (Ctxt.Value = nil) or (err = 0); end; end; procedure _JL_Extended(Data: PSynExtended; var Ctxt: TJsonParserContext); var err: integer; begin if Ctxt.ParseNext then begin Data^ := GetExtended(Ctxt.Value, err); Ctxt.Valid := (Ctxt.Value = nil) or (err = 0); end; end; procedure _JL_Int64(Data: PInt64; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString and (Ctxt.ValueLen = SizeOf(Data^) * 2) then Ctxt.Valid := (jpoAllowInt64Hex in Ctxt.Options) and HexDisplayToBin(PAnsiChar(Ctxt.Value), pointer(Data), SizeOf(Data^)) else SetInt64(Ctxt.Value, Data^); end; procedure _JL_QWord(Data: PQWord; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString and (Ctxt.ValueLen = SizeOf(Data^) * 2) then Ctxt.Valid := (jpoAllowInt64Hex in Ctxt.Options) and HexDisplayToBin(PAnsiChar(Ctxt.Value), pointer(Data), SizeOf(Data^)) else SetQWord(Ctxt.Value, Data^); end; procedure _JL_RawByteString(Data: PRawByteString; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.Value = nil then // null Data^ := '' else if not Ctxt.WasString then Ctxt.Valid := false else if Base64MagicTryAndDecode(Ctxt.Value, Ctxt.ValueLen, Data^) then exit // base64-encoded, with magic or not else FastSetRawByteString(Data^, Ctxt.Value, Ctxt.ValueLen); // fallback as text end; procedure _JL_RawJson(Data: PRawJson; var Ctxt: TJsonParserContext); begin GetJsonItemAsRawJson(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, Data^, @Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); Ctxt.Valid := Ctxt.Json <> nil; end; procedure _JL_RawUtf8(Data: PRawByteString; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then // will handle RawUtf8 but also AnsiString, WinAnsiString or other CP if Ctxt.Info.Cache.CodePage = CP_UTF8 then Ctxt.Interning.Unique(RawUtf8(Data^), Ctxt.Value, Ctxt.ValueLen) else if Ctxt.Info.Cache.CodePage >= CP_RAWBLOB then Ctxt.Valid := false // paranoid check (RawByteString should handle it) else Ctxt.Info.Cache.Engine.Utf8BufferToAnsi(Ctxt.Value, Ctxt.ValueLen, Data^); end; procedure _JL_Single(Data: PSingle; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := GetExtended(Ctxt.Value); end; procedure _JL_String(Data: PString; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Utf8DecodeToString(Ctxt.Value, Ctxt.ValueLen, Data^); end; procedure _JL_SynUnicode(Data: PSynUnicode; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Utf8ToSynUnicode(Ctxt.Value, Ctxt.ValueLen, Data^); end; procedure _JL_Char(Data: PByte; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString then if Ctxt.ValueLen <> 0 then Data^ := ord(Ctxt.Value[0]) // get the first char of the input string else Data^ := 0 // _JS_Char serializes #0 as "" else Data^ := GetCardinal(Ctxt.Value); // allow serialization as integer end; procedure _JL_WideChar(Data: PWord; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString then Data^ := GetUtf8WideChar(Ctxt.Value) else Data^ := GetCardinal(Ctxt.Value); end; procedure _JL_DateTime(Data: PDateTime; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString then Iso8601ToDateTimePUtf8CharVar(Ctxt.Value, Ctxt.ValueLen, Data^) else Data^ := GetExtended(Ctxt.Value); // was propbably stored as double end; procedure _JL_GUID(Data: PByteArray; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Ctxt.Valid := TextToGuid(Ctxt.Value, Data) <> nil; end; procedure _JL_Hash(Data: PByte; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Ctxt.Valid := (Ctxt.ValueLen = Ctxt.Info.Size * 2) and HexDisplayToBin(PAnsiChar(Ctxt.Value), Data, Ctxt.Info.Size); end; procedure _JL_Binary(Data: PByte; var Ctxt: TJsonParserContext); var v: QWord; begin if Ctxt.ParseNext then if Ctxt.WasString then begin FillZeroSmall(Data, Ctxt.Info.Size); // BinarySize may be < Size if Ctxt.ValueLen > 0 then // "" -> is valid 0 Ctxt.Valid := (Ctxt.ValueLen = Ctxt.Info.BinarySize * 2) and HexDisplayToBin(PAnsiChar(Ctxt.Value), Data, Ctxt.Info.BinarySize); end else begin SetQWord(Ctxt.Value, v{%H-}); MoveFast(v, Data^, Ctxt.Info.Size); end; end; procedure _JL_TimeLog(Data: PQWord; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString then Data^ := Iso8601ToTimeLogPUtf8Char(Ctxt.Value, Ctxt.ValueLen) else SetQWord(Ctxt.Value, Data^); end; procedure _JL_UnicodeString(Data: pointer; var Ctxt: TJsonParserContext); begin Ctxt.ParseNext; {$ifdef HASVARUSTRING} if Ctxt.Valid then Utf8DecodeToUnicodeString(Ctxt.Value, Ctxt.ValueLen, PUnicodeString(Data)^); {$endif HASVARUSTRING} end; procedure _JL_UnixTime(Data: PQWord; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then if Ctxt.WasString then Data^ := TimeLogToUnixTime(Iso8601ToTimeLogPUtf8Char( Ctxt.Value, Ctxt.ValueLen)) else SetQWord(Ctxt.Value, Data^); end; procedure _JL_UnixMSTime(Data: PQWord; var Ctxt: TJsonParserContext); var dt: TDateTime; // for ms resolution begin if Ctxt.ParseNext then if Ctxt.WasString then begin Iso8601ToDateTimePUtf8CharVar(Ctxt.Value, Ctxt.ValueLen, dt); Data^ := DateTimeToUnixMSTime(dt); end else SetQWord(Ctxt.Value, Data^); end; procedure _JL_Variant(Data: PVariant; var Ctxt: TJsonParserContext); begin JsonToAnyVariant(Data^, Ctxt{$ifdef USERECORDWITHMETHODS}.Get{$endif}, Ctxt.CustomVariant, jpoAllowDouble in Ctxt.Options); Ctxt.Valid := Ctxt.Json <> nil; end; procedure _JL_WideString(Data: PWideString; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Utf8ToWideString(Ctxt.Value, Ctxt.ValueLen, Data^); end; procedure _JL_WinAnsi(Data: PRawByteString; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then WinAnsiConvert.Utf8BufferToAnsi(Ctxt.Value, Ctxt.ValueLen, Data^); end; procedure _JL_Word(Data: PWord; var Ctxt: TJsonParserContext); begin if Ctxt.ParseNext then Data^ := GetCardinal(Ctxt.Value); end; procedure _JL_Enumeration(Data: pointer; var Ctxt: TJsonParserContext); var v: PtrInt; err: integer; begin if Ctxt.ParseNext then begin if Ctxt.WasString then v := Ctxt.Info.Cache.EnumInfo.GetEnumNameValue(Ctxt.Value, Ctxt.ValueLen) else begin v := GetInteger(Ctxt.Value, err); if (err <> 0) or (PtrUInt(v) > Ctxt.Info.Cache.EnumMax) or (PtrUInt(v) < Ctxt.Info.Cache.EnumMin) then v := -1; end; if v < 0 then if jpoIgnoreUnknownEnum in Ctxt.Options then v := 0 else Ctxt.Valid := false; MoveFast(v, Data^, Ctxt.Info.Size); end; end; procedure _JL_Set(Data: pointer; var Ctxt: TJsonParserContext); var v: QWord; begin with Ctxt.Info.Cache do v := GetSetNameValue(EnumList, EnumMin, EnumMax, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); Ctxt.Valid := Ctxt.Json <> nil; MoveFast(v, Data^, Ctxt.Info.Size); end; function JsonLoadProp(Data: PAnsiChar; Prop: PRttiCustomProp; var Ctxt: TJsonParserContext): boolean; {$ifdef HASINLINE} inline; {$endif} var load: TRttiJsonLoad; begin Ctxt.Info := Prop^.Value; // caller will restore it afterwards Ctxt.Prop := Prop; load := Ctxt.Info.JsonLoad; if not Assigned(load) then Ctxt.Valid := false else if Prop^.OffsetSet >= 0 then if (rcfHookReadProperty in Ctxt.Info.Flags) and TCCHook(Data).RttiBeforeReadPropertyValue(@Ctxt, Prop) then // custom parsing method (e.g. TOrm nested TOrm properties) else // default fast parsing into the property/field memory load(Data + Prop^.OffsetSet, Ctxt) else // we need to call a setter Ctxt.ParsePropComplex(Data); Ctxt.Prop := nil; result := Ctxt.Valid; end; procedure _JL_RttiCustomProps(Data: PAnsiChar; var Ctxt: TJsonParserContext); var j: PUtf8Char; root: TRttiJson; prop: PRttiCustomProp; propname: PUtf8Char; p, propnamelen: integer; label no, nxt, any; begin // regular JSON unserialization using nested fields/properties j := GotoNextNotSpace(Ctxt.Json); if j^ <> '{' then begin no: Ctxt.Valid := false; exit; end; repeat inc(j); until not (j^ in [#1..' ']); if j^ <> '}' then begin Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := j; root := pointer(Ctxt.Info); // Ctxt.Info overriden in JsonLoadProp() prop := pointer(root.Props.List); for p := 1 to root.Props.Count do begin nxt: propname := GetJsonPropName( Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, @propnamelen); if (Ctxt.Json = nil) or (propname = nil) then goto no; // O(1) optimistic process of the property name, following RTTI order if prop^.NameMatch(propname, propnamelen) then if JsonLoadProp(Data, prop, Ctxt) then if Ctxt.EndOfObject = '}' then break else inc(prop) else break else if (Ctxt.Info.Kind = rkClass) and (propnamelen = 9) and // fast "ClassName" case sensitive match (PIntegerArray(propname)[0] = ord('C') + ord('l') shl 8 + ord('a') shl 16 + ord('s') shl 24) and (PIntegerArray(propname)[1] = ord('s') + ord('N') shl 8 + ord('a') shl 16 + ord('m') shl 24) and (propname[8] = 'e') then // woStoreClassName was used -> just ignore the class name begin Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextJsonItem( Ctxt.Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); if Ctxt.Json <> nil then goto nxt; goto no; end else begin // we didn't find the property in its natural place -> full lookup repeat prop := FindCustomProp(pointer(root.Props.List), propname, propnamelen, root.Props.Count); if prop = nil then // unexpected "prop": value if (rcfReadIgnoreUnknownFields in root.Flags) or (jpoIgnoreUnknownProperty in Ctxt.Options) then begin Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextJsonItem( Ctxt.Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject); if Ctxt.Json = nil then goto no; end else goto no else if not JsonLoadProp(Data, prop, Ctxt) then goto no; if Ctxt.EndOfObject = '}' then break; any: propname := GetJsonPropName( Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, @propnamelen); if (Ctxt.Json = nil) or (propname = nil) then goto no; until false; break; end; end; if Ctxt.Valid and (Ctxt.EndOfObject = ',') and ((rcfReadIgnoreUnknownFields in root.Flags) or (jpoIgnoreUnknownProperty in Ctxt.Options)) then goto any; Ctxt.Info := root; // restore end else // {} Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := j + 1; Ctxt.ParseEndOfObject; // mimics GetJsonField() - set Ctxt.EndOfObject end; procedure _JL_RttiCustom(Data: PAnsiChar; var Ctxt: TJsonParserContext); begin if Ctxt.Json <> nil then Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextNotSpace(Ctxt.Json); if TRttiJson(Ctxt.Info).fJsonReader.Code <> nil then begin // TRttiJson.RegisterCustomSerializer() - e.g. TOrm.RttiJsonRead if Ctxt.Info.Kind = rkClass then begin if PPointer(Data)^ = nil then // e.g. from _JL_DynArray for T*ObjArray PPointer(Data)^ := TRttiJson(Ctxt.Info).fNewInstance(Ctxt.Info); Data := PPointer(Data)^; // as expected by the callback end; TOnRttiJsonRead(TRttiJson(Ctxt.Info).fJsonReader)(Ctxt, Data) end else begin // always finalize and reset the existing values (in case of missing props) if Ctxt.Info.Kind = rkClass then begin if Ctxt.ParseNull then begin if not (jpoNullDontReleaseObjectInstance in Ctxt.Options) then FreeAndNil(PObject(Data)^); exit; end; if PPointer(Data)^ = nil then // e.g. from _JL_DynArray for T*ObjArray PPointer(Data)^ := TRttiJson(Ctxt.Info).fNewInstance(Ctxt.Info) else if (jpoClearValues in Ctxt.Options) and not (rcfClassMayBeID in Ctxt.Info.Flags) then Ctxt.Info.Props.FinalizeAndClearPublishedProperties(PPointer(Data)^); // class instances are accessed by reference, records are stored by value Data := PPointer(Data)^; if (rcfHookRead in Ctxt.Info.Flags) and TCCHook(Data).RttiBeforeReadObject(@Ctxt) then exit; end else begin if jpoClearValues in Ctxt.Options then Ctxt.Info.ValueFinalizeAndClear(Data); if Ctxt.ParseNull then exit; end; // regular JSON unserialization using nested fields/properties _JL_RttiCustomProps(Data, Ctxt); if rcfHookRead in Ctxt.Info.Flags then TCCHook(Data).RttiAfterReadObject; end; end; procedure _JL_RttiObjectWithID(Data: PAnsiChar; var Ctxt: TJsonParserContext); var P: PUtf8Char; begin P := Ctxt.Json; if P <> nil then // in-place replace trailing RowID -> ID for unserialization begin while (P^ <= ' ') and (P^ <> #0) do inc(P); if P^ = '{' then begin repeat inc(P); until (P^ > ' ') or (P^ = #0); if PInt64(P)^ and $00ffdfdfdfdfdfff = // case insensitive search ord('"') + ord('R') shl 8 + ord('O') shl 16 + ord('W') shl 24 + Int64(ord('I')) shl 32 + Int64(ord('D')) shl 40 + Int64(ord('"')) shl 48 then begin // "RowID" -> __{"ID" PCardinal(P)^ := $2020 + ord('{') shl 16 + ord('"') shl 24; Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P + 2; end else if PInt64(P)^ and $0000ffdfdfdfdfdf = ord('R') + ord('O') shl 8 + ord('W') shl 16 + ord('I') shl 24 + Int64(ord('D')) shl 32 + Int64(ord(':')) shl 40 then begin // RowID: -> __{ID: PCardinal(P)^ := $2020 + ord('{') shl 16 + ord('I') shl 24; Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := P + 2; end; end; end; _JL_RttiCustom(Data, Ctxt); // use default serialization end; procedure _JL_Array(Data: PAnsiChar; var Ctxt: TJsonParserContext); var n: integer; arrinfo: TRttiCustom; begin if not Ctxt.ParseArray then // detect void (i.e. []) or invalid array exit; if PCardinal(Ctxt.Json)^ = JSON_BASE64_MAGIC_QUOTE_C then // raw RTTI binary layout with a single Base64 encoded item Ctxt.Valid := Ctxt.ParseNext and (Ctxt.EndOfObject = ']') and (Ctxt.Value <> nil) and (PCardinal(Ctxt.Value)^ and $ffffff = JSON_BASE64_MAGIC_C) and BinaryLoadBase64(pointer(Ctxt.Value + 3), Ctxt.ValueLen - 3, Data, Ctxt.Info.Info, {uri=}false, [rkArray], {withcrc=}false) else begin // efficient load of all JSON items arrinfo := Ctxt.Info; Ctxt.Info := arrinfo.ArrayRtti; // nested context = item n := arrInfo.Cache.ItemCount; repeat TRttiJsonLoad(Ctxt.Info.JsonLoad)(Data, Ctxt); dec(n); if Ctxt.Valid then if (n > 0) and (Ctxt.EndOfObject = ',') then begin // continue with the next item inc(Data, arrinfo.Cache.ItemSize); continue; end else if (n = 0) and (Ctxt.EndOfObject = ']') then // reached end of arrray break; Ctxt.Valid := false; // unexpected end exit; until false; Ctxt.Info := arrinfo; end; Ctxt.ParseEndOfObject; // mimics GetJsonField() / Ctxt.ParseNext end; procedure _JL_DynArray_Custom(Data: PAnsiChar; var Ctxt: TJsonParserContext); begin // TRttiJson.RegisterCustomSerializer() custom callback for each item TOnRttiJsonRead(TRttiJson(Ctxt.Info).fJsonReader)(Ctxt, Data); end; function _JL_DynArray_FromResults(Data: PPointer; var Ctxt: TJsonParserContext): boolean; var fieldcount, rowcount, r, f: PtrInt; arrinfo, iteminfo: TRttiCustom; item: PAnsiChar; prop: PRttiCustomProp; props: PRttiCustomPropDynArray; begin // Not Expanded (more optimized) format as array of values // {"fieldCount":2,"values":["f1","f2","1v1",1v2,"2v1",2v2...],"rowCount":20} result := IsNotExpandedBuffer(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json, nil, fieldcount, rowcount); if not result then exit; // indicates not the expected format: caller will try Ctxt.ParseArray // 1. check rowcount and fieldcount Ctxt.Valid := false; if (rowcount < 0) or (fieldcount = 0) then exit; // 2. initialize the items lookup from the trailing field names arrinfo := Ctxt.Info; iteminfo := arrinfo.ArrayRtti; if (iteminfo = nil) or (iteminfo.Props.CountNonVoid = 0) then exit; // expect an array of objects (classes or records) SetLength(props, fieldcount); for f := 0 to fieldcount - 1 do begin if not Ctxt.ParseNext or not Ctxt.WasString then exit; // should start with field names prop := nil; if Ctxt.ValueLen <> 0 then begin prop := FindCustomProp(pointer(iteminfo.Props.List), Ctxt.Value, Ctxt.ValueLen, iteminfo.Props.Count); if (prop = nil) and (itemInfo.ValueRtlClass = vcObjectWithID) and (PInteger(Ctxt.Value)^ and $dfdfdfdf = ord('R') + ord('O') shl 8 + ord('W') shl 16 + ord('I') shl 24) and (PWord(Ctxt.Value + 4)^ and $ffdf = ord('D')) then prop := @iteminfo.Props.List[0]; // 'RowID' = first TObjectWithID field end; if (prop = nil) and not (jpoIgnoreUnknownProperty in Ctxt.Options) then exit; props[f] := prop; end; // 3. fill all nested items from incoming values Data := DynArrayNew(Data, rowcount, arrinfo.Cache.ItemSize); // alloc for r := 1 to rowcount do begin if iteminfo.Kind = rkClass then begin Ctxt.Info := iteminfo; // as in _JL_RttiCustom() Data^ := TRttiJson(iteminfo).fNewInstance(iteminfo); item := Data^; // class are accessed by reference if (rcfHookRead in iteminfo.Flags) and TCCHook(item).RttiBeforeReadObject(@Ctxt) then begin inc(Data); if Ctxt.Valid then continue else break; end; end else item := pointer(Data); // record (or object) are stored by value for f := 0 to fieldcount - 1 do if props[f] = nil then // skip jpoIgnoreUnknownProperty Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextJsonItem( Ctxt.Json, Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}EndOfObject) else if not JsonLoadProp(item, props[f], Ctxt) then begin Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := nil; break; end else if Ctxt.EndOfObject = '}' then break; if Ctxt.Json = nil then break; if rcfHookRead in iteminfo.Flags then TCCHook(item).RttiAfterReadObject; inc(PAnsiChar(Data), arrinfo.Cache.ItemSize); end; Ctxt.Valid := false; if Ctxt.Json <> nil then begin while not (Ctxt.Json^ in [#0, '}']) do inc(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json); if Ctxt.Json^ = '}' then begin // reached final ..],"rowCount":20} inc(Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json); Ctxt.Valid := true; end; end; Ctxt.Info := arrinfo; // restore end; procedure _JL_DynArray(Data: PAnsiChar; var Ctxt: TJsonParserContext); var load: TRttiJsonLoad; n, cap: PtrInt; arr: PPointer; arrinfo: TRttiCustom; begin arr := pointer(Data); if arr^ <> nil then Ctxt.Info.ValueFinalize(arr); // reset whole array variable if Ctxt.Json = nil then begin Ctxt.Valid := false; exit; end; Ctxt.{$ifdef USERECORDWITHMETHODS}Get.{$endif}Json := GotoNextNotSpace(Ctxt.Json); if (PCardinal(Ctxt.Json)^ <> ord('{') + ord('"') shl 8 + ord('f') shl 16 + ord('i') shl 24) or // FIELDCOUNT_PATTERN = '{"fieldCount":... not _JL_DynArray_FromResults(arr, Ctxt) then if not Ctxt.ParseArray then // detect void (i.e. []) or invalid array exit else if PCardinal(Ctxt.Json)^ = JSON_BASE64_MAGIC_QUOTE_C then // raw RTTI binary layout with a single Base64 encoded item Ctxt.Valid := Ctxt.ParseNext and (Ctxt.EndOfObject = ']') and (Ctxt.Value <> nil) and (PCardinal(Ctxt.Value)^ and $ffffff = JSON_BASE64_MAGIC_C) and BinaryLoadBase64(pointer(Ctxt.Value + 3), Ctxt.ValueLen - 3, Data, Ctxt.Info.Info, {uri=}false, [rkDynArray], {withcrc=}false) else begin // efficient load of all JSON items arrinfo := Ctxt.Info; if TRttiJson(arrinfo).fJsonReader.Code <> nil then load := @_JL_DynArray_Custom // custom callback else begin Ctxt.Info := arrinfo.ArrayRtti; if Ctxt.Info = nil then load := nil else begin load := Ctxt.Info.JsonLoad; if (@load = @_JL_RttiCustom) and (TRttiJson(Ctxt.Info).fJsonReader.Code = nil) and (Ctxt.Info.Kind <> rkClass) and (not (jpoClearValues in Ctxt.Options)) then load := @_JL_RttiCustomProps; // somewhat faster direct record load end; end; // initial guess of the JSON array count - will browse up to 64KB of input cap := abs(JsonArrayCount(Ctxt.Json, Ctxt.Json + JSON_PREFETCH)); if (cap = 0) or (not Assigned(load)) then begin Ctxt.Valid := false; exit; end; Data := DynArrayNew(arr, cap, arrinfo.Cache.ItemSize); // alloc zeroed mem // main JSON unserialization loop n := 0; repeat if n = cap then begin // grow if our initial guess was aborted due to huge input cap := NextGrow(cap); Data := DynArrayGrow(arr, cap, arrinfo.Cache.ItemSize) + (n * arrinfo.Cache.ItemSize); end; // unserialize one item load(Data, Ctxt); // will call _JL_RttiCustom() for T*ObjArray inc(n); if Ctxt.Valid then if Ctxt.EndOfObject = ',' then begin // continue with the next item inc(Data, arrinfo.Cache.ItemSize); continue; end else if Ctxt.EndOfObject = ']' then // reached end of arrray break; Ctxt.Valid := false; // unexpected end arrinfo.ValueFinalize(arr); // whole array clear on error exit; until false; if n <> cap then if n = 0 then FastDynArrayClear(arr^, arrinfo.Cache.ItemInfo) else DynArrayFakeLength(arr^, n); // faster than SetLength() Ctxt.Info := arrinfo; // restore end; Ctxt.ParseEndOfObject; // mimics GetJsonField() / Ctxt.ParseNext end; procedure _JL_Interface(Data: PInterface; var Ctxt: TJsonParserContext); begin // _JS_Interface() may have serialized the object instance properties, but we // can't unserialize it since we don't know which class to create Ctxt.Valid := Ctxt.ParseNull; Data^ := nil; end; procedure _JL_PUtf8Char(Data: PPUtf8Char; var Ctxt: TJsonParserContext); begin // _JS_PUtf8Char() may have been serialized as JSON string, but we can't // unserialize it since we can't allocate the memory and Ctxt.Json // input is transient by definition Ctxt.Valid := Ctxt.ParseNull; Data^ := nil; end; // defined here to have _JL_RawJson and _JL_Variant known procedure TJsonParserContext.ParsePropComplex(Data: pointer); var v: TRttiVarData; tmp: TObject; begin // handle special cases of a setter method case Info.Parser of ptClass: // for a class property: use a temp instance for the setter call begin if jpoSetterNoCreate in Options then Valid := false else begin tmp := TRttiJson(Info).fNewInstance(Info); try v.Prop := Prop; // JsonLoad() would reset Prop := nil TRttiJsonLoad(Info.JsonLoad)(@tmp, self); // JsonToObject(tmp) if not Valid then FreeAndNil(tmp) else begin v.Prop.Prop.SetOrdProp(Data, PtrInt(tmp)); if jpoSetterExpectsToFreeTempInstance in Options then FreeAndNil(tmp); end; except on Exception do tmp.Free; end; end; end; ptRawJson: // TRttiProp.SetValue() assume RawUtf8 -> dedicated RawJson code begin v.Data.VAny := nil; try _JL_RawJson(@v.Data.VAny, self); if Valid then Prop^.Prop.SetLongStrProp(Data, RawJson(v.Data.VAny)); finally FastAssignNew(v.Data.VAny); end; end; ptSet: // use a local temp variable before calling the setter begin v.Data.VInt64 := 0; _JL_Set(@v.Data.VInt64, self); if Valid then Prop^.Prop.SetOrdProp(Data, v.Data.VInt64); end; else // call the getter via TRttiProp.SetValue() of a transient TRttiVarData begin v.VType := 0; try _JL_Variant(@v, self); // VariantLoadJson() over Ctxt Valid := Valid and Prop^.Prop.SetValue(Data, variant(v)); finally VarClearProc(v.Data); end; end; end; end; procedure _JL_TObjectList(Data: PObjectList; var Ctxt: TJsonParserContext); var root: TRttiCustom; item: TObject; begin if Data^ = nil then begin Ctxt.Valid := Ctxt.ParseNull; exit; end; Data^.Clear; if Ctxt.ParseNull or not Ctxt.ParseArray then exit; root := Ctxt.Info; Ctxt.Info := Ctxt.ObjectListItem; repeat item := Ctxt.ParseNewObject; if item = nil then break; Data^.Add(item); until Ctxt.EndOfObject = ']'; Ctxt.Info := root; Ctxt.ParseEndOfObject; end; procedure _JL_TCollection(Data: PCollection; var Ctxt: TJsonParserContext); var root: TRttiJson; load: TRttiJsonLoad; item: TCollectionItem; begin if Data^ = nil then begin Ctxt.Valid := Ctxt.ParseNull; exit; end; Data^.BeginUpdate; try Data^.Clear; if Ctxt.ParseNull or not Ctxt.ParseArray then exit; root := TRttiJson(Ctxt.Info); load := nil; repeat item := Data^.Add; if not Assigned(load) then begin if root.fCollectionItemRtti = nil then begin // RegisterCollection() was not called -> compute after Data^.Add root.fCollectionItem := PPointer(item)^; root.fCollectionItemRtti := Rtti.RegisterClass(PClass(item)^); end; Ctxt.Info := root.fCollectionItemRtti; load := Ctxt.Info.JsonLoad; end; load(@item, Ctxt); until (not Ctxt.Valid) or (Ctxt.EndOfObject = ']'); Ctxt.Info := root; Ctxt.ParseEndOfObject; finally Data^.EndUpdate; end; end; procedure _JL_TSynObjectList(Data: PSynObjectList; var Ctxt: TJsonParserContext); var root: TRttiCustom; item: TObject; begin if Data^ = nil then begin Ctxt.Valid := Ctxt.ParseNull; exit; end; Data^.Clear; if Ctxt.ParseNull or not Ctxt.ParseArray then exit; root := Ctxt.Info; Ctxt.Info := Ctxt.ObjectListItem; if (Ctxt.Info = nil) and (Data^.ItemClass <> nil) then Ctxt.Info := Rtti.RegisterClass(Data^.ItemClass); repeat item := Ctxt.ParseNewObject; if item = nil then break; Data^.Add(item); until Ctxt.EndOfObject = ']'; Ctxt.Info := root; Ctxt.ParseEndOfObject; end; procedure _JL_TStrings(Data: PStrings; var Ctxt: TJsonParserContext); var item: string; begin if Data^ = nil then begin Ctxt.Valid := Ctxt.ParseNull; exit; end; Data^.BeginUpdate; try Data^.Clear; if Ctxt.ParseNull or not Ctxt.ParseArray then exit; repeat if Ctxt.ParseNext then begin Utf8DecodeToString(Ctxt.Value, Ctxt.ValueLen, item); Data^.Add(item); end; until (not Ctxt.Valid) or (Ctxt.EndOfObject = ']'); finally Data^.EndUpdate; end; Ctxt.ParseEndOfObject; end; procedure _JL_TRawUtf8List(Data: PRawUtf8List; var Ctxt: TJsonParserContext); var item: RawUtf8; begin if Data^ = nil then begin Ctxt.Valid := Ctxt.ParseNull; exit; end; Data^.BeginUpdate; try Data^.Clear; if Ctxt.ParseNull or not Ctxt.ParseArray then exit; repeat if Ctxt.ParseNext then begin Ctxt.Interning.Unique(item, Ctxt.Value, Ctxt.ValueLen); Data^.AddObject(item, nil); end; until (not Ctxt.Valid) or (Ctxt.EndOfObject = ']'); finally Data^.EndUpdate; end; Ctxt.ParseEndOfObject; end; var /// use pointer to allow any kind of Data^ type in above functions // - typecast to TRttiJsonSave for proper function call // - rkRecord and rkClass are set in TRttiJson.SetParserType PT_JSONLOAD: array[TRttiParserType] of pointer = ( nil, @_JL_Array, @_JL_Boolean, @_JL_Byte, @_JL_Cardinal, @_JL_Currency, @_JL_Double, @_JL_Extended, @_JL_Int64, @_JL_Integer, @_JL_QWord, @_JL_RawByteString, @_JL_RawJson, @_JL_RawUtf8, nil, @_JL_Single, @_JL_String, @_JL_SynUnicode, @_JL_DateTime, @_JL_DateTime, @_JL_GUID, @_JL_Hash, @_JL_Hash, @_JL_Hash, @_JL_Int64, @_JL_TimeLog, @_JL_UnicodeString, @_JL_UnixTime, @_JL_UnixMSTime, @_JL_Variant, @_JL_WideString, @_JL_WinAnsi, @_JL_Word, @_JL_Enumeration, @_JL_Set, nil, @_JL_DynArray, @_JL_Interface, @_JL_PUtf8Char, nil); procedure JsonDecode(var Json: RawUtf8; const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean); begin JsonDecode(UniqueRawUtf8(Json), Names, Values, HandleValuesAsObjectOrArray); end; procedure JsonDecode(var Json: RawJson; const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean); begin JsonDecode(UniqueRawUtf8(RawUtf8(Json)), Names, Values, HandleValuesAsObjectOrArray); end; function JsonDecode(P: PUtf8Char; Names: PPUtf8CharArray; NamesCount: integer; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean): PUtf8Char; var v: PValuePUtf8Char; name: PUtf8Char; namelen, i: integer; info: TGetJsonField; begin result := nil; if (Values = nil) or (NamesCount <= 0) then exit; // avoid GPF FillCharFast(Values[0], NamesCount * SizeOf(Values[0]), 0); dec(NamesCount); if P = nil then exit; while P^ <> '{' do if P^ = #0 then exit else inc(P); info.Json := P + 1; // jump { repeat name := GetJsonPropName(info.Json, @namelen); if name = nil then exit; // invalid Json content info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray); if not (info.EndOfObject in [',', '}']) then exit; // invalid item separator v := pointer(Values); for i := 0 to NamesCount do if (v^.Text = nil) and IdemPropNameU(Names[i], name, namelen) then begin v^.Text := info.Value; v^.Len := info.ValueLen; break; end else inc(v); until (info.Json = nil) or (info.EndOfObject = '}'); if info.Json = nil then // result=nil indicates failure -> points to #0 result := @NULCHAR else result := info.Json; end; function JsonDecode(P: PUtf8Char; const Names: array of RawUtf8; Values: PValuePUtf8CharArray; HandleValuesAsObjectOrArray: boolean): PUtf8Char; begin result := JsonDecode(P, @Names[0], high(Names) + 1, Values, HandleValuesAsObjectOrArray); end; function JsonDecode(var Json: RawUtf8; const aName: RawUtf8; WasString: PBoolean; HandleValuesAsObjectOrArray: boolean): RawUtf8; begin result := JsonDecode(pointer(Json), aName, WasString, HandleValuesAsObjectOrArray); end; function JsonDecode(Json: PUtf8Char; const aName: RawUtf8; WasString: PBoolean; HandleValuesAsObjectOrArray: boolean): RawUtf8; var info: TGetJsonField; begin result := ''; if Json = nil then exit; while Json^ <> '{' do if Json^ = #0 then exit else inc(Json); info.Json := Json + 1; // jump { repeat info.Value := GetJsonPropName(info.Json, @info.ValueLen); if info.Value = nil then exit; // invalid Json content if IdemPropNameU(aName, info.Value, info.ValueLen) then begin info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray); if info.Json <> nil then FastSetString(result, info.Value, info.ValueLen); exit; end; info.Json := GotoNextJsonItem(info.Json, info.EndOfObject); if not (info.EndOfObject in [',', '}']) then exit; // invalid item separator until (info.Json = nil) or (info.EndOfObject = '}'); end; function JsonDecode(P: PUtf8Char; out Values: TNameValuePUtf8CharDynArray; HandleValuesAsObjectOrArray: boolean): PUtf8Char; var n: PtrInt; info: TGetJsonField; nametext: PUtf8Char; namelen: integer; begin {$ifdef FPC} Values := nil; {$endif FPC} result := nil; n := 0; if P <> nil then begin while P^ <> '{' do if P^ = #0 then exit else inc(P); inc(P); // jump { info.Json := P; repeat nametext := GetJsonPropName(info.Json, @nameLen); if nametext = nil then exit; // invalid JSON content info.GetJsonFieldOrObjectOrArray(HandleValuesAsObjectOrArray); if not (info.EndOfObject in [',', '}']) then exit; // invalid item separator if n = length(Values) then SetLength(Values, NextGrow(n)); with Values[n] do begin Name.Text := nametext; Name.Len := namelen; Value.Text := info.Value; Value.Len := info.ValueLen; end; inc(n); until (info.Json = nil) or (info.EndOfObject = '}'); P := info.Json; end; if n <> 0 then DynArrayFakeLength(Values, n); // SetLength() would have made a realloc() if P = nil then // result=nil indicates failure -> points to #0 result := @NULCHAR else result := P; end; { ************ JSON-aware TSynNameValue TSynPersistentStoreJson } { TSynNameValue } procedure TSynNameValue.Add(const aName, aValue: RawUtf8; aTag: PtrInt); var added: boolean; i: PtrInt; begin i := DynArray.FindHashedForAdding(aName, added); with List[i] do begin if added then Name := aName; Value := aValue; Tag := aTag; end; if Assigned(fOnAdd) then fOnAdd(List[i], i); end; procedure TSynNameValue.InitFromIniSection(Section: PUtf8Char; const OnTheFlyConvert: TOnSynNameValueConvertRawUtf8; const OnAdd: TOnSynNameValueNotify); var s: RawUtf8; i: PtrInt; begin Init(false); fOnAdd := OnAdd; while (Section <> nil) and (Section^ <> '[') do begin s := GetNextLine(Section, Section); i := PosExChar('=', s); if (i > 1) and not (s[1] in [';', '[']) then if Assigned(OnTheFlyConvert) then Add(copy(s, 1, i - 1), OnTheFlyConvert(copy(s, i + 1, 1000))) else Add(copy(s, 1, i - 1), copy(s, i + 1, 1000)); end; end; procedure TSynNameValue.InitFromCsv(Csv: PUtf8Char; NameValueSep, ItemSep: AnsiChar); var n, v: RawUtf8; begin Init(false); while Csv <> nil do begin GetNextItem(Csv, NameValueSep, n); if ItemSep = #10 then GetNextItemTrimedCRLF(Csv, v) else GetNextItem(Csv, ItemSep, v); if n = '' then break; Add(n, v); end; end; procedure TSynNameValue.InitFromNamesValues(const Names, Values: array of RawUtf8); var i: PtrInt; begin Init(false); if high(Names) <> high(Values) then exit; DynArray.Capacity := length(Names); for i := 0 to high(Names) do Add(Names[i], Values[i]); end; function TSynNameValue.InitFromJson(Json: PUtf8Char; aCaseSensitive: boolean): boolean; var N: PUtf8Char; nam, val: RawUtf8; Nlen, c: integer; info: TGetJsonField; begin result := false; Init(aCaseSensitive); if Json = nil then exit; while (Json^ <= ' ') and (Json^ <> #0) do inc(Json); if Json^ <> '{' then exit; repeat inc(Json) until (Json^ = #0) or (Json^ > ' '); info.Json := Json; c := JsonObjectPropCount(Json); // fast 900MB/s parsing if c <= 0 then exit; DynArray.Capacity := c; repeat N := GetJsonPropName(info.Json, @Nlen); if N = nil then exit; info.GetJsonFieldOrObjectOrArray; if info.Json = nil then exit; FastSetString(nam, N, Nlen); FastSetString(val, info.Value, info.Valuelen); Add(nam, val); until info.EndOfObject = '}'; result := true; end; procedure TSynNameValue.Init(aCaseSensitive: boolean); begin // release dynamic arrays memory before FillcharFast() List := nil; Finalize(PDynArrayHasher(@DynArray.Hasher)^); // initialize hashed storage FillCharFast(self, SizeOf(self), 0); DynArray.InitSpecific(TypeInfo(TSynNameValueItemDynArray), List, ptRawUtf8, @Count, not aCaseSensitive); end; function TSynNameValue.Find(const aName: RawUtf8): PtrInt; begin result := DynArray.FindHashed(aName); end; function TSynNameValue.FindStart(const aUpperName: RawUtf8): PtrInt; begin for result := 0 to Count - 1 do if IdemPChar(pointer(List[result].Name), pointer(aUpperName)) then exit; result := -1; end; function TSynNameValue.FindByValue(const aValue: RawUtf8): PtrInt; begin for result := 0 to Count - 1 do if List[result].Value = aValue then exit; result := -1; end; function TSynNameValue.Delete(const aName: RawUtf8): boolean; begin result := DynArray.FindHashedAndDelete(aName) >= 0; end; function TSynNameValue.DeleteByValue(const aValue: RawUtf8; Limit: integer): integer; var ndx: PtrInt; begin result := 0; if Limit < 1 then exit; for ndx := Count - 1 downto 0 do if List[ndx].Value = aValue then begin DynArray.Delete(ndx); inc(result); if result >= Limit then break; end; if result > 0 then DynArray.ForceReHash; end; function TSynNameValue.Value(const aName: RawUtf8; const aDefaultValue: RawUtf8): RawUtf8; var i: PtrInt; begin if @self = nil then i := -1 else i := DynArray.FindHashed(aName); if i < 0 then result := aDefaultValue else result := List[i].Value; end; function TSynNameValue.ValueInt(const aName: RawUtf8; const aDefaultValue: Int64): Int64; var i: PtrInt; err: integer; begin i := DynArray.FindHashed(aName); if i < 0 then result := aDefaultValue else begin result := GetInt64(pointer(List[i].Value), err); if err <> 0 then result := aDefaultValue; end; end; function TSynNameValue.ValueBool(const aName: RawUtf8): boolean; begin result := GetBoolean(pointer(Value(aName))); end; function TSynNameValue.ValueEnum(const aName: RawUtf8; aEnumTypeInfo: PRttiInfo; out aEnum; aEnumDefault: PtrUInt): boolean; var rtti: PRttiEnumType; v: RawUtf8; err: integer; i: PtrInt; begin result := false; rtti := aEnumTypeInfo.EnumBaseType; if rtti = nil then exit; RTTI_TO_ORD[rtti.RttiOrd](@aEnum, aEnumDefault); // always set default value v := Value(aName, ''); TrimSelf(v); if v = '' then exit; i := GetInteger(pointer(v), err); if (err <> 0) or (PtrUInt(i) > PtrUInt(rtti.MaxValue)) then i := rtti.GetEnumNameValue(pointer(v), length(v), {alsotrimleft=}true); if i >= 0 then begin RTTI_TO_ORD[rtti.RttiOrd](@aEnum, i); // we found a proper value result := true; end; end; function TSynNameValue.Initialized: boolean; begin result := DynArray.Value = @List; end; function TSynNameValue.GetBlobData: RawByteString; begin result := DynArray.SaveTo; end; procedure TSynNameValue.SetBlobDataPtr(aValue, aValueMax: pointer); begin DynArray.LoadFrom(aValue, aValueMax); DynArray.ForceReHash; end; procedure TSynNameValue.SetBlobData(const aValue: RawByteString); begin DynArray.LoadFromBinary(aValue); DynArray.ForceReHash; end; function TSynNameValue.GetStr(const aName: RawUtf8): RawUtf8; begin result := Value(aName, ''); end; function TSynNameValue.GetInt(const aName: RawUtf8): Int64; begin result := ValueInt(aName, 0); end; function TSynNameValue.AsCsv(const KeySeparator, ValueSeparator, IgnoreKey: RawUtf8): RawUtf8; var i: PtrInt; temp: TTextWriterStackBuffer; begin with TTextWriter.CreateOwnedStream(temp) do try for i := 0 to Count - 1 do if (IgnoreKey = '') or (List[i].Name <> IgnoreKey) then begin AddNoJsonEscapeUtf8(List[i].Name); AddNoJsonEscapeUtf8(KeySeparator); AddNoJsonEscapeUtf8(List[i].Value); AddNoJsonEscapeUtf8(ValueSeparator); end; SetText(result); finally Free; end; end; function TSynNameValue.AsJson: RawUtf8; var i: PtrInt; temp: TTextWriterStackBuffer; begin with TJsonWriter.CreateOwnedStream(temp) do try Add('{'); for i := 0 to Count - 1 do with List[i] do begin AddProp(pointer(Name), length(Name)); AddDirect('"'); AddJsonEscape(pointer(Value)); AddDirect('"', ','); end; CancelLastComma('}'); SetText(result); finally Free; end; end; procedure TSynNameValue.AsNameValues(out Names, Values: TRawUtf8DynArray); var i: PtrInt; begin SetLength(Names, Count); SetLength(Values, Count); for i := 0 to Count - 1 do begin Names[i] := List[i].Name; Values[i] := List[i].Value; end; end; function TSynNameValue.ValueVariantOrNull(const aName: RawUtf8): variant; var i: PtrInt; begin i := Find(aName); if i < 0 then SetVariantNull(result{%H-}) else RawUtf8ToVariant(List[i].Value, result); end; procedure TSynNameValue.AsDocVariant(out DocVariant: variant; ExtendedJson, ValueAsString, AllowVarDouble: boolean); var ndx: PtrInt; dv: TDocVariantData absolute DocVariant; begin if Count > 0 then begin dv.Init(JSON_NAMEVALUE[ExtendedJson], dvObject); dv.SetCount(Count); dv.Capacity := Count; for ndx := 0 to Count - 1 do begin dv.Names[ndx] := List[ndx].Name; if ValueAsString or not GetVariantFromNotStringJson(pointer(List[ndx].Value), TVarData(dv.Values[ndx]), AllowVarDouble) then RawUtf8ToVariant(List[ndx].Value, dv.Values[ndx]); end; end else TVarData(DocVariant).VType := varNull; end; function TSynNameValue.AsDocVariant(ExtendedJson, ValueAsString: boolean): variant; begin AsDocVariant(result, ExtendedJson, ValueAsString); end; function TSynNameValue.MergeDocVariant(var DocVariant: variant; ValueAsString: boolean; ChangedProps: PVariant; ExtendedJson, AllowVarDouble: boolean): integer; var dv: TDocVariantData absolute DocVariant; i, ndx: PtrInt; v: variant; intvalues: TRawUtf8Interning; begin if dv.VarType <> DocVariantVType then TDocVariant.New(DocVariant, JSON_NAMEVALUE[ExtendedJson]); if ChangedProps <> nil then TDocVariant.New(ChangedProps^, dv.Options); if dvoInternValues in dv.Options then intvalues := DocVariantType.InternValues else intvalues := nil; result := 0; // returns number of changed values for i := 0 to Count - 1 do if List[i].Name <> '' then begin VarClear(v{%H-}); if ValueAsString or not GetVariantFromNotStringJson( pointer(List[i].Value), TVarData(v), AllowVarDouble) then RawUtf8ToVariant(List[i].Value, v); ndx := dv.GetValueIndex(List[i].Name); if ndx < 0 then ndx := dv.InternalAdd(List[i].Name) else if FastVarDataComp(@v, @dv.Values[ndx], false) = 0 then continue; // value not changed -> skip if ChangedProps <> nil then PDocVariantData(ChangedProps)^.AddValue(List[i].Name, v); SetVariantByValue(v, dv.Values[ndx]); if intvalues <> nil then intvalues.UniqueVariant(dv.Values[ndx]); inc(result); end; end; { TSynPersistentStoreJson } procedure TSynPersistentStoreJson.AddJson(W: TJsonWriter); begin W.AddPropJsonString('name', fName); end; function TSynPersistentStoreJson.SaveToJson(reformat: TTextWriterJsonFormat): RawUtf8; var W: TJsonWriter; begin W := TJsonWriter.CreateOwnedStream(65536); try W.Add('{'); AddJson(W); W.CancelLastComma('}'); W.SetText(result, reformat); finally W.Free; end; end; { TSynCache } constructor TSynCache.Create(aMaxCacheRamUsed: cardinal; aCaseSensitive: boolean; aTimeoutSeconds: cardinal); begin inherited Create; // may have been overriden fNameValue.Init(aCaseSensitive); fMaxRamUsed := aMaxCacheRamUsed; fTimeoutSeconds := aTimeoutSeconds; end; procedure TSynCache.ResetIfNeeded; var tix: cardinal; begin if fRamUsed > fMaxRamUsed then Reset; if fTimeoutSeconds > 0 then begin tix := GetTickCount64 shr 10; if fTimeoutTix > tix then Reset; fTimeoutTix := tix + fTimeoutSeconds; end; end; function TSynCache.Find(const aKey: RawUtf8; aResultTag: PPtrInt): RawUtf8; var ndx: PtrInt; begin result := ''; if (self = nil) or (aKey = '') then exit; fSafe.ReadOnlyLock; {$ifdef HASFASTTRYFINALLY} try {$else} begin {$endif HASFASTTRYFINALLY} ndx := fNameValue.Find(aKey); if ndx >= 0 then with fNameValue.List[ndx] do begin result := Value; if aResultTag <> nil then aResultTag^ := Tag; end; {$ifdef HASFASTTRYFINALLY} finally {$endif HASFASTTRYFINALLY} fSafe.ReadOnlyUnLock; end; end; function TSynCache.AddOrUpdate(const aKey, aValue: RawUtf8; aTag: PtrInt): boolean; var ndx: PtrInt; begin result := false; if self = nil then exit; // avoid GPF fSafe.WriteLock; try ResetIfNeeded; ndx := fNameValue.DynArray.FindHashedForAdding(aKey, result); with fNameValue.List[ndx] do begin Name := aKey; dec(fRamUsed, length(Value)); Value := aValue; inc(fRamUsed, length(Value)); Tag := aTag; end; finally fSafe.WriteUnlock; end; end; function TSynCache.Reset: boolean; begin result := false; if (self = nil) or (fNameValue.Count = 0) then exit; // avoid GPF or a lock for nothing fSafe.WriteLock; try if fNameValue.Count <> 0 then begin fNameValue.DynArray.Clear; fNameValue.DynArray.ForceReHash; result := true; // mark something was flushed end; fRamUsed := 0; fTimeoutTix := 0; finally fSafe.WriteUnlock; end; end; { *********** JSON-aware TSynDictionary Storage } { TSynDictionary } constructor TSynDictionary.Create(aKeyTypeInfo, aValueTypeInfo: PRttiInfo; aKeyCaseInsensitive: boolean; aTimeoutSeconds: cardinal; aCompressAlgo: TAlgoCompress; aHasher: THasher); begin inherited Create; fSafe.Padding[DIC_KEYCOUNT].VType := varInteger; // Keys.Count fSafe.Padding[DIC_KEY].VType := varUnknown; // Key.Value fSafe.Padding[DIC_VALUECOUNT].VType := varInteger; // Values.Count fSafe.Padding[DIC_VALUE].VType := varUnknown; // Values.Value fSafe.Padding[DIC_TIMECOUNT].VType := varInteger; // Timeouts.Count fSafe.Padding[DIC_TIMESEC].VType := varInteger; // Timeouts Seconds fSafe.Padding[DIC_TIMETIX].VType := varInteger; // GetTickCount64 shr 10 fSafe.PaddingUsedCount := DIC_TIMETIX + 1; // manual registration fKeys.Init(aKeyTypeInfo, fSafe.Padding[DIC_KEY].VAny, nil, nil, aHasher, @fSafe.Padding[DIC_KEYCOUNT].VInteger, aKeyCaseInsensitive); fValues.Init(aValueTypeInfo, fSafe.Padding[DIC_VALUE].VAny, @fSafe.Padding[DIC_VALUECOUNT].VInteger); fValues.Compare := DynArraySortOne(fValues.Info.ArrayFirstField, aKeyCaseInsensitive); fTimeouts.Init(TypeInfo(TIntegerDynArray), fTimeOut, @fSafe.Padding[DIC_TIMECOUNT].VInteger); if aCompressAlgo = nil then aCompressAlgo := AlgoSynLZ; fCompressAlgo := aCompressAlgo; fSafe.Padding[DIC_TIMESEC].VInteger := aTimeoutSeconds; end; {$ifdef HASGENERICS} class function TSynDictionary.New(aKeyCaseInsensitive: boolean; aTimeoutSeconds: cardinal; aCompressAlgo: TAlgoCompress; aHasher: THasher): TSynDictionary; begin result := TSynDictionary.Create(TypeInfo(TArray), TypeInfo(TArray), aKeyCaseInsensitive, aTimeoutSeconds, aCompressAlgo, aHasher); end; {$endif HASGENERICS} function TSynDictionary.ComputeNextTimeOut: cardinal; begin result := fSafe.Padding[DIC_TIMESEC].VInteger; if result <> 0 then result := cardinal(GetTickCount64 shr 10) + result; end; function TSynDictionary.GetCapacity: integer; begin result := fKeys.Capacity; // no need to lock for an evolving value end; procedure TSynDictionary.SetCapacity(const Value: integer); begin fSafe.Lock; // = RWLock(cWrite); try fKeys.Capacity := Value; fValues.Capacity := Value; if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then fTimeOuts.Capacity := Value; finally fSafe.UnLock; end; end; function TSynDictionary.GetTimeOutSeconds: cardinal; begin result := fSafe.Padding[DIC_TIMESEC].VInteger; end; procedure TSynDictionary.SetTimeOutSeconds(Value: cardinal); begin // no fSafe.Lock because RWLock(cWrite) in DeleteAll is enough DeleteAll; fSafe.Padding[DIC_TIMESEC].VInteger := Value; end; procedure TSynDictionary.SetTimeouts; var i: PtrInt; timeout: cardinal; begin if fSafe.Padding[DIC_TIMESEC].VInteger = 0 then exit; fTimeOuts.Count := fSafe.Padding[DIC_KEYCOUNT].VInteger; timeout := ComputeNextTimeOut; for i := 0 to fSafe.Padding[DIC_TIMECOUNT].VInteger - 1 do fTimeOut[i] := timeout; end; function TSynDictionary.DeleteDeprecated(tix64: Int64): integer; var i: PtrInt; now: cardinal; begin result := 0; if (self = nil) or (fSafe.Padding[DIC_TIMECOUNT].VInteger = 0) or // no entry (fSafe.Padding[DIC_TIMESEC].VInteger = 0) then // nothing in fTimeOut[] exit; if tix64 = 0 then tix64 := GetTickCount64; now := tix64 shr 10; if fSafe.Padding[DIC_TIMETIX].VInteger = integer(now) then exit; // no need to search more often than every second fSafe.ReadWriteLock; // would upgrade to cWrite only if needed try fSafe.Padding[DIC_TIMETIX].VInteger := now; for i := fSafe.Padding[DIC_TIMECOUNT].VInteger - 1 downto 0 do if (now > fTimeOut[i]) and (fTimeOut[i] <> 0) and (not Assigned(fOnCanDelete) or fOnCanDelete(fKeys.ItemPtr(i)^, fValues.ItemPtr(i)^, i)) then begin if result = 0 then fSafe.Lock; // = cWrite fKeys.Delete(i); fValues.Delete(i); fTimeOuts.Delete(i); inc(result); end; if result > 0 then fKeys.ForceReHash; // mandatory after manual fKeys.Delete(i) finally if result > 0 then fSafe.UnLock; // = cWrite fSafe.ReadWriteUnLock; end; end; procedure TSynDictionary.DeleteAll; begin if self = nil then exit; fSafe.Lock; // = RWLock(cWrite); try fKeys.Clear; fKeys.Hasher.ForceReHash(nil); // mandatory to avoid GPF fValues.Clear; if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then fTimeOuts.Clear; finally fSafe.UnLock; end; end; destructor TSynDictionary.Destroy; begin fKeys.Clear; fValues.Clear; inherited Destroy; end; function TSynDictionary.GetThreadUse: TSynLockerUse; begin result := fSafe^.RWUse; end; procedure TSynDictionary.SetThreadUse(const Value: TSynLockerUse); begin fSafe^.RWUse := Value; end; function TSynDictionary.InternalAddUpdate( aKey, aValue: pointer; aUpdate: boolean): PtrInt; var added: boolean; tim: cardinal; begin tim := ComputeNextTimeOut; fSafe.Lock; // = RWLock(cWrite) - cReadWrite is not possible here try result := fKeys.FindHashedForAdding(aKey^, added); if added then begin // fKey[result] := aKey; with fKeys{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif} do ItemCopy(aKey, PAnsiChar(Value^) + (result * Info.Cache.ItemSize)); if fValues.Add(aValue^) <> result then raise ESynDictionary.CreateUtf8('%.Add fValues.Add', [self]); if tim <> 0 then fTimeOuts.Add(tim); end else if aUpdate then begin fValues.ItemCopyFrom(aValue, result, {ClearBeforeCopy=}true); if tim <> 0 then fTimeOut[result] := tim; end else result := -1; finally fSafe.UnLock; end; end; function TSynDictionary.Add(const aKey, aValue): PtrInt; begin result := InternalAddUpdate(@aKey, @aValue, {update=}false) end; function TSynDictionary.AddOrUpdate(const aKey, aValue): PtrInt; begin result := InternalAddUpdate(@aKey, @aValue, {update=}true) end; function TSynDictionary.Clear(const aKey): PtrInt; begin fSafe.ReadWriteLock; try result := fKeys.FindHashed(aKey); if result >= 0 then begin fSafe.Lock; fValues.ItemClear(fValues.ItemPtr(result)); if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then fTimeOut[result] := 0; fSafe.UnLock; end; finally fSafe.ReadWriteUnLock; end; end; function TSynDictionary.Delete(const aKey): PtrInt; begin fSafe.Lock; try result := fKeys.FindHashedAndDelete(aKey); if result >= 0 then begin fValues.Delete(result); if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then fTimeOuts.Delete(result); end; finally fSafe.UnLock; end; end; function TSynDictionary.DeleteAt(aIndex: PtrInt): boolean; begin if cardinal(aIndex) < cardinal(fSafe.Padding[DIC_KEYCOUNT].VInteger) then // use Delete(aKey) to have efficient hash table update result := Delete(fKeys.ItemPtr(aIndex)^) = aIndex else result := false; end; function TSynDictionary.InArray(const aKey, aArrayValue; aAction: TSynDictionaryInArray; aCompare: TDynArraySortCompare): boolean; var nested: TDynArray; ndx: PtrInt; new: pointer; begin result := false; if (fValues.Info.ArrayRtti = nil) or (fValues.Info.ArrayRtti.Kind <> rkDynArray) then raise ESynDictionary.CreateUtf8('%.Values: % items are not dynamic arrays', [self, fValues.Info.Name]); if aAction = iaFind then fSafe.ReadLock else fSafe.Lock; // other actions may need to write the internal data try ndx := fKeys.FindHashed(aKey); if ndx < 0 then if aAction <> iaAddForced then exit else begin new := nil; ndx := Add(aKey, new); end; nested.InitRtti(fValues.Info.ArrayRtti, fValues.ItemPtr(ndx)^); nested.Compare := aCompare; case aAction of iaFind: result := nested.Find(aArrayValue) >= 0; iaFindAndDelete: result := nested.FindAndDelete(aArrayValue) >= 0; iaFindAndUpdate: result := nested.FindAndUpdate(aArrayValue) >= 0; iaFindAndAddIfNotExisting: result := nested.FindAndAddIfNotExisting(aArrayValue) >= 0; iaAdd, iaAddForced: result := nested.Add(aArrayValue) >= 0; end; finally if aAction = iaFind then fSafe.ReadUnLock else fSafe.UnLock; end; end; function TSynDictionary.FindInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare): boolean; begin result := InArray(aKey, aArrayValue, iaFind, aCompare); end; function TSynDictionary.FindKeyFromValue(const aValue; out aKey; aUpdateTimeOut: boolean): boolean; var ndx: PtrInt; begin fSafe.ReadLock; // cReadOnly is good enough for SetTimeoutAtIndex() try ndx := fValues.IndexOf(aValue); // use fast RTTI for value search result := ndx >= 0; if result then begin fKeys.ItemCopyAt(ndx, @aKey); if aUpdateTimeOut then SetTimeoutAtIndex(ndx); // no cWrite lock needed end; finally fSafe.ReadUnLock; end; end; function TSynDictionary.DeleteInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare): boolean; begin result := InArray(aKey, aArrayValue, iaFindAndDelete, aCompare); end; function TSynDictionary.UpdateInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare): boolean; begin result := InArray(aKey, aArrayValue, iaFindAndUpdate, aCompare); end; function TSynDictionary.AddInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare): boolean; begin result := InArray(aKey, aArrayValue, iaAdd, aCompare); end; function TSynDictionary.AddInArrayForced(const aKey, aArrayValue; aCompare: TDynArraySortCompare): boolean; begin result := InArray(aKey, aArrayValue, iaAddForced, aCompare); end; function TSynDictionary.AddOnceInArray(const aKey, aArrayValue; aCompare: TDynArraySortCompare): boolean; begin result := InArray(aKey, aArrayValue, iaFindAndAddIfNotExisting, aCompare); end; function TSynDictionary.Find(const aKey; aUpdateTimeOut: boolean): PtrInt; var tim: cardinal; begin // caller is expected to call fSafe.Lock/Unlock if self = nil then result := -1 else begin result := fKeys.Hasher.FindOrNew(fKeys.Hasher.HashOne(@aKey), @aKey, nil); if result < 0 then result := -1 else if aUpdateTimeOut then begin tim := fSafe.Padding[DIC_TIMESEC].VInteger; if tim > 0 then // inlined fTimeout[result] := GetTimeout fTimeout[result] := cardinal(GetTickCount64 shr 10) + tim; end; end; end; function TSynDictionary.FindValue(const aKey; aUpdateTimeOut: boolean; aIndex: PPtrInt): pointer; var ndx: PtrInt; begin ndx := Find(aKey, aUpdateTimeOut); if aIndex <> nil then aIndex^ := ndx; if ndx < 0 then result := nil else result := PAnsiChar(fValues.Value^) + ndx * fValues.Info.Cache.ItemSize; end; function TSynDictionary.FindValueOrAdd(const aKey; var added: boolean; aIndex: PPtrInt): pointer; var ndx: PtrInt; tim: cardinal; begin tim := fSafe.Padding[DIC_TIMESEC].VInteger; // inlined tim := GetTimeout if tim <> 0 then tim := cardinal(GetTickCount64 shr 10) + tim; ndx := fKeys.FindHashedForAdding(aKey, added); if added then begin fKeys{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif}. ItemCopyFrom(@aKey, ndx); // fKey[i] := aKey fValues.Count := ndx + 1; // reserve new place for associated value if tim > 0 then fTimeOuts.Add(tim); end else if tim > 0 then fTimeOut[ndx] := tim; if aIndex <> nil then aIndex^ := ndx; result := PAnsiChar(fValues.Value^) + ndx * fValues.Info.Cache.ItemSize; end; function TSynDictionary.FindAndCopy(const aKey; var aValue; aUpdateTimeOut: boolean): boolean; var ndx: PtrInt; begin result := false; if self = nil then exit; fSafe.ReadLock; {$ifdef HASFASTTRYFINALLY} try {$else} begin {$endif HASFASTTRYFINALLY} ndx := Find(aKey, aUpdateTimeOut); if ndx >= 0 then begin fValues.ItemCopy( // inlined ItemCopyAt(ndx, @aValue) PAnsiChar(fValues.Value^) + ndx * fValues.Info.Cache.ItemSize, @aValue); result := true; end; {$ifdef HASFASTTRYFINALLY} finally {$endif HASFASTTRYFINALLY} fSafe.ReadUnLock; end; end; function TSynDictionary.FindAndGetElapsedSeconds(const aKey): integer; var tim: cardinal; ndx: PtrInt; begin result := -1; if self = nil then exit; tim := ComputeNextTimeOut; if tim = 0 then exit; fSafe.ReadLock; try ndx := Find(aKey, {aUpdateTimeOut=}false); if ndx >= 0 then result := tim - fTimeOut[ndx]; finally fSafe.ReadUnLock; end; end; function TSynDictionary.FindAndDeleteDeprecated(const aKey; aSeconds: integer): boolean; var tim: cardinal; ndx: PtrInt; begin result := false; if (self = nil) or (aSeconds <= 0) then exit; tim := ComputeNextTimeOut; if tim = 0 then exit; fSafe.ReadWriteLock; try ndx := Find(aKey, {aUpdateTimeOut=}false); if (ndx >= 0) and (tim - fTimeOut[ndx] > cardinal(aSeconds)) then result := DeleteAt(ndx); finally fSafe.ReadWriteUnLock; end; end; function TSynDictionary.FindAndExtract(const aKey; var aValue): boolean; var ndx: PtrInt; begin result := false; if self = nil then exit; fSafe.ReadWriteLock; try ndx := fKeys.FindHashedAndDelete(aKey); if ndx >= 0 then begin fSafe.Lock; fValues.ItemMoveTo(ndx, @aValue); // faster than ItemCopy() fValues.Delete(ndx); if fSafe.Padding[DIC_TIMESEC].VInteger > 0 then fTimeOuts.Delete(ndx); fSafe.UnLock; result := true; end; finally fSafe.ReadWriteUnLock; end; end; function TSynDictionary.Exists(const aKey): boolean; begin result := false; if self = nil then exit; fSafe.ReadLock; {$ifdef HASFASTTRYFINALLY} try {$else} begin {$endif HASFASTTRYFINALLY} result := fKeys.FindHashed(aKey) >= 0; {$ifdef HASFASTTRYFINALLY} finally {$endif HASFASTTRYFINALLY} fSafe.ReadUnLock; end; end; function TSynDictionary.ExistsValue( const aValue; aCompare: TDynArraySortCompare): boolean; begin result := false; if self = nil then exit; fSafe.ReadLock; try result := fValues.Find(aValue, aCompare) >= 0; finally fSafe.ReadUnLock; end; end; procedure TSynDictionary.CopyValues(out Dest; ObjArrayByRef: boolean); begin fSafe.ReadLock; try fValues.CopyTo(Dest, ObjArrayByRef); finally fSafe.ReadUnLock; end; end; function TSynDictionary.ForEach(const OnEach: TOnSynDictionary; Opaque: pointer; MayModify: boolean): integer; var k, v: PAnsiChar; i, n, ks, vs: PtrInt; begin result := 0; if MayModify then fSafe.ReadWriteLock else fSafe.ReadLock; try n := fSafe.Padding[DIC_KEYCOUNT].VInteger; if (n = 0) or (not Assigned(OnEach)) then exit; k := fKeys.Value^; ks := fKeys.Info.Cache.ItemSize; v := fValues.Value^; vs := fValues.Info.Cache.ItemSize; for i := 0 to n - 1 do begin inc(result); if not OnEach(k^, v^, i, n, Opaque) then break; inc(k, ks); inc(v, vs); end; finally if MayModify then fSafe.ReadWriteUnLock else fSafe.ReadUnLock; end; end; function TSynDictionary.ForEach(const OnMatch: TOnSynDictionary; KeyCompare, ValueCompare: TDynArraySortCompare; const aKey, aValue; Opaque: pointer; MayModify: boolean): integer; var k, v: PAnsiChar; i, n, ks, vs: PtrInt; begin if MayModify then fSafe.ReadWriteLock else fSafe.ReadLock; try result := 0; if (not Assigned(OnMatch)) or (not (Assigned(KeyCompare) or Assigned(ValueCompare))) then exit; n := fSafe.Padding[DIC_KEYCOUNT].VInteger; k := fKeys.Value^; ks := fKeys.Info.Cache.ItemSize; v := fValues.Value^; vs := fValues.Info.Cache.ItemSize; for i := 0 to n - 1 do begin if (Assigned(KeyCompare) and (KeyCompare(k^, aKey) = 0)) or (Assigned(ValueCompare) and (ValueCompare(v^, aValue) = 0)) then begin inc(result); if not OnMatch(k^, v^, i, n, Opaque) then break; end; inc(k, ks); inc(v, vs); end; finally if MayModify then fSafe.ReadWriteUnLock else fSafe.ReadUnLock; end; end; procedure TSynDictionary.SetTimeoutAtIndex(aIndex: PtrInt); var tim: cardinal; begin if cardinal(aIndex) >= cardinal(fSafe.Padding[DIC_KEYCOUNT].VInteger) then exit; tim := fSafe.Padding[DIC_TIMESEC].VInteger; if tim > 0 then fTimeOut[aIndex] := cardinal(GetTickCount64 shr 10) + tim; end; function TSynDictionary.Count: integer; begin result := fSafe.Padding[DIC_KEYCOUNT].VInteger; end; procedure TSynDictionary.SaveToJson(W: TJsonWriter; EnumSetsAsText: boolean); var k, v: RawUtf8; begin fSafe.ReadLock; try if fSafe.Padding[DIC_KEYCOUNT].VInteger > 0 then begin fKeys{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif}. SaveToJson(k, EnumSetsAsText); fValues.SaveToJson(v, EnumSetsAsText); end; finally fSafe.ReadUnLock; end; W.AddJsonArraysAsJsonObject(pointer(k), pointer(v)); end; function TSynDictionary.SaveToJson(EnumSetsAsText: boolean): RawUtf8; var W: TJsonWriter; temp: TTextWriterStackBuffer; begin W := TJsonWriter.CreateOwnedStream(temp) as TJsonWriter; try SaveToJson(W, EnumSetsAsText); W.SetText(result); finally W.Free; end; end; function TSynDictionary.SaveValuesToJson(EnumSetsAsText: boolean; ReFormat: TTextWriterJsonFormat): RawUtf8; begin if self = nil then begin result := ''; exit; end; fSafe.ReadLock; try fValues.SaveToJson(result, EnumSetsAsText, ReFormat); finally fSafe.ReadUnLock; end; end; function TSynDictionary.LoadFromJson(const Json: RawUtf8; CustomVariantOptions: PDocVariantOptions): boolean; begin // pointer(Json) is not modified in-place thanks to JsonObjectAsJsonArrays() result := LoadFromJson(pointer(Json), CustomVariantOptions); end; function TSynDictionary.LoadFromJson(Json: PUtf8Char; CustomVariantOptions: PDocVariantOptions): boolean; var k, v: RawUtf8; // private copy of the Json input, expanded as Keys/Values arrays n: integer; begin result := false; n := JsonObjectAsJsonArrays(Json, k, v); if n <= 0 then exit; fSafe.Lock; try if (fKeys.LoadFromJson(pointer(k), nil, CustomVariantOptions) <> nil) and (fKeys.Count = n) and (fValues.LoadFromJson(pointer(v), nil, CustomVariantOptions) <> nil) and (fValues.Count = n) then begin SetTimeouts; fKeys.ForceRehash; // warning: duplicated keys won't be identified result := true; end; finally fSafe.UnLock; end; end; function TSynDictionary.LoadFromBinary(const binary: RawByteString): boolean; var plain: RawByteString; rdr: TFastReader; n: integer; begin result := false; plain := fCompressAlgo.Decompress(binary); if plain = '' then exit; rdr.Init(plain); fSafe.Lock; try try RTTI_BINARYLOAD[rkDynArray](fKeys.Value, rdr, fKeys.Info.Info); RTTI_BINARYLOAD[rkDynArray](fValues.Value, rdr, fValues.Info.Info); n := fKeys.Capacity; if n = fValues.Capacity then begin // RTTI_BINARYLOAD[rkDynArray]() did not set the external count fSafe.Padding[DIC_KEYCOUNT].VInteger := n; fSafe.Padding[DIC_VALUECOUNT].VInteger := n; SetTimeouts; // set ComputeNextTimeOut for all items fKeys.ForceReHash; // optimistic: input from TSynDictionary.SaveToBinary result := true; end; except result := false; end; finally fSafe.UnLock; end; end; class function TSynDictionary.OnCanDeleteSynPersistentLock( const aKey, aValue; aIndex: PtrInt): boolean; begin result := not TSynPersistentLock(aValue).Safe^.IsLocked; end; {$ifndef PUREMORMOT2} class function TSynDictionary.OnCanDeleteSynPersistentLocked( const aKey, aValue; aIndex: PtrInt): boolean; begin result := not TSynPersistentLocked(aValue).Safe^.IsLocked; end; {$endif PUREMORMOT2} function TSynDictionary.SaveToBinary( NoCompression: boolean; Algo: TAlgoCompress): RawByteString; var tmp: TTextWriterStackBuffer; W: TBufferWriter; begin result := ''; if fSafe.Padding[DIC_KEYCOUNT].VInteger = 0 then exit; W := TBufferWriter.Create(tmp{%H-}); try fSafe.ReadLock; try if fSafe.Padding[DIC_KEYCOUNT].VInteger = 0 then exit; DynArraySave(pointer(fKeys.Value), @fSafe.Padding[DIC_KEYCOUNT].VInteger, W, fKeys.Info.Info); DynArraySave(pointer(fValues.Value), @fSafe.Padding[DIC_VALUECOUNT].VInteger, W, fValues.Info.Info); finally fSafe.ReadUnLock; end; result := W.FlushAndCompress(NoCompression, Algo); finally W.Free; end; end; { ********** Custom JSON Serialization } { TRttiJson } function _New_ObjectList(Rtti: TRttiCustom): pointer; begin result := TObjectListClass(Rtti.ValueClass).Create; end; function _New_InterfacedObjectWithCustomCreate(Rtti: TRttiCustom): pointer; begin result := TInterfacedObjectWithCustomCreateClass(Rtti.ValueClass).Create; end; function _New_PersistentWithCustomCreate(Rtti: TRttiCustom): pointer; begin result := TPersistentWithCustomCreateClass(Rtti.ValueClass).Create; end; function _New_Component(Rtti: TRttiCustom): pointer; begin result := TComponentClass(Rtti.ValueClass).Create(nil); end; function _New_ObjectWithCustomCreate(Rtti: TRttiCustom): pointer; begin result := TObjectWithCustomCreateClass(Rtti.ValueClass).Create; end; function _New_SynObjectList(Rtti: TRttiCustom): pointer; begin result := TSynObjectListClass(Rtti.ValueClass).Create({ownobjects=}true); end; function _New_SynLocked(Rtti: TRttiCustom): pointer; begin result := TSynLockedClass(Rtti.ValueClass).Create; end; function _New_InterfacedCollection(Rtti: TRttiCustom): pointer; begin result := TInterfacedCollectionClass(Rtti.ValueClass).Create; end; function _New_Collection(Rtti: TRttiCustom): pointer; begin if Rtti.CollectionItem = nil then raise ERttiException.CreateUtf8('% with CollectionItem=nil: please call ' + 'Rtti.RegisterCollection()', [Rtti.ValueClass]); result := TCollectionClass(Rtti.ValueClass).Create(Rtti.CollectionItem); end; function _New_CollectionItem(Rtti: TRttiCustom): pointer; begin result := TCollectionItemClass(Rtti.ValueClass).Create(nil); end; function _New_List(Rtti: TRttiCustom): pointer; begin result := TListClass(Rtti.ValueClass).Create; end; function _New_Object(Rtti: TRttiCustom): pointer; begin result := Rtti.ValueClass.Create; // non-virtual TObject.Create constructor end; function _BC_RawByteString(A, B: PPUtf8Char; Info: PRttiInfo; out Compared: integer): PtrInt; begin {$ifdef CPUINTEL} compared := SortDynArrayAnsiString(A^, B^); // i386/x86_64 asm uses length {$else} compared := SortDynArrayRawByteString(A^, B^); // will use length not #0 {$endif CPUINTEL} result := SizeOf(pointer); end; function _BC_PUtf8Char(A, B: PPUtf8Char; Info: PRttiInfo; out Compared: integer): PtrInt; begin compared := StrComp(A^, B^); result := SizeOf(pointer); end; function _BCI_PUtf8Char(A, B: PPUtf8Char; Info: PRttiInfo; out Compared: integer): PtrInt; begin compared := StrIComp(A^, B^); result := SizeOf(pointer); end; function _BC_Default(A, B: pointer; Info: PRttiInfo; out Compared: integer): PtrInt; begin Compared := ComparePointer(A, B); // weak fallback result := 0; // not used in TRttiJson.ValueCompare / fCompare[] end; function TRttiJson.SetParserType(aParser: TRttiParserType; aParserComplex: TRttiParserComplexType): TRttiCustom; var C: TClass; n: integer; begin // set Name and Flags from Props[] inherited SetParserType(aParser, aParserComplex); // set comparison functions fCompare[true] := RTTI_COMPARE[true][Kind]; // generic comparison fCompare[false] := RTTI_COMPARE[false][Kind]; if rcfHasRttiOrd in fCache.Flags then begin fCompare[true] := @RTTI_ORD_COMPARE[fCache.RttiOrd]; // tuned compare fCompare[false] := fCompare[true]; end else if rcfGetInt64Prop in fCache.Flags then begin if rcfQWord in fCache.Flags then fCompare[true] := @_BC_UQWord // QWord compare else fCompare[true] := @_BC_SQWord; // Int64 compare fCompare[false] := fCompare[true]; end else if Kind = rkFloat then begin fCompare[true] := @RTTI_FLOAT_COMPARE[fCache.RttiFloat]; // tuned compare fCompare[false] := fCompare[true]; end else if rcfObjArray in fFlags then begin fCompare[true] := _BCI_ObjArray; // direct compare fCompare[false] := _BC_ObjArray; end else if aParser = ptPUtf8Char then begin fCompare[true] := @_BCI_PUtf8Char; // rkPointer with no RTTI fCompare[false] := @_BC_PUtf8Char; end else if Kind = rkLString then // override default StrComp/StrIComp if Cache.CodePage >= CP_RAWBLOB then begin fCompare[true] := @_BC_RawByteString; // ignore #0 or CaseInsensitive fCompare[false] := @_BC_RawByteString; end else if Cache.CodePage = CP_UTF16 then begin fCompare[true] := RTTI_COMPARE[true][rkWString]; // StrCompW fCompare[false] := RTTI_COMPARE[false][rkWString]; // StrICompW end; if not Assigned(fCompare[true]) then begin // fallback to ComparePointer(A, B) if not enough RTTI fCompare[true] := @_BC_Default; fCompare[false] := @_BC_Default; end; // set class serialization and initialization if aParser = ptClass then begin // default JSON serialization of published props fJsonSave := @_JS_RttiCustom; fJsonLoad := @_JL_RttiCustom; // prepare efficient ClassNewInstance() and recognize most parents C := fValueClass; repeat if C = TObjectList then // any branch taken will break below begin fNewInstance := @_New_ObjectList; fJsonSave := @_JS_TObjectList; fJsonLoad := @_JL_TObjectList; end else if C = TInterfacedObjectWithCustomCreate then fNewInstance := @_New_InterfacedObjectWithCustomCreate else if C = TPersistentWithCustomCreate then fNewInstance := @_New_PersistentWithCustomCreate else if C = TObjectWithCustomCreate then begin fNewInstance := @_New_ObjectWithCustomCreate; // allow any kind of customization for TObjectWithCustomCreate children // - is used e.g. by TOrm or TObjectWithID n := Props.Count; TObjectWithCustomCreateRttiCustomSetParser( TObjectWithCustomCreateClass(fValueClass), self); if n <> Props.Count then fFlags := fFlags + fProps.AdjustAfterAdded; // added a prop end else if C = TSynObjectList then begin fNewInstance := @_New_SynObjectList; fJsonSave := @_JS_TSynObjectList; fJsonLoad := @_JL_TSynObjectList; end else if C = TSynLocked then fNewInstance := @_New_SynLocked else if C = TComponent then fNewInstance := @_New_Component else if C = TInterfacedCollection then begin if fValueClass <> C then begin fCollectionItem := TInterfacedCollectionClass(fValueClass).GetClass; fCollectionItemRtti := Rtti.RegisterClass(fCollectionItem); end; fNewInstance := @_New_InterfacedCollection; fJsonSave := @_JS_TCollection; fJsonLoad := @_JL_TCollection; end else if C = TCollection then begin fNewInstance := @_New_Collection; fJsonSave := @_JS_TCollection; fJsonLoad := @_JL_TCollection; end else if C = TCollectionItem then fNewInstance := @_New_CollectionItem else if C = TList then fNewInstance := @_New_List else if C = TObject then fNewInstance := @_New_Object else begin // customize JSON serialization if C = TSynList then fJsonSave := @_JS_TSynList else if C = TObjectWithID then fJsonLoad := @_JL_RttiObjectWithID; // also accepts "RowID" field C := C.ClassParent; // continue with the parent class continue; end; break; // we reached the root supported class until false; case fValueRtlClass of vcStrings: begin fJsonSave := @_JS_TStrings; fJsonLoad := @_JL_TStrings; end; vcList: fJsonSave := @_JS_TList; vcRawUtf8List: begin fJsonSave := @_JS_TRawUtf8List; fJsonLoad := @_JL_TRawUtf8List; end; end; end else if rcfBinary in Flags then begin fJsonSave := @_JS_Binary; fJsonLoad := @_JL_Binary; end else case Kind of rkChar: begin fJsonSave := @_JS_Char; fJsonLoad := @_JL_Char; include(fFlags, rcfJsonString); end; rkWChar {$ifdef FPC}, rkUChar {$endif}: begin fJsonSave := @_JS_WideChar; fJsonLoad := @_JL_WideChar; include(fFlags, rcfJsonString); end; else begin // default well-known serialization fJsonSave := PTC_JSONSAVE[aParserComplex]; if not Assigned(fJsonSave) then fJsonSave := PT_JSONSAVE[aParser]; fJsonLoad := PT_JSONLOAD[aParser]; // rkRecordTypes serialization with proper fields RTTI if (not Assigned(fJsonSave)) and (Flags * [rcfWithoutRtti, rcfHasNestedProperties] <> []) then fJsonSave := @_JS_RttiCustom; if (not Assigned(fJsonLoad)) and (Flags * [rcfWithoutRtti, rcfHasNestedProperties] <> []) then fJsonLoad := @_JL_RttiCustom end; end; // TRttiJson.RegisterCustomSerializer() custom callbacks have priority if Assigned(fJsonWriter.Code) then fJsonSave := @_JS_RttiCustom; if Assigned(fJsonReader.Code) then fJsonLoad := @_JL_RttiCustom; result := self; end; procedure TRttiJson.SetValueClass(aClass: TClass; aInfo: PRttiInfo); begin inherited SetValueClass(aClass, aInfo); if aClass.InheritsFrom(TSynList) then fValueRtlClass := vcSynList else if aClass.InheritsFrom(TRawUtf8List) then fValueRtlClass := vcRawUtf8List; end; function TRttiJson.ParseNewInstance(var Context: TJsonParserContext): TObject; begin result := fNewInstance(self); TRttiJsonLoad(fJsonLoad)(@result, Context); if not Context.Valid then FreeAndNil(result); end; function TRttiJson.ValueCompare(Data, Other: pointer; CaseInsensitive: boolean): integer; begin fCompare[CaseInsensitive](Data, Other, Info, result); // at least _BC_Default end; function TRttiJson.ValueToVariant(Data: pointer; out Dest: TVarData; Options: pointer{PDocVariantOptions}): PtrInt; var tmp: pointer; vt: cardinal; ctx: TGetJsonField; begin // see TRttiCustomProp.GetValueDirect vt := Cache.VarDataVType; TRttiVarData(Dest).VType := vt; case vt of varInt64, varBoolean: // rkInteger,rkBool,rkEnumeration,rkSet using VInt64 for unsigned 32-bit Dest.VInt64 := RTTI_FROM_ORD[Cache.RttiOrd](Data); varWord64: // rkInt64, rkQWord begin if not (rcfQWord in Cache.Flags) then TRttiVarData(Dest).VType := varInt64; // fix VType Dest.VInt64 := PInt64(Data)^; end; varSingle: Dest.VInteger := PInteger(Data)^; varDate, varDouble, varCurrency: Dest.VInt64 := PInt64(Data)^; varString: // rkString begin Dest.VAny := nil; // avoid GPF RawByteString(Dest.VAny) := PRawByteString(Data)^; end; varOleStr: // rkWString begin Dest.VAny := nil; // avoid GPF WideString(Dest.VAny) := PWideString(Data)^; end; {$ifdef HASVARUSTRING} varUString: // rkUString begin Dest.VAny := nil; // avoid GPF UnicodeString(Dest.VAny) := PUnicodeString(Data)^; end; {$endif HASVARUSTRING} varVariant: // rkVariant SetVariantByValue(PVariant(Data)^, PVariant(@Dest)^); varUnknown: // rkChar, rkWChar, rkSString converted into temporary RawUtf8 begin TRttiVarData(Dest).VType := varString; Dest.VAny := nil; // avoid GPF Info.StringToUtf8(Data, RawUtf8(Dest.VAny)); end; else begin tmp := nil; // use temporary JSON conversion SaveJson(Data^, Info, [], RawUtf8(tmp)); // =TJsonWriter.AddTypedJson() TRttiVarData(Dest).VType := varEmpty; ctx.Json := tmp; JsonToAnyVariant(variant(Dest), ctx, Options, true); FastAssignNew(tmp); end; end; result := Cache.ItemSize; end; procedure TRttiJson.ValueLoadJson(Data: pointer; var Json: PUtf8Char; EndOfObject: PUtf8Char; ParserOptions: TJsonParserOptions; CustomVariantOptions: PDocVariantOptions; ObjectListItemClass: TClass; Interning: TRawUtf8Interning); var ctxt: TJsonParserContext; begin if Assigned(self) then begin ctxt.InitParser(Json, self, ParserOptions, CustomVariantOptions, ObjectListItemClass, Interning); if Assigned(fJsonLoad) then // efficient direct Json parsing TRttiJsonLoad(fJsonLoad)(Data, ctxt) else // try if binary serialization was used ctxt.Valid := ctxt.ParseNext and (Ctxt.Value <> nil) and (PCardinal(Ctxt.Value)^ and $ffffff = JSON_BASE64_MAGIC_C) and BinaryLoadBase64(pointer(Ctxt.Value + 3), Ctxt.ValueLen - 3, Data, Ctxt.Info.Info, {uri=}false, rkAllTypes, {withcrc=}false); if ctxt.Valid then Json := ctxt.Json else Json := nil; end else Json := nil; end; function TRttiJson.ValueIterateCount(Data: pointer): integer; begin result := -1; // unsupported if Data <> nil then case Kind of rkDynArray: result := length(PByteDynArray(Data)^); // length() is for all types rkClass: begin Data := PPointer(Data)^; // TObject are stored by reference if Data <> nil then case ValueRtlClass of // vcStrings can't be supported since TStrings.Items[] is a getter vcCollection: result := TCollection(Data).Count; vcObjectList, vcList: result := TList(Data).Count; vcSynList: result := TSynList(Data).Count; vcRawUtf8List: result := TRawUtf8List(Data).Count; end; end; end; end; function TRttiJson.ValueIterate(Data: pointer; Index: PtrUInt; out ResultRtti: TRttiCustom): pointer; begin result := nil; if Data <> nil then case Kind of rkDynArray: if Index < PtrUInt(length(PByteDynArray(Data)^)) then begin result := PPAnsiChar(Data)^ + (Index * PtrUInt(ArrayRtti.Size)); ResultRtti := ArrayRtti; // also available for (most) unmanaged types if ArrayRtti.Kind in [rkClass, rkLString] then result := PPointer(result)^; // resolved as for rkClass below end; rkClass: begin Data := PPointer(Data)^; // TObject are stored by reference if Data <> nil then case ValueRtlClass of // getter methods do require resolved results vcCollection: if Index < PtrUInt(TCollection(Data).Count) then begin result := TCollection(Data).Items[Index]; ResultRtti := fCollectionItemRtti; end; vcObjectList, vcList: if Index < PtrUInt(TList(Data).Count) then begin result := TList(Data).List[Index]; if result <> nil then ResultRtti := Rtti.RegisterClass(PClass(result)^); end; vcSynList: if Index < PtrUInt(TSynList(Data).Count) then begin result := TSynList(Data).List[Index]; if result <> nil then ResultRtti := Rtti.RegisterClass(PClass(result)^); end; vcRawUtf8List: if Index < PtrUInt(TRawUtf8List(Data).Count) then begin result := TRawUtf8List(Data).TextPtr[Index]; ResultRtti := PT_RTTI[ptRawUtf8]; exit; end; end; end; end; end; function StrEquA(n, str: PByte): boolean; var c: byte; begin result := false; if str = nil then exit; repeat c := n^; if c <> str^ then // UTF-8 case-sensitive search exit else if c = 0 then break; // n = str inc(n); inc(str); until false; result := true; end; function StrEquAW(n: PByte; str: PWord): boolean; var c: cardinal; begin result := false; if str = nil then exit; repeat c := n^; if c <> str^ then // 7-bit ASCII case-sensitive search exit else if c = 0 then break; // n = str inc(n); inc(str); until false; result := true; end; function TRttiJson.ValueByPath(var Data: pointer; Path: PUtf8Char; var Temp: TVarData; PathDelim: AnsiChar): TRttiCustom; var vt: TSynInvokeableVariantType; p: PRttiCustomProp; v: TVarData; i: PtrInt; n: ShortString; begin result := self; if (self <> nil) and (Data <> nil) then repeat GetNextItemShortString(Path, @n, PathDelim); if n[0] = #0 then break; if result.Props.CountNonVoid <> 0 then begin // search name in rkRecord/rkObject or rkClass properties p := FindCustomProp( pointer(result.Props.List), @n[1], ord(n[0]), result.Props.Count); if (p = nil) or (p^.OffsetGet < 0) then // we don't support getters yet break; result := p^.Value; inc(PAnsiChar(Data), p.OffsetGet); if Path = nil then exit; // reach last path if result.Kind = rkClass then // stored by reference Data := PPointer(PAnsiChar(Data) + p.OffsetGet)^; continue; end else case result.Kind of rkVariant: // try TDocVariant/TBsonVariant name lookup if DocVariantType.FindSynVariantType(PVarData(Data)^.VType, vt) then begin TRttiVarData(v).VType := varEmpty; // IntGet() would clear it vt.IntGet(v, PVarData(Data)^, @n[1], ord(n[0]), {noexc=}true); if v.VType = varEmpty then break; Temp := v; Data := @Temp; result := PT_RTTI[ptVariant]; if Path = nil then exit; continue; end; rkEnumeration, rkSet: // check enumeration/set name against the stored value if Path = nil then // last path only begin i := result.Cache.EnumInfo^.GetEnumNameValue(@n[1], ord(n[0])); if i < 0 then break; // enum name match: return a boolean to stop searching if result.Kind = rkEnumeration then begin // true = enum name matches the stored enum value result.ValueToVariant(Data, v); // calls RTTI_FROM_ORD[] PBoolean(@Temp)^ := v.VInt64 = i; end else // true = enum name is part of the set value PBoolean(@Temp)^ := GetBitPtr(Data, i); Data := @Temp; result := PT_RTTI[ptBoolean]; // true/false if enum name found exit; end; rkLString: // case-sensitive comparison of a UTF-8 value with the name if Path = nil then // last path only if StrEquA(@n[1], PPByte(Data)^) then // n[1] ends with #0 exit; // return self as non nil value {$ifdef HASVARUSTRING} rkUstring, {$endif HASVARUSTRING} rkWString: // case-sensitive comparison of a UTF-16 value with the name if Path = nil then // last path only if StrEquAW(@n[1], PPWord(Data)^) then exit; end; break; until false; result := nil; // path not found end; procedure TRttiJson.RawSaveJson(Data: pointer; const Ctxt: TJsonSaveContext); begin TRttiJsonSave(fJsonSave)(Data, Ctxt); end; procedure TRttiJson.RawLoadJson(Data: pointer; var Ctxt: TJsonParserContext); begin TRttiJsonLoad(fJsonLoad)(Data, Ctxt); end; class function TRttiJson.Find(Info: PRttiInfo): TRttiJson; begin result := pointer(Rtti.FindType(Info)); end; class function TRttiJson.RegisterCustomSerializer(Info: PRttiInfo; const Reader: TOnRttiJsonRead; const Writer: TOnRttiJsonWrite): TRttiJson; begin result := Rtti.RegisterType(Info) as TRttiJson; // (re)set fJsonSave/fJsonLoad result.fJsonWriter := TMethod(Writer); result.fJsonReader := TMethod(Reader); if result.Kind <> rkDynArray then // Reader/Writer are for items, not array result.SetParserType(result.Parser, result.ParserComplex); end; class function TRttiJson.RegisterCustomSerializerClass(ObjectClass: TClass; const Reader: TOnClassJsonRead; const Writer: TOnClassJsonWrite): TRttiJson; begin // without {$M+} ObjectClasss.ClassInfo=nil -> ensure fake RTTI is available result := Rtti.RegisterClass(ObjectClass) as TRttiJson; result.fJsonWriter := TMethod(Writer); result.fJsonReader := TMethod(Reader); result.SetParserType(ptClass, pctNone); end; class function TRttiJson.UnRegisterCustomSerializer(Info: PRttiInfo): TRttiJson; begin result := Rtti.RegisterType(Info) as TRttiJson; result.fJsonWriter.Code := nil; // force reset of the JSON serialization result.fJsonReader.Code := nil; if result.Kind <> rkDynArray then // Reader/Writer are for items, not array result.SetParserType(result.Parser, result.ParserComplex); end; class function TRttiJson.UnRegisterCustomSerializerClass(ObjectClass: TClass): TRttiJson; begin // without {$M+} ObjectClasss.ClassInfo=nil -> ensure fake RTTI is available result := Rtti.RegisterClass(ObjectClass) as TRttiJson; result.fJsonWriter.Code := nil; // force reset of the JSON serialization result.fJsonReader.Code := nil; result.SetParserType(result.Parser, result.ParserComplex); end; class function TRttiJson.RegisterFromText(DynArrayOrRecord: PRttiInfo; const RttiDefinition: RawUtf8; IncludeReadOptions: TJsonParserOptions; IncludeWriteOptions: TTextWriterWriteObjectOptions): TRttiJson; begin result := Rtti.RegisterFromText(DynArrayOrRecord, RttiDefinition) as TRttiJson; result.fIncludeReadOptions := IncludeReadOptions; result.fIncludeWriteOptions := IncludeWriteOptions; end; procedure _GetDataFromJson(Data: pointer; var Json: PUtf8Char; EndOfObject: PUtf8Char; Rtti: TRttiCustom; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8InterningAbstract); begin (Rtti as TRttiJson).ValueLoadJson(Data, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant], CustomVariantOptions, nil, TRawUtf8Interning(Interning)); end; { ********** JSON Serialization Wrapper Functions } function JsonEncode(const NameValuePairs: array of const): RawUtf8; var temp: TTextWriterStackBuffer; begin if high(NameValuePairs) < 1 then // return void JSON object on error result := '{}' else with TJsonWriter.CreateOwnedStream(temp) do try AddJsonEscape(NameValuePairs); SetText(result); finally Free end; end; function JsonEncode(const Format: RawUtf8; const Args, Params: array of const): RawUtf8; var temp: TTextWriterStackBuffer; begin with TJsonWriter.CreateOwnedStream(temp) do try AddJson(Format, Args, Params); SetText(result); finally Free end; end; function JsonEncodeArrayDouble(const Values: array of double): RawUtf8; var W: TJsonWriter; temp: TTextWriterStackBuffer; begin W := TJsonWriter.CreateOwnedStream(temp); try W.Add('['); W.AddCsvDouble(Values); W.Add(']'); W.SetText(result); finally W.Free end; end; function JsonEncodeArrayUtf8(const Values: array of RawUtf8): RawUtf8; var W: TJsonWriter; temp: TTextWriterStackBuffer; begin W := TJsonWriter.CreateOwnedStream(temp); try W.Add('['); W.AddCsvUtf8(Values); W.Add(']'); W.SetText(result); finally W.Free end; end; function JsonEncodeArrayInteger(const Values: array of integer): RawUtf8; var W: TJsonWriter; temp: TTextWriterStackBuffer; begin W := TJsonWriter.CreateOwnedStream(temp); try W.Add('['); W.AddCsvInteger(Values); W.Add(']'); W.SetText(result); finally W.Free end; end; function JsonEncodeArrayOfConst(const Values: array of const; WithoutBraces: boolean): RawUtf8; begin JsonEncodeArrayOfConst(Values, WithoutBraces, result); end; procedure JsonEncodeArrayOfConst(const Values: array of const; WithoutBraces: boolean; var result: RawUtf8); var temp: TTextWriterStackBuffer; begin if length(Values) = 0 then if WithoutBraces then result := '' else result := '[]' else with TJsonWriter.CreateOwnedStream(temp) do try if not WithoutBraces then Add('['); AddCsvConst(Values); if not WithoutBraces then Add(']'); SetText(result); finally Free end; end; procedure JsonEncodeNameSQLValue(const Name, SQLValue: RawUtf8; var result: RawUtf8); var temp: TTextWriterStackBuffer; begin if (SQLValue <> '') and (SQLValue[1] in ['''', '"']) then // unescape SQL quoted string value into a valid JSON string with TJsonWriter.CreateOwnedStream(temp) do try Add('{', '"'); AddNoJsonEscapeUtf8(Name); Add('"', ':'); AddQuotedStringAsJson(SQLValue); Add('}'); SetText(result); finally Free; end else // Value is a number or null/true/false result := '{"' + Name + '":' + SQLValue + '}'; end; procedure SaveJson(const Value; TypeInfo: PRttiInfo; Options: TTextWriterOptions; var result: RawUtf8; ObjectOptions: TTextWriterWriteObjectOptions); var temp: TTextWriterStackBuffer; begin with TJsonWriter.CreateOwnedStream(temp, twoNoSharedStream in Options) do try CustomOptions := CustomOptions + Options; AddTypedJson(@Value, TypeInfo, ObjectOptions); SetText(result); finally Free; end; end; function SaveJson(const Value; TypeInfo: PRttiInfo; EnumSetsAsText: boolean): RawUtf8; begin SaveJson(Value, TypeInfo, TEXTWRITEROPTIONS_SETASTEXT[EnumSetsAsText], result); end; function SaveJson(const Value; TypeInfo: PRttiInfo): RawUtf8; begin SaveJson(Value, TypeInfo, [], Result, []); end; function SaveJson(const Value; const TypeName: RawUtf8; Options: TTextWriterOptions): RawUtf8; var nfo: TRttiCustom; begin nfo := Rtti.RegisterTypeFromName(TypeName); if nfo = nil then result := '' else SaveJson(Value, nfo.Cache.Info, Options, result); end; {$ifdef FPC} procedure JsonForDebug(Value: pointer; var TypeName: RawUtf8; out JsonResultText: RawUtf8); var nfo: TRttiCustom; vmt: PAnsiChar; begin if (TypeName <> '') and (Value <> nil) then try nfo := Rtti.RegisterTypeFromName(TypeName); // from Rtti.Register*() functions {$ifdef HASINTERFACEASTOBJECT} // we target FPC/Lazarus anyway if (nfo = nil) and (TypeName[1] = 'I') then // guess class instance from interface variable nfo := Rtti.RegisterClass(PInterface(Value)^ as TObject); {$endif HASINTERFACEASTOBJECT} if (nfo = nil) and (TypeName[1] = 'T') then begin vmt := PPointer(Value)^; // guess if seems to be a real TObject instance if (vmt <> nil) and SeemsRealPointer(vmt) and (PPtrInt(vmt + vmtInstanceSize)^ >= sizeof(vmt)) and SeemsRealPointer(PPointer(vmt + vmtClassName)^) and IdemPropName(PShortString(vmt + vmtClassName)^, pointer(TypeName), length(TypeName)) then nfo := Rtti.RegisterClass(TClass(pointer(vmt))); end; if nfo <> nil then begin SaveJson(Value^, nfo.Cache.Info, [twoEnumSetsAsBooleanInRecord], JsonResultText, [woEnumSetsAsText]); exit; end; except // especially if Value is no class JsonResultText := ''; // impossible to serialization this value end; end; {$endif FPC} function RecordSaveJson(const Rec; TypeInfo: PRttiInfo; EnumSetsAsText: boolean): RawUtf8; begin if (TypeInfo <> nil) and (TypeInfo^.Kind in rkRecordTypes) then SaveJson(Rec, TypeInfo, TEXTWRITEROPTIONS_SETASTEXT[EnumSetsAsText], result) else result := NULL_STR_VAR; end; function DynArraySaveJson(const Value; TypeInfo: PRttiInfo; EnumSetsAsText: boolean): RawUtf8; begin if (TypeInfo = nil) or (TypeInfo^.Kind <> rkDynArray) then result := NULL_STR_VAR else if pointer(Value) = nil then result := '[]' else SaveJson(Value, TypeInfo, TEXTWRITEROPTIONS_SETASTEXT[EnumSetsAsText], result); end; function DynArrayBlobSaveJson(TypeInfo: PRttiInfo; BlobValue: pointer; BlobLen: PtrInt): RawUtf8; var DynArray: TDynArray; Value: pointer; // decode BlobValue into a temporary dynamic array temp: TTextWriterStackBuffer; begin Value := nil; DynArray.Init(TypeInfo, Value); try if DynArray.LoadFrom(BlobValue, PAnsiChar(BlobValue) + BlobLen) = nil then result := '' else with TJsonWriter.CreateOwnedStream(temp) do try AddDynArrayJson(DynArray); SetText(result); finally Free; end; finally DynArray.Clear; // release temporary memory end; end; function ObjArrayToJson(const aObjArray; aOptions: TTextWriterWriteObjectOptions): RawUtf8; var temp: TTextWriterStackBuffer; begin with TJsonWriter.CreateOwnedStream(temp) do try if woEnumSetsAsText in aOptions then CustomOptions := CustomOptions + [twoEnumSetsAsTextInRecord]; AddObjArrayJson(aObjArray, aOptions); SetText(result); finally Free; end; end; function ObjectsToJson(const Names: array of RawUtf8; const Values: array of TObject; Options: TTextWriterWriteObjectOptions): RawUtf8; var i, n: PtrInt; temp: TTextWriterStackBuffer; begin with TJsonWriter.CreateOwnedStream(temp) do try n := high(Names); BlockBegin('{', Options); i := 0; if i <= high(Values) then repeat if i <= n then AddFieldName(Names[i]) else if Values[i] = nil then AddFieldName(SmallUInt32Utf8[i]) else AddPropName(ClassNameShort(Values[i])^); WriteObject(Values[i], Options); if i = high(Values) then break; BlockAfterItem(Options); inc(i); until false; CancelLastComma; BlockEnd('}', Options); SetText(result); finally Free; end; end; function ObjectToJsonFile(Value: TObject; const JsonFile: TFileName; Options: TTextWriterWriteObjectOptions): boolean; var humanread: boolean; json: RawUtf8; begin humanread := woHumanReadable in Options; if humanread and (woHumanReadableEnumSetAsComment in Options) then humanread := false else // JsonReformat() erases comments exclude(Options, woHumanReadable); json := ObjectToJson(Value, Options); if humanread then // woHumanReadable not working with custom JSON serializers, e.g. T*ObjArray // TODO: check if this is always the case with our mORMot2 new serialization result := JsonBufferReformatToFile(pointer(json), JsonFile) else result := FileFromString(json, JsonFile); end; function GetValueObject(Instance: TObject; const Path: RawUtf8; out Value: variant): boolean; var p: PRttiCustomProp; begin result := GetInstanceByPath(Instance, Path, p); if result then p^.GetValueVariant(Instance, TVarData(Value), @JSON_[mFastFloat]); end; function LoadJsonInPlace(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8Interning): PUtf8Char; begin TRttiJson(Rtti.RegisterType(TypeInfo)).ValueLoadJson( @Value, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant], CustomVariantOptions, nil, Interning); result := Json; end; function LoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8Interning): boolean; var tmp: TSynTempBuffer; begin tmp.Init(Json); // make private copy before in-place decoding try result := LoadJsonInPlace(Value, tmp.buf, TypeInfo, EndOfObject, CustomVariantOptions, Tolerant, Interning) <> nil; finally tmp.Done; end; end; function RecordLoadJson(var Rec; Json: PUtf8Char; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8Interning): PUtf8Char; begin if (TypeInfo = nil) or not (TypeInfo.Kind in rkRecordTypes) then raise EJsonException.CreateUtf8('RecordLoadJson: % is not a record', [TypeInfo.Name]); TRttiJson(Rtti.RegisterType(TypeInfo)).ValueLoadJson( @Rec, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant], CustomVariantOptions, nil, Interning); result := Json; end; function RecordLoadJson(var Rec; const Json: RawUtf8; TypeInfo: PRttiInfo; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8Interning): boolean; var tmp: TSynTempBuffer; begin tmp.Init(Json); // make private copy before in-place decoding try result := RecordLoadJson(Rec, tmp.buf, TypeInfo, nil, CustomVariantOptions, Tolerant, Interning) <> nil; finally tmp.Done; end; end; function DynArrayLoadJson(var Value; Json: PUtf8Char; TypeInfo: PRttiInfo; EndOfObject: PUtf8Char; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8Interning): PUtf8Char; begin if (TypeInfo = nil) or (TypeInfo.Kind <> rkDynArray) then raise EJsonException.CreateUtf8('DynArrayLoadJson: % is not a dynamic array', [TypeInfo.Name]); TRttiJson(Rtti.RegisterType(TypeInfo)).ValueLoadJson( @Value, Json, EndOfObject, JSONPARSER_DEFAULTORTOLERANTOPTIONS[Tolerant], CustomVariantOptions, nil, Interning); result := Json; end; function DynArrayLoadJson(var Value; const Json: RawUtf8; TypeInfo: PRttiInfo; CustomVariantOptions: PDocVariantOptions; Tolerant: boolean; Interning: TRawUtf8Interning): boolean; var tmp: TSynTempBuffer; begin tmp.Init(Json); // make private copy before in-place decoding try result := DynArrayLoadJson(Value, tmp.buf, TypeInfo, nil, CustomVariantOptions, Tolerant, Interning) <> nil; finally tmp.Done; end; end; function JsonToObject(var ObjectInstance; From: PUtf8Char; out Valid: boolean; TObjectListItemClass: TClass; Options: TJsonParserOptions; Interning: TRawUtf8Interning): PUtf8Char; var ctxt: TJsonParserContext; begin if pointer(ObjectInstance) = nil then raise ERttiException.Create('JsonToObject(nil)'); ctxt.InitParser(From, Rtti.RegisterClass(TObject(ObjectInstance)), Options, nil, TObjectListItemClass, Interning); TRttiJsonLoad(Ctxt.Info.JsonLoad)(@ObjectInstance, ctxt); Valid := ctxt.Valid; result := ctxt.Json; end; function JsonSettingsToObject(const JsonContent: RawUtf8; Instance: TObject): boolean; var tmp: TSynTempBuffer; begin result := false; if JsonContent = '' then exit; tmp.Init(JsonContent); // copy for in-place comment removal and JSON parsing try RemoveCommentsFromJson(tmp.buf); JsonToObject(Instance, tmp.buf, result, nil, JSONPARSER_TOLERANTOPTIONS); finally tmp.Done; end; end; function ObjectLoadJson(var ObjectInstance; const Json: RawUtf8; TObjectListItemClass: TClass; Options: TJsonParserOptions; Interning: TRawUtf8Interning): boolean; var tmp: TSynTempBuffer; begin tmp.Init(Json); if tmp.len <> 0 then try JsonToObject(ObjectInstance, tmp.buf, result, TObjectListItemClass, Options, Interning); finally tmp.Done; end else result := false; end; function JsonToNewObject(var From: PUtf8Char; var Valid: boolean; Options: TJsonParserOptions; Interning: TRawUtf8Interning): TObject; var ctxt: TJsonParserContext; begin ctxt.InitParser(From, nil, Options, nil, nil, Interning); result := ctxt.ParseNewObject; Valid := ctxt.Valid; end; function PropertyFromJson(Prop: PRttiCustomProp; Instance: TObject; From: PUtf8Char; var Valid: boolean; Options: TJsonParserOptions; Interning: TRawUtf8Interning): PUtf8Char; var ctxt: TJsonParserContext; begin Valid := false; result := nil; if (Prop = nil) or (Prop^.Value.Kind <> rkClass) or (Instance = nil) then exit; ctxt.InitParser(From, Prop^.Value, Options, nil, nil, Interning); if not JsonLoadProp(pointer(Instance), Prop, ctxt) then exit; Valid := true; result := ctxt.Json; end; function UrlDecodeObject(U: PUtf8Char; Upper: PAnsiChar; var ObjectInstance; Next: PPUtf8Char; Options: TJsonParserOptions): boolean; var tmp: RawUtf8; begin result := UrlDecodeValue(U, Upper, tmp, Next); if result then JsonToObject(ObjectInstance, Pointer(tmp), result, nil, Options); end; function JsonFileToObject(const JsonFile: TFileName; var ObjectInstance; TObjectListItemClass: TClass; Options: TJsonParserOptions; Interning: TRawUtf8Interning): boolean; var tmp: RawUtf8; begin tmp := RawUtf8FromFile(JsonFile); if tmp = '' then result := false else begin RemoveCommentsFromJson(pointer(tmp)); JsonToObject(ObjectInstance, pointer(tmp), result, TObjectListItemClass, Options, Interning); end; end; procedure JsonBufferToXML(P: PUtf8Char; const Header, NameSpace: RawUtf8; out result: RawUtf8); var i, j, L: PtrInt; temp: TTextWriterStackBuffer; begin if P = nil then result := Header else with TJsonWriter.CreateOwnedStream(temp) do try AddNoJsonEscape(pointer(Header), length(Header)); L := length(NameSpace); if L <> 0 then AddNoJsonEscape(pointer(NameSpace), L); AddJsonToXML(P); if L <> 0 then for i := 1 to L do if NameSpace[i] = '<' then begin for j := i + 1 to L do if NameSpace[j] in [' ', '>'] then begin Add('<', '/'); AddStringCopy(NameSpace, i + 1, j - i - 1); Add('>'); break; end; break; end; SetText(result); finally Free; end; end; function JsonToXML(const Json, Header, NameSpace: RawUtf8): RawUtf8; var tmp: TSynTempBuffer; begin tmp.Init(Json); try JsonBufferToXML(tmp.buf, Header, NameSpace, result); finally tmp.Done; end; end; { ********************* Abstract Classes with Auto-Create-Fields } function DoRegisterAutoCreateFields(ObjectInstance: TObject): TRttiJson; begin // sub procedure for smaller code generation in AutoCreateFields/Create result := Rtti.RegisterAutoCreateFieldsClass(PClass(ObjectInstance)^) as TRttiJson; end; function AutoCreateFields(ObjectInstance: TObject): TRttiJson; var n: integer; p: PPRttiCustomProp; begin // inlined Rtti.RegisterClass() {$ifdef NOPATCHVMT} result := pointer(Rtti.FindType(PPointer(PPAnsiChar(ObjectInstance)^ + vmtTypeInfo)^)); {$else} result := PPointer(PPAnsiChar(ObjectInstance)^ + vmtAutoTable)^; {$endif NOPATCHVMT} if (result = nil) or not (rcfAutoCreateFields in result.Flags) then result := DoRegisterAutoCreateFields(ObjectInstance); p := pointer(result.fAutoCreateInstances); if p = nil then exit; // create all published class (or IDocList/IDocDict) fields n := PDALen(PAnsiChar(p) - _DALEN)^ + _DAOFF; // length(AutoCreateClasses) repeat with p^^ do PPointer(PAnsiChar(ObjectInstance) + OffsetGet)^ := TRttiJson(Value).fNewInstance(Value); // class or interface inc(p); dec(n); until n = 0; end; procedure AutoDestroyFields(ObjectInstance: TObject; Info: TRttiJson); var n: integer; p: PPRttiCustomProp; arr: pointer; o: TObject; begin if Info = nil then {$ifdef NOPATCHVMT} Info := pointer(Rtti.FindType(PPointer(PPAnsiChar(ObjectInstance)^ + vmtTypeInfo)^)); {$else} Info := PPointer(PPAnsiChar(ObjectInstance)^ + vmtAutoTable)^; {$endif NOPATCHVMT} // free all published class fields p := pointer(Info.fAutoDestroyClasses); if p <> nil then begin n := PDALen(PAnsiChar(p) - _DALEN)^ + _DAOFF; repeat o := PObject(PAnsiChar(ObjectInstance) + p^^.OffsetGet)^; if o <> nil then // inlined o.Free o.Destroy; inc(p); dec(n); until n = 0; end; // release all published T*ObjArray fields p := pointer(Info.fAutoCreateObjArrays); if p = nil then exit; n := PDALen(PAnsiChar(p) - _DALEN)^ + _DAOFF; repeat arr := PPointer(PAnsiChar(ObjectInstance) + p^^.OffsetGet)^; if arr <> nil then // inlined ObjArrayClear() RawObjectsClear(arr, PDALen(PAnsiChar(arr) - _DALEN)^ + _DAOFF); inc(p); dec(n); until n = 0; end; { TPersistentAutoCreateFields } constructor TPersistentAutoCreateFields.Create; begin AutoCreateFields(self); end; // no need to call the void inherited TPersistentWithCustomCreate destructor TPersistentAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TSynAutoCreateFields } constructor TSynAutoCreateFields.Create; begin AutoCreateFields(self); end; // no need to call the void inherited TSynPersistent destructor TSynAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TSynAutoCreateFieldsLocked } constructor TSynAutoCreateFieldsLocked.Create; begin AutoCreateFields(self); inherited Create; // initialize fSafe := NewSynLocker end; destructor TSynAutoCreateFieldsLocked.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TInterfacedObjectAutoCreateFields } constructor TInterfacedObjectAutoCreateFields.Create; begin AutoCreateFields(self); end; // no need to call TInterfacedObjectWithCustomCreate.Create destructor TInterfacedObjectAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TInterfacedSerializable } class function TInterfacedSerializable.SerializableInterface: TRttiCustom; begin result := Rtti.FindClass(self).Cache.SerializableInterface; end; class function TInterfacedSerializable.Guid: PGuid; begin result := SerializableInterface.Cache.InterfaceGuid; end; function _New_ISerializable(Rtti: TRttiCustom): pointer; begin result := TInterfacedSerializableClass(Rtti.Cache.SerializableClass).Create(nil); TInterfacedSerializable(result).fRefCount := 1; // inlined GetInterface() inc(PByte(result), Rtti.Cache.SerializableInterfaceEntryOffset); end; class procedure TInterfacedSerializable.NewInterface(out Obj); begin pointer(Obj) := _New_ISerializable(SerializableInterface); end; class function TInterfacedSerializable.RegisterToRtti( InterfaceInfo: PRttiInfo): TRttiJson; var ent: PInterfaceEntry; begin ent := nil; if (self <> nil) and InterfaceInfo^.InterfaceImplements(ISerializable) then ent := GetInterfaceEntry(InterfaceInfo^.InterfaceGuid^); // resolve TGuid if (ent = nil) or not InterfaceEntryIsStandard(ent) then raise ERttiException.CreateUtf8('Unexpected %.RegisterToRtti(%)', [self, InterfaceInfo^.Name^]); result := Rtti.RegisterType(InterfaceInfo) as TRttiJson; result.fCache.SerializableClass := self; result.fCache.SerializableInterfaceEntryOffset := ent^.IOffset; // get once TOnRttiJsonRead(result.fJsonReader) := JL; TOnRttiJsonWrite(result.fJsonWriter) := JS; result.SetParserType(result.Parser, result.ParserComplex); // needed result.fNewInstance := @_New_ISerializable; TRttiJson(Rtti.RegisterClass(self)).fCache.SerializableInterface := result; end; procedure TInterfacedSerializable.SetJson(const value: RawUtf8); var tmp: TSynTempBuffer; ctx: TJsonParserContext; begin tmp.Init(value); try ctx.InitParser(tmp.buf, SerializableInterface, [], nil, nil, nil); FromJson(ctx); finally tmp.Done; end; end; class procedure TInterfacedSerializable.JS(W: TJsonWriter; data: pointer; options: TTextWriterWriteObjectOptions); begin data := PPointer(data)^; if data = nil then W.AddNull // avoid GPF if ISerializable = nil else ISerializable(data).ToJson(W, options); end; class procedure TInterfacedSerializable.JL(var context: TJsonParserContext; data: pointer); var o: TInterfacedSerializable; i: ^ISerializable absolute data; begin if not Assigned(i^) then begin // inlined Create + GetInterface() o := Create(context.CustomVariant); o.fRefCount := 1; inc(PByte(o), context.Info.Cache.SerializableInterfaceEntryOffset); PPointer(data)^ := o; end; i^.FromJson(context) end; function TInterfacedSerializable.GetJson: RawUtf8; begin result := ToJson(jsonCompact, []); end; function TInterfacedSerializable.ToJson(format: TTextWriterJsonFormat; options: TTextWriterWriteObjectOptions): RawUtf8; var W: TJsonWriter; temp: TTextWriterStackBuffer; begin W := TJsonWriter.CreateOwnedStream(temp); try ToJson(W, options); W.SetText(result, Format); finally W.Free; end; end; function TInterfacedSerializable.ToString(format: TTextWriterJsonFormat; options: TTextWriterWriteObjectOptions): string; begin Utf8ToStringVar(ToJson(format, options), result); end; { TInterfacedSerializableAutoCreateFields } constructor TInterfacedSerializableAutoCreateFields.Create( options: PDocVariantOptions); begin fRttiJson := AutoCreateFields(self); end; destructor TInterfacedSerializableAutoCreateFields.Destroy; begin AutoDestroyFields(self, fRttiJson); inherited Destroy; end; procedure TInterfacedSerializableAutoCreateFields.ToJson(W: TJsonWriter; options: TTextWriterWriteObjectOptions); var ctx: TJsonSaveContext; begin ctx.W := W; ctx.Info := fRttiJson; ctx.Options := options + fRttiJson.IncludeWriteOptions; _JS_RttiCustom(@self, ctx); // all done via known RTTI end; procedure TInterfacedSerializableAutoCreateFields.FromJson( var context: TJsonParserContext); begin context.Info := fRttiJson; // from interface RTTI to class RTTI _JL_RttiCustom(@self, context); end; { TCollectionItemAutoCreateFields } constructor TCollectionItemAutoCreateFields.Create(Collection: TCollection); begin AutoCreateFields(self); inherited Create(Collection); end; destructor TCollectionItemAutoCreateFields.Destroy; begin AutoDestroyFields(self); inherited Destroy; end; { TSynJsonFileSettings } function TSynJsonFileSettings.AfterLoad: boolean; begin result := true; // success end; function TSynJsonFileSettings.LoadFromJson(const aJson: RawUtf8; const aSectionName: RawUtf8): boolean; begin if fsoReadIni in fSettingsOptions then begin fSectionName := aSectionName; result := false; end else result := JsonSettingsToObject(aJson, self); if not result then begin result := IniToObject(aJson, self, aSectionName, @JSON_[mFastFloat]); if result then begin fSectionName := aSectionName; include(fSettingsOptions, fsoWriteIni); // save back as INI end; end; if result then result := AfterLoad; end; function TSynJsonFileSettings.LoadFromFile(const aFileName: TFileName; const aSectionName: RawUtf8): boolean; begin fFileName := aFileName; fInitialJsonContent := RawUtf8FromFile(aFileName); // may detect BOM result := LoadFromJson(fInitialJsonContent, aSectionName); if not result then fInitialJsonContent := ''; // file was neither valid JSON nor INI: ignore end; function TSynJsonFileSettings.FolderName: TFileName; begin if self = nil then result := '' else result := ExtractFilePath(fFileName); end; procedure TSynJsonFileSettings.SaveIfNeeded; var saved: RawUtf8; begin if (self = nil) or (fFileName = '') or (fsoDisableSaveIfNeeded in fSettingsOptions) then exit; if fsoWriteIni in fSettingsOptions then saved := ObjectToIni(self, fSectionName) else saved := ObjectToJson(self, SETTINGS_WRITEOPTIONS); if saved = fInitialJsonContent then exit; FileFromString(saved, fFileName); fInitialJsonContent := saved; end; type // local type definitions for their own RTTI to be found by name RawUtf8 = type Utf8String; {$ifdef CPU64} PtrInt = type Int64; PtrUInt = type QWord; {$else} PtrInt = type integer; PtrUInt = type cardinal; {$endif CPU64} procedure InitializeUnit; var i: integer; // not PtrInt since has just been overriden c: AnsiChar; {$ifdef FPC} dummy: RawUtf8; {$endif} begin // branchless JSON escaping - JSON_ESCAPE_NONE=0 if no JSON escape needed JSON_ESCAPE[0] := JSON_ESCAPE_ENDINGZERO; // 1 for #0 end of input for i := 1 to 31 do JSON_ESCAPE[i] := JSON_ESCAPE_UNICODEHEX; // 2 to escape #1..#31 as \u00xx JSON_ESCAPE[8] := ord('b'); // others contain the escaped character JSON_ESCAPE[9] := ord('t'); JSON_ESCAPE[10] := ord('n'); JSON_ESCAPE[12] := ord('f'); JSON_ESCAPE[13] := ord('r'); JSON_ESCAPE[ord('\')] := ord('\'); JSON_ESCAPE[ord('"')] := ord('"'); for c := #32 to #127 do JSON_UNESCAPE[c] := c; JSON_UNESCAPE['b'] := #8; JSON_UNESCAPE['t'] := #9; JSON_UNESCAPE['n'] := #10; JSON_UNESCAPE['f'] := #12; JSON_UNESCAPE['r'] := #13; JSON_UNESCAPE['u'] := JSON_UNESCAPE_UTF16; for c := low(c) to high(c) do begin if c in [#0, ',', ']', '}', ':'] then include(JSON_CHARS[c], jcEndOfJsonFieldOr0); if c in [#0, ',', ']', '}'] then include(JSON_CHARS[c], jcEndOfJsonFieldNotName); if c in [#0, #9, #10, #13, ' ', ',', '}', ']'] then include(JSON_CHARS[c], jcEndOfJsonValueField); if c in [#0, '"', '\'] then include(JSON_CHARS[c], jcJsonStringMarker); if c in ['-', '0'..'9'] then begin include(JSON_CHARS[c], jcDigitFirstChar); JSON_TOKENS[c] := jtFirstDigit; end; if c in ['-', '+', '0'..'9', '.', 'E', 'e'] then include(JSON_CHARS[c], jcDigitFloatChar); if c in ['_', '0'..'9', 'a'..'z', 'A'..'Z', '$'] then include(JSON_CHARS[c], jcJsonIdentifierFirstChar); if c in ['_', '0'..'9', 'a'..'z', 'A'..'Z', '.', '[', ']'] then include(JSON_CHARS[c], jcJsonIdentifier); if c in ['_', 'a'..'z', 'A'..'Z', '$'] then // exclude '0'..'9' as already in jcDigitFirstChar JSON_TOKENS[c] := jtIdentifierFirstChar; end; JSON_TOKENS[#0 ] := jtEndOfBuffer; JSON_TOKENS['{'] := jtObjectStart; JSON_TOKENS['}'] := jtObjectStop; JSON_TOKENS['['] := jtArrayStart; JSON_TOKENS[']'] := jtArrayStop; JSON_TOKENS[':'] := jtAssign; JSON_TOKENS['='] := jtEqual; JSON_TOKENS[','] := jtComma; JSON_TOKENS[''''] := jtSingleQuote; JSON_TOKENS['"'] := jtDoubleQuote; JSON_TOKENS['t'] := jtTrueFirstChar; JSON_TOKENS['f'] := jtFalseFirstChar; JSON_TOKENS['n'] := jtNullFirstChar; JSON_TOKENS['/'] := jtSlash; // initialize JSON serialization Rtti.GlobalClass := TRttiJson; // will ensure Rtti.Count = 0 // now we can register some local type alias to be found by name or ASAP Rtti.RegisterTypes([TypeInfo(RawUtf8), TypeInfo(PtrInt), TypeInfo(PtrUInt), TypeInfo(TRawUtf8DynArray), TypeInfo(TIntegerDynArray)]); // prepare some JSON wrappers GetDataFromJson := _GetDataFromJson; InitializeVariantsJson; // from mormot.core.variants {$ifdef FPC} // we need to call it once so that it is linked to the executable JsonForDebug(nil, dummy, dummy); {$endif FPC} end; initialization InitializeUnit; DefaultJsonWriter := TJsonWriter; end.