001: /*
002: ******************************************************************
003: Copyright (c) 2001-2007, Jeff Martin, Tim Bacon
004: All rights reserved.
005:
006: Redistribution and use in source and binary forms, with or without
007: modification, are permitted provided that the following conditions
008: are met:
009:
010: * Redistributions of source code must retain the above copyright
011: notice, this list of conditions and the following disclaimer.
012: * Redistributions in binary form must reproduce the above
013: copyright notice, this list of conditions and the following
014: disclaimer in the documentation and/or other materials provided
015: with the distribution.
016: * Neither the name of the xmlunit.sourceforge.net nor the names
017: of its contributors may be used to endorse or promote products
018: derived from this software without specific prior written
019: permission.
020:
021: THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
022: "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
023: LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
024: FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
025: COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
026: INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
027: BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
028: LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
029: CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
030: LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
031: ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
032: POSSIBILITY OF SUCH DAMAGE.
033:
034: ******************************************************************
035: */
036:
037: package org.custommonkey.xmlunit;
038:
039: import org.custommonkey.xmlunit.exceptions.ConfigurationException;
040: import org.custommonkey.xmlunit.exceptions.XMLUnitRuntimeException;
041:
042: import java.io.IOException;
043: import java.io.InputStreamReader;
044: import java.io.Reader;
045: import java.io.StringReader;
046:
047: import org.w3c.dom.Document;
048: import org.xml.sax.ErrorHandler;
049: import org.xml.sax.InputSource;
050: import org.xml.sax.SAXException;
051: import org.xml.sax.SAXNotRecognizedException;
052: import org.xml.sax.SAXNotSupportedException;
053: import org.xml.sax.SAXParseException;
054: import org.xml.sax.helpers.DefaultHandler;
055:
056: import javax.xml.parsers.ParserConfigurationException;
057: import javax.xml.parsers.SAXParser;
058: import javax.xml.parsers.SAXParserFactory;
059:
060: /**
061: * Validates XML against its internal or external DOCTYPE, or a completely
062: * different DOCTYPE.
063: * Usage:
064: * <ul>
065: * <li><code>new Validator(readerForXML);</code> <br/>
066: * to validate some XML that contains or references an accessible DTD or
067: * schema
068: * </li>
069: * <li><code>new Validator(readerForXML, systemIdForValidation);</code> <br/>
070: * to validate some XML that references a DTD but using a local systemId
071: * to perform the validation
072: * </li>
073: * <li><code>new Validator(readerForXML, systemIdForValidation, doctypeName);</code> <br/>
074: * to validate some XML against a completely different DTD
075: * </li>
076: * </ul>
077: * <br />Examples and more at <a href="http://xmlunit.sourceforge.net"/>xmlunit.sourceforge.net</a>
078: */
079: public class Validator extends DefaultHandler implements ErrorHandler {
080: private final InputSource validationInputSource;
081: private final SAXParser parser;
082: private final StringBuffer messages;
083: private final boolean usingDoctypeReader;
084:
085: private Boolean isValid;
086:
087: /**
088: * Kept for backwards compatibility.
089: * @deprecated Use the protected three arg constructor instead.
090: */
091: protected Validator(InputSource inputSource,
092: boolean usingDoctypeReader) throws SAXException,
093: ConfigurationException {
094: this (inputSource, null, usingDoctypeReader);
095: }
096:
097: /**
098: * Baseline constructor: called by all others
099: *
100: * @param inputSource
101: * @param systemId
102: * @param usingDoctypeReader
103: * @throws SAXException
104: * @throws ConfigurationException if validation could not be turned on
105: */
106: protected Validator(InputSource inputSource, String systemId,
107: boolean usingDoctypeReader) throws SAXException,
108: ConfigurationException {
109: isValid = null;
110: messages = new StringBuffer();
111: SAXParserFactory factory = XMLUnit.getSAXParserFactory();
112: factory.setValidating(true);
113: try {
114: parser = factory.newSAXParser();
115: } catch (ParserConfigurationException ex) {
116: throw new ConfigurationException(ex);
117: }
118:
119: this .validationInputSource = inputSource;
120: if (systemId != null) {
121: validationInputSource.setSystemId(systemId);
122: }
123: this .usingDoctypeReader = usingDoctypeReader;
124: }
125:
126: /**
127: * DOM-style constructor: allows Document validation post-manipulation
128: * of the DOM tree's contents.
129: * This takes a fairly tortuous route to validation as DOM level 2 does
130: * not allow creation of Doctype nodes.
131: * The supplied systemId and doctype name will replace any Doctype
132: * settings in the Document.
133: *
134: * @param document
135: * @param systemID
136: * @param doctype
137: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
138: * @throws ConfigurationException if validation could not be turned on
139: */
140: public Validator(Document document, String systemID, String doctype)
141: throws SAXException, ConfigurationException {
142: this (new InputStreamReader(new NodeInputStream(document)),
143: systemID, doctype);
144: }
145:
146: /**
147: * Basic constructor.
148: * Validates the contents of the Reader using the DTD or schema referenced
149: * by those contents.
150: *
151: * @param readerForValidation
152: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
153: * @throws ConfigurationException if validation could not be turned on
154: */
155: public Validator(Reader readerForValidation) throws SAXException,
156: ConfigurationException {
157: this (readerForValidation, null);
158: }
159:
160: /**
161: * Basic constructor.
162: * Validates the contents of the String using the DTD or schema referenced
163: * by those contents.
164: *
165: * @param stringForValidation
166: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
167: * @throws ConfigurationException if validation could not be turned on
168: */
169: public Validator(String stringForValidation) throws SAXException,
170: ConfigurationException {
171: this (new StringReader(stringForValidation));
172: }
173:
174: /**
175: * Basic constructor.
176: * Validates the contents of the InputSource using the DTD or
177: * schema referenced by those contents.
178: *
179: * @param readerForValidation
180: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
181: * @throws ConfigurationException if validation could not be turned on
182: */
183: public Validator(InputSource sourceForValidation)
184: throws SAXException, ConfigurationException {
185: this (sourceForValidation, null);
186: }
187:
188: /**
189: * Extended constructor.
190: * Validates the contents of the Reader using the DTD specified with the
191: * systemID. There must be DOCTYPE instruction in the markup that
192: * references the DTD or else the markup will be considered invalid: if
193: * there is no DOCTYPE in the markup use the 3-argument constructor
194: *
195: * @param readerForValidation
196: * @param systemID
197: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
198: * @throws ConfigurationException if validation could not be turned on
199: */
200: public Validator(Reader readerForValidation, String systemID)
201: throws SAXException, ConfigurationException {
202: this (new InputSource(readerForValidation), systemID,
203: (readerForValidation instanceof DoctypeReader));
204: }
205:
206: /**
207: * Extended constructor.
208: * Validates the contents of the String using the DTD specified with the
209: * systemID. There must be DOCTYPE instruction in the markup that
210: * references the DTD or else the markup will be considered invalid: if
211: * there is no DOCTYPE in the markup use the 3-argument constructor
212: *
213: * @param stringForValidation
214: * @param systemID
215: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
216: * @throws ConfigurationException if validation could not be turned on
217: */
218: public Validator(String stringForValidation, String systemID)
219: throws SAXException, ConfigurationException {
220: this (new StringReader(stringForValidation), systemID);
221: }
222:
223: /**
224: * Extended constructor.
225: * Validates the contents of the InputSource using the DTD
226: * specified with the systemID. There must be DOCTYPE instruction
227: * in the markup that references the DTD or else the markup will
228: * be considered invalid: if there is no DOCTYPE in the markup use
229: * the 3-argument constructor
230: *
231: * @param sourceForValidation
232: * @param systemID
233: * @throws SAXException if unable to obtain new Sax parser via JAXP factory
234: * @throws ConfigurationException if validation could not be turned on
235: */
236: public Validator(InputSource sourceForValidation, String systemID)
237: throws SAXException, ConfigurationException {
238: this (sourceForValidation, systemID, false);
239: }
240:
241: /**
242: * Full constructor.
243: * Validates the contents of the InputSource using the DTD
244: * specified with the systemID and named with the doctype name.
245: *
246: * @param sourceForValidation
247: * @param systemID
248: * @param doctype
249: * @throws SAXException
250: * @throws ConfigurationException if validation could not be turned on
251: */
252: public Validator(InputSource sourceForValidation, String systemID,
253: String doctype) throws SAXException, ConfigurationException {
254: this (
255: sourceForValidation.getCharacterStream() != null ? new InputSource(
256: new DoctypeReader(sourceForValidation
257: .getCharacterStream(), doctype,
258: systemID))
259: : new InputSource(new DoctypeInputStream(
260: sourceForValidation.getByteStream(),
261: sourceForValidation.getEncoding(),
262: doctype, systemID)), systemID, true);
263: }
264:
265: /**
266: * Full constructor.
267: * Validates the contents of the Reader using the DTD specified with the
268: * systemID and named with the doctype name.
269: *
270: * @param readerForValidation
271: * @param systemID
272: * @param doctype
273: * @throws SAXException
274: * @throws ConfigurationException if validation could not be turned on
275: */
276: public Validator(Reader readerForValidation, String systemID,
277: String doctype) throws SAXException, ConfigurationException {
278: this (
279: readerForValidation instanceof DoctypeReader ? readerForValidation
280: : new DoctypeReader(readerForValidation,
281: doctype, systemID), systemID);
282: }
283:
284: /**
285: * Turn on XML Schema validation.
286: *
287: * <p><b>This feature should work with any XML parser that is JAXP
288: * 1.2 compliant and supports XML Schema validation.</b></p>
289: *
290: * <p>For a fully JAXP 1.2 compliant parser the property {@link
291: * JAXPConstants.Properties.SCHEMA_LANGUAGE
292: * http://java.sun.com/xml/jaxp/properties/schemaLanguage} is set,
293: * if this fails the method falls back to the features
294: * http://apache.org/xml/features/validation/schema &
295: * http://apache.org/xml/features/validation/dynamic which should
296: * cover early versions of Xerces 2 as well.</p>
297: *
298: * @param use indicate that XML Schema should be used to validate
299: * documents.
300: * @throws SAXException
301: * @see #setJAXP12SchemaSource(Object)
302: */
303: public void useXMLSchema(boolean use) throws SAXException {
304: boolean tryXercesProperties = false;
305: try {
306: if (use) {
307: parser.setProperty(
308: JAXPConstants.Properties.SCHEMA_LANGUAGE,
309: XMLConstants.W3C_XML_SCHEMA_NS_URI);
310: }
311: } catch (SAXNotRecognizedException e) {
312: tryXercesProperties = true;
313: } catch (SAXNotSupportedException e) {
314: tryXercesProperties = true;
315: }
316:
317: if (tryXercesProperties) {
318: parser.getXMLReader().setFeature(
319: "http://apache.org/xml/features/validation/schema",
320: use);
321: parser
322: .getXMLReader()
323: .setFeature(
324: "http://apache.org/xml/features/validation/dynamic",
325: use);
326: }
327: }
328:
329: /**
330: * Perform the validation of the source against DTD / Schema.
331: *
332: * @return true if the input supplied to the constructor passes validation,
333: * false otherwise
334: */
335: public boolean isValid() {
336: validate();
337: return isValid.booleanValue();
338: }
339:
340: /**
341: * Assert that a document is valid.
342: */
343: public void assertIsValid() {
344: if (!isValid()) {
345: junit.framework.Assert.fail(messages.toString());
346: }
347: }
348:
349: /**
350: * Append any validation message(s) to the specified StringBuffer.
351: *
352: * @param toAppendTo
353: * @return specified StringBuffer with message(s) appended
354: */
355: private StringBuffer appendMessage(StringBuffer toAppendTo) {
356: if (isValid()) {
357: return toAppendTo.append("[valid]");
358: }
359: return toAppendTo.append(messages);
360: }
361:
362: /**
363: * @return class name appended with validation messages
364: */
365: public String toString() {
366: StringBuffer buf = new StringBuffer(super .toString())
367: .append(':');
368: return appendMessage(buf).toString();
369: }
370:
371: /**
372: * Actually perform validation.
373: */
374: private void validate() {
375: if (isValid != null) {
376: return;
377: }
378:
379: try {
380: parser.parse(validationInputSource, this );
381: } catch (SAXException e) {
382: parserException(e);
383: } catch (IOException e) {
384: parserException(e);
385: }
386:
387: if (isValid == null) {
388: isValid = Boolean.TRUE;
389: } else if (usingDoctypeReader) {
390: try {
391: messages.append("\nContent was: ").append(
392: getOriginalContent(validationInputSource));
393: } catch (IOException e) {
394: // silent but deadly?
395: }
396: }
397: }
398:
399: /**
400: * Deal with any parser exceptions not handled by the ErrorHandler interface.
401: *
402: * @param e
403: */
404: private void parserException(Exception e) {
405: invalidate(e.getMessage());
406: }
407:
408: /**
409: * ErrorHandler interface method.
410: *
411: * @param exception
412: * @throws SAXException
413: */
414: public void warning(SAXParseException exception)
415: throws SAXException {
416: errorHandlerException(exception);
417: }
418:
419: /**
420: * ErrorHandler interface method.
421: *
422: * @param exception
423: * @throws SAXException
424: */
425: public void error(SAXParseException exception) throws SAXException {
426: errorHandlerException(exception);
427: }
428:
429: /**
430: * ErrorHandler interface method.
431: *
432: * @param exception
433: * @throws SAXException
434: */
435: public void fatalError(SAXParseException exception)
436: throws SAXException {
437: errorHandlerException(exception);
438: }
439:
440: /**
441: * Entity Resolver method: allows us to override an existing systemID
442: * referenced in the markup DOCTYPE instruction.
443: *
444: * @param publicId
445: * @param systemId
446: * @return the sax InputSource that points to the overridden systemID
447: */
448: public InputSource resolveEntity(String publicId, String systemId) {
449: if (validationInputSource.getSystemId() != null) {
450: return new InputSource(validationInputSource.getSystemId());
451: } else {
452: InputSource s = null;
453: try {
454: if (XMLUnit.getControlEntityResolver() != null) {
455: s = XMLUnit.getControlEntityResolver()
456: .resolveEntity(publicId, systemId);
457: }
458: } catch (SAXException e) {
459: throw new XMLUnitRuntimeException(
460: "failed to resolve entity: " + publicId, e);
461: } catch (IOException e) {
462: // even if we wanted to re-throw IOException (which we
463: // can't because of backwards compatibility) the mere
464: // fact that DefaultHandler has stripped IOException
465: // from EntityResolver's method's signature wouldn't
466: // let us.
467: throw new XMLUnitRuntimeException(
468: "failed to resolve entity: " + publicId, e);
469: }
470: if (s != null) {
471: return s;
472: } else if (systemId != null) {
473: return new InputSource(systemId);
474: }
475: }
476: return null;
477: }
478:
479: /**
480: * Deal with exceptions passed to the ErrorHandler interface by the parser.
481: */
482: private void errorHandlerException(Exception e) {
483: invalidate(e.getMessage());
484: }
485:
486: /**
487: * Set the validation status flag to false and capture the message for use
488: * later.
489: *
490: * @param message
491: */
492: private void invalidate(String message) {
493: isValid = Boolean.FALSE;
494: messages.append(message).append(' ');
495: }
496:
497: /**
498: * As per JAXP 1.2 changes, which introduced a standard way for parsers to
499: * support schema validation. Since only W3C Schema support was included in
500: * JAXP 1.2, this is the only mechanism currently supported by this method.
501: *
502: * @param schemaSource
503: * This can be one of the following:
504: * <ul>
505: * <li>String that points to the URI of the schema</li>
506: * <li>InputStream with the contents of the schema</li>
507: * <li>SAX InputSource</li>
508: * <li>File</li>
509: * <li>an array of Objects with the contents being one of the
510: * types defined above. An array of Objects can be used only when
511: * the schema language has the ability to assemble a schema at
512: * runtime. When an array of Objects is passed it is illegal to
513: * have two schemas that share the same namespace.</li>
514: * </ul>
515: * @throws SAXException if this method of validating isn't supported.
516: * @see http://java.sun.com/webservices/jaxp/change-requests-11.html
517: */
518: public void setJAXP12SchemaSource(Object schemaSource)
519: throws SAXException {
520: parser.setProperty(JAXPConstants.Properties.SCHEMA_SOURCE,
521: schemaSource);
522: }
523:
524: private static String getOriginalContent(InputSource s)
525: throws IOException {
526: return s.getCharacterStream() instanceof DoctypeReader ? ((DoctypeReader) s
527: .getCharacterStream()).getContent()
528: : ((DoctypeInputStream) s.getByteStream()).getContent(s
529: .getEncoding());
530: }
531: }
|