JasperReports - JSONQL Data Source Sample
Shows how the JSONQL data source and the JSONQL query executer implementations could be used to fill reports.
Main Features in This Sample
JSONQL Data Source
The JSONQL query language
JSONQL Data Source
Description / Goal
How to fill a report with data from a JSON file using the JSONQL query language.
Since: 6.3.1
JSONQL Data Source Overview
JSONQL stands for JSON Query Language and it is meant to replace the existing language(simply called JSON) for querying JSON data, but in a separate implementation as they are not compatible in terms of syntax and complexity.
It is recommended that you choose JSONQL over JSON due to its extended capabilities of traversing and filtering JSON structures.
The table below gives an overview of some of the features found in the new JSONQL language in comparison to the existing ones in the JSON language.
The examples are based on the restructured data/northwind.json
source file.
Feature | JSONQL | JSON | Description |
---|---|---|---|
Dot-separated path for simple object keys | Northwind.Customers.Address | [the same] | The addresses of all customers |
Path for complex object keys | Northwind.Customers["Company Name"] | [not possible] | The company names of all customers |
Array index based selection | Northwind.Customers[0].Orders[0] | [the same] | The first order of the first customer |
Object construction expression | Northwind.Customers[0].Orders[OrderId, OrderDate] | [not possible] | Select only OrderId and OrderDate from the orders of the first customer |
Deep traversal | ..[OrderId, OrderDate, ShippedDate, Freight] | [not possible] | Select the OrderId , OrderDate , ShippedDate and Freight from anywhere |
Filtering with simple expression | Northwind.Customers.*(City == "México D.F.") | Northwind.Customers(City == México D.F.) | Customers from México D.F. |
Filtering with complex expression | Northwind.Customers.Orders.*(Freight > 200 && OrderDate *= "1997" ) | [not possible] | Orders for which the Freight is greater than 200 and OrderDate contains 1997 |
Going up the JSON tree | ..OrderDate(@val == "1997-10-03")^{3} or ..OrderDate(@val == "1997-10-03")^^^ | [not possible] | The parent customers(3 levels up) for which there is an order with OrderDate that equals "1997-10-03" |
The JSONQL Query Executer
It is triggered when the jsonql language is specified:
- at report level:
<query language="jsonql"><![CDATA[ ..Orders(@size > 1).* ]] ></query>
- or in the data adapter:
<jsonDataAdapter class="net.sf.jasperreports.data.json.JsonDataAdapterImpl">
...
<language>jsonql</language>
...
</jsonDataAdapter>
The JsonQLQueryExecuter uses the query string to produce a JsonQLDataSource instance, based on the same built-in parameters (or equivalent report properties) as for the existing JsonDataSource. This query executer is registered via JsonQLQueryExecuterFactory factory class.
As in the case of the existing JSON query executer, in order to prepare the data source, the JSONQL query executer looks for the JSON_INPUT_STREAM parameter that contains the JSON source objects in the form of an java.io.InputStream
. If no JSON_INPUT_STREAM parameter is provided, then the query executer looks for the alternate net.sf.jasperreports.json.source
String parameter or report property that stores the path to the location of the JSON source file.
JsonQLQueryExecuter runs the query over the input source and stores the result in an in-memory JsonQLDataSource object.
During the JsonQLDataSource instantiation, the query executer also looks for the following additional parameters or report properties, containing the required localization settings:
net.sf.jasperreports.json.date.pattern
net.sf.jasperreports.json.number.pattern
JSON_LOCALE
(parameter only) of typejava.util.Locale
net.sf.jasperreports.json.locale.code
of typejava.lang.String
; this can be used if nojava.util.Locale
parameter is availableJSON_TIME_ZONE
(parameter only) of typejava.util.TimeZone
net.sf.jasperreports.json.timezone.id
of typejava.lang.String
; this can be used if nojava.util.TimeZone
parameter is available
In the next section you can see how these additional parameters are provided in the /src/JsonQLDataSourceApp.java
class.
The JSONQL Data Source Sample
In our example, the data is stored as a hierarchy of Northwind.Customers.Orders
objects in the data/northwind.json
file.
The JSONQL configuration is done in the data/northwindDataAdapter.jrdax
data adapter file that is provided in the reports/NorthwindOrdersReport.jrxml
via the report property:
<property name="net.sf.jasperreports.data.adapter" value="data/northwindDataAdapter.jrdax"/>
In the NorthwindOrdersReport
we run a JSONQL query in order to retrieve orders from anywhere:
<query language="jsonql"><![CDATA[ ..Orders(@size > 1).* ]] ></query>
In reversed order of application, the query simply translates to: "Select the children(.*) of Orders - with size greater than 1 - from anywhere"
This query is possible since the Orders property of each customer is an array. Therefore we can apply an array specific filtering function to each value for the Orders property. In this case we are interested in the Orders that have more than one element.
The "get children" expression .*
is necessary here because without it we would be looking at an array of arrays(an array with the arrays that the Orders
properties point to) and not an array of objects that we are interested in.
The properties (fields) that we are interested in are:
- properties of the order itself:
OrderID, OrderDate
andFreight
:
<field name="Id" class="java.lang.String">
<property name="net.sf.jasperreports.jsonql.field.expression" value="OrderID"/>
</field>
<field name="OrderDate" class="java.util.Date">
<property name="net.sf.jasperreports.jsonql.field.expression" value="OrderDate"/>
</field>
<field name="Freight" class="java.lang.Float">
<property name="net.sf.jasperreports.jsonql.field.expression" value="Freight"/>
</field>
properties of the parent customer object: City, CustomerID and Company Name
that are obtained by going up 2 levels
<field name="ShipCity" class="java.lang.String">
<description>Go up by two levels then select City</description>
<property name="net.sf.jasperreports.jsonql.field.expression" value="^{2}.City"/>
</field>
<field name="CustomerID" class="java.lang.String">
<description>Go up by two levels then select CustomerID</description>
<property name="net.sf.jasperreports.jsonql.field.expression" value="^{2}.CustomerID"/>
</field>
<field name="CompanyName" class="java.lang.String">
<description>Go up twice by 1 level then select CompanyName</description>
<propertyExpression name="net.sf.jasperreports.jsonql.field.expression"><![CDATA["^^[\"Company Name\"]"]] ></propertyExpression>
</field>
The rest of the configuration necessary for this sample to work is done in the data adapter file: data/northwindDataAdapter.jrdax
<?xml version="1.0" encoding="UTF-8" ?>
<jsonDataAdapter class="net.sf.jasperreports.data.json.JsonDataAdapterImpl">
<name>NorthWind JsonQL DA</name>
<fileName>/data/northwind.json</fileName>
<useConnection>true</useConnection>
<language>jsonql</language>
<datePattern>yyyy-MM-dd</datePattern>
<numberPattern>#,##0.##</numberPattern>
</jsonDataAdapter>
Here we specify:
- the source file with the JSON data:
/data/northwind.json
- the fact that we want to use the existing connection and the report's query by setting the
useConnection
flag totrue
- the language of our query:
jsonql
; it is optional in this case since we are using the report's query that already specifies the language - the date pattern used to parse the date fields
- the numeric pattern used to parse the numeric fields
Running the Sample
Running the sample requires the Apache Maven library. Make sure that maven is already installed on your system (version 3.6 or later). In a command prompt/terminal window set the current folder to demo/samples/jsonqldatasource
within the JasperReports source project and run the following command:
> mvn clean compile exec:exec@all
It will generate all supported document types containing the sample report in the demo/samples/jsonqldatasource/target/reports
directory.
The JSONQL query language
Description / Goal
Detailed description of the JSONQL query language.
Since: 6.3.1
Overview
JSONQL is a language used for querying JSON data structures. It appeared from the need to perform more complex JSON traversals and filtering. JSONQL is not compatible with the existing JSON language due to its possibly complicated syntax.
We typically write JSONQL queries in query expressions marked with the appropriate jsonql
language:
<query language="jsonql"><![CDATA[ ..Orders(@size > 1).* ]] ></query>
Syntax
A JSONQL query is an expression containing one or more chained members:
[member1][member2]...[memberN]
For example, in the following expression:
Northwind.Customers.Orders.*(Freight > 200 && OrderDate *= "1997")
we distinguish the following members:
[Northwind]
[.Customers]
[.Orders]
[.*(Freight > 200 && OrderDate *= "1997")]
Each member is applied on the result(s) produced by the previous member. If a member produces no result, the rest of the members, if any, are skipped and no results are returned.
When successful, the final result of the JSONQL expression is a collection of JSON nodes, where each node may be an Array
, an Object
or a Value(boolean, string, number or null)
.
Absolute expressions
Absolute expressions start with the $
symbol placed in front of the first member expression:
$.Northwind.Customers[0]["Company Name"]
or
$["Northwind"].Customers[0]["Company Name"]
etc.
When used in the report's query, it makes no difference if an expression is absolute or not. The result is always calculated from the root of the JSON data.
The only place where the absolute expression matters is the field's property expression. For example:
<field name="FirstCustomerCompanyName" class="java.lang.String">
<description><![CDATA[ The company name of the first customer]] ></description>
<propertyExpression name="net.sf.jasperreports.jsonql.field.expression"><![CDATA[ "$.Northwind.Customers[0][\"Company Name\"]" ]] ></propertyExpression>
</field>
defines a field whose value is calculated directly from the root Northwind object and not relative to the field in the dataSet produced by the query. ` You may want to use an absolute expression like this when the value you are interested in is not part of any of the results produced by the main dataSet's query.
Comments inside expressions
Comments are supported inside JSONQL expressions. A JSONQL expression can be a single or multi-line expression, therefore we have single line comments and multi-line comments:
- Single line comments start with the
//
symbol:
Northwind
.Customers
.Orders
.*
//(Freight > 200 && OrderDate *= "1997")
Here we comment out the entire filtering expression.
- Multi-line comments start with
/*
and end with*/
symbols:
Northwind
.Customers
/*.Orders
.*
(Freight > 200 && OrderDate *= "1997")*/
Here we comment out everything that follows the Customers
member.
We can also use the multi-line comment inside a single line JSONQL expression:
Northwind.Customers.Orders.*(Freight > 200 /*&& OrderDate *= "1997"*/)
Here we comment out only a part of the filtering expression.
JSONQL Members
A member is an expression that tells us how to navigate a single level (up or down or anywhere down) into the JSON tree. The generic member expression has this form:
[direction][key(s)][filterExpression]
The tables below presents all the supported member types within a JSONQL expression. Only the [direction][key(s)]
part is shown here as the [filterExpression]
is applicable to all member types and will be described separately.
Member type | Key(s) value | Direction | Example in context | Description | Expected result type | Explanation | |
---|---|---|---|---|---|---|---|
Symbol | Description | ||||||
Simple Object Key | Legal JavaScript Identifier(1) | . | Down(implicit) | Northwind | Go down on Norhtwind key | Object | The dot symbol is not necessary at the beginning of the expression. Direction is "Down" by default. The result is an Object because the JSON root is of type Object and the Northwind key also points to an Object. |
Northwind.Customers | Go down on Customers key of the object produced by navigating down Northwind key | Array of Objects | The result is an Array of Objects because Northwind member produces an Object whose Customers key points to an Array. | ||||
Northwind.Customers.Address | Select the addresses of all customers | Array of String values | The result is an Array of Strings because the Customers key points to an array that is traversed, and each object is queried for the Address key, which in turn, points to a String. | ||||
.. | Anywhere Down | Northwind..Orders | Select Orders from anywhere down starting from the Northwind root | Array of Arrays | The Anywhere Down selection usually builds a collection(array) of nodes. The type of the key dictates the type of the nodes contained. In this case each Orders key points to an array. | ||
..Orders | Select Orders from anywhere down starting from the root of the JSON source. | Array of Arrays | |||||
Wildcard | * | . | Children from the first level | ..Orders.* | Select the children of all orders. | Array of Objects | Because the Orders keys produce Arrays, we need to select their first level of children in order to have an Array with all of them. |
Consider the JSON structure:
{ "Products": { "Product_1": { "Id": 123, "Category": "books" }, "Product_2": { "Id": 234, "Category": "dvds" } } }and the expression: Products.* |
Select the children of Products. | Array of Objects
[{ "Id": 123, "Category": "books" }, { "Id": 234, "Category": "dvds" }] |
The first level children of the Products Object are the values for its keys. | ||||
.. | All children from all levels | Northwind.Customers[0]..* | Select all children of the first customer. | Array of Nodes(Array(s), Objects, Values) | This kind of query:
|
Member type | Key(s) value | Direction | Example in context | Description | Expected result type | Explanation | |
---|---|---|---|---|---|---|---|
Symbol | Description | ||||||
Complex Object Key | String Literal(2)
enclosed in square brackets |
[none] | Down(implicit) | Northwind.Customers["Company Name"] | Select the company name of all customers | Array of Strings | Direction is "Down" by default. The result is an Array because the Customers array is queried on each of its items for the "Customer Name" key/property. |
.. | Anywhere Down | ..["Company Name"] | Select the company name from anywhere | Array of Strings | The result is an Array because applying Anywhere Down traversal usually produces a collection of Nodes
whose type is dictated by the value found at the requested key.
"Company Name" always produces a String in this case. |
||
Object Construction | At least 2 - legal JavaScript identifiers or String literals - separated by comma and enclosed in square brackets | [none] | Down(implicit) | Northwind.Customers[0].Orders[OrderID, OrderDate] | Select the OrderID and OrderDate from the orders of the first customer | Array of Objects
[{ "OrderID": 10643, "OrderDate": "1997-08-25" }, ... { "OrderID": 11011, "OrderDate": "1998-04-09" }] |
Direction is "Down" by default. The result is an Array because the Orders array is queried on each of its items for the OrderID and OrderDate keys. |
.. | Anywhere Down | ..["Company Name", Address] | Select the company name and address from anywhere | Array of Objects
[{ "Company Name": "Alfreds Futterkiste", "Address": "Obere Str. 57" }, ... { "Company Name": "Wolski Zajazd", "Address": "ul. Filtrowa 68" }] |
The result is an Array of Objects because applying Anywhere Down traversal usually produces a collection of Nodes
whose type is Object in this case.
We are constructing objects with "Company Name" and Address keys. |
Member type | Key(s) value | Direction | Example in context | Description | Expected result type | Explanation | |
---|---|---|---|---|---|---|---|
Symbol | Description | ||||||
Array Index | [Integer] | [none] | Down(implicit) | Northwind.Customers[2] | Select the third customer | Object | JavaScript array indexes are 0-based. Direction is "Down" by default. The result is an Object because
the 3rd element in the Customers array is an object.
Indexes outside the valid range(from 0 to array size - 1) produce no results. |
.. | Anywhere Down | Northwind.Customers.Orders..[0] | Select the first order of each customer's Orders | Array of Objects | The result is an Array of Objects because applying Anywhere Down traversal usually produces a collection of Nodes
whose type is Object in this case.
We are selecting the first order(which is an object) from each Orders array. |
||
Array Construction | [Integer1, Integer2, ...] | [none] | Down(implicit) | Northwind.Customers[0].Orders[0,2] | Select the first and third orders of the first customer | Array of Objects | JavaScript array indexes are 0-based. Direction is "Down" by default. The result is an Array of Objects because
we are constructing an array with only the 1st and 3rd orders.
Indexes outside the valid range(from 0 to array size - 1) are ignored. |
.. | Anywhere Down | Northwind.Customers.Orders..[0,1,3] | Select the first, second and fourth orders of each customer's Orders | Array of Objects | The result is an Array of Objects because we are constructing an array with only the
1st, 2nd and 4th orders.
Indexes outside the valid range(from 0 to array size - 1) are ignored. |
Member type | Key(s) value | Direction | Example in context | Description | Expected result type | Explanation | |
---|---|---|---|---|---|---|---|
Symbol | Description | ||||||
Array Slice | [Integer startIndex : Integer endIndex] | [none] | Down(implicit) | ..Orders.*[-1:] | Select the last of all orders. | Object |
Direction is "Down" by default.
Each of the start/end indexes is 0-based and optional, but not both at the same time. When startIndex is missing it defaults to 0. When endIndex is missing it defaults to the array size. The element at endIndex is not included in the results list. The indexes can have negative values that suggest operating at the end of the array. -1 points to the last item. -2 points to the item before last and so on. The indexes outside the array index range are ignored. The result is Object because the last order is an object. |
.. | Anywhere Down | Northwind.Customers.Orders..[0:2] | Select the first two orders(indexes 0 and 1) of each customer's Orders | Array of Arrays | The result is an Array of Arrays because applying Anywhere Down traversal usually produces a collection of Nodes.
Each node is an array in this case because from each Orders array we select only the first two items.
If we want to reach the deeper order nodes, the .* member(first level children) could be added to the expression, thus obtaining an Array of Objects. |
||
Multi Level Up | [no key] | ^ | One level up | ..OrderID^ | Select the parents of all objects that have the OrderID property = Select all orders | Array of Objects | The result is an Array of Objects because the parent of each OrderID key is the object that contains the key. |
^{N} | Up by N levels; N >= 1 | ..[OrderID,OrderDate](^{3}.City == "Berlin") | Select the OrderID and OrderDate keys for objects(orders) that have the parent customer's(which is 3 levels up) city Berlin. | Array of Objects
[{ "OrderID": 10643, "OrderDate": "1997-08-25" }, ... { "OrderID": 11011, "OrderDate": "1998-04-09" }] |
The query examples are equivalent: ^^^ produces the same result as
^{3}.
The result is an Array of Objects because we are constructing objects with the OrderID and OrderDate keys. The filter expression travels up to the Customer parent to reach the City key. The immediate parent of an object with the [OrderID, OrderDate] keys is the object that contains these keys - an order. The parent of an order is the Orders array that contains the order. The parent of the Orders array is the object holding the Orders key - the customer object. From here we go down the City key to perform the filtering. |
||
..[OrderID,OrderDate](^^^.City == "Berlin") |
(1) A Legal JavaScript Identifier is a character sequence that:
- must start with one of: letter(lowercase or uppercase),
$
or_
- and can be followed by any combination of letter,
$
,_
or digit(0-9
).
(2) A String Literal is an escaped and double quoted JavaScript String.
The Filter Expression
Each member expression described above can be accompanied by a filter expression.
Let's consider the following JSONQL expression:
Northwind.Customers.Orders.*(Freight > 200 && OrderDate *= "1997")
It contains the following members:
[Northwind]
[.Customers]
[.Orders]
[.*(Freight > 200 && OrderDate *= "1997")]
Members #1
, #2
and #3
contain no filter expression, only #4
does:
(Freight > 200 && OrderDate *= "1997")
In this case we have a compound filter expression made out of two basic filter expressions joined by the logical and(&&
) operator.
The filter expression is evaluated as member results are added to the whole expression's result. The result of a filter expression is a boolean true or false.
The Basic Filter Expression
The form of a basic filter expression is one of:
( [filterMember1][filterMember2]...[filterMemberN]@Function
(*) Operator Value )
( [filterMember1][filterMember2]...[filterMemberN]@Function)
(*) The @Function
element is optional
The filterMember
is similar to the JSONQL Member
except it lacks the filter expression. It contains only the: [direction][key(s)]
part.
This means that the above member type definitions also apply in this case.
The type and presence of the @Function
element dictates the form of the basic filter expression as follows:
Function type | Function expression | Example | Description | Observation |
---|---|---|---|---|
Path Type Check functions | @isNull | ..Orders.*(["nonexistent key"]@isNull) | Select Orders keys from anywhere and get the children that have a "nonexistent key" property with null value. The result of the filter expression is true for all orders, so we are getting all of them. | The path type check functions are logical functions that return true/false. No Operator/Value can be used here. |
@isNotNull | ..Orders.*(Products@isNotNull) | Select Orders keys from anywhere and get the children that have a non-null Products property. The result of the filter expression is false for all orders, so we are getting nothing as result. | ||
@isArray | ..Orders(@isArray).* | Select Orders keys from anywhere that are arrays and get the first level children. The result of the filter expression is true for all Orders keys, so we are getting all orders. | ||
@isObject | ..Orders(@isObject) | Select Orders keys from anywhere that are of type Object. The result of the filter expression is false for all Orders keys because each points to an Array, so we are getting no results. | ||
@isValue | ..*(@isValue) | Select everything that is a value: string, number, boolean, null. The result is a huge collection of strings and numbers that probably needs additional filtering. | ||
Value check function | @val | ..OrderID(@val > 10600) | Select the OrderID keys from anywhere that have a value greater than 10600.
This function can be used only at the beginning of the filter expression when we want to filter the resulting nodes directly. It is applicable only to value nodes. If we want to check the value of a path we do not need to use this function as the function operators apply to values only. |
|
Array size check function | @size | Northwind.Customers.*(Orders@size > 10).Orders.* | Select the orders that come from Orders arrays with more than 10 items. Both query expressions produce the same output. | |
..Orders(@size > 10).* | ||||
[none] | [none] | ..Orders.*(OrderID > 10600) | Select the orders for which the OrderID has a value greater than 10600.
We don't need functions to check the value of a key. |
Operator to Operand type application
There is an enforcement at the JSONQL language level that requires the operators to be applied to the right kind of operands. The query expression is not valid unless the matching is correctly done.
Operator(s) | Description | Operand type | JSONQL Expression Example | Observations |
---|---|---|---|---|
==, != | Equal to, Not equal to | Number, String, Boolean, null | ..Orders.*(OrderID == 10643) | |
>, >=, <, <= | Greater than, Greater than or equal to, Lower than, Lower than or equal to | Number | ..Orders.*(Freight >= 60) | |
*= | Contains | String | ..Orders.*(OrderDate *= "1997") | String values are String literals (escaped and double quoted). |
Logical Operators for Basic Filter Expressions
Basic Filter Expressions can be negated and/or combined in order to allow more complex filtering capabilities.
Operator | Description | JSONQL Expression Example |
---|---|---|
&& | AND | ..Orders.*(OrderID >= 10600 && OrderDate *= "1997") |
\| | OR | ..Orders.*(Freight > 120 \|\| ShipVia == 2) |
! | NOT | ..Orders(!@size > 10).* |
mixed | Grouping expressions is possible | ..Orders.*(!(OrderID >= 10600 && (OrderDate *= "1998" \|\| OrderDate *= "1997"))) |