1 /*
2  * JasperReports - Free Java Reporting Library.
3  * Copyright (C) 2001 - 2019 TIBCO Software Inc. All rights reserved.
4  * http://www.jaspersoft.com
5  *
6  * Unless you have purchased a commercial license agreement from Jaspersoft,
7  * the following license terms apply:
8  *
9  * This program is part of JasperReports.
10  *
11  * JasperReports is free software: you can redistribute it and/or modify
12  * it under the terms of the GNU Lesser General Public License as published by
13  * the Free Software Foundation, either version 3 of the License, or
14  * (at your option) any later version.
15  *
16  * JasperReports is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19  * GNU Lesser General Public License for more details.
20  *
21  * You should have received a copy of the GNU Lesser General Public License
22  * along with JasperReports. If not, see <http://www.gnu.org/licenses/>.
23  */

24 package net.sf.jasperreports.engine.data;
25
26 import java.io.File;
27 import java.io.FileNotFoundException;
28 import java.io.InputStream;
29 import java.util.ArrayList;
30 import java.util.Date;
31 import java.util.HashMap;
32 import java.util.Iterator;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.StringTokenizer;
36
37 import com.fasterxml.jackson.databind.JsonNode;
38 import com.fasterxml.jackson.databind.ObjectMapper;
39 import com.fasterxml.jackson.databind.node.ArrayNode;
40
41 import net.sf.jasperreports.annotations.properties.Property;
42 import net.sf.jasperreports.annotations.properties.PropertyScope;
43 import net.sf.jasperreports.engine.DefaultJasperReportsContext;
44 import net.sf.jasperreports.engine.JRException;
45 import net.sf.jasperreports.engine.JRField;
46 import net.sf.jasperreports.engine.JRPropertiesUtil;
47 import net.sf.jasperreports.engine.JasperReportsContext;
48 import net.sf.jasperreports.engine.query.JsonQueryExecuterFactory;
49 import net.sf.jasperreports.engine.util.JsonUtil;
50 import net.sf.jasperreports.properties.PropertyConstants;
51 import net.sf.jasperreports.repo.RepositoryContext;
52 import net.sf.jasperreports.repo.SimpleRepositoryContext;
53
54
55 /**
56  * JSON data source implementation
57  * 
58  * @author Narcis Marcu (narcism@users.sourceforge.net)
59  */

60 public class JsonDataSource extends JRAbstractTextDataSource implements JsonData<JsonDataSource>, RandomAccessDataSource {
61
62     public static final String EXCEPTION_MESSAGE_KEY_JSON_FIELD_VALUE_NOT_RETRIEVED = "data.json.field.value.not.retrieved";
63     public static final String EXCEPTION_MESSAGE_KEY_INVALID_ATTRIBUTE_SELECTION = "data.json.invalid.attribute.selection";
64     public static final String EXCEPTION_MESSAGE_KEY_INVALID_EXPRESSION = "data.json.invalid.expression";
65     public static final String EXCEPTION_MESSAGE_KEY_NO_DATA = "data.json.no.data";
66
67     /**
68      * Property specifying the JSON expression for the dataset field.
69      */

70     @Property (
71             category = PropertyConstants.CATEGORY_DATA_SOURCE,
72             scopes = {PropertyScope.FIELD},
73             scopeQualifications = {JsonQueryExecuterFactory.JSON_QUERY_EXECUTER_NAME},
74             sinceVersion = PropertyConstants.VERSION_6_3_1
75     )
76     public static final String PROPERTY_FIELD_EXPRESSION = JRPropertiesUtil.PROPERTY_PREFIX + "json.field.expression";
77
78     // the JSON select expression that gives the nodes to iterate
79     private String selectExpression;
80
81     private Map<String, String> fieldExpressions = new HashMap<String, String>();
82
83     private JsonNode dataNode;
84     private Iterator<JsonNode> jsonNodesIterator;
85     
86     private int currentNodeIndex;
87
88     // the current node
89     private JsonNode currentJsonNode;
90
91     private final String PROPERTY_SEPARATOR = ".";//FIXME static?
92
93     private final String ARRAY_LEFT = "[";
94
95     private final String ARRAY_RIGHT = "]";
96     
97     private final String ATTRIBUTE_LEFT = "(";
98     
99     private final String ATTRIBUTE_RIGHT = ")";
100     
101     // the JSON tree as it is obtained from the JSON source
102     private JsonNode jsonTree;
103     
104     private ObjectMapper mapper;
105     
106     public JsonDataSource(InputStream stream) throws JRException {
107         this(stream, null);
108     }
109     
110     public JsonDataSource(InputStream jsonStream, String selectExpression) throws JRException {
111         this(JsonUtil.parseJson(jsonStream), selectExpression);
112     }
113     
114     protected JsonDataSource(JsonNode jsonTree, String selectExpression) throws JRException {
115         this.mapper = JsonUtil.createObjectMapper();
116         
117         this.jsonTree = jsonTree;
118         this.selectExpression = selectExpression;
119         
120         moveFirst();
121     }
122
123
124     public JsonDataSource(File file) throws FileNotFoundException, JRException {
125         this(file, null);
126     }
127     
128
129     public JsonDataSource(File file, String selectExpression) throws FileNotFoundException, JRException {
130         this(JsonUtil.parseJson(file), selectExpression);
131     }
132
133     /**
134      * Creates a data source instance that reads JSON data from a given location
135      * @param jasperReportsContext the JasperReportsContext
136      * @param location a String representing JSON data source
137      * @param selectExpression a String representing the select expression
138      */

139     public JsonDataSource(JasperReportsContext jasperReportsContext, String location, String selectExpression) throws JRException
140     {
141         this(SimpleRepositoryContext.of(jasperReportsContext), location, selectExpression);
142     }
143     
144     public JsonDataSource(RepositoryContext repositoryContext, String location, String selectExpression) throws JRException 
145     {
146         this(JsonUtil.parseJson(repositoryContext, location), selectExpression);
147     }
148
149     /**
150      * @see #JsonDataSource(JasperReportsContext, String, String)
151      */

152     public JsonDataSource(String location, String selectExpression) throws JRException 
153     {
154         this(DefaultJasperReportsContext.getInstance(), location, selectExpression);
155     }
156     
157     /*
158      * (non-Javadoc)
159      * 
160      * @see net.sf.jasperreports.engine.JRRewindableDataSource#moveFirst()
161      */

162     @Override
163     public void moveFirst() throws JRException {
164         if (jsonTree == null || jsonTree.isMissingNode()) {
165             throw 
166                 new JRException(
167                     EXCEPTION_MESSAGE_KEY_NO_DATA,
168                     (Object[])null);
169         }
170
171         currentNodeIndex = -1;
172         currentJsonNode = null;
173         JsonNode result = getJsonData(jsonTree, selectExpression);
174         if (result != null && result.isObject()) {
175             dataNode = result;
176             final List<JsonNode> list = new ArrayList<JsonNode>();
177             list.add(result);
178             jsonNodesIterator = new Iterator<JsonNode>() {
179                 private int count = -1;
180                 @Override
181                 public void remove() {
182                     list.remove(count);
183                 }
184                 
185                 @Override
186                 public JsonNode next() {
187                     count ++;
188                     return list.get(count);
189                 }
190                 
191                 @Override
192                 public boolean hasNext() {
193                     return count < list.size()-1;
194                 }
195             };
196         } else if (result != null && result.isArray()) {
197             dataNode = result;
198             jsonNodesIterator = result.elements();
199         }
200     }
201
202     /*
203      * (non-Javadoc)
204      * 
205      * @see net.sf.jasperreports.engine.JRDataSource#next()
206      */

207     @Override
208     public boolean next() {
209         if(jsonNodesIterator == null || !jsonNodesIterator.hasNext()) {
210             return false;
211         }
212         ++currentNodeIndex;
213         currentJsonNode = jsonNodesIterator.next();
214         return true;
215     }
216
217     @Override
218     public int recordCount() {
219         int count;
220         if (dataNode != null) {
221             if (dataNode.isObject()) {
222                 count = 1;
223             } else if (dataNode.isArray()) {
224                 count = dataNode.size();
225             } else {
226                 //shouldn't happen
227                 throw new IllegalStateException();
228             }
229         } else {
230             count = 0;
231         }
232         return count;
233     }
234
235     @Override
236     public int currentIndex() {
237         return currentNodeIndex;
238     }
239
240     @Override
241     public void moveToRecord(int index) throws NoRecordAtIndexException {
242         if (dataNode != null) {
243             if (dataNode.isObject()) {
244                 if (index == 0) {
245                     currentNodeIndex = 0;
246                     currentJsonNode = dataNode;
247                 } else {
248                     throw new NoRecordAtIndexException(index);
249                 }
250             } else if (dataNode.isArray()) {
251                 if (index >= 0 && index < dataNode.size()) {
252                     currentNodeIndex = index;
253                     currentJsonNode = dataNode.get(index);
254                 } else {
255                     throw new NoRecordAtIndexException(index);
256                 }
257             }
258         } else {
259             throw new NoRecordAtIndexException(index);
260         }
261     }
262
263     /*
264      * (non-Javadoc)
265      * 
266      * @see net.sf.jasperreports.engine.JRDataSource#getFieldValue(net.sf.jasperreports.engine.JRField)
267      */

268     @Override
269     public Object getFieldValue(JRField jrField) throws JRException 
270     {
271         if(currentJsonNode == null) {
272             return null;
273         }
274         
275         String expression = null;
276         if (fieldExpressions.containsKey(jrField.getName()))
277         {
278             expression = fieldExpressions.get(jrField.getName());
279         }
280         else
281         {
282             expression = getFieldExpression(jrField);
283             fieldExpressions.put(jrField.getName(), expression);
284         }
285         if (expression == null || expression.length() == 0)
286         {
287             return null;
288         }
289
290         Object value = null;
291         
292         Class<?> valueClass = jrField.getValueClass();
293         JsonNode selectedObject = getJsonData(currentJsonNode, expression);
294         
295         if(Object.class != valueClass) 
296         {
297             boolean hasValue = selectedObject != null 
298                     && !selectedObject.isMissingNode() && !selectedObject.isNull();
299             if (hasValue) 
300             {
301                 try {
302                     if (valueClass.equals(String.class)) {
303                         if (selectedObject.isArray()) {
304                             value = selectedObject.toString();
305                         } else {
306                             value = selectedObject.asText();
307                         }
308
309                     } else if (valueClass.equals(Boolean.class)) {
310                         value = selectedObject.booleanValue();
311                         
312                     } else if (Number.class.isAssignableFrom(valueClass)) {
313                         //FIXME if the json node is a number, avoid converting to string and parsing back the value
314                             value = convertStringValue(selectedObject.asText(), valueClass);
315                             
316                     }
317                     else if (Date.class.isAssignableFrom(valueClass)) {
318                             value = convertStringValue(selectedObject.asText(), valueClass);
319                             
320                     } else {
321                         throw 
322                             new JRException(
323                                 EXCEPTION_MESSAGE_KEY_CANNOT_CONVERT_FIELD_TYPE,
324                                 new Object[]{jrField.getName(), valueClass.getName()});
325                     }
326                 } catch (Exception e) {
327                     throw 
328                         new JRException(
329                             EXCEPTION_MESSAGE_KEY_JSON_FIELD_VALUE_NOT_RETRIEVED,
330                             new Object[]{jrField.getName(), valueClass.getName()}, 
331                             e);
332                 }
333             }
334         }
335         else
336         {
337             value = selectedObject;
338         }
339         
340         return value;
341     }
342     
343     /**
344      * Extracts the JSON nodes based on the query expression
345      * 
346      * @param rootNode
347      * @param jsonExpression
348      * @throws JRException
349      */

350     protected JsonNode getJsonData(JsonNode rootNode, String jsonExpression) throws JRException {
351         if (jsonExpression == null || jsonExpression.length() == 0) {
352             return rootNode;
353         }
354         JsonNode tempNode = rootNode;
355         StringTokenizer tokenizer = new StringTokenizer(jsonExpression, PROPERTY_SEPARATOR);
356         
357         while(tokenizer.hasMoreTokens()) {
358             String currentToken = tokenizer.nextToken();
359             int currentTokenLength = currentToken.length();
360             int indexOfLeftSquareBracket = currentToken.indexOf(ARRAY_LEFT);
361
362             // got Left Square Bracket - LSB
363             if (indexOfLeftSquareBracket != -1) {
364                 // a Right Square Bracket must be the last character in the current token
365                 if(currentToken.lastIndexOf(ARRAY_RIGHT) != (currentTokenLength-1)) {
366                     throw 
367                         new JRException(
368                             EXCEPTION_MESSAGE_KEY_INVALID_EXPRESSION,
369                             new Object[]{jsonExpression, currentToken});
370                 }
371                 
372                 // LSB not first character
373                 if (indexOfLeftSquareBracket > 0) {
374                     // extract nodes at property
375                     String property = currentToken.substring(0, indexOfLeftSquareBracket);
376                     tempNode = goDownPathWithAttribute(tempNode, property);
377                 }
378
379                 String arrayOperators = currentToken.substring(indexOfLeftSquareBracket);
380                 StringTokenizer arrayOpsTokenizer = new StringTokenizer(arrayOperators,ARRAY_RIGHT);
381                 while(arrayOpsTokenizer.hasMoreTokens()) {
382                     if (tempNode == null || tempNode.isMissingNode() || !tempNode.isArray()) {
383                         return null;
384                     }
385
386                     String currentArrayOperator = arrayOpsTokenizer.nextToken();
387                     tempNode = tempNode.path(Integer.parseInt(currentArrayOperator.substring(1)));
388                 }
389             } else {
390                 tempNode = goDownPathWithAttribute(tempNode, currentToken);
391             }
392         }
393         
394         return tempNode;
395     }
396     
397     
398     /**
399      * Extracts the JSON nodes that match the attribute expression
400      * 
401      * @param rootNode
402      * @param pathWithAttributeExpression : e.g. Orders(CustomerId == HILAA)
403      * @throws JRException
404      */

405     protected JsonNode goDownPathWithAttribute(JsonNode rootNode, String pathWithAttributeExpression) throws JRException {
406         // check if path has attribute selector
407         int indexOfLeftRoundBracket = pathWithAttributeExpression.indexOf(ATTRIBUTE_LEFT); 
408         if (indexOfLeftRoundBracket != -1) {
409             
410             // a Right Round Bracket must be the last character in the current pathWithAttribute
411             if(pathWithAttributeExpression.indexOf(ATTRIBUTE_RIGHT) != (pathWithAttributeExpression.length() - 1)) {
412                 throw 
413                     new JRException(
414                         EXCEPTION_MESSAGE_KEY_INVALID_ATTRIBUTE_SELECTION,
415                         new Object[]{pathWithAttributeExpression});
416             }
417             
418             if(rootNode != null && !rootNode.isMissingNode()) {
419                 
420                 String path = pathWithAttributeExpression.substring(0, indexOfLeftRoundBracket);
421                 
422                 // an expression in a form like: attribute==value
423                 String attributeExpression = pathWithAttributeExpression.substring(indexOfLeftRoundBracket + 1, pathWithAttributeExpression.length() - 1);
424                 
425                 JsonNode result = null;
426                 if (rootNode.isObject()) {
427                     // select only those nodes for which the attribute expression applies
428                     if (!rootNode.path(path).isMissingNode()) {
429                         if (rootNode.path(path).isObject()) {
430                             if (isValidExpression(rootNode.path(path), attributeExpression)) {
431                                 result = rootNode.path(path);
432                             }
433                         } else if (rootNode.path(path).isArray()) {
434                             result = mapper.createArrayNode();
435                             for (JsonNode node: rootNode.path(path)) {
436                                 if (isValidExpression(node, attributeExpression)) {
437                                     ((ArrayNode)result).add(node);
438                                 } 
439                             }
440                         }
441                     }
442                 } else if (rootNode.isArray()) {
443                     result = mapper.createArrayNode();
444                     for (JsonNode node: rootNode) {
445                         JsonNode deeperNode = node.path(path);
446                         if (!deeperNode.isMissingNode()) {
447                             if (deeperNode.isArray()) {
448                                 for(JsonNode arrayNode: deeperNode) {
449                                     if (isValidExpression(arrayNode, attributeExpression)) {
450                                         ((ArrayNode)result).add(arrayNode);
451                                     }
452                                 }
453                             } else if (isValidExpression(deeperNode, attributeExpression)){
454                                 ((ArrayNode)result).add(deeperNode);
455                             }
456                         } 
457                     }
458                 }
459                 return result;
460             } 
461             
462         } else { // path has no attribute selectors
463             return goDownPath(rootNode, pathWithAttributeExpression);
464         }
465         return rootNode;
466     }
467     
468     
469     /**
470      * Extracts the JSON nodes under the simple path
471      * 
472      * @param rootNode
473      * @param simplePath - a simple field name, with no selection by attribute
474      */

475     protected JsonNode goDownPath(JsonNode rootNode, String simplePath) {
476         if(rootNode != null && !rootNode.isMissingNode()) {
477             JsonNode result = null;
478             if (rootNode.isObject()) {
479                 result = rootNode.path(simplePath);
480             } else if (rootNode.isArray()) {
481                 result = mapper.createArrayNode();
482                 for (JsonNode node: rootNode) {
483                     JsonNode deeperNode = node.path(simplePath);
484                     if (!deeperNode.isMissingNode()) {
485                         if (deeperNode.isArray()) {
486                             for(JsonNode arrayNode: deeperNode) {
487                                 ((ArrayNode)result).add(arrayNode);
488                             }
489                         } else {
490                             ((ArrayNode)result).add(deeperNode);
491                         }
492                     } 
493                 }
494             }
495             return result;
496         } 
497         return rootNode;
498     }
499     
500     
501     /**
502      * Validates an attribute expression on a JsonNode
503      * 
504      * @param operand
505      * @param attributeExpression
506      * @throws JRException
507      */

508     protected boolean isValidExpression(JsonNode operand, String attributeExpression) throws JRException {
509         return JsonUtil.evaluateJsonExpression(operand, attributeExpression);
510     }
511
512
513     /**
514      * Creates a sub data source using the current node as the base for its input stream.
515      * 
516      * @return the JSON sub data source
517      * @throws JRException
518      */

519     @Override
520     public JsonDataSource subDataSource() throws JRException {
521         return subDataSource(null);
522     }
523
524
525     /**
526      * Creates a sub data source using the current node as the base for its input stream.
527      * An additional expression specifies the select criteria that will be applied to the
528      * JSON tree node. 
529      * 
530      * @param selectExpression
531      * @return the JSON sub data source
532      * @throws JRException
533      */

534     @Override
535     public JsonDataSource subDataSource(String selectExpression) throws JRException {
536         if(currentJsonNode == null)
537         {
538             throw 
539                 new JRException(
540                     EXCEPTION_MESSAGE_KEY_NODE_NOT_AVAILABLE,
541                     (Object[])null);
542         }
543
544         JsonDataSource subDataSource = new JsonDataSource(currentJsonNode, selectExpression);
545         subDataSource.setTextAttributes(this);
546
547         return subDataSource;
548     }
549
550
551     /**
552      * @deprecated no longer required
553      */

554     @Deprecated
555     public void close() {
556         //NOP
557     }
558
559     
560     protected String getFieldExpression(JRField field)
561     {
562         String fieldExpression = null;
563         if (field.hasProperties())
564         {
565             fieldExpression = field.getPropertiesMap().getProperty(PROPERTY_FIELD_EXPRESSION);
566         }
567         if (fieldExpression == null)
568         {
569             fieldExpression = field.getDescription();
570             if (fieldExpression == null || fieldExpression.length() == 0)
571             {
572                 fieldExpression = field.getName();
573             }
574         }
575         return fieldExpression;
576     }
577 }
578