/* File: DasMLValidator.java * Copyright (C) 2002-2003 The University of Iowa * Created by: Jeremy Faden * Jessica Swanner * Edward E. West * * This file is part of the das2 library. * * das2 is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.das2.dasml; import org.xml.sax.*; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import org.das2.datum.LoggerManager; /** * A validator for the dasML language developed for the University of * Iowa Space Plasma Wave Group. This class is used as a pre-processor * to (hopefully) provide clear and helpful error messages. * * Warning: This class is not thread-safe. Unexpected results can occur * if multiple threads use an instance of this class concurrently. * * @author Edward West */ public class DasMLValidator extends DefaultHandler { private static final Logger logger= LoggerManager.getLogger("das2.dasml"); public static final Pattern INTEGER_PATTERN = Pattern.compile("(0|[1-9][0-9]*)"); public static final Pattern WINDOW_POSITION_PATTERN = Pattern.compile("\\((0|[1-9][0-9]*),(0|[1-9][0-9]*)\\)"); public static final Pattern FLOAT_PATTERN = Pattern.compile("-?[0-9]*(\\.[0-9]*)?([eE]-?[0-9]+)?"); /** * Instance of the SAXParserFactory that this class uses to create * instances of SAXParser. */ private static SAXParserFactory factory; /** * Static initialization block to property initialize factory */ static { factory = SAXParserFactory.newInstance(); factory.setValidating(true); } /** * Instance of SAXParser used by this validator * to parse documents. */ private SAXParser parser; /** * Instance of ErrorHandler that the error events * from the SAXParser are delegated to. This member is * only valid during a call to validate() */ private ErrorHandler errorHandler; /** * The last error encountered by this validator. */ private SAXException lastError; /** * Locator used to locate the position in the * XML document that where certain events have taken place. */ private Locator locator; /** * Mapping of 'name' attributes to 'type' of element (element name) */ private Map typeMap; /** * A list of TypeCheck that are to be processed once the whole * document is loaded. */ private List typeCheckList; /** Creates a new instance of DasMLValidator */ public DasMLValidator() throws ParserConfigurationException, SAXException { parser = factory.newSAXParser(); typeMap = new HashMap(); typeCheckList = new LinkedList(); } /** * Parses and validates a dasML document. All errors are * passed to the ErrorHandler instance specified. SAXExceptions * thrown by the underlying parser are caught and suppressed by * this method. If an application needs access to the errors, * an ErrorHandler must be provided. * * @param source The source of the XML document * @param errorHandler The ErrorHandler instance that will receive * error messages from the parser. This can be null * @return true if the document is a valid dasML document. * @throws java.io.IOException if the there is an error while reading the document. */ public boolean validate(InputSource source, ErrorHandler errorHandler) throws java.io.IOException { this.errorHandler = errorHandler; if (this == errorHandler) throw new IllegalArgumentException("cannot pass an instance of DasMLValidator to its own validate() method"); lastError = null; try { typeMap.clear(); typeCheckList.clear(); parser.parse(source, this); } catch (SAXException se) { //Save a reference to the error and return false lastError = se; } return lastError == null; } /** * Returns the last error encountered by this validator * or null if no error has been found. This method * will only return an error if the last call to * validate(InputSource, ErrorHandler) returned false. * If an application wishes to have access to warnings * and non-fatal errors then an ErrorHandler must be provided. * @return the last error. */ public SAXException getLastError() { return lastError; } /** Report a fatal XML parsing error. * * @param e The error information encoded as an exception. * @exception org.xml.sax.SAXException Any SAX exception, possibly * wrapping another exception. * @see org.xml.sax.ErrorHandler#fatalError * @see org.xml.sax.SAXParseException */ @Override public void fatalError(SAXParseException e) throws SAXException { if (errorHandler != null) { errorHandler.fatalError(e); } throw e; } /** Receive a Locator object for document events. * * @param locator A locator for all SAX document events. * @see org.xml.sax.ContentHandler#setDocumentLocator * @see org.xml.sax.Locator */ @Override public void setDocumentLocator(Locator locator) { this.locator = locator; } /** Receive notification of a recoverable parser error. * * @param e The warning information encoded as an exception. * @exception org.xml.sax.SAXException Any SAX exception, possibly * wrapping another exception. * @see org.xml.sax.ErrorHandler#warning * @see org.xml.sax.SAXParseException */ @Override public void error(SAXParseException e) throws SAXException { if (errorHandler != null) { errorHandler.error(e); } lastError = e; } /** Receive notification of a parser warning. * * @param e The warning information encoded as an exception. * @exception org.xml.sax.SAXException Any SAX exception, possibly * wrapping another exception. * @see org.xml.sax.ErrorHandler#warning * @see org.xml.sax.SAXParseException */ @Override public void warning(SAXParseException e) throws SAXException { if (errorHandler != null) { errorHandler.warning(e); } lastError = e; } /** Receive notification of the beginning of the document. * * @exception org.xml.sax.SAXException Any SAX exception, possibly * wrapping another exception. * @see org.xml.sax.ContentHandler#startDocument */ @Override public void startDocument() throws SAXException { } /** Receive notification of the start of an element. * * @param qName The element type name. * @param attributes The specified or defaulted attributes. * @exception org.xml.sax.SAXException Any SAX exception, possibly * wrapping another exception. * @see org.xml.sax.ContentHandler#startElement */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { String name = attributes.getValue("name"); if (name != null) { if (typeMap.containsKey(name)) errorInternal("An element with the name " + name + " already exists. " + "The values of name attributes must be unique."); typeMap.put(name, qName); } if (qName.equals("window")) checkWindow(attributes); else if (qName.equals("form")) checkForm(attributes); else if (qName.equals("textfield")) checkTextfield(attributes); else if (qName.equals("checkbox")) checkCheckbox(attributes); else if (qName.equals("if")) ;//CHECK EXPRESSION else if (qName.equals("elseif")) ;//CHECK EXPRESSION else if (qName.equals("update")) //KLUDGE - Will be removed once is implemented typeCheckList.add(new TypeCheck("update", "target", "spectrogram", attributes.getValue("target"), locator)); else if (qName.equals("radiobutton")) checkRadiobutton(attributes); else if (qName.equals("panel")) checkPanel(attributes); else if (qName.equals("glue")) checkGlue(attributes); else if (qName.equals("canvas")) checkCanvas(attributes); else if (qName.equals("row")) checkRowColumn("row", attributes); else if (qName.equals("column")) checkRowColumn("column", attributes); else if (qName.equals("spectrogram")) checkSpectrogram(attributes); else if (qName.equals("xAxis")) checkXAxis(attributes); else if (qName.equals("yAxis")) checkYAxis(attributes); else if (qName.equals("zAxis")) checkZAxis(attributes); else if (qName.equals("axis")) checkAxis(attributes); else if (qName.equals("timeaxis")) checkTimeaxis(attributes); else if (qName.equals("attachedaxis")) checkAttachedaxis(attributes); else if (qName.equals("colorbar")) checkColorbar(attributes); } /** * Checks the attribute values for a colorbar element */ private void checkColorbar(Attributes attributes) throws SAXException { String minimum = attributes.getValue("minimum"); if (!FLOAT_PATTERN.matcher(minimum).matches() || minimum.charAt(0)=='-') errorInternal("The minimum attribute of a colorbar element must be a positive number"); String maximum = attributes.getValue("maximum"); if (!FLOAT_PATTERN.matcher(maximum).matches() || maximum.charAt(0)=='-') errorInternal("The maximum attribute of a colorbar element must be a positive number"); String row = attributes.getValue("row"); if (row != null) { typeCheckList.add(new TypeCheck("colorbar", "row", "row", row, locator)); } else if (!insideSpectrogram) { errorInternal("The \"row\" attribute of a \"colorbar\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String column = attributes.getValue("column"); if (column != null) { typeCheckList.add(new TypeCheck("colorbar", "column", "column", column, locator)); } else if (!insideSpectrogram) { errorInternal("The \"column\" attribute of a \"colorbar\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String log = attributes.getValue("log"); if (!(log.equals("true") || log.equals("false"))) errorInternal("The log attribute of a colorbar must be either 'true' or 'false'"); } private boolean hasXAxis = false; private boolean hasYAxis = false; private boolean hasZAxis = false; private boolean insideSpectrogram = false; /** * Checks the attribute values for a spectrogram element */ private void checkSpectrogram(Attributes attributes) throws SAXParseException { String row = attributes.getValue("row"); typeCheckList.add(new TypeCheck("spectrogram", "row", "row", row, locator)); String column = attributes.getValue("column"); typeCheckList.add(new TypeCheck("spectrogram", "column", "column", column, locator)); String xAxis = attributes.getValue("xAxis"); if (xAxis != null) { typeCheckList.add(new TypeCheck("spectrogram", "xAxis", "axis|timeaxis|attachedaxis", xAxis, locator)); hasXAxis = true; } String yAxis = attributes.getValue("yAxis"); if (yAxis != null) { typeCheckList.add(new TypeCheck("spectrogram", "yAxis", "axis|timeaxis|attachedaxis", yAxis, locator)); hasYAxis = true; } String colorbar = attributes.getValue("colorbar"); if (colorbar != null) { typeCheckList.add(new TypeCheck("spectrogram", "colorbar", "colorbar", colorbar, locator)); hasZAxis = true; } insideSpectrogram = true; } private void endCheckSpectrogram() throws SAXException { if (!hasXAxis) { errorInternal("No xAxis specified. Spectrograms require an xAxis to be specified"); } if (!hasYAxis) { errorInternal("No yAxis specified. Spectrograms required a yAxis to be specified"); } if (!hasZAxis) { errorInternal("No zAxis specified. Spectrograms required a zAxis to be specified"); } hasXAxis = false; hasYAxis = false; hasZAxis = false; insideSpectrogram = false; } private void checkXAxis(Attributes attributes) throws SAXException { hasXAxis = true; } private void checkYAxis(Attributes attributes) throws SAXException { hasYAxis = true; } private void checkZAxis(Attributes attributes) throws SAXException { hasZAxis = true; } /** * Checks the attribute values for a attachedaxis element */ private void checkAttachedaxis(Attributes attributes) throws SAXException { String ref = attributes.getValue("ref"); typeCheckList.add(new TypeCheck("attachedaxis", "ref", "axis|timeaxis", ref, locator)); String row = attributes.getValue("row"); if (row != null) { typeCheckList.add(new TypeCheck("attachedaxis", "row", "row", row, locator)); } else if (!insideSpectrogram) { errorInternal("The \"row\" attribute of an \"attachedaxis\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String column = attributes.getValue("column"); if (column != null) { typeCheckList.add(new TypeCheck("attachedaxis", "column", "column", column, locator)); } else if (!insideSpectrogram) { errorInternal("The \"column\" attribute of an \"attachedaxis\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String orientation = attributes.getValue("orientation"); if (!(orientation.equals("horizontal") || orientation.equals("vertical"))) errorInternal("The orientation attibute of an attachedaxis element must be either 'horizontal' or 'vertical'"); } /** * Checks the attribute values for a timeaxis element */ private void checkTimeaxis(Attributes attributes) throws SAXException { String showTca = attributes.getValue("showTca"); if (!(showTca.equals("true") || showTca.equals("false"))) errorInternal("The showTca attribute of a timeaxis element must be either 'true' or 'false'"); String row = attributes.getValue("row"); if (row != null) { typeCheckList.add(new TypeCheck("timeaxis", "row", "row", row, locator)); } else if (!insideSpectrogram) { errorInternal("The \"row\" attribute of a \"timeaxis\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String column = attributes.getValue("column"); if (column != null) { typeCheckList.add(new TypeCheck("timeaxis", "column", "column", column, locator)); } else if (!insideSpectrogram) { errorInternal("The \"column\" attribute of a \"timeaxis\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String orientation = attributes.getValue("orientation"); if (!(orientation.equals("horizontal") || orientation.equals("vertical"))) errorInternal("The orientation attibute of an axis element must be either 'horizontal' or 'vertical'"); if (showTca.equals("true") && orientation.equals("vertical")) errorInternal("Vertical axes cannot diplay time correlated annotations"); } /** * Checks the attribute values for an axis element */ private void checkAxis(Attributes attributes) throws SAXException { String log = attributes.getValue("log"); if (!(log.equals("true") || log.equals("false"))) errorInternal("The log attribute of an axis element must be either 'true' or 'false'"); String dataMinimum = attributes.getValue("dataMinimum"); if (!FLOAT_PATTERN.matcher(dataMinimum).matches()) errorInternal("The dataMinimum attribute of an axis element must be a valid number'"); String dataMaximum = attributes.getValue("dataMaximum"); if (!FLOAT_PATTERN.matcher(dataMinimum).matches()) errorInternal("The dataMaximum attribute of an axis element must be a valid number'"); String row = attributes.getValue("row"); if (row != null) { typeCheckList.add(new TypeCheck("axis", "row", "row", row, locator)); } else if (!insideSpectrogram) { errorInternal("The \"row\" attribute of an \"axis\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String column = attributes.getValue("column"); if (column != null) { typeCheckList.add(new TypeCheck("axis", "column", "column", column, locator)); } else if (!insideSpectrogram) { errorInternal("The \"column\" attribute of an \"axis\" element must be specified" + " if the element is not nested in a \"spectrogram\" element"); } String orientation = attributes.getValue("orientation"); if (!(orientation.equals("horizontal") || orientation.equals("vertical"))) errorInternal("The orientation attibute of an axis element must be either 'horizontal' or 'vertical'"); } /** * Checks the attribute values for a row or column element */ private void checkRowColumn(String tagName, Attributes attributes) throws SAXException { String minimum = attributes.getValue("minimum"); if (!FLOAT_PATTERN.matcher(minimum).matches() || minimum.charAt(0)=='-') errorInternal("The minimum attribute of a " + tagName + " element must be a positive number"); String maximum = attributes.getValue("maximum"); if (!FLOAT_PATTERN.matcher(maximum).matches() || maximum.charAt(0)=='-') errorInternal("The maximum attribute of a " + tagName + " element must be a positive number"); } /** * Checks the attribute values for a canvas element */ private void checkCanvas(Attributes attributes) throws SAXException { String width = attributes.getValue("width"); if (!INTEGER_PATTERN.matcher(width).matches()) errorInternal("The width attribute of a canvas element must be a positive integer"); String height = attributes.getValue("height"); if (!INTEGER_PATTERN.matcher(height).matches()) errorInternal("The height attribute of a canvas element must be a positive integer"); } /** * Checks the attribute values for a glue element */ private void checkGlue(Attributes attributes) throws SAXException { String direction = attributes.getValue("direction"); if (!(direction.equals("horizontal") || direction.equals("vertical"))) errorInternal("The direction attribute of a glue element must be either 'horizontal' or 'vertical'"); } /** * Checks the attribute values for a panel element */ private void checkPanel(Attributes attributes) throws SAXException { String direction = attributes.getValue("direction"); if (!(direction.equals("horizontal") || direction.equals("vertical"))) errorInternal("The direction attribute of a panel element must be either 'horizontal' or 'vertical'"); String border = attributes.getValue("border"); if (!(border.equals("true") || border.equals("false"))) errorInternal("The border attribute of a panel element must be either 'true' or 'false'"); } /** * Checks the attribute values for a checkbox element */ private void checkRadiobutton(Attributes attributes) throws SAXException { String group = attributes.getValue("group"); typeCheckList.add(new TypeCheck("radiobutton", "group", "buttongroup", group, locator)); String selected = attributes.getValue("selected"); if (!(selected.equals("true") || selected.equals("false"))) errorInternal("The selected attribute of a radiobutton element must be either 'true' or 'false'"); } /** * Checks the attribute values for a checkbox element */ private void checkCheckbox(Attributes attributes) throws SAXException { String selected = attributes.getValue("selected"); if (!(selected.equals("true") || selected.equals("false"))) errorInternal("The selected attribute of a checkbox element must be either 'true' or 'false'"); } /** * Checks the attribute values for a textfield element */ private void checkTextfield(Attributes attributes) throws SAXException { String length = attributes.getValue("length"); if (!INTEGER_PATTERN.matcher(length).matches()) errorInternal("The length attribute of textfield elements must be a positive integer"); } /** * Checks the attribute values for a form element */ private void checkForm(Attributes attributes) throws SAXException { String alignment = attributes.getValue("alignment"); if (!(alignment.equals("left") || alignment.equals("center") || alignment.equals("right"))) errorInternal("The alignment attribute of a form element must be 'left', 'center', or 'right'"); } /** * Checks the attribute values for a window element */ private void checkWindow(Attributes attributes) throws SAXException { String width = attributes.getValue("width"); if (!INTEGER_PATTERN.matcher(width).matches()) errorInternal("The width attribute of a window element must be a positve integer."); String height = attributes.getValue("height"); if (!INTEGER_PATTERN.matcher(height).matches()) errorInternal("The height attribute of a window element must be a positive integer."); String location = attributes.getValue("location"); if (!WINDOW_POSITION_PATTERN.matcher(location).matches()) errorInternal("The location attribute of a window element must be a pair of the form (x,y)"); String visible = attributes.getValue("visible"); if (!visible.equals("true") && !visible.equals("false")) errorInternal("The visible attribute of a window element must be either 'true' or 'false'"); } /** Receive notification of the end of the document. * * @exception org.xml.sax.SAXException Any SAX exception, possibly * wrapping another exception. * @see org.xml.sax.ContentHandler#endDocument */ public void endDocument() throws SAXException { Iterator iterator = typeCheckList.iterator(); SAXParseException exception = null; while (iterator.hasNext()) { TypeCheck check = (TypeCheck)iterator.next(); String type = (String)typeMap.get(check.value); if (type == null) { exception = new SAXParseException("No element of type \"" + check.type + "\" with attribute " + "name=\"" + check.value + "\" exists.", check); error(exception); } if (!Pattern.matches(check.type, type)) { exception = new SAXParseException("Element '" + check.elementName + "', " + "attribute '" + check.attributeName + "' : " + type + " expected, but found " + type + " (" + check.value + ")", check); error(exception); } } if (exception != null) throw exception; } /** * This class encapsulates the information necessary * to check that the value of an attribute references * an element of the property type. */ private static class TypeCheck implements Locator { public String elementName; public String attributeName; public String type; public String value; private int lineNumber; private int columnNumber; private String publicId; private String systemId; public TypeCheck(String elementName, String attributeName, String type, String value, Locator locator) { this.elementName = elementName; this.attributeName = attributeName; this.type = type; this.value = value; this.lineNumber = locator.getLineNumber(); this.columnNumber = locator.getColumnNumber(); this.publicId = locator.getPublicId(); this.systemId = locator.getSystemId(); } public int getColumnNumber() { return columnNumber; } public int getLineNumber() { return lineNumber; } public String getPublicId() { return publicId; } public String getSystemId() { return systemId; } } private void errorInternal(String message) throws SAXException { lastError = new SAXParseException(message, locator); error((SAXParseException)lastError); } public static void main(String[] args) { if (args.length < 1) { System.err.println("usage: java org.das2.dasml.DasMLValidator "); return; } ErrorHandler errorHandler = new ErrorHandler() { public void warning(SAXParseException e) throws SAXException { logger.log(Level.WARNING,"Line " + e.getLineNumber() + ", " + e.getMessage(),e); } public void error(SAXParseException e) throws SAXException { logger.log(Level.WARNING,"Line " + e.getLineNumber() + ", " + e.getMessage(),e); } public void fatalError(SAXParseException e) throws SAXException { logger.log(Level.WARNING,"Line " + e.getLineNumber() + ", " + e.getMessage(),e); } }; try { String path = new java.io.File(args[0]).getCanonicalPath(); DasMLValidator validator = new DasMLValidator(); if (validator.validate(new InputSource("file://" + path), errorHandler)) { logger.fine("No errors"); } } catch (ParserConfigurationException e) { logger.log(Level.WARNING, e.getMessage(),e); } catch (SAXException e) { logger.log(Level.WARNING, e.getMessage(),e); } catch (java.io.IOException e) { logger.log(Level.WARNING, e.getMessage(),e); } } public void endElement(String uri, String localName, String qName) throws SAXException { if (qName.equals("spectrogram")) { endCheckSpectrogram(); } } }