001    package com.thaiopensource.validate;
002    
003    import org.xml.sax.SAXException;
004    import org.xml.sax.InputSource;
005    import org.xml.sax.DTDHandler;
006    import org.xml.sax.XMLReader;
007    import org.xml.sax.ErrorHandler;
008    
009    import java.io.IOException;
010    import java.io.File;
011    import java.net.MalformedURLException;
012    
013    import com.thaiopensource.util.UriOrFile;
014    import com.thaiopensource.util.PropertyMap;
015    import com.thaiopensource.util.PropertyMapBuilder;
016    import com.thaiopensource.util.PropertyId;
017    import com.thaiopensource.xml.sax.XMLReaderCreator;
018    import com.thaiopensource.xml.sax.CountingErrorHandler;
019    import com.thaiopensource.xml.sax.Jaxp11XMLReaderCreator;
020    import com.thaiopensource.xml.sax.ErrorHandlerImpl;
021    import com.thaiopensource.validate.auto.AutoSchemaReader;
022    
023    /**
024     * Provides a simplified API for validating XML documents against schemas.
025     * This class is neither reentrant nor safe for access from multiple threads.
026     *
027     * @author <a href="mailto:jjc@jclark.com">James Clark</a>
028     */
029    
030    public class ValidationDriver {
031      private static final PropertyId[] requiredProperties = {
032        ValidateProperty.XML_READER_CREATOR,
033        ValidateProperty.ERROR_HANDLER
034      };
035    
036      private static final Class[] defaultClasses = {
037        Jaxp11XMLReaderCreator.class,
038        ErrorHandlerImpl.class
039      };
040    
041      private final XMLReaderCreator xrc;
042      private XMLReader xr;
043      private final CountingErrorHandler eh;
044      private final SchemaReader sr;
045      private final PropertyMap schemaProperties;
046      private final PropertyMap instanceProperties;
047      private Validator validator;
048      private Schema schema;
049    
050      /**
051       * Creates and initializes a ValidationDriver.
052       *
053       * @param schemaProperties a PropertyMap specifying properties controlling schema creation;
054       * must not be <code>null</code>
055       * @param instanceProperties a PropertyMap specifying properties controlling validation;
056       * must not be <code>null</code>
057       * @param schemaReader the SchemaReader to use; if this is <code>null</code>, then the schema
058       * must be in XML, and the namespace URI of the root element will be used to determine what
059       * the schema language is
060       */
061      public ValidationDriver(PropertyMap schemaProperties,
062                              PropertyMap instanceProperties,
063                              SchemaReader schemaReader) {
064        PropertyMapBuilder builder = new PropertyMapBuilder(schemaProperties);
065        for (int i = 0; i < requiredProperties.length; i++) {
066          if (!builder.contains(requiredProperties[i])) {
067            try {
068              builder.put(requiredProperties[i],
069                          defaultClasses[i].newInstance());
070            }
071            catch (InstantiationException e) {
072            }
073            catch (IllegalAccessException e) {
074            }
075          }
076        }
077        this.schemaProperties = builder.toPropertyMap();
078        builder = new PropertyMapBuilder(instanceProperties);
079        for (int i = 0; i < requiredProperties.length; i++) {
080          if (!builder.contains(requiredProperties[i]))
081            builder.put(requiredProperties[i],
082                        this.schemaProperties.get(requiredProperties[i]));
083        }
084        eh = new CountingErrorHandler((ErrorHandler)builder.get(ValidateProperty.ERROR_HANDLER));
085        ValidateProperty.ERROR_HANDLER.put(builder, eh);
086        this.instanceProperties = builder.toPropertyMap();
087        this.xrc = ValidateProperty.XML_READER_CREATOR.get(this.instanceProperties);
088        this.sr = schemaReader == null ? new AutoSchemaReader() : schemaReader;
089      }
090    
091      /**
092       * Equivalent to ValidationDriver(schemaProperties, instanceProperties, null).
093       *
094       * @see #ValidationDriver(PropertyMap,PropertyMap,SchemaReader)
095       */
096       public ValidationDriver(PropertyMap schemaProperties, PropertyMap instanceProperties) {
097         this(schemaProperties, instanceProperties, null);
098      }
099    
100      /**
101       * Equivalent to ValidationDriver(properties, properties, sr).
102       *
103       * @see #ValidationDriver(PropertyMap,PropertyMap,SchemaReader)
104       */
105       public ValidationDriver(PropertyMap properties, SchemaReader sr) {
106        this(properties, properties, sr);
107      }
108    
109      /**
110       * Equivalent to ValidationDriver(properties, properties, null).
111       *
112       * @see #ValidationDriver(PropertyMap,PropertyMap,SchemaReader)
113       */
114       public ValidationDriver(PropertyMap properties) {
115        this(properties, properties, null);
116      }
117    
118      /**
119       * Equivalent to ValidationDriver(PropertyMap.EMPTY, PropertyMap.EMPTY, null).
120       *
121       * @see #ValidationDriver(PropertyMap,PropertyMap,SchemaReader)
122       */
123       public ValidationDriver(SchemaReader sr) {
124        this(PropertyMap.EMPTY, sr);
125      }
126    
127      /**
128       * Equivalent to ValidationDriver(PropertyMap.EMPTY, PropertyMap.EMPTY, null).
129       *
130       * @see #ValidationDriver(PropertyMap,PropertyMap,SchemaReader)
131       */
132      public ValidationDriver() {
133        this(PropertyMap.EMPTY, PropertyMap.EMPTY, null);
134      }
135    
136      /**
137       * Loads a schema. Subsequent calls to <code>validate</code> will validate with
138       * respect the loaded schema. This can be called more than once to allow
139       * multiple documents to be validated against different schemas.
140       *
141       * @param in the InputSource for the schema
142       * @return <code>true</code> if the schema was loaded successfully; <code>false</code> otherwise
143       * @throws IOException if an I/O error occurred
144       * @throws SAXException if an XMLReader or ErrorHandler threw a SAXException
145       */
146      public boolean loadSchema(InputSource in) throws SAXException, IOException {
147        try {
148          schema = sr.createSchema(in, schemaProperties);
149          validator = null;
150          return true;
151        }
152        catch (IncorrectSchemaException e) {
153          return false;
154        }
155      }
156    
157      /**
158       * Validates a document against the currently loaded schema. This can be called
159       * multiple times in order to validate multiple documents.
160       *
161       * @param in the InputSource for the document to be validated
162       * @return <code>true</code> if the document is valid; <code>false</code> otherwise
163       * @throws java.lang.IllegalStateException if there is no currently loaded schema
164       * @throws java.io.IOException if an I/O error occurred
165       * @throws org.xml.sax.SAXException if an XMLReader or ErrorHandler threw a SAXException
166       */
167      public boolean validate(InputSource in) throws SAXException, IOException {
168        if (schema == null)
169          throw new IllegalStateException("cannot validate without schema");
170        if (validator == null)
171          validator = schema.createValidator(instanceProperties);
172        if (xr == null) {
173          xr = xrc.createXMLReader();
174          xr.setErrorHandler(eh);
175        }
176        eh.reset();
177        xr.setContentHandler(validator.getContentHandler());
178        DTDHandler dh = validator.getDTDHandler();
179        if (dh != null)
180          xr.setDTDHandler(dh);
181        try {
182          xr.parse(in);
183          return !eh.getHadErrorOrFatalError();
184        }
185        finally {
186          validator.reset();
187        }
188      }
189    
190      /**
191       * Returns an <code>InputSource</code> for a filename.
192       *
193       * @param filename a String specifying the filename
194       * @return an <code>InputSource</code> for the filename
195       */
196      static public InputSource fileInputSource(String filename) throws MalformedURLException {
197        return ValidationDriver.fileInputSource(new File(filename));
198      }
199    
200      /**
201       * Returns an <code>InputSource</code> for a <code>File</code>.
202       *
203       * @param file the <code>File</code>
204       * @return an <code>InputSource</code> for the filename
205       */
206      static public InputSource fileInputSource(File file) throws MalformedURLException {
207        return new InputSource(UriOrFile.fileToUri(file));
208      }
209    
210      /**
211       * Returns an <code>InputSource</code> for a string that represents either a file
212       * or an absolute URI. If the string looks like an absolute URI, it will be
213       * treated as an absolute URI, otherwise it will be treated as a filename.
214       *
215       * @param uriOrFile a <code>String</code> representing either a file or an absolute URI
216       * @return an <code>InputSource</code> for the file or absolute URI
217       */
218      static public InputSource uriOrFileInputSource(String uriOrFile) throws MalformedURLException {
219        return new InputSource(UriOrFile.toUri(uriOrFile));
220      }
221    }