// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.config.model.application.provider; import com.thaiopensource.util.PropertyMap; import com.thaiopensource.util.PropertyMapBuilder; import com.thaiopensource.validate.ValidateProperty; import com.thaiopensource.validate.ValidationDriver; import com.thaiopensource.validate.rng.CompactSchemaReader; import com.yahoo.config.application.api.DeployLogger; import com.yahoo.io.IOUtils; import com.yahoo.io.reader.NamedReader; import com.yahoo.yolean.Exceptions; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import java.io.File; import java.io.IOException; import java.io.LineNumberReader; import java.io.Reader; import java.io.StringReader; import java.util.logging.Level; /** * Validates xml files against a schema. * Note: Tested by SchemaValidatorTest in config-model module, since * needed schema files are in that module * * @author Tony Vaagenes */ public class SchemaValidator { private static final int linesOfContextForErrors = 3; private final CustomErrorHandler errorHandler = new CustomErrorHandler(); private final ValidationDriver driver; private final DeployLogger deployLogger; /** * Initializes the validator by using the given file as schema file * * @param schemaFile schema file * @throws IOException if it is not possible to read schema files */ SchemaValidator(File schemaFile, DeployLogger deployLogger) throws IOException, SAXException { this.deployLogger = deployLogger; this.driver = new ValidationDriver(PropertyMap.EMPTY, instanceProperties(), CompactSchemaReader.getInstance()); driver.loadSchema(ValidationDriver.fileInputSource(schemaFile)); } public void validate(File file) throws IOException { validate(file, file.getName()); } public void validate(File file, String fileName) throws IOException { validate(IOUtils.createReader(file.getAbsolutePath()), fileName); } public void validate(Reader reader) throws IOException { validate(reader, null); } public void validate(NamedReader reader) throws IOException { validate(reader, reader.getName()); } @Deprecated /* @deprecated Will not give proper context from errors, use another validate method instead */ public void validate(InputSource inputSource, String fileName) throws IOException { errorHandler.fileName = (fileName == null ? "input" : fileName); errorHandler.reader = inputSource.getCharacterStream(); try { if ( ! driver.validate(inputSource)) { // Shouldn't happen, error handler should have thrown throw new RuntimeException("Aborting due to earlier XML errors."); } } catch (SAXException e) { // Shouldn't happen, error handler should have thrown throw new IllegalArgumentException("XML error in " + errorHandler.fileName + ": " + Exceptions.toMessageString(e)); } } private void validate(Reader reader, String fileName) throws IOException { errorHandler.fileName = (fileName == null ? "input" : fileName); // We need to read from a reader in error handler, so need to read all content into a new one Reader newReader = new StringReader(IOUtils.readAll(reader)); errorHandler.reader = newReader; try { if ( ! driver.validate(new InputSource(newReader))) { // Shouldn't happen, error handler should have thrown throw new RuntimeException("Aborting due to earlier XML errors."); } } catch (SAXException e) { // Shouldn't happen, error handler should have thrown throw new IllegalArgumentException("XML error in " + errorHandler.fileName + ": " + Exceptions.toMessageString(e)); } } private PropertyMap instanceProperties() { PropertyMapBuilder builder = new PropertyMapBuilder(); builder.put(ValidateProperty.ERROR_HANDLER, errorHandler); return builder.toPropertyMap(); } private class CustomErrorHandler implements ErrorHandler { volatile String fileName; volatile Reader reader; public void warning(SAXParseException e) { deployLogger.logApplicationPackage(Level.WARNING, message(e)); } public void error(SAXParseException e) { throw new IllegalArgumentException(message(e)); } public void fatalError(SAXParseException e) { throw new IllegalArgumentException(message(e)); } private String message(SAXParseException e) { return "Invalid XML according to XML schema, error in " + fileName + ": " + Exceptions.toMessageString(e) + " [" + e.getLineNumber() + ":" + e.getColumnNumber() + "]" + ", input:\n" + getErrorContext(e.getLineNumber()); } private String getErrorContext(int lineNumberWithError) { if (!(reader instanceof StringReader)) return ""; int fromLine = Math.max(0, lineNumberWithError - linesOfContextForErrors); int toLine = lineNumberWithError + linesOfContextForErrors; LineNumberReader r = new LineNumberReader(reader); StringBuilder sb = new StringBuilder(); String line; try { reader.reset(); while ((line = r.readLine()) != null) { int lineNumber = r.getLineNumber(); if (lineNumber >= fromLine && lineNumber <= toLine) sb.append(lineNumber).append(":").append(line).append("\n"); } } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } } }