001    package com.thaiopensource.validate.schematron;
002    
003    import com.thaiopensource.util.PropertyMap;
004    import com.thaiopensource.util.PropertyMapBuilder;
005    import com.thaiopensource.util.Localizer;
006    import com.thaiopensource.util.PropertyId;
007    import com.thaiopensource.validate.IncorrectSchemaException;
008    import com.thaiopensource.validate.Schema;
009    import com.thaiopensource.validate.SchemaReader;
010    import com.thaiopensource.validate.ValidateProperty;
011    import com.thaiopensource.validate.Validator;
012    import com.thaiopensource.validate.Option;
013    import com.thaiopensource.validate.rng.CompactSchemaReader;
014    import com.thaiopensource.validate.rng.RngProperty;
015    import com.thaiopensource.xml.sax.CountingErrorHandler;
016    import com.thaiopensource.xml.sax.DelegatingContentHandler;
017    import com.thaiopensource.xml.sax.DraconianErrorHandler;
018    import com.thaiopensource.xml.sax.ForkContentHandler;
019    import com.thaiopensource.xml.sax.Jaxp11XMLReaderCreator;
020    import com.thaiopensource.xml.sax.XMLReaderCreator;
021    import org.xml.sax.Attributes;
022    import org.xml.sax.ContentHandler;
023    import org.xml.sax.EntityResolver;
024    import org.xml.sax.ErrorHandler;
025    import org.xml.sax.InputSource;
026    import org.xml.sax.Locator;
027    import org.xml.sax.SAXException;
028    import org.xml.sax.SAXParseException;
029    import org.xml.sax.XMLReader;
030    
031    import javax.xml.transform.ErrorListener;
032    import javax.xml.transform.Source;
033    import javax.xml.transform.SourceLocator;
034    import javax.xml.transform.Templates;
035    import javax.xml.transform.Transformer;
036    import javax.xml.transform.TransformerConfigurationException;
037    import javax.xml.transform.TransformerException;
038    import javax.xml.transform.TransformerFactory;
039    import javax.xml.transform.URIResolver;
040    import javax.xml.transform.sax.SAXResult;
041    import javax.xml.transform.sax.SAXSource;
042    import javax.xml.transform.stream.StreamSource;
043    import java.io.IOException;
044    import java.io.InputStream;
045    import java.net.MalformedURLException;
046    import java.net.URL;
047    
048    class SchemaReaderImpl implements SchemaReader {
049      static final String SCHEMATRON_URI = "http://www.ascc.net/xml/schematron";
050      private static final String LOCATION_URI = "http://www.thaiopensource.com/ns/location";
051      private static final String ERROR_URI = "http://www.thaiopensource.com/ns/error";
052      private final Localizer localizer = new Localizer(SchemaReaderImpl.class);
053    
054      private final Class transformerFactoryClass;
055      private final Templates schematron;
056      private final Schema schematronSchema;
057      private static final String SCHEMATRON_SCHEMA = "schematron.rnc";
058      private static final String SCHEMATRON_STYLESHEET = "schematron.xsl";
059      private static final PropertyId[] supportedPropertyIds = {
060        ValidateProperty.ERROR_HANDLER,
061        ValidateProperty.XML_READER_CREATOR,
062        SchematronProperty.DIAGNOSE,
063        SchematronProperty.PHASE,
064      };
065    
066      SchemaReaderImpl(TransformerFactory transformerFactory) throws TransformerConfigurationException, IncorrectSchemaException {
067        this.transformerFactoryClass = transformerFactory.getClass();
068        String resourceName = fullResourceName(SCHEMATRON_STYLESHEET);
069        StreamSource source = new StreamSource(getResourceAsStream(resourceName));
070        initTransformerFactory(transformerFactory);
071        schematron = transformerFactory.newTemplates(source);
072        InputSource schemaSource = new InputSource(getResourceAsStream(fullResourceName(SCHEMATRON_SCHEMA)));
073        PropertyMapBuilder builder = new PropertyMapBuilder();
074        ValidateProperty.ERROR_HANDLER.put(builder, new DraconianErrorHandler());
075        RngProperty.CHECK_ID_IDREF.add(builder);
076        try {
077          schematronSchema = CompactSchemaReader.getInstance().createSchema(schemaSource, builder.toPropertyMap());
078        }
079        catch (SAXException e) {
080          throw new IncorrectSchemaException();
081        }
082        catch (IOException e) {
083          throw new IncorrectSchemaException();
084        }
085      }
086    
087      public Option getOption(String uri) {
088        return SchematronProperty.getOption(uri);
089      }
090    
091      private void initTransformerFactory(TransformerFactory factory) {
092        String name = factory.getClass().getName();
093        try {
094          if (name.equals("com.icl.saxon.TransformerFactoryImpl"))
095            factory.setAttribute("http://icl.com/saxon/feature/linenumbering",
096                                 Boolean.TRUE);
097          else if (name.equals("org.apache.xalan.processor.TransformerFactoryImpl")) {
098            // Try both the documented URI and the URI that the code expects.
099            try {
100              // This is the URI that the code expects.
101              factory.setAttribute("http://xml.apache.org/xalan/properties/source-location",
102                                   Boolean.TRUE);
103            }
104            catch (IllegalArgumentException e) {
105              // This is the URI that's documented.
106              factory.setAttribute("http://apache.org/xalan/features/source_location",
107                                   Boolean.TRUE);
108            }
109          }
110        }
111        catch (IllegalArgumentException e) {
112        }
113      }
114    
115      static class ValidateStage extends XMLReaderImpl {
116        private final ContentHandler validator;
117        private ContentHandler contentHandler;
118        private final XMLReader reader;
119        private final CountingErrorHandler ceh;
120    
121        ValidateStage(XMLReader reader, Validator validator, CountingErrorHandler ceh) {
122          this.reader = reader;
123          this.validator = validator.getContentHandler();
124          this.ceh = ceh;
125        }
126    
127        public void parse(InputSource input)
128                throws SAXException, IOException {
129          reader.parse(input);
130          if (ceh.getHadErrorOrFatalError())
131            throw new SAXException(new IncorrectSchemaException());
132        }
133    
134        public void setContentHandler(ContentHandler handler) {
135          this.contentHandler = handler;
136          reader.setContentHandler(new ForkContentHandler(validator, contentHandler));
137        }
138    
139        public ContentHandler getContentHandler() {
140          return contentHandler;
141        }
142      }
143    
144      static class UserException extends Exception {
145        private final SAXException exception;
146    
147        UserException(SAXException exception) {
148          this.exception = exception;
149        }
150    
151        SAXException getException() {
152          return exception;
153        }
154      }
155    
156      static class UserWrapErrorHandler extends CountingErrorHandler {
157        UserWrapErrorHandler(ErrorHandler errorHandler) {
158          super(errorHandler);
159        }
160    
161        public void warning(SAXParseException exception)
162                throws SAXException {
163          try {
164            super.warning(exception);
165          }
166          catch (SAXException e) {
167            throw new SAXException(new UserException(e));
168          }
169        }
170    
171        public void error(SAXParseException exception)
172                throws SAXException {
173          try {
174            super.error(exception);
175          }
176          catch (SAXException e) {
177            throw new SAXException(new UserException(e));
178          }
179        }
180    
181        public void fatalError(SAXParseException exception)
182                throws SAXException {
183          try {
184            super.fatalError(exception);
185          }
186          catch (SAXException e) {
187            throw new SAXException(new UserException(e));
188          }
189        }
190      }
191    
192      static class ErrorFilter extends DelegatingContentHandler {
193        private final ErrorHandler eh;
194        private final Localizer localizer;
195        private Locator locator;
196    
197        ErrorFilter(ContentHandler delegate, ErrorHandler eh, Localizer localizer) {
198          super(delegate);
199          this.eh = eh;
200          this.localizer = localizer;
201        }
202    
203        public void setDocumentLocator(Locator locator) {
204          this.locator = locator;
205          super.setDocumentLocator(locator);
206        }
207    
208        public void startElement(String namespaceURI, String localName,
209                                 String qName, Attributes atts)
210                throws SAXException {
211          if (namespaceURI.equals(ERROR_URI) && localName.equals("error"))
212            eh.error(new SAXParseException(localizer.message(atts.getValue("", "message"),
213                                                             atts.getValue("", "arg")),
214                                           locator));
215          super.startElement(namespaceURI, localName, qName, atts);
216        }
217      }
218    
219      static class LocationFilter extends DelegatingContentHandler implements Locator {
220        private final String systemId;
221        private int lineNumber = -1;
222        private SAXException exception = null;
223    
224        LocationFilter(ContentHandler delegate, String systemId) {
225          super(delegate);
226          this.systemId = systemId;
227        }
228    
229        SAXException getException() {
230          return exception;
231        }
232    
233        public void setDocumentLocator(Locator locator) {
234        }
235    
236        public void startDocument()
237                throws SAXException {
238          getDelegate().setDocumentLocator(this);
239          super.startDocument();
240        }
241    
242        public void startElement(String namespaceURI, String localName,
243                                 String qName, Attributes atts)
244                throws SAXException {
245          String value = atts.getValue(LOCATION_URI, "line-number");
246          if (value != null) {
247            try {
248              lineNumber = Integer.parseInt(value);
249            }
250            catch (NumberFormatException e) {
251              lineNumber = -1;
252            }
253          }
254          else
255            lineNumber = -1;
256          try {
257            super.startElement(namespaceURI, localName, qName, atts);
258          }
259          catch (SAXException e) {
260            this.exception = e;
261            setDelegate(null);
262          }
263          lineNumber = -1;
264        }
265    
266        public String getPublicId() {
267          return null;
268        }
269    
270        public String getSystemId() {
271          return systemId;
272        }
273    
274        public int getLineNumber() {
275          return lineNumber;
276        }
277    
278        public int getColumnNumber() {
279          return -1;
280        }
281      }
282    
283      static class TransformStage extends XMLReaderImpl {
284        private ContentHandler contentHandler;
285        private final Transformer transformer;
286        private final SAXSource transformSource;
287        private final String systemId;
288        private final CountingErrorHandler ceh;
289        private final Localizer localizer;
290    
291        TransformStage(Transformer transformer, SAXSource transformSource, String systemId,
292                       CountingErrorHandler ceh, Localizer localizer) {
293          this.transformer = transformer;
294          this.transformSource = transformSource;
295          this.systemId = systemId;
296          this.ceh = ceh;
297          this.localizer = localizer;
298        }
299    
300        public void parse(InputSource input)
301                throws IOException, SAXException {
302          try {
303            LocationFilter handler = new LocationFilter(new ErrorFilter(contentHandler, ceh, localizer),
304                                                                        systemId);
305            transformer.transform(transformSource, new SAXResult(handler));
306            SAXException exception = handler.getException();
307            if (exception != null)
308              throw exception;
309          }
310          catch (TransformerException e) {
311            if (e.getException() instanceof IOException)
312              throw (IOException)e.getException();
313            throw ValidatorImpl.toSAXException(e);
314          }
315          if (ceh.getHadErrorOrFatalError())
316            throw new SAXException(new IncorrectSchemaException());
317        }
318    
319        public ContentHandler getContentHandler() {
320          return contentHandler;
321        }
322    
323        public void setContentHandler(ContentHandler contentHandler) {
324          this.contentHandler = contentHandler;
325        }
326      }
327    
328      static class SAXErrorListener implements ErrorListener {
329         private final ErrorHandler eh;
330         private final String systemId;
331         private boolean hadError = false;
332         SAXErrorListener(ErrorHandler eh, String systemId) {
333           this.eh = eh;
334           this.systemId = systemId;
335         }
336    
337         boolean getHadError() {
338           return hadError;
339         }
340    
341         public void warning(TransformerException exception)
342                 throws TransformerException {
343           SAXParseException spe = transform(exception);
344           try {
345             eh.warning(spe);
346           }
347           catch (SAXException e) {
348             throw new TransformerException(new UserException(e));
349           }
350         }
351    
352         public void error(TransformerException exception)
353                 throws TransformerException {
354           hadError = true;
355           SAXParseException spe = transform(exception);
356           try {
357             eh.error(spe);
358           }
359           catch (SAXException e) {
360             throw new TransformerException(new UserException(e));
361           }
362         }
363    
364         public void fatalError(TransformerException exception)
365                 throws TransformerException {
366           hadError = true;
367           SAXParseException spe = transform(exception);
368           try {
369             eh.fatalError(spe);
370           }
371           catch (SAXException e) {
372             throw new TransformerException(new UserException(e));
373           }
374         }
375    
376         SAXParseException transform(TransformerException exception) throws TransformerException {
377           Throwable cause = exception.getException();
378           // Xalan takes it upon itself to catch exceptions and pass them to the ErrorListener.
379           if (cause instanceof RuntimeException)
380             throw (RuntimeException)cause;
381           if (cause instanceof SAXException
382               || cause instanceof IncorrectSchemaException
383               || cause instanceof IOException)
384             throw exception;
385           SourceLocator locator = exception.getLocator();
386           if (locator == null)
387             return new SAXParseException(exception.getMessage(), null);
388           // Xalan sometimes loses the systemId; work around this.
389           String s = locator.getSystemId();
390           if (s == null)
391            s = systemId;
392           return new SAXParseException(exception.getMessage(),
393                                        null,
394                                        s,
395                                        locator.getLineNumber(),
396                                        -1);
397         }
398       }
399      
400      public Schema createSchema(InputSource in, PropertyMap properties)
401              throws IOException, SAXException, IncorrectSchemaException {
402        ErrorHandler eh = ValidateProperty.ERROR_HANDLER.get(properties);
403        SAXErrorListener errorListener = new SAXErrorListener(eh, in.getSystemId());
404        UserWrapErrorHandler ueh1 = new UserWrapErrorHandler(eh);
405        UserWrapErrorHandler ueh2 = new UserWrapErrorHandler(eh);
406        EntityResolver er = ValidateProperty.ENTITY_RESOLVER.get(properties);
407        XMLReaderCreator xrc = ValidateProperty.XML_READER_CREATOR.get(properties);
408        try {
409          PropertyMapBuilder builder = new PropertyMapBuilder(properties);
410          ValidateProperty.ERROR_HANDLER.put(builder, ueh1);
411          SAXSource source = createValidatingSource(in, builder.toPropertyMap(), ueh1);
412          source = createTransformingSource(source,
413                                            SchematronProperty.PHASE.get(properties),
414                                            properties.contains(SchematronProperty.DIAGNOSE),
415                                            in.getSystemId(),
416                                            ueh2);
417          TransformerFactory transformerFactory = (TransformerFactory)transformerFactoryClass.newInstance();
418          initTransformerFactory(transformerFactory);
419          transformerFactory.setErrorListener(errorListener);
420          if (er != null) {
421            if (xrc == null) {
422              xrc = new Jaxp11XMLReaderCreator();
423            }
424            transformerFactory.setURIResolver(new SAXURIResolver(xrc, er));  
425          }
426          Templates templates = transformerFactory.newTemplates(source);
427          return new SchemaImpl(templates, properties, supportedPropertyIds);
428        }
429        catch (TransformerConfigurationException e) {
430          throw toSAXException(e, errorListener.getHadError()
431                                  || ueh1.getHadErrorOrFatalError()
432                                  || ueh2.getHadErrorOrFatalError());
433        }
434        catch (InstantiationException e) {
435          throw new SAXException(e);
436        }
437        catch (IllegalAccessException e) {
438          throw new SAXException(e);
439        }
440      }
441    
442      private SAXSource createValidatingSource(InputSource in, PropertyMap properties, CountingErrorHandler ceh) throws SAXException {
443        Validator validator = schematronSchema.createValidator(properties);
444        XMLReaderCreator xrc = ValidateProperty.XML_READER_CREATOR.get(properties);
445        XMLReader xr = xrc.createXMLReader();
446        xr.setErrorHandler(ceh);
447        return new SAXSource(new ValidateStage(xr, validator, ceh), in);
448      }
449    
450      private SAXSource createTransformingSource(SAXSource in, String phase, boolean diagnose,
451                                                 String systemId, CountingErrorHandler ceh) throws SAXException {
452        try {
453          Transformer transformer = schematron.newTransformer();
454          transformer.setErrorListener(new DraconianErrorListener());
455          if (phase != null)
456            transformer.setParameter("phase", phase);
457          if (diagnose)
458            transformer.setParameter("diagnose", Boolean.TRUE);
459          return new SAXSource(new TransformStage(transformer, in, systemId, ceh, localizer),
460                               new InputSource(systemId));
461        }
462        catch (TransformerConfigurationException e) {
463          throw new SAXException(e);
464        }
465      }
466    
467      private SAXException toSAXException(TransformerException e, boolean hadError) throws IOException, IncorrectSchemaException {
468          return causeToSAXException(e.getException(), hadError);
469        }
470    
471      private SAXException causeToSAXException(Throwable cause, boolean hadError) throws IOException, IncorrectSchemaException {
472          if (cause instanceof RuntimeException)
473            throw (RuntimeException)cause;
474          if (cause instanceof IOException)
475            throw (IOException)cause;
476          if (cause instanceof IncorrectSchemaException)
477            throw (IncorrectSchemaException)cause;
478          if (cause instanceof SAXException)
479            return causeToSAXException(((SAXException)cause).getException(), hadError);
480          if (cause instanceof TransformerException)
481            return toSAXException((TransformerException)cause, hadError);
482          if (cause instanceof UserException)
483            return toSAXException((UserException)cause);
484          if (hadError)
485            throw new IncorrectSchemaException();
486          return new SAXException(localizer.message("unexpected_schema_creation_error"),
487                                  cause instanceof Exception ? (Exception)cause : null);
488        }
489    
490      private static SAXException toSAXException(UserException e) throws IOException, IncorrectSchemaException {
491          SAXException se = e.getException();
492          Exception cause = se.getException();
493          if (cause instanceof IncorrectSchemaException)
494            throw (IncorrectSchemaException)cause;
495          if (cause instanceof IOException)
496            throw (IOException)cause;
497          return se;
498        }
499    
500      private static String fullResourceName(String name) {
501        String className = SchemaReaderImpl.class.getName();
502        return className.substring(0, className.lastIndexOf('.')).replace('.', '/') + "/resources/" + name;
503      }
504    
505      private static InputStream getResourceAsStream(String resourceName) {
506        ClassLoader cl = SchemaReaderImpl.class.getClassLoader();
507        // XXX see if we should borrow 1.2 code from Service
508        if (cl == null)
509          return ClassLoader.getSystemResourceAsStream(resourceName);
510        else
511          return cl.getResourceAsStream(resourceName);
512      }
513    }