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 }