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
25 /*
26  * Contributors:
27  * Tim Thomas - tthomas48@users.sourceforge.net 
28  */

29 package net.sf.jasperreports.engine.data;
30
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStream;
34
35 import org.w3c.dom.Document;
36 import org.w3c.dom.Node;
37 import org.w3c.dom.NodeList;
38 import org.xml.sax.InputSource;
39
40 import net.sf.jasperreports.engine.DefaultJasperReportsContext;
41 import net.sf.jasperreports.engine.JRException;
42 import net.sf.jasperreports.engine.JasperReportsContext;
43 import net.sf.jasperreports.engine.util.JRXmlUtils;
44 import net.sf.jasperreports.engine.util.xml.JRXPathExecuter;
45 import net.sf.jasperreports.engine.util.xml.JRXPathExecuterUtils;
46 import net.sf.jasperreports.repo.RepositoryContext;
47 import net.sf.jasperreports.repo.RepositoryUtil;
48 import net.sf.jasperreports.repo.SimpleRepositoryContext;
49
50 /**
51  * XML data source implementation that allows to access the data from a xml
52  * document using XPath expressions.
53  * <p>
54  * The data source is constructed around a node set (record set) selected
55  * by an XPath expression from the XML document.
56  * </p>
57  * <p>
58  * Each field can provide an additional XPath expression that will be used to
59  * select its value. This expression must be specified using the {@link #PROPERTY_FIELD_EXPRESSION} 
60  * custom property at field level. The use of the {@link net.sf.jasperreports.engine.JRField#getDescription() field description} to specify the XPath expression 
61  * is still supported, but is now discouraged, the above mentioned custom property taking precedence 
62  * over the field description. 
63  * The expression is evaluated in the context of the current
64  * node thus the expression should be relative to the current node.
65  * </p>
66  * <p>
67  * To support subreports, sub data sources can be created. There are two different methods 
68  * for creating sub data sources. The first one allows to create a sub data source rooted 
69  * at the current node. The current node can be seen as a new document around which the 
70  * sub data source is created. The second method allows to create a sub data source that
71  * is rooted at the same document that is used by the data source but uses a different 
72  * XPath select expression. 
73  * </p>
74  * <p>
75  * Example:
76  * <pre>
77  * &lt;A&gt;
78  *     &lt;B id="0"&gt;
79  *         &lt;C&gt;
80  *         &lt;C&gt;
81  *     &lt;/B&gt;
82  *     &lt;B id="1"&gt;
83  *         &lt;C&gt;
84  *         &lt;C&gt;
85  *     &lt;/B&gt;
86  *     &lt;D id="3"&gt;
87  *         &lt;E&gt;
88  *         &lt;E&gt;
89  *     &lt;/D&gt;
90  * &lt;/A&gt;
91  * </pre>
92  * <p>
93  * Data source creation
94  * <ul>
95  * <li>new JRXmlDataSource(document, "/A/B") - creates a data source with two nodes of type /A/B
96  * <li>new JRXmlDataSource(document, "/A/D") - creates a data source with two nodes of type /A/D
97  * </ul>
98  * Field selection
99  * <ul>
100  * <li>@id - will select the "id" attribute from the current node
101  * <li>C - will select the value of the first node of type C under the current node. 
102  * </ul>
103  * Sub data source creation
104  * <ul>
105  * <li>"((net.sf.jasperreports.engine.data.JRXmlDataSource)$P{REPORT_DATA_SOURCE}).subDataSource("/B/C")
106  *     - in the context of the node B, creates a data source with elements of type /B/C
107  * <li>"((net.sf.jasperreports.engine.data.JRXmlDataSource)$P{REPORT_DATA_SOURCE}).dataSource("/A/D")
108  *     - creates a data source with elements of type /A/D
109  * </ul>
110  * </p>
111  * <p>
112  * Generally the full power of XPath expression is available. As an example, "/A/B[@id &gt; 0"] will select all the
113  * nodes of type /A/B having the id greater than 0. 
114  * You'll find a short XPath tutorial <a href="http://www.zvon.org/xxl/XPathTutorial/General/examples.html" target="_blank">here</a>.
115  * 
116  * </p>
117  * <p>
118  * Note on performance. Due to the fact that all the XPath expression are interpreted the
119  * data source performance is not great. For the cases where more speed is required,
120  * consider implementing a custom data source that directly accesses the Document through the DOM API. 
121  * </p>
122  * @author Peter Severin (peter_p_s@sourceforge.net, contact@jasperassistant.com)
123  * @see JRXPathExecuterUtils
124  */

125 public class JRXmlDataSource extends AbstractXmlDataSource<JRXmlDataSource>
126 {
127
128     // the xml document
129     private Document document;
130
131     // the XPath select expression that gives the nodes to iterate
132     private String selectExpression;
133
134     // the node list
135     private NodeList nodeList;
136
137     // the node list length
138     private int nodeListLength;
139     
140     // the current node
141     private Node currentNode;
142
143     // current node index
144     private int currentNodeIndex = - 1;
145
146     private final JRXPathExecuter xPathExecuter;
147     
148     private InputStream inputStream;
149     private boolean closeInputStream;
150     
151     // -----------------------------------------------------------------
152     // Constructors
153
154     /**
155      * Creates the data source by parsing the xml document from the given file.
156      * The data source will contain exactly one record consisting of the document node itself.
157      * 
158      * @param document the document
159      * @throws JRException if the data source cannot be created
160      */

161     public JRXmlDataSource(JasperReportsContext jasperReportsContext, Document document) throws JRException {
162         this(jasperReportsContext, document, ".");
163     }
164
165     /**
166      * @see #JRXmlDataSource(JasperReportsContext, Document)
167      */

168     public JRXmlDataSource(Document document) throws JRException {
169         this(DefaultJasperReportsContext.getInstance(), document);
170     }
171
172     /**
173      * Creates the data source by parsing the xml document from the given file.
174      * An additional XPath expression specifies the select criteria that produces the 
175      * nodes (records) for the data source.
176      * 
177      * @param document the document
178      * @param selectExpression the XPath select expression
179      * @throws JRException if the data source cannot be created
180      */

181     public JRXmlDataSource(
182         JasperReportsContext jasperReportsContext,
183         Document document, 
184         String selectExpression
185         ) throws JRException 
186     {
187         this.document = document;
188         this.selectExpression = selectExpression;
189         
190         this.xPathExecuter = JRXPathExecuterUtils.getXPathExecuter(jasperReportsContext);
191         
192         moveFirst();
193     }
194
195
196     /**
197      * @see #JRXmlDataSource(JasperReportsContext, Document, String)
198      */

199     public JRXmlDataSource(Document document, String selectExpression) throws JRException 
200     {
201         this(DefaultJasperReportsContext.getInstance(), document, selectExpression);
202     }
203
204
205     /**
206      * Creates the data source by parsing the xml document from the given input stream.
207      *  
208      * @param in the input stream
209      * @see JRXmlDataSource#JRXmlDataSource(Document) 
210      */

211     public JRXmlDataSource(JasperReportsContext jasperReportsContext, InputStream in) throws JRException 
212     {
213         this(jasperReportsContext, in, false);
214     }
215
216     public JRXmlDataSource(JasperReportsContext jasperReportsContext, InputStream in, boolean isNamespaceAware) throws JRException 
217     {
218         this(jasperReportsContext, in, ".", isNamespaceAware);
219     }
220
221     /**
222      * @see #JRXmlDataSource(JasperReportsContext, InputStream)
223      */

224     public JRXmlDataSource(InputStream in) throws JRException {
225         this(in, false);
226     }
227
228     public JRXmlDataSource(InputStream in, boolean isNamespaceAware) throws JRException {
229         this(DefaultJasperReportsContext.getInstance(), in, isNamespaceAware);
230     }
231
232     /**
233      * Creates the data source by parsing the xml document from the given input stream.
234      * 
235      * @see JRXmlDataSource#JRXmlDataSource(InputStream) 
236      * @see JRXmlDataSource#JRXmlDataSource(Document, String) 
237      */

238     public JRXmlDataSource(
239         JasperReportsContext jasperReportsContext,
240         InputStream in, 
241         String selectExpression
242         ) throws JRException 
243     {
244         this(jasperReportsContext, in, selectExpression, false);
245     }
246
247     public JRXmlDataSource(
248             JasperReportsContext jasperReportsContext,
249             InputStream in, 
250             String selectExpression,
251             boolean isNamespaceAware
252             ) throws JRException 
253     {
254         this(jasperReportsContext, JRXmlUtils.parse(new InputSource(in), isNamespaceAware), selectExpression);
255         
256         this.inputStream = in;
257         this.closeInputStream = false;
258     }
259
260     /**
261      * @see #JRXmlDataSource(JasperReportsContext, InputStream, String)
262      */

263     public JRXmlDataSource(InputStream in, String selectExpression)
264             throws JRException {
265         this(in, selectExpression, false);
266     }
267
268     public JRXmlDataSource(InputStream in, String selectExpression, boolean isNamespaceAware)
269             throws JRException {
270         this(DefaultJasperReportsContext.getInstance(), in, selectExpression, isNamespaceAware);
271     }
272
273     /**
274      * Creates the data source by parsing the xml document from the given system identifier (URI).
275      * <p>If the system identifier is a URL, it must be full resolved.</p>
276      * 
277      * @param uri the system identifier
278      * @see JRXmlDataSource#JRXmlDataSource(Document) 
279      */

280     public JRXmlDataSource(JasperReportsContext jasperReportsContext, String uri) throws JRException {
281         this(jasperReportsContext, uri, false);
282     }
283
284     public JRXmlDataSource(JasperReportsContext jasperReportsContext, String uri, boolean isNamespaceAware) throws JRException {
285         this(jasperReportsContext, uri, ".", isNamespaceAware);
286     }
287
288     /**
289      * @see #JRXmlDataSource(JasperReportsContext, String)
290      */

291     public JRXmlDataSource(String uri) throws JRException {
292         this(uri, false);
293     }
294
295     public JRXmlDataSource(String uri, boolean isNamespaceAware) throws JRException {
296         this(DefaultJasperReportsContext.getInstance(), uri, isNamespaceAware);
297     }
298
299     /**
300      * Creates the data source by parsing the xml document from the given system identifier (URI).
301      * 
302      * @see JRXmlDataSource#JRXmlDataSource(String) 
303      * @see JRXmlDataSource#JRXmlDataSource(Document, String) 
304      */

305     public JRXmlDataSource(
306         JasperReportsContext jasperReportsContext, 
307         String uri, 
308         String selectExpression
309         ) throws JRException 
310     {
311         this(jasperReportsContext, uri, selectExpression, false);
312     }
313
314     public JRXmlDataSource(
315             JasperReportsContext jasperReportsContext, 
316             String uri, 
317             String selectExpression,
318             boolean isNamespaceAware
319             ) throws JRException
320     {
321         this(SimpleRepositoryContext.of(jasperReportsContext), uri, selectExpression, isNamespaceAware);
322     }
323
324     public JRXmlDataSource(
325             RepositoryContext context, 
326             String uri, 
327             String selectExpression,
328             boolean isNamespaceAware
329             ) throws JRException 
330     {
331         this(
332             context.getJasperReportsContext(), 
333             RepositoryUtil.getInstance(context).getInputStreamFromLocation(uri), 
334             selectExpression,
335             isNamespaceAware
336             );
337         this.closeInputStream = true;//FIXME close the stream immediately
338     }
339
340     /**
341      * @see #JRXmlDataSource(JasperReportsContext, String, String)
342      */

343     public JRXmlDataSource(String uri, String selectExpression)
344             throws JRException {
345         this(uri, selectExpression, false);
346     }
347
348     public JRXmlDataSource(String uri, String selectExpression, boolean isNamespaceAware)
349             throws JRException {
350         this(DefaultJasperReportsContext.getInstance(), uri, selectExpression, isNamespaceAware);
351     }
352
353     /**
354      * Creates the data source by parsing the xml document from the given file.
355      * 
356      * @param file the file
357      * @see JRXmlDataSource#JRXmlDataSource(Document) 
358      */

359     public JRXmlDataSource(JasperReportsContext jasperReportsContext, File file) throws JRException {
360         this(jasperReportsContext, file, false);
361     }
362
363     public JRXmlDataSource(JasperReportsContext jasperReportsContext, File file, boolean isNamespaceAware) throws JRException {
364         this(jasperReportsContext, file, ".", isNamespaceAware);
365     }
366
367     /**
368      * @see #JRXmlDataSource(JasperReportsContext, File)
369      */

370     public JRXmlDataSource(File file) throws JRException {
371         this(file, false);
372     }
373
374     public JRXmlDataSource(File file, boolean isNamespaceAware) throws JRException {
375         this(DefaultJasperReportsContext.getInstance(), file, isNamespaceAware);
376     }
377
378     /**
379      * Creates the data source by parsing the xml document from the given file.
380      * 
381      * @see JRXmlDataSource#JRXmlDataSource(File) 
382      * @see JRXmlDataSource#JRXmlDataSource(Document, String) 
383      */

384     public JRXmlDataSource(JasperReportsContext jasperReportsContext, File file, String selectExpression)
385             throws JRException {
386         this(jasperReportsContext, file, selectExpression, false);
387     }
388
389     public JRXmlDataSource(JasperReportsContext jasperReportsContext, File file, String selectExpression, boolean isNamespaceAware)
390             throws JRException {
391         this(jasperReportsContext, JRXmlUtils.parse(file, isNamespaceAware), selectExpression);
392     }
393
394     /**
395      * @see #JRXmlDataSource(JasperReportsContext, File, String)
396      */

397     public JRXmlDataSource(File file, String selectExpression)
398             throws JRException {
399         this(file, selectExpression, false);
400     }
401
402     public JRXmlDataSource(File file, String selectExpression, boolean isNamespaceAware)
403             throws JRException {
404         this(DefaultJasperReportsContext.getInstance(), file, selectExpression, isNamespaceAware);
405     }
406     
407     // -----------------------------------------------------------------
408     // Implementation
409     
410     /*
411      * (non-Javadoc)
412      * 
413      * @see net.sf.jasperreports.engine.JRRewindableDataSource#moveFirst()
414      */

415     @Override
416     public void moveFirst() throws JRException {
417         if (document == null)
418         {
419             throw 
420                 new JRException(
421                     EXCEPTION_MESSAGE_KEY_NULL_DOCUMENT,
422                     (Object[])null);
423         }
424         if (selectExpression == null)
425         {
426             throw 
427             new JRException(
428                 EXCEPTION_MESSAGE_KEY_NULL_SELECT_EXPRESSION,
429                 (Object[])null);
430         }
431
432         currentNode = null;
433         currentNodeIndex = -1;
434         nodeListLength = 0;
435         nodeList = xPathExecuter.selectNodeList(document,
436                 selectExpression);
437         nodeListLength = nodeList.getLength();
438     }
439
440     /*
441      * (non-Javadoc)
442      * 
443      * @see net.sf.jasperreports.engine.JRDataSource#next()
444      */

445     @Override
446     public boolean next() 
447     {
448         if(currentNodeIndex == nodeListLength - 1)
449         {
450             return false;
451         }
452         currentNode = nodeList.item(++ currentNodeIndex);
453         return true;
454     }
455
456     @Override
457     public int recordCount()
458     {
459         return nodeListLength;
460     }
461
462     @Override
463     public int currentIndex()
464     {
465         return currentNodeIndex;
466     }
467
468     @Override
469     public void moveToRecord(int index) throws NoRecordAtIndexException
470     {
471         if (index >= 0 && index < nodeListLength)
472         {
473             currentNodeIndex = index;
474             currentNode = nodeList.item(index);
475         }
476         else
477         {
478             throw new NoRecordAtIndexException(index);
479         }
480     }
481
482     @Override
483     public Node getCurrentNode() 
484     {
485         return currentNode;
486     }
487
488     @Override
489     public Object getSelectObject(Node currentNode, String expression) throws JRException 
490     {
491         return xPathExecuter.selectObject(currentNode, expression);
492     }
493
494     /**
495      * Creates a sub data source using the current node (record) as the root
496      * of the document. An additional XPath expression specifies the select criteria applied to
497      * this new document and that produces the nodes (records) for the data source.
498      * 
499      * @param selectExpr the XPath select expression
500      * @return the xml sub data source
501      * @throws JRException if the sub data source couldn't be created
502      * @see JRXmlDataSource#JRXmlDataSource(Document, String)
503      */

504     @Override
505     public JRXmlDataSource subDataSource(String selectExpr)
506             throws JRException {
507         // create a new document from the current node
508         Document doc = subDocument();
509         JRXmlDataSource subDataSource = new JRXmlDataSource(doc, selectExpr);
510         subDataSource.setTextAttributes(this);
511         return subDataSource;
512     }
513
514     @Override
515     public JRXmlDataSource subDataSource() throws JRException // need to override this method here to keep binary compatibility with older releases 
516     {
517         return super.subDataSource();
518     }
519
520     
521     /**
522      * Creates a document using the current node as root.
523      * 
524      * @return a document having the current node as root
525      * @throws JRException
526      */

527     @Override
528     public Document subDocument() throws JRException
529     {
530         if(currentNode == null)
531         {
532             throw 
533                 new JRException(
534                     EXCEPTION_MESSAGE_KEY_NODE_NOT_AVAILABLE,
535                     (Object[])null);
536         }
537         
538         // create a new document from the current node
539         return JRXmlUtils.createDocument(currentNode);
540     }
541     
542     
543     /**
544      * Creates a sub data source using as root document the document used by "this" data source.
545      * An additional XPath expression specifies the select criteria applied to
546      * this document and that produces the nodes (records) for the data source.
547      * 
548      * @param selectExpr the XPath select expression
549      * @return the xml sub data source
550      * @throws JRException if the sub data source couldn't be created
551      * @see JRXmlDataSource#JRXmlDataSource(Document, String)
552      */

553     @Override
554     public JRXmlDataSource dataSource(String selectExpr)
555             throws JRException {
556         JRXmlDataSource subDataSource = new JRXmlDataSource(document, selectExpr);
557         subDataSource.setTextAttributes(this);
558         return subDataSource;
559     }
560
561     @Override
562     public JRXmlDataSource dataSource() throws JRException // need to override this method here to keep binary compatibility with older releases 
563     {
564         return super.dataSource();
565     }
566
567     /**
568      * Closes the reader. Users of this data source should close it after usage.
569      */

570     public void close()
571     {
572         try
573         {
574             if (closeInputStream)
575             {
576                 inputStream.close();
577             }
578         }
579         catch(IOException e)
580         {
581             //nothing to do
582         }
583     }
584
585 }
586