jFiles is derived from code written by Dries Driessen.
It is used here under license.
For the purposes of this section it is assumed that
the JSON object is declared as follows;
json JSONClass
Loading from a JSON File into a Group,
Queue or Table
Loading a JSON file into a structure is a single
line of code.
json.Load(Structure, JsonFileName,
<boundary>)
You can use a Table, Queue or Group using this approach.
If the JSON file contains named objects, then specify the name as the
boundary parameter. If you omit this parameter, but the JSON you want to
import is inside a named JSON object, then nothing will be imported.
The fields in the JSON file are matched to fields in your record
structure. There are three properties which affect this matching. For
more information on matching see the
section on
Field Matching Tips.
After the load is complete two methods will be available for your use;
GetRecordsInserted()
GetRecordsUpdated()
These methods return a counter of the records inserted (into the Table
or Queue) and the number of records updated (in the Table). These
counters are reset to zero by a call to the
Start
method.
When the
Load method completes it will
return either
jf:Ok (0) or
jf:ERROR
(-1). If
jf:Error is returned then a
description of the error is in the Error property. A call to the
ErrorTrap method will have been made as well.
The use of a JSON Array (ie list)
structure usually implies that the Clarion structure should also be a
List, and equally the use of a JSON Object (not a list) structure
implies a group, or value on the Clarion side. However jFiles
automatically supports simple object into a List structure (the same as
a list with one value) and also importing a
List
structure into a Clarion Group structure.
Loading from a JSON String into a
Group, Queue or Table
Loading a JSON string into a structure is a single
line of code. It's the same as for a table, but uses a StringTheory
object instead of a file name.
str StringTheory
code
json.Load(Structure, str, <boundary>)
This is the same as the
Loading from a File
method described above, except that the source is a StringTheory string,
and not a file.
Loading a JSON String into the JSON
object for processing
The JSONobject can be loaded from a string or file
using one of these methods;
json.LoadString(StringTheory)
or
json.LoadFile(Filename)
Once the JSON text has been loaded into the object it is automatically
parsed, and then you can directly inspect the contents of the JSON.
jsonItem &JSONClass
str StringTheory
code
str.SetValue('json text is here')
json.LoadString(str)
Remember that all JSON files are just a collection of items. Each item
in turn can be a collection of more items. Once the string is loaded
into the JSON object the Records method returns the number of items at
the top level;
x = json.Records()
Like a queue you can loop through the items in the object using the Get
method
loop x = 1 to json.records()
jsonItem &= json.Get(x)
end
You can check the name of the item using the Name method
loop x = 1 to json.records()
jsonItem &= json.Get(x)
if jsonItem.Name() = 'Customers'
! do something
end
end
an alternative to looping through the items in the object is to use the
GetByName method.
jsonItem &= json.GetByName('customers')
You can test if you got something back by checking that
jsonItem
is not NULL. This is important - using a NULL object will
result in a GPF
If not jsonItem &= Null
Once you have a specific item, you can load this into a structure;
jsonItem.Load(structure)
The field names in your JSON will need to match the fieldnames in your
structure. See
Field Matching Tips for
hits on making the matching process better.
Putting the code together it looks something like this;
jsonItem &= json.GetByName('customers')
If not jsonItem &= Null
jsonItem.Load(structure)
End
If you have a specific field in the JSON you can extract it using the
GetValueByName method
somestring = json.GetValuebyName('surname')
Deformatting incoming JSON Values
By default the contents of a JSON field will be
copied into your Clarion field during the LOAD. However the format of
the JSON field may not be the format you wish to store in your Clarion
field. For example an incoming date, formatted as yyyy/mm/dd
may need to be deformatted in order to store it in a Clarion LONG field.
To do this a method is provided;
json Class(jsonClass)
DeformatValue Procedure (String pName, String
pValue),STRING,VIRTUAL
End
Please note that the name being passed in here, in the pName
parameter, is the JSON field name, not the Clarion field name.
Using this name as the identifier it is possible to create a Case
statement before the parent call, deformatting the value as required.
For example;
case pName
of 'DATUM'
Return deformat(pValue,'@d1')
of 'TIME'
Return deformat(pValue,'@t4')
end
Filtering Records when Loading
When loading into a Table or Queue it can be useful
to filter out records which are not desired.
This is done by embedding code into the
ValidateRecord
method.
json Class(jsonClass)
ValidateRecord Procedure(),Long,Proc,Virtual
End
If (your code in) this method returns
jf:filtered
then the record is not added to the Table or Queue and the next
record is processed. If (your code in) this method returns
jf:outofrange
then the import is considered complete, and no further records are
loaded. If (your code in)this method returns
jf:ok
then the record is added to the table or queue.
When this method is called the table record, or queue record, has been
primed with the record that is about to be written.
Example
json.ValidateRecord Procedure ()
Code
If inv:date < date(1,1,2018) then Return jf:filtered.
Return jf:ok
Pointers
Consider a queue or group structure which contains a
reference. For example;
SomeQueue Queue
Date Long
Time Long
Notes &StringTheory
End
When data is moved into the Notes field
then a more complicated assignment needs to take place (for each
incoming record in the JSON).
In order to make this possible code needs to be added to the AssignField
and GetColumnType methods.
json Class(jsonClass)
GetColumnType Procedure (*Group pGroup, *Cstring
pColumnName),Long,VIRTUAL
AssignField Procedure (*Group pGroup, *Cstring pColumnName,
JSONClass pJson),VIRTUAL
End
The first thing to do is tell the system that a complex assignment needs
to take place for a specific field. This is done in the GetColumnType
method. In this example;
json.GetColumnType Procedure (*Group pGroup,
*Cstring pColumnName)
CODE
Case pColumnName
of 'Notes'
return jf:StringTheory
end
jf:StringTheory is an equate, but any
non-zero value will cause the assignment to be done in AssignField.
(At which point you are responsible for the assignment.) The assignment
is done as follows:
json.AssignField Procedure (*Group pGroup, *Cstring
pColumnName, JSONClass pJson)
CODE
Case pColumnName
of 'Notes'
SomeQueue.Notes &= NEW(StringTheory)
SomeQueue.Notes.SetValue(pJson.GetValue())
End
Parent.AssignField(pGroup,pColumnName,pJson)
As you can see in the above code, the column name is tested, and if it
is the reference column (Notes) then a StringTheory object is created,
and the value is placed in there.
This is done for each record in the queue. This means that you need to
be very careful when deleting records from the queue and when freeing
the queue. And the queue MUST be correctly emptied before ending the
procedure or a memory leak will occur.
For example;
FreeSomeQueue Routine
loop while records(q:Product)
Get(q:Product,1)
Dispose(SomeQueue.Notes)
Delete(q:Product)
End
Match By Number
While JSON is commonly formatted as "name value
pairs", it doesn't have to be this way. It could just be a collection of
arrays, with no names at all. For example say this is the contents of
the a.json file;
[
[45,80],
[30,85],
[16,4]
]
To load this into a structure it's clearly not possible to match the
json to field names in the structure, rather it needs to import based on
the position of the value. This can be done by setting the MatchByNumber
property to true. For example;
aq QUEUE
tem string(255)
pressure string(255)
end
code
json.start()
json.SetMatchByNumber(true)
json.load(aq,'a.json')
Loading a Queue within a Queue
Consider this JSON structure;
[{
"Id": 60,
"Data": {
"TotalPaymentAmount": 90.63,
"Discounts": [{
"DiscountType": "DASH",
"Amount": 136.27
}, {
"DiscountType": "LOTS",
"Amount": 210.27
}]
}
}, {
"Id": 61,
"Data": {
"TotalPaymentAmount": 90.63,
"Discounts": [{
"DiscountType": "DASH",
"Amount": 325.27
}, {
"DiscountType": "LOTS",
"Amount": 499.27
}]
}
}, {
"Id": 63,
"Data": {
"TotalPaymentAmount": 90.63,
"Discounts": {
"DiscountType": "SPECIAL",
"Amount": 525.27
}
}
}]
This is a List, containing multiple records. Inside each record is a
group (Data) and inside each group is a queue of discounts.
The (simplified) Clarion version of this structure would ideally look
like this (but as you'll see this is not allowed)
Messages Queue
Id Long
Data Group
TotalPaymentAmount Decimal(10,2)
Discounts Queue
Amount Decimal(10,2)
End
End
End
Clarion does however not allow queues to be declared inside groups, or
inside other queues. There are two approaches to solving this problem;
The one approach is to create a queue inside the group by using a queue
pointer, and then NEWing and DISPOSEing as necessary. But this approach
can be complicated if you are not used to it, and it can cause memory
leaks if you are not careful.
An alternative approach is to use two Queues.
MessagesQ Queue
Id Long,name('Id')
Data Group,name('Data')
TotalPaymentAmount Decimal(10,2),name('TotalPaymentAmount')
End
End
DiscountsQ Queue
MessageId Long
DiscountType String(20),name('DiscountType')
Amount Decimal(10,2),name('Amount')
End
All the messages go in one queue, and all the discounts go in another.
The MessageID serves to determine which discounts belong in which queue.
The code to populate these two queues looks like this;
json Class(JSONClass)
AddQueueRecord Procedure (Long pFirst=0),Long,Proc,Virtual
End
oneMess &jsonClass
node &jsonClass
MessageId long
CODE
Free(MessagesQ)
Free(DiscountsQ)
json.Start()
json.SetFreeQueueBeforeLoad(False)
json.SetRemovePrefix(True)
json.SetReplaceColons(True)
json.SetTagCase(jf:CaseAsIs) !
json.LoadString(jsonStr)
json.Load(MessagesQ)
Loop x = 1 to json.records()
oneMess &= json.Get(x)
node &= onemess.GetByName('Id')
if not node &= NULL
MessageId = node.GetValue()
! now get the discounts
node &= oneMess.GetByName('Discounts',2)
if not node &= NULL
node.Load(DiscountsQ) end
end
end
json.AddQueueRecord Procedure (Long pFirst=0)
Q &Queue
CODE
Q &= self.Q
If Q &= DiscountsQ
DiscountsQ.MessageId = MessageId
End
Parent.AddQueueRecord(pFirst)
As you can see in the above approach the AddQueueRecord
is overridden so that the extra field in the DiscountsQ
is properly primed.
Aside: You may notice that the call to load the DiscountsQ
uses the node object ( node.Load(DiscountsQ)
) but the object being overridden is the json
object. Usually this would mean the code would not run, however jFiles
automatically manages this, as node is a reference to a jFiles object
inside the object called json, code in the
json object automatically applies to the
nodes as well.
Importing Recursive Nodes
Consider the following json;
[{
"id": 1,
"Parent": 0,
"name": "John Smith",
"children": [{
"id": 2,
"Parent": 1,
"name": "Sally Smith"
},{
"id": 3,
"Parent": 1,
"name": "Teresa Smith"
}]
}]
At first glance this looks like a list, but it's not. It's a collection
of nodes, related to each other, in a tree like pattern. The parent and
child notes however do have a common structure, which suggests this
could be loaded into a queue.
TestQ QUEUE
id LONG,NAME('id')
Parent LONG,NAME('Parent')
name STRING(100),NAME('name')
END
To import a node structure into a list structure requires the engine to
walk through the nodes, adding each one to the queue. This is done using
the LoadNodes method.
json.start()
json.SetTagCase(jF:CaseAsIs)
json.LoadFile('whatever.json')
json.LoadNodes(TestQ,'children')
One advantage of the JSON above is that it contains a parent field,
where every child node explicitly links to its parent. In many cases
though this is not the case. Consider this JSON
[{
"id": 1,
"name": "John Smith",
"children": [{
"id": 2,
"name": "Sally Smith"
},{
"id": 3,
"name": "Teresa Smith"
}]
}]
Now the identity of the parent is determined by the location of the node
in the tree. If we move this into a queue then the location is lost.
The above structure suggests a queue like this;
TestQ QUEUE
id LONG,NAME('id')
name STRING(100),NAME('name')
END
Since the location is being lost an additional field to store the
"parentId" is required.
TestQ QUEUE
id LONG,NAME('id')
name STRING(100),NAME('name')
ParentID LONG
END
Then in the call to LoadNodes this field, and the name of the
identifying field, must be included.
json.start()
json.SetTagCase(jF:CaseAsIs)
json.LoadFile('whatever.json')
json.LoadNodes(TestQ,'children',TestQ.ParentID,'id')
The above code tells jFiles to use the ID field of one node as the
ParentID of all child nodes.
Detecting Omitted and NULL values
When importing into a structure (Group, Queue or
Table), each field in the structure is primed from the incoming JSON.
This works perfectly if the JSON contains a field with the same name.
If the field does not exist in the incoming JSON then the field in the
structure is cleared, to a blank string or a zero value.
If the field does exist, but the value is set as null, then again the
field is cleared or set to zero.
This approach is convenient and simple, but does make it difficult when
you are working later on with the structure (perhaps a group or queue)
to identify fields which were explicitly set to blank as distinct from
fields which were omitted, as distinct from fields set to null.
To overcome this problem jFiles allows you to set specific values for
omitted strings or numbers, and specific values for null strings and
null numbers. While this doesn't necessarily solve the problem in all
cases (you still need to have some values the user cannot use) it will
be suitable in most cases.
For example;
json.Start()
json.SetTagCase(jf:CaseAsIs)
json.SetWriteOmittedString(True)
json.SetOmittedStringDefaultValue('//omitted//')
json.SetWriteOmittedNumeric(true)
json.SetOmittedNumericDefaultValue(2415919000)
json.SetNullStringValue('null')
json.SetNullNumericValue(2415919001)
json.Load(structure,'a.json')
Note that you would need to parse, and manage these values in your
structure (group, queue, table) but at least you can tell that they have
been omitted.
The properties for omitted and null are unrelated, you can use one set
without the other set if you like.
Loading a JSON List into a Group
Typically if you have an incoming List of data you
would load this into a matching Queue on the program site. It is however
possible to use a Group structure, and have jFiles load that group
structure, even if the incoming JSON contains a List structure.
For example;
Settings Group
Server String(255)
Port Long
End
this would usually expect JSON like this
{
"Server":"www.capesoft.com",
"Port":80
}
However the incoming JSON may occasionally be a list, like this
[{
"Server":"https://www.capesoft.com",
"Port":80
},
{
"Server":"https://www.capesoft.com",
"Port":443
}
]
If the Load method was called with the Group as the destination
structure then each item in turn will be loaded into the group,
resulting in the last record in the JSON being left in the group when
the Load completes.
For each record loaded into the group the
ValidateRecord
method is called. So you can embed code in here if you want to
iterate through the list. If the method then returns
jf:StopHere
then the loop will terminate and the Group will be left with the current
contents.
json Class(jsonClass)
ValidateRecord Procedure(),Long,Proc,Virtual
End
json.ValidateRecord Procedure ()
Code
If inv:date < date(1,1,2018) then Return jf:filtered.
Return jf:ok
For the purposes of this section it is assumed that
the JSON object is declared as follows;
json JSONClass
Reusing a JSON object
If you reuse a json object multiple times then
properties set in one use may inadvertently cascade to the next use. To
"clean" an object so that it starts fresh, call the Start method. For
example;
json.Start()
This will reset the internal properties back to their default values.
Saving a Clarion structure to a JSON File
on disk
The simplest way to create a JSON file is simply to
use the .Save method and an existing Clarion structure.
json.Save(Structure,<FileName>,
<boundary>, <format>, <compressed>)
For example
json.Start()
json.Save(CustomerTable,'.\customer.json')
or
json.Start()
json.Save(CustomerTable,'.\customer.json','Customers')
or
json.Start()
json.Save(CustomerTable,'.\customer.json','',json:Object)
You can use a Group, Queue, View or File as the structure holding the
data being saved.
The method returns 0 if successful, non-zero otherwise.
The boundary parameter allows you to "name" the records. For example, if
the boundary parameter is omitted the JSON is
[ { record }, {record} , ... {record} ]
If the boundary is included then the JSON becomes
{ "boundary" : [ { record }, {record} , ...
{record} ] }
The Format property determines if the output is formatted to
human-readable or if all formatting is removed (to make the file a bit
smaller). If omitted it defaults to true - meaning that the output is
human readable. This is recommended, especially while developing as it
makes understanding the JSON and debugging your code a lot easier.
If the Compressed parameter is omitted, then the default value is false.
If the Compressed parameter is set to true then the file will be gzip
compressed before writing it to the disk.
If the FileName parameter is omitted, or blank, then the json object
will be populated with the file, but no disk write will take place. You
can then save the object to a
StringTheory
object, or to a
File, or use it
in a
collection later on.
Saving a Clarion structure to a JSON
String in Memory
json.Save(Structure,
StringTheory, <boundary>, <format>)
This is the same as saving the JSON to a File, except that the second
parameter is a StringTheory object not a string.
For example;
str StringTheory
Code
json.Start()
json.Save(CustomerTable,str)
For explanation of the
Boundary and
Format parameters see the section
above.
Constructing JSON Manually
In some cases constructing the correct Clarion
structure may be difficult, or the structure itself may not be known at
compile time. In these situations you can use the low-level Add method
to simply construct the JSON manually.
The Add method takes 3 parameters, the name, the value, and the type of
the item. In turn it returns a pointer to the node created. Using this
pointer allows you to embed inside nodes as you create them. For
example;
{
"company" : "capesoft",
"location" : "cape town",
"phone" : "087 828 0123",
"product" : [
{ "name" : "jfiles" },
{ "quality" : "great" },
{ "sales" : "strong" }
]
}
In the above JSON there is a simply group structure, followed by a list
containing a variable number of name/value pairs.
The code to create the above could be written as;
Json JSONClass
Product &JSONClass
KeyValue &JSONClass
code
json.start()
json.add('company','capesoft')
json.add('location','cape town')
json.add('phone','087 828 0123')
Product &= json.add('product','', json:Array)
KeyValue &= Product.add('','',json:Object)
KeyValue.add('name','jfiles')
KeyValue &= Product.add('','',json:Object)
KeyValue.add('quality','great')
KeyValue &= Product.add('','',json:Object)
KeyValue.add('sales','strong')
Of course this is just an example. Using the Add method, and the fact
that it returns the node added, allow you to construct any JSON you
like.
That said, this method can be tedious, and making use of Clarion
structures is often easier to manage in the long run.
Storing Multiple Items in a JSON
object
The Save methods described above are perfect for
creating a simple JSON structure based on a simple Clarion Data Type.
However there are times when you will need to create a single JSON
object which contains multiple different elements (known as a
Collection.)
collection &JSONClass
The collection is created using the CreateCollection method.
collection &=
json.CreateCollection(<boundary>)
If the boundary is omitted then a default boundary ("subSection")
will be used.
[Aside: You do not need to dispose the Collection object - that will be
done for you when the json object disposes.]
You can then use the Append method to add
items to the collection. There are a number of forms of the Append
Method.
Append (File, <Boundary>)
Append (Queue, <Boundary>)
Append (View, <Boundary>)
Append (Group, <Boundary>)
As with the Save methods the Boundary parameter is optional and can be
omitted. If the parameter is omitted then a default object name will be
used.
You can also
Append (Name, Value, <Type>)
to add a single name value pair to the collection. The type is the JSON
type of the Value. It should be one of
json:String EQUATE(1)
json:Numeric EQUATE(2)
json:Object EQUATE(3)
json:Array EQUATE(4)
json:Boolean EQUATE(5)
json:Nil EQUATE(6)
if the Type parameter is omitted then the default
json:string is used.
Here is a complete example;
json JSONClass
collection &JSONClass
code
json.Start()
collection &= json.CreateCollection('Collection')
collection.Append(Customer,'Customers')
collection.Append(Queue:Browse:1)
collection.Append(MemoView)
Once you have created your collection you can save it to Disk or String
using the techniques described below.
Saving a JSON Object to Disk
After you have constructed the JSON object to your
satisfaction, you may want to store it as a file. This can be done using
the SaveFile method. For example;
json.SaveFile('filename',format)
If the format parameter is set to true, then the file will be formatted
with line breaks, and indentation (using tabs), suitable for a person to
read.
If the format parameter is false (or omitted) then the file will be kept
as small as possible by leaving out the formatting.
Saving a JSON Object to
StringTheory
Internally the JSON is stored as a collection of
objects. To use the result in a program it must be turned into a String
and stored in a StringTheory object. This is done by passing a
StringTheory object to the SaveString method.
For example;
str StringTheory
code
json.SaveString(str,format)
Once in the StringTheory object it can then be manipulated, compressed,
saved or managed in any way you like.
If the format parameter is set to true, then the string will be
formatted with line breaks, and indentation (using tabs), suitable for a
person to read.
If the format parameter is false (or omitted) then the string will be
kept as small as possible by leaving out the formatting.
Arrays
JSON supports Arrays. They look something like this
{"phone": [ "011 111 2345","011 123 4567"]}
The square brackets indicate that the field ("phone") contains a list of
values, ie an array.
Clarion also supports arrays, using the DIM attribute. So creating
fields like the one above is very straight-forward
PhoneGroup Group
phone string(20),DIM(5)
End
code
json.Save(PhoneGroup)
This would result in JSON that looks like this
{
"PHONE" : ["012 345 6789","023 456 7890"]
}
Empty items in the array, which come after the last set value, are
suppressed. In the above example, only the first two array values were
set, so only 2 values were included in the output. Position in the array
is preserved. If phone[1] and phone[3] were set, but phone[2] was left
blank then the output would read
{
"PHONE" : ["012 345 6789","","023 456 7890"]
}
Items are considered empty if the field is a string, and contains no
characters, of if the field is a number and contains a 0.
Clarion supports multi-dimensional arrays. These are written out as if
they were a single dimension array.
For example;
Matrix[1,1] = 1010
Matrix[2,1] = 2010
Matrix[1,2] = 1020
Matrix[2,2] = 2020
becomes
"MATRIX" : [1010,1020,2010,2020]
Note that variables of DIM(1) are not
allowed. To be an array the dimension value must be greater than 1.
It is possible (and valid) for an entire JSON file to consist of a
single array.
[1,2,3,4,5]
In this case there is neither a field name, nor an array or object
wrapper around the value.
dude long,dim(5)
code
dude[1] = 12
dude[2] = 22
dude[3] = 43
json.Start()
json.SetType(json:Null)
json.AddArray('',dude)
this results in
[12,22,43]
As noted earlier, trailing empty values (0 if a number, blank if a
string) are removed.
Blobs
Creating JSON files from Tables or Views that
contain Blobs are supported, but unfortunately coverage can vary a bit
based on the driver in use.
Tables
Creating JSON using any of the Save(Table...) ,
Add(Table...) or Append(Table...) methods is supported.
If the table has memo or blob fields then these are included in the
export, and there is nothing specific you need to do.
If you wish to suppress MEMOs and BLOBs when saving a table set the NoMemo property to true. For example
json.start()
json.SetNomemo(True)
json.save(...)
Views
Clarion Views behave differently when using
TopSpeed or SQL drivers. So if you need to save a BLOB with a VIEW
then read this section carefully.
For all drivers, VIEWs can contain BLOB fields. For example;
ViewProduct View(Product)
Project(PRO:Name)
Project(PRO:RRP)
Project(PRO:Description)
End
TopSpeed
The TopSpeed driver is able to detect the BLOB fields in the VIEW, and
so there's no extra code for you to do.
Note 1: If the NoMemo property is set to
true then the BLOB will be suppressed
even if it is in the VIEW.
Note 2 : The shorthand method of projecting all fields in a table, by
projecting no fields, does not include BLOB or MEMO fields. If you
want to PROJECT MEMOS or BLOBs then you must PROJECT it (and all other
fields) explicitly.
SQL
The SQL drivers are unable to detect BLOB fields in the VIEW
structure. The BLOBS are still populated, but the explicit method to
determine if the BLOB is in the VIEW or not, does not work for SQL
Drivers.
jFiles adopts the following work-arounds to this issue.
a) [Default behavior]. When looping through the VIEW all the BLOB
fields are checked for content. If the value is not blank then it is
included in the output. In other words BLOBs with content are
exported, BLOBs without content are not included. In most cases this
will likely be sufficient as JSON allows fields to be excluded when
they are blank.
b) If all the BLOBs from the Table are included in the VIEW then you
can set the property ExportBlobsWithView to
true. If this value is true then all the
BLOBs will be included in each JSON record. If they are blank (or not
included in the VIEW) then they will be included in the JSON as blank.
So in order to export BLOBs with VIEW records set the property ExportBlobsWithView to true. For example;
ViewProduct View(Product)
Project(PRO:Name)
Project(PRO:RRP)
Project(PRO:Description)
! This is a blob
End
json.Start()
json.SetExportBlobsWithView(True)
json.Save(ViewProduct)
Note1: If the NoMemo property is set to
true then the memos and blobs will not be included even if ExportBlobsWithView
is set to true.
Creating Nested JSON structures
This section follows on from the
Storing Multiple Items in a JSON Object section above.
Another form of the Append method exists, which allows you to start a
new collection within your collection.
Append(<Boundary>)
This starts a new collection inside an existing collection. To use this,
first you need to declare a pointer to this collection;
subItem &JSONClass
Then (after doing the
CreateCollection call
and so on) you can do
subItem &=
Collection.Append('VersionInformation')
and after that do as many
subItem.Appends
as you like.
This nesting can continue to as many levels as you like.
Here is a complete example;
json JSONClass
collection &JSONClass
subItem &JsonClass
code
json.Start()
collection &= json.CreateCollection('Collection')
collection.Append(Customer,'Customers')
collection.Append(Queue:Browse:1)
collection.Append(MemoView)
subItem &= Collection.Append('VersionInformation')
subItem.Append('Version','6.0.3')
subItem.Append('Build',1234,json:numeric)
Formatting Field Values on Save
Up to now all the exporting of the fields has resulted in the raw data
being stored in the JSON file. In some cases though it is preferable to
export the data formatted in some way so that it appears in the JSON as
a more portable value. For example in Clarion Dates are stored as a
LONG, but if the data needs to be imported into another system then
displaying the date as yyyy/mm/dd might
make the transfer a lot easier.
This is easy enough to do by embedding in the FormatValue
method in your json class. The method is declared as;
json.FormatValue PROCEDURE (String pName, String
pValue, *LONG pLiteralType),String
Note the LiteralType parameter. If you are changing the type of the data
(for example, changing the DATE from a Numeric to a String) then you
need to change the LiteralType value as well. The value of this
parameter should be one of
json:String EQUATE(1)
json:Numeric EQUATE(2)
json:Object EQUATE(3)
json:Array EQUATE(4)
json:Boolean EQUATE(5)
json:Null EQUATE(6)
As the name of the field is passed into the method, it is
straight-forward to create a simple CASE statement formatting the fields
as required. This code is embedded before the parent call. Also note
that the value in pName is the JSON field name - not the Clarion field
name. And this value is case sensitive.
case pName
of 'DATUM'
pLiteralType = json:string
Return clip(left(format(pValue,'@d1')))
of 'TIME'
pLiteralType = json:string
Return clip(left(format(pValue,'@t4')))
end
In the above case the Datum and Time fields are formatted, all other
fields are left alone and placed in the file "as is".
Renaming Fields on Save
When exporting JSON from a structure the External
Name of each field is used as the "Tag" : name in the JSON. For example
xQueue Queue
field1 string(255),Name('Total')
End
results in JSON like this;
{"Total" : "whatever"}
Ideally the external Name attribute of the field contains the correct
value for the tag.
There are times however when you need to override this, and this is done
by embedding code into the AdjustFieldName method, AFTER (or BEFORE) the
PARENT call.
Example
json.AdjustFieldName PROCEDURE (StringTheory pName,
Long pTagCase)
CODE
PARENT.AdjustFieldName (pName,pTagCase)
case pName.GetValue()
of 'Total'
pName.SetValue('Totalizer')
End
Note that the field name in the above CASE statement is a case-sensitive
match. If you need a case insensitive match then UPPER or LOWER both the
CASE and OF values.
The Parent call performs the replaceColons and remove prefix work. So,
if you put the CASE before the parent call, then the field will come in
"Clarion Style", if after the parent call then "JSON Style". It is up to
you which side of the Parent call you put your code onto.
Saving Nested Structures -
another approach
Consider the following JSON;
{ "customer" : {
"Name" : "Bruce",
"Phone" : "1234 567 89",
"Invoices" : [
{
"InvoiceNumber" : 1,
"LineItems" : [
{
"Product" : "iDash",
"Amount" : 186.66
}
]
},
{
"InvoiceNumber" : 2,
"LineItems" : [
{
"Product" : "Runscreen",
"Amount" : 179.75
}
]
}
]
}
}
This is constructed from a Group (Customer information) which contains a
Queue (of Invoices) and each invoice contains a Queue of Line Numbers.
It's worth pointing out that the line items queue is a simple JSON form
of a queue, and the Invoice Queue is again just a JSON form of a queue.
The Clarion structures for the above are as follows;
CustomerGroup Group
Name string(50),name('Name')
Phone string(50),name('Phone')
End
InvoicesQueue Queue
InvoiceNumber Long,name('InvoiceNumber')
End
LineItemsQueue Queue
Product String(50),name('Product')
Amount Decimal(8,2),name('Amount')
End
In this case the structures are a Group and Queues, but you could also
use Views or Tables if you wanted to.
In order to achieve the result three jFiles objects are used;
CustomersJson Class(JSONClass)
AssignValue PROCEDURE (JSONClass pJson,StringTheory pName,|
*Group pGroup,*Long pIndex,Long
pColumnOffset),VIRTUAL
End
InvoicesJson Class(JSONClass)
AssignValue PROCEDURE (JSONClass pJson,StringTheory pName, |
*Group pGroup,*Long pIndex,Long
pColumnOffset),VIRTUAL
End
LineItemsJson JSONClass
As you can see two of the classes (the ones that have children) will
have some override code in the AssignValue method.
(More on that in a moment.)
For the purposes of this example, the code for populating the structures
is omitted.
The basic code to generate the JSON file looks like this;
CustomersJson.Start()
CustomersJson.SetTagCase(jF:CaseAsIs)
CustomersJson.Save(CustomerGroup,'customer.json','customer',true)
In order to include the InvoicesQueue inside the group some code is
added to the CustomersJson.AssignValue
method. The code looks like this;
CustomersJson.AssignValue PROCEDURE (JSONClass
pJson,StringTheory pName,|
*Group pGroup,*Long pIndex, Long
pColumnOffset)
Code
PARENT.AssignValue (pJson,pName,pGroup,pIndex,pColumnOffset)
If pName.GetValue() = 'Phone'
do PrimeInvoicesQueue
InvoicesJson.Start()
InvoicesJson.SetTagCase(jF:CaseAsIs)
InvoicesJson.Save(InvoicesQueue, ,'Invoices')
pJson.AddCopy(InvoicesJson)
End
There are a few interesting things to note in the above code.
a) Notice it's checking for the JSON tag 'Phone'
as it appears in the JSON file. This is simply the position in which the
Invoice queue will be injected. As it is after the parent call, it will
come after the Phone field in the JSON
file. If it was before the parent call it would come before the Phone
field. If it came before the parent call, and the parent was
not called at all, then the Phone field
would be excluded from the JSON.
b) The code to Prime the Queue, and Save that Queue to the InvoicesJson
object is standard code as described earlier in this document.
Notice the omitted parameter in the call to .Save.
cc) The AddCopy call is where the magic
happens. This adds a copy of the InvoicesJson object
into the CustomersJson object at the
position specified by the passed in parameter pJson.
d) The parameters pGroup, pIndex
and pColumnOffset are not useful
in your embed code, they are used in the call to the parent method.
As this example covers three layers of JSON, the technique is repeated
for the InvoicesJson object. It too has
an AssignValue method, and it uses
similar code to inject the LineItems at
that point;
InvoicesJson.AssignValue PROCEDURE (JSONClass
pJson,StringTheory pName,|
*Group pGroup,*Long pIndex,Long
pColumnOffset)
CODE
PARENT.AssignValue (pJson,pName,pGroup,pIndex,pColumnOffset)
if pName.GetValue() = 'InvoiceNumber'
do PrimeLineItemsQueue
LineItemsJson.Start()
LineItemsJson.SetTagCase(jF:CaseAsIs)
LineItemsJson.Save(LineItemsQueue, ,'LineItems')
pJson.AddCopy(LineItemsJson)
End
Saving Nested Structures - yet
another approach
Consider the following JSON;
[
{
"_id" : "7123098",
"accountName" : "Charlies Plumbing",
"mainContact" : "",
"mainPhone" : "",
"accountLogins" : [
{
"loginName" : "Administrator",
"loginPwd" : "secret",
"loginHistory" : [
{
"loginDate" : "2017/07/17",
"loginTime" : "16:27"
},
{
"loginDate" : "2017/07/18",
"loginTime" : "15:26"
}
]
},
{
"loginName" : "Operator",
"loginPwd" : "1234",
"loginHistory" : [
{
"loginDate" : "2017/07/17",
"loginTime" : " 8:15"
},
{
"loginDate" : "2017/07/18",
"loginTime" : "15:51"
}
]
}
],
"accountContacts" : [
{
"contactName" : "Beatrice",
"contactPosition" : "CEO"
},
{
"contactName" : "Timothy",
"contactPosition" : "Sales"
}
]
}
]
This is a highly nested structure. It is an AccountsQueue, which in turn
contains a Logins Queue and a Contacts Queue. The Logins Queue contains
a Login History queue.
Here is the Accounts queue declaration;
AccountsQueue Queue
id string(20),name('_id')
accountName string(255),name('accountName')
mainContact string(255),name('mainContact')
mainPhone string(255),name('mainPhone')
accountLogins &accountLoginsQueue,name('accountLogins')
accountContacts &accountContactsQueue,name('accountContacts')
End
The Contacts queue declaration
accountContactsQueue Queue,type
contactName string(100),name('contactName')
contactPosition string(100),name('contactPosition')
End
The Logins queue
accountLoginsQueue Queue,type
loginName string(100),name('loginName')
loginPwd string(100),name('loginPwd')
loginHistory &loginHistoryQueue,name('loginHistory')
End
and finally the History queue
loginHistoryQueue Queue,type
loginDate string(10),name('loginDate')
loginTime string(10),name('loginTime')
End
Populating nested queues has to be done carefully. The queue pointers
are allocated using the NEW statement whenever a record is created. Here
is a single record added to the Accounts queue (with various child queue
entries added as well.)
clear(AccountsQueue)
AccountsQueue.accountLogins &= new(accountLoginsQueue)
AccountsQueue.accountContacts &= new(accountContactsQueue)
AccountsQueue.id = '7123098'
AccountsQueue.AccountName = 'Charlies Plumbing'
AccountsQueue.accountLogins.loginName =
'Administrator'
AccountsQueue.accountLogins.loginPwd = 'secret'
AccountsQueue.accountLogins.loginHistory &= new loginHistoryQueue
AccountsQueue.accountLogins.loginHistory.loginDate =
format(today()-1,@d10)
AccountsQueue.accountLogins.loginHistory.loginTime =
format(random(360000*8, 360000*18),@t1)
add(AccountsQueue.accountLogins.loginHistory)
AccountsQueue.accountLogins.loginHistory.loginDate =
format(today(),@d10)
AccountsQueue.accountLogins.loginHistory.loginTime =
format(random(360000*8, 360000*18),@t1)
add(AccountsQueue.accountLogins.loginHistory)
add(AccountsQueue.accountLogins)
AccountsQueue.accountLogins.loginName = 'Operator'
AccountsQueue.accountLogins.loginPwd = '1234'
AccountsQueue.accountLogins.loginHistory &= new loginHistoryQueue
AccountsQueue.accountLogins.loginHistory.loginDate =
format(today()-1,@d10)
AccountsQueue.accountLogins.loginHistory.loginTime =
format(random(360000*8, 360000*18),@t1)
add(AccountsQueue.accountLogins.loginHistory)
AccountsQueue.accountLogins.loginHistory.loginDate =
format(today(),@d10)
AccountsQueue.accountLogins.loginHistory.loginTime =
format(random(360000*8, 360000*18),@t1)
add(AccountsQueue.accountLogins.loginHistory)
add(AccountsQueue.accountLogins)
AccountsQueue.accountContacts.contactName =
'Beatrice'
AccountsQueue.accountContacts.contactPosition = 'CEO'
add(AccountsQueue.accountContacts)
AccountsQueue.accountContacts.contactName = 'Timothy'
AccountsQueue.accountContacts.contactPosition = 'Sales'
add(AccountsQueue.accountContacts)
Add(AccountsQueue) ! save the queue record.
Sending even a complex structure like to this to JSON is relatively
easy.
First the JSON object is declared. You can do this in code, or let the
extension template declare it for you. Notice the
AddByReference
method, that will be fleshed out in a moment.
json Class(JSONClass)
AddByReference PROCEDURE (StringTheory pName,JSONClass
pJson),VIRTUAL
End
Secondly the json object is called as normal;
json.Start()
json.SetTagCase(jF:CaseAsIs)
json.SetColumnType('accountLogins',jf:Reference) json.SetColumnType('loginHistory',jf:Reference)
json.SetColumnType('accountContacts',jf:Reference)
json.Save(AccountsQueue,'json.txt')
Notice the extra calls to
SetColumnType.
These tell the class that these fields are reference values, and so need
to be saved separately.
The final step is to flesh out the AddByReference method. When the class
encounters one of these reference fields it calls the
AddByReference
method. The code in there looks something like this;
json.AddByReference PROCEDURE (StringTheory
pName,JSONClass pJson)
CODE
case pName.GetValue()
of 'accountLogins'
pJson.Add(AccountsQueue.accountLogins)
of 'accountContacts'
pJson.Add(AccountsQueue.accountContacts)
of 'loginHistory'
pJson.Add(AccountsQueue.accountLogins.loginHistory)
end
PARENT.AddByReference (pName,pJson)
Remember the tag names are case sensitive so be careful entering them
here.
Disposing Nested Queues
This section has nothing to do with jFiles, but
since the above example shows how to load a nested Queue structure,
it's probably worth covering the Disposal of nested queue structures
here. If disposal is not done correctly then a memory leak will occur.
The key lines to worry about in the above code are;
AccountsQueue.accountLogins &=
new(accountLoginsQueue)
AccountsQueue.accountContacts &= new(accountContactsQueue)
and
AccountsQueue.accountLogins.loginHistory &=
new loginHistoryQueue
These lines are creating queues on the fly, and each call to new MUST
have a matching call to dispose. When deleting a row from
AccountsQueue or AccountsQueue.AccountLogins (and that includes
deleting all rows) the child queues themselves must first be disposed.
It's important to manually do this before the procedure ends - it will
not be done automatically.
the basic idea is to loop through the queue, deleting the sub queues
as you go.
For example;
Loop While Records(AccountsQueue)
Get(AccountsQueue,1)
Loop while records(AccountsQueue.accountLogins)
Get(AccountsQueue.accountLogins,1)
Free(AccountsQueue.accountLogins.loginHistory)
Dispose(AccountsQueue.accountLogins.loginHistory)
Delete(AccountsQueue.accountLogins)
End
Free(AccountsQueue.accountLogins)
Dispose(AccountsQueue.accountLogins)
Free(AccountsQueue.accountContacts)
Dispose(AccountsQueue.accountContacts)
Delete(AccountsQueue)
End
When creating a JSON file, or loading a JSON file into
a structure, it is necessary to match the field names in the JSON file
with the field names in your structure. There are properties which assist
in making a good match.
These properties can be set to default values via the global extension,
the local extension, or can be set in embed code before the object is
used. These properties are not reset by a call to
json.Start().
Note that all the properties should be
set using their
SET method, and retrieved
using their
GET method. For example setting
the
RemovePrefix property is done using the
SetRemovePrefix(whatever) method. And it can
be retrieved using the
GetRemovePrefix()
method.
RemovePrefix
Clarion structures allow for the use of prefixes,
which then form part of the field name. If this property is set when you
create JSON then the prefix (and colon) are omitted from the JSON and
only the "name" part of the fieldname is used.
IF you are importing JSON, and the JSON was created by another entity,
then it's likely the fields in the JSON are not prefixed. In that case
you should set this property to true as well, so that the matcher
matches on names-without-prefixes.
PrefixChars
In Clarion a colon (:) character is used to separate
the prefix from the field name. Incoming JSON may be using an alternate
character (often an underscore(_) ) to separate the prefix from the rest
of the name.
To complicate the issue colons, and underscores, are valid characters in
Clarion field names, table names, and prefixes. If you do have colons or
underscores in the name then that brings MaxPrefixLengthInJSON into
play.
MaxPrefixLengthInJSON
To make identifying a prefix easier, it can be
helpful to tell jFiles about the length of any expected prefix. So if
the length of all your prefixes are say 3 characters, then you should
set this value to 4. (3 for the prefix, plus one for the separator.) Any
separators in the string AFTER this length will not be treated as a
prefix separator.
ReplaceColons
Colons are a legal character in Clarion field names.
However in most languages they are not. Therefore to create JSON which
is portable into other systems it may be necessary to replace any colons
with some other character (or characters) - most usually an underscore
character. If you are including the prefix in the name then this setting
becomes doubly important. The default value of this property is true.
ReplacementChars
The default replacement character for a colon is an
underscore character. However if you wish to replace it with some other
combination of characters then you can set this property to whatever you
like, up to 10 characters long.
TagCase
JSON is explicitly case sensitive. When creating
JSON you can control the case using the TagCase
property. Valid values for this property are;
jf:CaseUpper
jf:CaseLower
jf:CaseAsIs
jf:CaseAny
As the equates suggest, CaseUpper forces
all the tags to be uppercase, CaseLower forces
all the tags to be lower case, and CaseAsIs uses
the case as set in the fields External Name. (If there is no External
Name for a field then Upper case is used.)
CaseAny is only used on a Load.
It matches incoming node names to local field names regardless of case.
Labels vs Names
In Clarion fields (fields in a table, queue or
group, or just variables by themselves) have a
label, which is
the identifier in column 1 of the source code.
This is not the
name of the field (although they are often
called "
Field Names". The
Name of a field only exists
if you have set the
,Name property for the
field. Since Clarion is a case insensitive language all labels are seen
as UPPER case by the compiler.
If you are unclear on this please see
ClarionHub.
So when importing make sure you understand this point, especially when
setting the
TagCase property as mentioned
above.
The JSON object can be thought of as a tree. The root JSON object contains
other JSON objects, and those ones contain other ones and so on.
This is a very elegant approach to the code, but it does have one drawback
- code embedded in the methods of the root object (ie the object in your
procedure) does not get called when a method on one of the child objects
is called.
This means that embedding code in most of the methods will not be useful
because it will not execute when you expect it to. However some methods
will execute and are suitable for adding embed code. They are;
AddByReference
AddQueueRecord
AdjustFieldName
AssignField
AssignMissingField
AssignValue
DeformatValue
ErrorTrap
FormatValue
InsertFileRecord
SetColumnType
Trace
UpdatefileRecord
ValidateField
ValidateRecord
In addition many methods are called only by your program so are suitable
for embedding. They are
Start
CreateCollection
Save (any form), SaveFile, SaveString
Load (any form), LoadFile, LoadString
Append (any form)
This product is supplied as source files that are
included in your application. There are no additional files for you to add
to your distribution.