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 * <A>
74 * <B id="0">
75 * <C>
76 * <C>
77 * </B>
78 * <B id="1">
79 * <C>
80 * <C>
81 * </B>
82 * <D id="3">
83 * <E>
84 * <E>
85 * </D>
86 * </A>
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 > 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