View Javadoc

1   // $Id: ParserImpl.java 380 2011-05-25 20:51:07Z jg_hamburg $
2   /********************************************************************************
3    * DDTUnit, a Datadriven Approach to Unit- and Moduletesting
4    * Copyright (c) 2004, Joerg and Kai Gellien
5    * All rights reserved.
6    *
7    * The Software is provided under the terms of the Common Public License 1.0
8    * as provided with the distribution of DDTUnit in the file cpl-v10.html.
9    * Redistribution and use in source and binary forms, with or without
10   * modification, are permitted provided that the following conditions
11   * are met:
12   *
13   *     + Redistributions of source code must retain the above copyright
14   *       notice, this list of conditions and the following disclaimer.
15   *
16   *     + Redistributions in binary form must reproduce the above
17   *       copyright notice, this list of conditions and the following
18   *       disclaimer in the documentation and/or other materials provided
19   *       with the distribution.
20   *
21   *     + Neither the name of the authors or DDTUnit, nor the
22   *       names of its contributors may be used to endorse or promote
23   *       products derived from this software without specific prior
24   *       written permission.
25   *
26   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
27   * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
28   * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
29   * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
30   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31   * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
32   * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
33   * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
34   * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
35   * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
36   * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
37   ********************************************************************************/
38  package junitx.ddtunit.data.processing.parser;
39  
40  import java.io.File;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.io.Reader;
44  import java.io.StringReader;
45  import java.net.MalformedURLException;
46  
47  import javax.xml.parsers.ParserConfigurationException;
48  import javax.xml.parsers.SAXParser;
49  import javax.xml.parsers.SAXParserFactory;
50  
51  import junitx.ddtunit.DDTException;
52  import junitx.ddtunit.data.DDTTestDataException;
53  import junitx.ddtunit.data.TestClusterDataSet;
54  import junitx.ddtunit.data.TypedObject;
55  import junitx.ddtunit.data.processing.IParser;
56  import junitx.ddtunit.util.DDTConfiguration;
57  
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  import org.xml.sax.InputSource;
61  import org.xml.sax.Locator;
62  import org.xml.sax.SAXException;
63  import org.xml.sax.SAXNotRecognizedException;
64  import org.xml.sax.SAXParseException;
65  import org.xml.sax.XMLReader;
66  import org.xml.sax.helpers.DefaultHandler;
67  import org.xml.sax.helpers.LocatorImpl;
68  
69  /**
70   * XML SAX parser converting xml testdatea resourse into object structure used
71   * by DDTestCase class execution. <br/>Using JAXP 1.1 to specify and configure
72   * SAX conformant parser. <br/>As project default Apache Crimson provided in the
73   * distribution of JDK 1.4.2 is used.
74   * 
75   * @author jg
76   */
77  public final class ParserImpl implements IParser {
78  	private static final String XML_XERCES_NONAMESPACE_SCHEMA_LOCATION_URI = "http://apache.org/xml/properties/schema/external-noNamespaceSchemaLocation";
79  
80  	private static final String XML_XERCES_SCHEMA_FULL_CHECK_URI = "http://apache.org/xml/features/validation/schema-full-checking";
81  
82  	private static final String XML_VALIDATE_URI = "http://xml.org/sax/features/validation";
83  
84  	private static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
85  
86  	private static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
87  
88  	private static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";
89  
90  	private static final String XML_LEXICAL_HANDLER_URI = "http://xml.org/sax/properties/lexical-handler";
91  
92  	private static final String XML_XERCES_VALIDATE_URI = "http://apache.org/xml/features/validation/schema";
93  
94  	/**
95  	 * URL of xml schema for validation
96  	 */
97  	public final static String XSD_URL = "http://ddtunit.sourceforge.net/ddtunit.xsd";
98  
99  	/**
100 	 * resource path of xml schema used for validation
101 	 */
102 	public final static String XSD_RESOURCE_PATH = "/junitx/ddtunit/data/processing/parser/ddtunit.xsd";
103 
104 	private final Logger log = LoggerFactory.getLogger(ParserImpl.class);
105 
106 	private XMLReader producer;
107 
108 	private ContentHandler consumer;
109 
110 	private final static String XML_NAMESPACE_URI = "http://xml.org/sax/features/namespaces";
111 
112 	private final static String XML_NAMESPACE_PREFIX_URI = "http://xml.org/sax/features/namespace-prefixes";
113 
114 	private static final String LF = System.getProperty("line.separator");
115 
116 	private static final String XML_HEADER = "<?xml version=\"1.0\" ?>";
117 
118 	/**
119 	 * 
120 	 */
121 	public ParserImpl() {
122 		log.debug("DDTParser - constructor START");
123 		try {
124 			// the SAX way
125 			// this.producer = XMLReaderFactory.createXMLReader();
126 			// the jaxp1.1 way
127 			SAXParserFactory factory = SAXParserFactory.newInstance();
128 			factory.setNamespaceAware(true);
129 			factory.setValidating(DDTConfiguration.getInstance().isActiveXmlValidation());
130 			SAXParser saxParser = factory.newSAXParser();
131 			// Assert.assertTrue("SAX parser should be validating", saxParser
132 			// .isValidating());
133 			this.producer = saxParser.getXMLReader();
134 			this.producer.setFeature(XML_NAMESPACE_PREFIX_URI, false);
135 			this.producer.setFeature(XML_NAMESPACE_URI, true);
136 		} catch (SAXNotRecognizedException e) {
137 			throw DDTException
138 					.create(
139 							new StringBuffer(
140 									"XML ParserImpl does not support schema validation as of JAXP 1.2"),
141 							e);
142 		} catch (ParserConfigurationException e) {
143 			throw DDTException.create(new StringBuffer(
144 					"Error configuring parser."), e);
145 		} catch (SAXException e) {
146 			throw DDTException.create(new StringBuffer(
147 					"Error configuring parser."), e);
148 		}
149 		this.producer.setErrorHandler(new ErrorHandler());
150 		this.producer.setEntityResolver(new EntityResolver());
151 		log.debug("DDTParser - constructor END");
152 	}
153 
154 	/*
155 	 * (non-Javadoc)
156 	 * 
157 	 * @see junitx.ddtunit.data.processing.parser.IParser#parse(java.lang.String,
158 	 *      java.lang.String)
159 	 */
160 	public TestClusterDataSet parse(String resource, boolean byName,
161 			String clusterId, TestClusterDataSet baseDataSet) {
162 		log.debug("parse(" + resource + ", " + clusterId + ")-START");
163 		this.consumer = new ContentHandler(resource, baseDataSet);
164 		Locator locator = new LocatorImpl();
165 		this.consumer.setDocumentLocator(locator);
166 
167 		try {
168 			// add classId to consumer as a processing filter criterion
169 			this.consumer.setClusterId(clusterId);
170 			// process reource
171 			InputSource iSource = createInputSource(resource, true);
172 			if (validateSource(iSource)) {
173 				iSource = createInputSource(resource, byName);
174 			}
175 			this.producer.setProperty("http://xml.org/sax/properties/"
176 					+ "lexical-handler", consumer);
177 			this.producer.setContentHandler(consumer);
178 			this.producer.parse(iSource);
179 		} catch (IOException e) {
180 			log.error("Error on behalf of xml test resource.", e);
181 			throw DDTTestDataException.create(new StringBuffer(
182 					"Error on behalf of xml test resource."), e);
183 		} catch (SAXException e) {
184 			StringBuffer sb = new StringBuffer(
185 					"Error during parsing of xml testresource");
186 			if (SAXParseException.class.isInstance(e)) {
187 				sb.append(LF).append("Resource \'").append(resource).append(
188 						"\' line/column ").append(
189 						((SAXParseException) e).getLineNumber()).append("/")
190 						.append(((SAXParseException) e).getColumnNumber());
191 			}
192 			log.error(sb.toString(), e);
193 			throw DDTTestDataException.create(sb, e);
194 		} finally {
195 			log.debug("parse(" + resource + ", " + clusterId + ")-END");
196 		}
197 		if (baseDataSet.size() == 0) {
198 			StringBuffer sb = new StringBuffer(
199 					"No testdata provided for class id \'")
200 					.append(baseDataSet.getId())
201 					.append("\' in testresource \'")
202 					.append(resource)
203 					.append("\'\n")
204 					.append(
205 							"Check if referred class id in xml resources matches definition")
206 					.append(
207 							" of \n initTestData(resource, classId) inside of your testclass.");
208 			throw new DDTTestDataException(sb.toString());
209 		}
210 
211 		return baseDataSet;
212 	}
213 
214 	/**
215 	 * Parse object definition and use provided object as base class. If set to
216 	 * null a new instance of required type will be created during processing.
217 	 * 
218 	 * @param xmlObjectDef
219 	 *            string defining object to instanciate
220 	 * @param addXMLHeader -
221 	 *            set true if xml header should be added
222 	 * @return object to instanciate as defined
223 	 */
224 	public TypedObject parseElement(String xmlObjectDef, boolean addXMLHeader) {
225 		TypedObject myObj = null;
226 		String defaultCluster = "singleton";
227 		log
228 				.debug("parse(\"" + xmlObjectDef + "\", " + addXMLHeader
229 						+ ")-START");
230 		TestClusterDataSet dataSet = new TestClusterDataSet(defaultCluster,
231 				null);
232 		this.consumer = new ContentHandler(null, dataSet);
233 		Locator locator = new LocatorImpl();
234 		this.consumer.setDocumentLocator(locator);
235 		this.producer.setContentHandler(consumer);
236 		// dataSet = (TestClusterDataSet) parse(xmlObjectDef, false,
237 		// defaultCluster, dataSet);
238 		try {
239 			InputSource iSource = createInputSource(xmlObjectDef, false);
240 			validateSource(iSource);
241 			iSource = createInputSource(xmlObjectDef, false);
242 			this.producer.parse(iSource);
243 		} catch (IOException e) {
244 			log.error("Error on behalf of xml test resource.", e);
245 			throw DDTTestDataException.create(new StringBuffer(
246 					"Error on behalf of xml test resource."), e);
247 		} catch (SAXException e) {
248 			StringBuffer sb = new StringBuffer(
249 					"Error during parsing of xml testresource from reader.");
250 			throw DDTTestDataException.create(sb, e);
251 		} finally {
252 			log.debug("parse(reader)-END");
253 		}
254 		myObj = dataSet.getObject("singleton");
255 		return myObj;
256 	}
257 
258 	/**
259 	 * @param resource
260 	 *            to process
261 	 * @param byName
262 	 *            set to true if resource is a name of resource to process,
263 	 *            false if it is the resource itself.
264 	 * @return
265 	 * @throws MalformedURLException
266 	 */
267 	private InputSource createInputSource(String resource, boolean byName)
268 			throws MalformedURLException {
269 		// check if resource is file or resource
270 		InputSource iSource = null;
271 		if (byName) {
272 			InputStream in = this.getClass().getResourceAsStream(resource);
273 			if (in == null) {
274 				File inFile = new File(resource);
275 
276 				iSource = new InputSource(resource);
277 				iSource.setSystemId(inFile.toURL().toExternalForm());
278 			} else {
279 				iSource = new InputSource(in);
280 				iSource.setSystemId(this.getClass().getResource(resource)
281 						.toExternalForm());
282 			}
283 		} else {
284 			String xmlDef = null;
285 			xmlDef = XML_HEADER + resource;
286 			Reader reader = new StringReader(xmlDef);
287 			iSource = new InputSource(reader);
288 		}
289 		return iSource;
290 	}
291 
292 	/**
293 	 * @param iSource
294 	 *            input source to validate
295 	 * @return true if validation was activated and processed
296 	 */
297 	private boolean validateSource(InputSource iSource) {
298 		boolean validated = false;
299 		try {
300 			if (this.producer.getFeature(XML_XERCES_VALIDATE_URI)) {
301 				DDTConfiguration.getInstance().setActiveParserValidation(true);
302 			}
303 		} catch (SAXException ex) {
304 			// no Apache Xerces found, so just ignore validation
305 		}
306 		if (DDTConfiguration.getInstance().isActiveXmlValidation()
307 				&& DDTConfiguration.getInstance().isActiveParserValidation()) {
308 			try {
309 				validated = true;
310 				this.producer.setContentHandler(new DefaultHandler());
311 				// validation properties and features must be activated
312 				this.producer.setFeature(XML_VALIDATE_URI, true);
313 				this.producer.setFeature(XML_XERCES_VALIDATE_URI, true);
314 				this.producer
315 						.setFeature(XML_XERCES_SCHEMA_FULL_CHECK_URI, true);
316 				this.producer.setProperty(
317 						XML_XERCES_NONAMESPACE_SCHEMA_LOCATION_URI,
318 						ParserImpl.XSD_URL);
319 				this.producer.setProperty(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
320 				this.producer.setProperty(JAXP_SCHEMA_SOURCE,
321 						ParserImpl.XSD_URL);
322 
323 				this.producer.parse(iSource);
324 			} catch (DDTException ex) {
325 				if (ex.getMessage() != null
326 						&& ex.getMessage().startsWith("Error during parsing")) {
327 					throw ex;
328 				}
329 				log.warn("Error on validation", ex);
330 			} catch (IOException e) {
331 				log.error("Error on behalf of xml test resource.", e);
332 				throw DDTException.create(new StringBuffer(
333 						"Error on behalf of xml test resource."), e);
334 			} catch (SAXException e) {
335 				StringBuffer sb = new StringBuffer(
336 						"Error during parsing of xml testresource");
337 				if (SAXParseException.class.isInstance(e)) {
338 					sb.append(LF).append("Resource \'").append(
339 							iSource.getSystemId()).append("\' line/column ")
340 							.append(((SAXParseException) e).getLineNumber())
341 							.append("/").append(
342 									((SAXParseException) e).getColumnNumber());
343 				}
344 				log.error(sb.toString(), e);
345 				throw DDTException.create(sb, e);
346 			}
347 		}
348 		return validated;
349 	}
350 
351 	public TypedObject parseElement(String xmlObjectDef) {
352 		return parseElement(xmlObjectDef, true);
353 	}
354 
355 }