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 }