001: /*
002: * ============================================================================
003: * GNU Lesser General Public License
004: * ============================================================================
005: *
006: * JasperReports - Free Java report-generating library.
007: * Copyright (C) 2001-2006 JasperSoft Corporation http://www.jaspersoft.com
008: *
009: * This library is free software; you can redistribute it and/or
010: * modify it under the terms of the GNU Lesser General Public
011: * License as published by the Free Software Foundation; either
012: * version 2.1 of the License, or (at your option) any later version.
013: *
014: * This library is distributed in the hope that it will be useful,
015: * but WITHOUT ANY WARRANTY; without even the implied warranty of
016: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017: * Lesser General Public License for more details.
018: *
019: * You should have received a copy of the GNU Lesser General Public
020: * License along with this library; if not, write to the Free Software
021: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA.
022: *
023: * JasperSoft Corporation
024: * 303 Second Street, Suite 450 North
025: * San Francisco, CA 94107
026: * http://www.jaspersoft.com
027: */
028:
029: /*
030: * Contributors:
031: * Tim Thomas - tthomas48@users.sourceforge.net
032: */
033: package net.sf.jasperreports.engine.data;
034:
035: import java.io.File;
036: import java.io.FileInputStream;
037: import java.io.InputStream;
038: import java.math.BigDecimal;
039: import java.math.BigInteger;
040: import java.util.Date;
041: import java.util.Locale;
042: import java.util.TimeZone;
043:
044: import net.sf.jasperreports.engine.JRException;
045: import net.sf.jasperreports.engine.JRField;
046: import net.sf.jasperreports.engine.JRRewindableDataSource;
047: import net.sf.jasperreports.engine.design.JRDesignField;
048: import net.sf.jasperreports.engine.util.JRDateLocaleConverter;
049: import net.sf.jasperreports.engine.util.JRXmlUtils;
050: import net.sf.jasperreports.engine.util.xml.JRXPathExecuter;
051: import net.sf.jasperreports.engine.util.xml.JRXPathExecuterUtils;
052:
053: import org.apache.commons.beanutils.locale.LocaleConvertUtilsBean;
054: import org.w3c.dom.Document;
055: import org.w3c.dom.Node;
056: import org.w3c.dom.NodeList;
057: import org.xml.sax.InputSource;
058:
059: /**
060: * XML data source implementation that allows to access the data from a xml
061: * document using XPath expressions.
062: * <p>
063: * The data source is constructed around a node set (record set) selected
064: * by an XPath expression from the xml document.
065: * </p>
066: * <p>
067: * Each field can provide an additional XPath expresion that will be used to
068: * select its value. This expression must be specified using the "fieldDescription"
069: * element of the field. The expression is evaluated in the context of the current
070: * node thus the expression should be relative to the current node.
071: * </p>
072: * <p>
073: * To support subreports, sub data sources can be created. There are two different methods
074: * for creating sub data sources. The first one allows to create a sub data source rooted
075: * at the current node. The current node can be seen as a new document around which the
076: * sub data source is created. The second method allows to create a sub data source that
077: * is rooted at the same document that is used by the data source but uses a different
078: * XPath select expression.
079: * </p>
080: * <p>
081: * Example:
082: * <pre>
083: * <A>
084: * <B id="0">
085: * <C>
086: * <C>
087: * </B>
088: * <B id="1">
089: * <C>
090: * <C>
091: * </B>
092: * <D id="3">
093: * <E>
094: * <E>
095: * </D>
096: * </A>
097: * </pre>
098: * <p>
099: * Data source creation
100: * <ul>
101: * <li>new JRXmlDataSource(document, "/A/B") - creates a data source with two nodes of type /A/B
102: * <li>new JRXmlDataSource(document, "/A/D") - creates a data source with two nodes of type /A/D
103: * </ul>
104: * Field selection
105: * <ul>
106: * <li>@id - will select the "id" attribute from the current node
107: * <li>C - will select the value of the first node of type C under the current node.
108: * </ul>
109: * Sub data source creation
110: * <ul>
111: * <li>"((net.sf.jasperreports.engine.data.JRXmlDataSource)$P{REPORT_DATA_SOURCE}).subDataSource("/B/C")
112: * - in the context of the node B, creates a data source with elements of type /B/C
113: * <li>"((net.sf.jasperreports.engine.data.JRXmlDataSource)$P{REPORT_DATA_SOURCE}).dataSource("/A/D")
114: * - creates a data source with elements of type /A/D
115: * </ul>
116: * </p>
117: * <p>
118: * Generally the full power of XPath expression is available. As an example, "/A/B[@id > 0"] will select all the
119: * nodes of type /A/B having the id greater than 0.
120: * You'll find a short XPath tutorial <a href="http://www.zvon.org/xxl/XPathTutorial/General/examples.html" target="_blank">here</a>.
121: *
122: * </p>
123: * <p>
124: * Note on performance. Due to the fact that all the XPath expression are interpreted the
125: * data source performance is not great. For the cases where more speed is required,
126: * consider implementing a custom data source that directly accesses the Document through the DOM API.
127: * </p>
128: * @author Peter Severin (peter_p_s@sourceforge.net, contact@jasperassistant.com)
129: * @version $Id: JRXmlDataSource.java 1790 2007-07-26 10:45:46Z lucianc $
130: * @see JRXPathExecuterUtils
131: */
132: public class JRXmlDataSource implements JRRewindableDataSource {
133:
134: // the xml document
135: private Document document;
136:
137: // the XPath select expression that gives the nodes to iterate
138: private String selectExpression;
139:
140: // the node list
141: private NodeList nodeList;
142:
143: // the node list length
144: private int nodeListLength;
145:
146: // the current node
147: private Node currentNode;
148:
149: // current node index
150: private int currentNodeIndex = -1;
151:
152: private final JRXPathExecuter xPathExecuter;
153:
154: private LocaleConvertUtilsBean convertBean = null;
155:
156: private Locale locale = null;
157: private String datePattern = null;
158: private String numberPattern = null;
159: private TimeZone timeZone = null;
160:
161: // -----------------------------------------------------------------
162: // Constructors
163:
164: /**
165: * Creates the data source by parsing the xml document from the given file.
166: * The data source will contain exactly one record consisting of the document node itself.
167: *
168: * @param document the document
169: * @throws JRException if the data source cannot be created
170: */
171: public JRXmlDataSource(Document document) throws JRException {
172: this (document, ".");
173: }
174:
175: /**
176: * Creates the data source by parsing the xml document from the given file.
177: * An additional XPath expression specifies the select criteria that produces the
178: * nodes (records) for the data source.
179: *
180: * @param document the document
181: * @param selectExpression the XPath select expression
182: * @throws JRException if the data source cannot be created
183: */
184: public JRXmlDataSource(Document document, String selectExpression)
185: throws JRException {
186: this .document = document;
187: this .selectExpression = selectExpression;
188:
189: this .xPathExecuter = JRXPathExecuterUtils.getXPathExecuter();
190:
191: moveFirst();
192: }
193:
194: /**
195: * Creates the data source by parsing the xml document from the given input stream.
196: *
197: * @param in the input stream
198: * @see JRXmlDataSource#JRXmlDataSource(Document)
199: */
200: public JRXmlDataSource(InputStream in) throws JRException {
201: this (in, ".");
202: }
203:
204: /**
205: * Creates the data source by parsing the xml document from the given input stream.
206: *
207: * @see JRXmlDataSource#JRXmlDataSource(InputStream)
208: * @see JRXmlDataSource#JRXmlDataSource(Document, String)
209: */
210: public JRXmlDataSource(InputStream in, String selectExpression)
211: throws JRException {
212: this (JRXmlUtils.parse(new InputSource(in)), selectExpression);
213: }
214:
215: /**
216: * Creates the data source by parsing the xml document from the given system identifier (URI).
217: * <p>If the system identifier is a URL, it must be full resolved.</p>
218: *
219: * @param uri the system identifier
220: * @see JRXmlDataSource#JRXmlDataSource(Document)
221: */
222: public JRXmlDataSource(String uri) throws JRException {
223: this (uri, ".");
224: }
225:
226: /**
227: * Creates the data source by parsing the xml document from the given system identifier (URI).
228: *
229: * @see JRXmlDataSource#JRXmlDataSource(String)
230: * @see JRXmlDataSource#JRXmlDataSource(Document, String)
231: */
232: public JRXmlDataSource(String uri, String selectExpression)
233: throws JRException {
234: this (JRXmlUtils.parse(uri), selectExpression);
235: }
236:
237: /**
238: * Creates the data source by parsing the xml document from the given file.
239: *
240: * @param file the file
241: * @see JRXmlDataSource#JRXmlDataSource(Document)
242: */
243: public JRXmlDataSource(File file) throws JRException {
244: this (file, ".");
245: }
246:
247: /**
248: * Creates the data source by parsing the xml document from the given file.
249: *
250: * @see JRXmlDataSource#JRXmlDataSource(File)
251: * @see JRXmlDataSource#JRXmlDataSource(Document, String)
252: */
253: public JRXmlDataSource(File file, String selectExpression)
254: throws JRException {
255: this (JRXmlUtils.parse(file), selectExpression);
256: }
257:
258: // -----------------------------------------------------------------
259: // Implementation
260:
261: /*
262: * (non-Javadoc)
263: *
264: * @see net.sf.jasperreports.engine.JRRewindableDataSource#moveFirst()
265: */
266: public void moveFirst() throws JRException {
267: if (document == null)
268: throw new JRException("document cannot be null");
269: if (selectExpression == null)
270: throw new JRException("selectExpression cannot be null");
271:
272: currentNode = null;
273: currentNodeIndex = -1;
274: nodeListLength = 0;
275: nodeList = xPathExecuter.selectNodeList(document,
276: selectExpression);
277: nodeListLength = nodeList.getLength();
278: }
279:
280: /*
281: * (non-Javadoc)
282: *
283: * @see net.sf.jasperreports.engine.JRDataSource#next()
284: */
285: public boolean next() {
286: if (currentNodeIndex == nodeListLength - 1)
287: return false;
288:
289: currentNode = nodeList.item(++currentNodeIndex);
290: return true;
291: }
292:
293: /*
294: * (non-Javadoc)
295: *
296: * @see net.sf.jasperreports.engine.JRDataSource#getFieldValue(net.sf.jasperreports.engine.JRField)
297: */
298: public Object getFieldValue(JRField jrField) throws JRException {
299: if (currentNode == null)
300: return null;
301:
302: String expression = jrField.getDescription();
303: if (expression == null || expression.length() == 0)
304: return null;
305:
306: Object value = null;
307:
308: Class valueClass = jrField.getValueClass();
309:
310: if (Object.class != valueClass) {
311: Object selectedObject = xPathExecuter.selectObject(
312: currentNode, expression);
313:
314: if (selectedObject != null) {
315: if (selectedObject instanceof Node) {
316: String text = getText((Node) selectedObject);
317: value = convertStringValue(text, valueClass);
318: } else if (selectedObject instanceof Boolean
319: && valueClass.equals(Boolean.class)) {
320: value = selectedObject;
321: } else if (selectedObject instanceof Number
322: && Number.class.isAssignableFrom(valueClass)) {
323: value = convertNumber((Number) selectedObject,
324: valueClass);
325: } else {
326: String text = selectedObject.toString();
327: value = convertStringValue(text, valueClass);
328: }
329: }
330: }
331: return value;
332: }
333:
334: protected Object convertStringValue(String text, Class valueClass) {
335: Object value = null;
336: if (String.class.equals(valueClass)) {
337: value = text;
338: } else if (Number.class.isAssignableFrom(valueClass)) {
339: value = getConvertBean().convert(text.trim(), valueClass,
340: locale, numberPattern);
341: } else if (Date.class.isAssignableFrom(valueClass)) {
342: value = getConvertBean().convert(text.trim(), valueClass,
343: locale, datePattern);
344: } else if (Boolean.class.equals(valueClass)) {
345: value = Boolean.valueOf(text);
346: }
347: return value;
348: }
349:
350: protected Object convertNumber(Number number, Class valueClass)
351: throws JRException {
352: Number value = null;
353: if (valueClass.equals(Byte.class)) {
354: value = new Byte(number.byteValue());
355: } else if (valueClass.equals(Short.class)) {
356: value = new Short(number.shortValue());
357: } else if (valueClass.equals(Integer.class)) {
358: value = new Integer(number.intValue());
359: } else if (valueClass.equals(Long.class)) {
360: value = new Long(number.longValue());
361: } else if (valueClass.equals(Float.class)) {
362: value = new Float(number.floatValue());
363: } else if (valueClass.equals(Double.class)) {
364: value = new Double(number.doubleValue());
365: } else if (valueClass.equals(BigInteger.class)) {
366: value = BigInteger.valueOf(number.longValue());
367: } else if (valueClass.equals(BigDecimal.class)) {
368: value = new BigDecimal(Double
369: .toString(number.doubleValue()));
370: } else {
371: throw new JRException("Unknown number class "
372: + valueClass.getName());
373: }
374: return value;
375: }
376:
377: /**
378: * Creates a sub data source using the current node (record) as the root
379: * of the document. An additional XPath expression specifies the select criteria applied to
380: * this new document and that produces the nodes (records) for the data source.
381: *
382: * @param selectExpr the XPath select expression
383: * @return the xml sub data source
384: * @throws JRException if the sub data source couldn't be created
385: * @see JRXmlDataSource#JRXmlDataSource(Document, String)
386: */
387: public JRXmlDataSource subDataSource(String selectExpr)
388: throws JRException {
389: // create a new document from the current node
390: Document doc = subDocument();
391: JRXmlDataSource subDataSource = new JRXmlDataSource(doc,
392: selectExpr);
393: subDataSource.setLocale(locale);
394: subDataSource.setDatePattern(datePattern);
395: subDataSource.setNumberPattern(numberPattern);
396: subDataSource.setTimeZone(timeZone);
397: return subDataSource;
398: }
399:
400: /**
401: * Creates a sub data source using the current node (record) as the root
402: * of the document. The data source will contain exactly one record consisting
403: * of the document node itself.
404: *
405: * @return the xml sub data source
406: * @throws JRException if the data source cannot be created
407: * @see JRXmlDataSource#subDataSource(String)
408: * @see JRXmlDataSource#JRXmlDataSource(Document)
409: */
410: public JRXmlDataSource subDataSource() throws JRException {
411: return subDataSource(".");
412: }
413:
414: /**
415: * Creates a document using the current node as root.
416: *
417: * @return a document having the current node as root
418: * @throws JRException
419: */
420: public Document subDocument() throws JRException {
421: if (currentNode == null) {
422: throw new JRException(
423: "No node available. Iterate or rewind the data source.");
424: }
425:
426: // create a new document from the current node
427: Document doc = JRXmlUtils.createDocument(currentNode);
428: return doc;
429: }
430:
431: /**
432: * Creates a sub data source using as root document the document used by "this" data source.
433: * An additional XPath expression specifies the select criteria applied to
434: * this document and that produces the nodes (records) for the data source.
435: *
436: * @param selectExpr the XPath select expression
437: * @return the xml sub data source
438: * @throws JRException if the sub data source couldn't be created
439: * @see JRXmlDataSource#JRXmlDataSource(Document, String)
440: */
441: public JRXmlDataSource dataSource(String selectExpr)
442: throws JRException {
443: JRXmlDataSource subDataSource = new JRXmlDataSource(document,
444: selectExpr);
445: subDataSource.setLocale(locale);
446: subDataSource.setDatePattern(datePattern);
447: subDataSource.setNumberPattern(numberPattern);
448: subDataSource.setTimeZone(timeZone);
449: return subDataSource;
450: }
451:
452: /**
453: * Creates a sub data source using as root document the document used by "this" data source.
454: * The data source will contain exactly one record consisting of the document node itself.
455: *
456: * @return the xml sub data source
457: * @throws JRException if the data source cannot be created
458: * @see JRXmlDataSource#dataSource(String)
459: * @see JRXmlDataSource#JRXmlDataSource(Document)
460: */
461: public JRXmlDataSource dataSource() throws JRException {
462: return dataSource(".");
463: }
464:
465: /**
466: * Return the text that a node contains. This routine:
467: * <ul>
468: * <li>Ignores comments and processing instructions.
469: * <li>Concatenates TEXT nodes, CDATA nodes, and the results of recursively
470: * processing EntityRef nodes.
471: * <li>Ignores any element nodes in the sublist. (Other possible options
472: * are to recurse into element sublists or throw an exception.)
473: * </ul>
474: *
475: * @param node a DOM node
476: * @return a String representing node contents or null
477: */
478: public String getText(Node node) {
479: if (!node.hasChildNodes())
480: return node.getNodeValue();
481:
482: StringBuffer result = new StringBuffer();
483:
484: NodeList list = node.getChildNodes();
485: for (int i = 0; i < list.getLength(); i++) {
486: Node subnode = list.item(i);
487: if (subnode.getNodeType() == Node.TEXT_NODE) {
488: String value = subnode.getNodeValue();
489: if (value != null)
490: result.append(value);
491: } else if (subnode.getNodeType() == Node.CDATA_SECTION_NODE) {
492: String value = subnode.getNodeValue();
493: if (value != null)
494: result.append(value);
495: } else if (subnode.getNodeType() == Node.ENTITY_REFERENCE_NODE) {
496: // Recurse into the subtree for text
497: // (and ignore comments)
498: String value = getText(subnode);
499: if (value != null)
500: result.append(value);
501: }
502: }
503:
504: return result.toString();
505: }
506:
507: public static void main(String[] args) throws Exception {
508: JRXmlDataSource ds = new JRXmlDataSource(new FileInputStream(
509: "northwind.xml"), "/Northwind/Customers");
510: JRDesignField field = new JRDesignField();
511: field.setDescription("CustomerID");
512: field.setValueClass(String.class);
513:
514: ds.next();
515: String v = (String) ds.getFieldValue(field);
516: System.out.println(field.getDescription() + "=" + v);
517:
518: JRXmlDataSource subDs = ds.dataSource("/Northwind/Orders");
519:
520: JRDesignField field1 = new JRDesignField();
521: field1.setDescription("OrderID");
522: field1.setValueClass(String.class);
523:
524: subDs.next();
525: String v1 = (String) subDs.getFieldValue(field1);
526: System.out.println(field1.getDescription() + "=" + v1);
527:
528: }
529:
530: protected LocaleConvertUtilsBean getConvertBean() {
531: if (convertBean == null) {
532: convertBean = new LocaleConvertUtilsBean();
533: if (locale != null) {
534: convertBean.setDefaultLocale(locale);
535: convertBean.deregister();
536: //convertBean.lookup();
537: }
538: convertBean.register(new JRDateLocaleConverter(timeZone),
539: java.util.Date.class, locale);
540: }
541: return convertBean;
542: }
543:
544: public Locale getLocale() {
545: return locale;
546: }
547:
548: public void setLocale(Locale locale) {
549: this .locale = locale;
550: convertBean = null;
551: }
552:
553: public String getDatePattern() {
554: return datePattern;
555: }
556:
557: public void setDatePattern(String datePattern) {
558: this .datePattern = datePattern;
559: convertBean = null;
560: }
561:
562: public String getNumberPattern() {
563: return numberPattern;
564: }
565:
566: public void setNumberPattern(String numberPattern) {
567: this .numberPattern = numberPattern;
568: convertBean = null;
569: }
570:
571: public TimeZone getTimeZone() {
572: return timeZone;
573: }
574:
575: public void setTimeZone(TimeZone timeZone) {
576: this.timeZone = timeZone;
577: convertBean = null;
578: }
579:
580: }
|