/** * Copyright (C) 2010 Orbeon, Inc. * * This program is free software; you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation; either version * 2.1 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 Lesser General Public License for more details. * * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html */ package org.orbeon.oxf.xml; import org.apache.commons.lang3.StringUtils; import org.orbeon.oxf.common.ValidationException; import org.orbeon.oxf.xml.dom4j.LocationData; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.helpers.AttributesImpl; import java.util.Stack; /** * Just like ForwardingXMLReceiver (a SAX handler that forwards SAX events to another handler), but * checks the validity of the SAX stream. * * TODO: check for duplicate attributes. */ public class InspectingXMLReceiver extends ForwardingXMLReceiver { private Locator locator; private Stack<NameInfo> elementStack = new Stack<NameInfo>(); private boolean documentStarted = false; private boolean documentEnded = false; private NamespaceContext namespaceContext = new NamespaceContext(); public InspectingXMLReceiver(XMLReceiver xmlReceiver) { super(xmlReceiver); } public void startDocument() throws SAXException { if (documentStarted) throw new ValidationException("startDocument() called twice", new LocationData(locator)); documentStarted = true; super.startDocument(); } public void endDocument() throws SAXException { if (elementStack.size() != 0) throw new ValidationException("Document ended before all the elements are closed", new LocationData(locator)); if (documentEnded) throw new ValidationException("endDocument() called twice", new LocationData(locator)); documentEnded = true; super.endDocument(); } public void startElement(String uri, String localname, String qname, Attributes attributes) throws SAXException { namespaceContext.startElement(); final String error = checkInDocument(); if (error != null) throw new ValidationException(error + ": element " + qname, new LocationData(locator)); elementStack.push(new NameInfo(uri, localname, qname, new AttributesImpl(attributes))); // Check names checkElementName(uri, localname, qname); for (int i = 0; i < attributes.getLength(); i++) checkAttributeName(attributes.getURI(i), attributes.getLocalName(i), attributes.getQName(i)); super.startElement(uri, localname, qname, attributes); } public void endElement(String uri, String localname, String qname) throws SAXException { final String error = checkInElement(); if (error != null) throw new ValidationException(error + ": element " + qname, new LocationData(locator)); final NameInfo startElementNameInfo = elementStack.pop(); final NameInfo endElementNameInfo = new NameInfo(uri, localname, qname, null); if (!startElementNameInfo.compareNames(endElementNameInfo)) throw new ValidationException("endElement() doesn't match startElement(). startElement(): " + startElementNameInfo.toString() + "; endElement(): " + endElementNameInfo.toString(), new LocationData(locator)); // Check name checkElementName(uri, localname, qname); namespaceContext.endElement(); super.endElement(uri, localname, qname); } public void characters(char[] chars, int start, int length) throws SAXException { String error = checkInElement(); if (error != null) throw new ValidationException(error + ": '" + new String(chars, start, length) + "'", new LocationData(locator)); super.characters(chars, start, length); } public void setDocumentLocator(Locator locator) { this.locator = locator; super.setDocumentLocator(locator); } private String checkInElement() { String error = checkInDocument(); if (error != null) { return error; } else if (elementStack.size() == 0) { return "SAX event received after close of root element"; } else { return null; } } private String checkInDocument() { if (!documentStarted) { return "SAX event received before document start"; } else if (documentEnded) { return "SAX event received after document end"; } else { return null; } } private void checkAttributeName(String uri, String localname, String qname) { if (uri != null && qname != null && !"".equals(uri) && qname.indexOf(':') == -1) throw new ValidationException("Non-prefixed attribute cannot be in a namespace. URI: " + uri + "; localname: " + localname + "; QName: " + qname, new LocationData(locator)); checkName(uri, localname, qname); } private void checkElementName(String uri, String localname, String qname) { checkName(uri, localname, qname); } public void startPrefixMapping(String prefix, String uri) throws SAXException { namespaceContext.startPrefixMapping(prefix, uri); super.startPrefixMapping(prefix, uri); } private void checkName(String uri, String localname, String qname) { if (StringUtils.isEmpty(localname)) throw new ValidationException("Empty local name in SAX event. QName: " + qname, new LocationData(locator)); if (StringUtils.isEmpty(qname)) throw new ValidationException("Empty qualified name in SAX event. Localname: " + localname + "; QName: " + qname, new LocationData(locator)); if (uri == null) throw new ValidationException("Null URI. Localname: " + localname, new LocationData(locator)); if (uri.equals("") && !localname.equals(qname)) throw new ValidationException("Localname and QName must be equal when name is in no namespace. Localname: " + localname + "; QName: " + qname, new LocationData(locator)); if (!uri.equals("") && !localname.equals(qname.substring(qname.indexOf(':') + 1))) throw new ValidationException("Local part or QName must be equal to localname when name is in namespace. Localname: " + localname + "; QName: " + qname, new LocationData(locator)); final int colonIndex = qname.indexOf(':'); // Check namespace mappings if (!uri.equals("")) { // We are in a namespace if (colonIndex == -1) { // QName is not prefixed, check that we match the default namespace if (!uri.equals(namespaceContext.getURI(""))) throw new ValidationException("Namespace doesn't match default namespace. Namespace: " + uri + "; QName: " + qname, new LocationData(locator)); } else if (colonIndex == 0 || colonIndex == (qname.length() - 1)) { // Invalid position of colon in QName throw new ValidationException("Invalid position of colon in QName: " + qname, new LocationData(locator)); } else { // Name is prefixed: check that prefix is bound and maps to namespace final String prefix = qname.substring(0, colonIndex); if (namespaceContext.getURI(prefix) == null) throw new ValidationException("QName prefix is not in scope: " + qname, new LocationData(locator)); if (!uri.equals(namespaceContext.getURI(prefix))) throw new ValidationException("QName prefix maps to URI: " + namespaceContext.getURI(prefix) + "; but namespace provided is: " + uri, new LocationData(locator)); } } else { // We are not in a namespace if (colonIndex != -1) throw new ValidationException("QName has prefix but we are not in a namespace: " + qname, new LocationData(locator)); } } private static class NameInfo { private String uri; private String localname; private String qname; private AttributesImpl attributes; public NameInfo(String uri, String localname, String qname, AttributesImpl attributes) { this.uri = uri; this.localname = localname; this.qname = qname; this.attributes = attributes; } public String getUri() { return uri; } public String getLocalname() { return localname; } public String getQname() { return qname; } public AttributesImpl getAttributes() { return attributes; } public boolean compareNames(NameInfo other) { if (!uri.equals(other.uri)) return false; if (!localname.equals(other.localname)) return false; if (!qname.equals(other.qname)) return false; return true; } public String toString() { final StringBuilder sb = new StringBuilder(); sb.append('['); sb.append("uri = "); sb.append(uri); sb.append(" | localname = "); sb.append(localname); sb.append(" | qname = "); sb.append(qname); sb.append(']'); return sb.toString(); } } }