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

120 public abstract class AbstractXmlDataSource<T extends AbstractXmlDataSource<?>> extends JRAbstractTextDataSource implements RandomAccessDataSource, HierarchicalDataSource<T> 
121 {
122     /**
123      * Property specifying the XPath expression for the dataset field.
124      */

125     @Property (
126             category = PropertyConstants.CATEGORY_DATA_SOURCE,
127             scopes = {PropertyScope.FIELD},
128             scopeQualifications = {JRXPathQueryExecuterFactory.QUERY_EXECUTER_NAME},
129             sinceVersion = PropertyConstants.VERSION_6_3_1
130     )
131     public static final String PROPERTY_FIELD_EXPRESSION = JRPropertiesUtil.PROPERTY_PREFIX + "xpath.field.expression";
132
133     private Map<String, String> fieldExpressions = new HashMap<String, String>();
134
135
136     public abstract Node getCurrentNode();
137     
138     public abstract Object getSelectObject(Node currentNode, String expression)  throws JRException;
139     
140     
141     /*
142      * (non-Javadoc)
143      * 
144      * @see net.sf.jasperreports.engine.JRDataSource#getFieldValue(net.sf.jasperreports.engine.JRField)
145      */

146     @Override
147     public Object getFieldValue(JRField jrField) throws JRException 
148     {
149         if(getCurrentNode() == null)
150         {
151             return null;
152         }
153
154         String expression = null;
155         if (fieldExpressions.containsKey(jrField.getName()))
156         {
157             expression = fieldExpressions.get(jrField.getName());
158         }
159         else
160         {
161             expression = getFieldExpression(jrField);
162             fieldExpressions.put(jrField.getName(), expression);
163         }
164         if (expression == null || expression.length() == 0)
165         {
166             return null;
167         }
168
169         Object value = null;
170         
171         Class<?> valueClass = jrField.getValueClass();
172         Object selectedObject = getSelectObject(getCurrentNode(), expression);
173
174         if(Object.class != valueClass) 
175         {
176             if (selectedObject != null
177             {
178                 if (selectedObject instanceof Node) 
179                 {
180                     String text = getText((Node) selectedObject);
181                     if (text != null
182                     {
183                         value = convertStringValue(text, valueClass);
184                     }
185                 } 
186                 else if (selectedObject instanceof Boolean && valueClass.equals(Boolean.class)) 
187                 {
188                     value = selectedObject;
189                 }
190                 else if (selectedObject instanceof Number && Number.class.isAssignableFrom(valueClass)) 
191                 {
192                     value = convertNumber((Number) selectedObject, valueClass);
193                 } 
194                 else 
195                 {
196                     String text = selectedObject.toString();
197                     value = convertStringValue(text, valueClass);
198                 }
199             }
200         }
201         else
202         {
203             value = selectedObject;
204         }
205         return value;
206     }
207
208     /**
209      * Creates a sub data source using the current node (record) as the root
210      * of the document. An additional XPath expression specifies the select criteria applied to
211      * this new document and that produces the nodes (records) for the data source.
212      * 
213      * @param selectExpr the XPath select expression
214      * @return the xml sub data source
215      * @throws JRException if the sub data source couldn't be created
216      * @see JRXmlDataSource#JRXmlDataSource(Document, String)
217      */

218     @Override
219     public abstract T subDataSource(String selectExpr) throws JRException;
220
221     /**
222      * Creates a sub data source using the current node (record) as the root
223      * of the document. The data source will contain exactly one record consisting 
224      * of the document node itself.
225      * 
226      * @return the xml sub data source
227      * @throws JRException if the data source cannot be created
228      * @see JRXmlDataSource#subDataSource(String)
229      * @see JRXmlDataSource#JRXmlDataSource(Document)
230      */

231     @Override
232     public T subDataSource() throws JRException {
233         return subDataSource(".");
234     }
235
236     
237     /**
238      * Creates a document using the current node as root.
239      * 
240      * @return a document having the current node as root
241      * @throws JRException
242      */

243     public abstract Document subDocument() throws JRException;
244     
245     
246     /**
247      * Creates a sub data source using as root document the document used by "this" data source.
248      * An additional XPath expression specifies the select criteria applied to
249      * this document and that produces the nodes (records) for the data source.
250      * 
251      * @param selectExpr the XPath select expression
252      * @return the xml sub data source
253      * @throws JRException if the sub data source couldn't be created
254      * @see JRXmlDataSource#JRXmlDataSource(Document, String)
255      */

256     public abstract T dataSource(String selectExpr) throws JRException;
257
258     /**
259      * Creates a sub data source using as root document the document used by "this" data source.
260      * The data source will contain exactly one record consisting  of the document node itself.
261      * 
262      * @return the xml sub data source
263      * @throws JRException if the data source cannot be created
264      * @see JRXmlDataSource#dataSource(String)
265      * @see JRXmlDataSource#JRXmlDataSource(Document)
266      */

267     public T dataSource() throws JRException {
268         return dataSource(".");
269     }
270
271     /**
272      * Return the text that a node contains. This routine:
273      * <ul>
274      * <li>Ignores comments and processing instructions.
275      * <li>Concatenates TEXT nodes, CDATA nodes, and the results of recursively
276      * processing EntityRef nodes.
277      * <li>Ignores any element nodes in the sublist. (Other possible options
278      * are to recurse into element sublists or throw an exception.)
279      * </ul>
280      * 
281      * @param node a DOM node
282      * @return a String representing node contents or null
283      */

284     public String getText(Node node) {
285         if (!node.hasChildNodes())
286         {
287             return node.getNodeValue();
288         }
289         StringBuilder result = new StringBuilder();
290
291         NodeList list = node.getChildNodes();
292         for (int i = 0; i < list.getLength(); i++) {
293             Node subnode = list.item(i);
294             if (subnode.getNodeType() == Node.TEXT_NODE) 
295             {
296                 String value = subnode.getNodeValue();
297                 if(value != null)
298                 {
299                     result.append(value);
300                 }
301             } 
302             else if (subnode.getNodeType() == Node.CDATA_SECTION_NODE) 
303             {
304                 String value = subnode.getNodeValue();
305                 if(value != null)
306                 {
307                     result.append(value);
308                 }
309             } else if (subnode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
310                 // Recurse into the subtree for text
311                 // (and ignore comments)
312                 String value = getText(subnode);
313                 if(value != null)
314                 {
315                     result.append(value);
316                 }
317             }
318         }
319
320         return result.toString();
321     }
322     
323     protected String getFieldExpression(JRField field)
324     {
325         String fieldExpression = null;
326         if (field.hasProperties())
327         {
328             fieldExpression = field.getPropertiesMap().getProperty(PROPERTY_FIELD_EXPRESSION);
329         }
330         if (fieldExpression == null || fieldExpression.length() == 0)
331         {
332             fieldExpression = field.getDescription();
333             if (fieldExpression == null || fieldExpression.length() == 0)
334             {
335                 fieldExpression = field.getName();
336             }
337         }
338         return fieldExpression;
339     }
340 }
341