/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source 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. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.relaxng; import com.caucho.relaxng.program.EmptyItem; import com.caucho.relaxng.program.Item; import com.caucho.util.CharBuffer; import com.caucho.util.L10N; import com.caucho.util.LruCache; import com.caucho.vfs.Path; import com.caucho.vfs.ReadStream; import com.caucho.vfs.Vfs; import com.caucho.xml.QName; import org.xml.sax.Attributes; import org.xml.sax.ErrorHandler; import org.xml.sax.Locator; import org.xml.sax.SAXException; import org.xml.sax.SAXParseException; import org.xml.sax.helpers.DefaultHandler; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.logging.Level; import java.util.logging.Logger; /** * JARV verifier implementation */ public class VerifierHandlerImpl extends DefaultHandler implements VerifierHandler { private static final L10N L = new L10N(VerifierHandlerImpl.class); protected static final Logger log = Logger.getLogger(VerifierHandlerImpl.class.getName()); // very verbose logging private static final boolean _isDebug = false; private SchemaImpl _schema; private VerifierImpl _verifier; private boolean _hasProgram; private boolean _isValid = true; private LruCache<Object,Item> _programCache; private QName _name; private ArrayList<QName> _nameStack = new ArrayList<QName>(); private String _eltLocation; private ArrayList<String> _eltLocationStack = new ArrayList<String>(); private Item _item; private ArrayList<Item> _itemStack = new ArrayList<Item>(); private Locator _locator; private boolean _isLogFinest; private CharBuffer _text = new CharBuffer(); private boolean _hasText; private StartKey _startKey = new StartKey(); private EndElementKey _endElementKey = new EndElementKey(); /** * Creates the Verifier Handler. */ public VerifierHandlerImpl(SchemaImpl schema, VerifierImpl verifier) { _schema = schema; _programCache = _schema.getProgramCache(); _verifier = verifier; } /** * Creates the Verifier Handler. */ public VerifierHandlerImpl(SchemaImpl schema) { this(schema, (VerifierImpl) schema.newVerifier()); } /** * Sets the locator. */ public void setDocumentLocator(Locator locator) { _locator = locator; } /** * Sets the error handler */ public void setErrorHandler(ErrorHandler errorHandler) throws SAXException { _verifier.setErrorHandler(errorHandler); } private String getFileName() { if (_locator != null) return _locator.getSystemId(); else return null; } private int getLine() { if (_locator != null) return _locator.getLineNumber(); else return -1; } /** * Called when the document starts. */ public void startDocument() throws SAXException { try { _nameStack.clear(); _itemStack.clear(); _eltLocationStack.clear(); _name = new QName("#top", ""); _item = _schema.getStartItem(); _itemStack.add(_item); _eltLocation = getLocation(); _isLogFinest = _isDebug && log.isLoggable(Level.FINEST); _hasText = false; _text.clear(); } catch (Exception e) { error(e); } } /** * Called when an element starts. */ public void startElement(String uri, String localName, String qName, Attributes attrs) throws SAXException { if (! _isValid) return; if (_hasText) sendText(); if (_isLogFinest) log.finest("element start: " + qName); try { QName parent = _name; _nameStack.add(parent); String parentLocation = _eltLocation; _eltLocationStack.add(parentLocation); QName name = new QName(qName, uri); _name = name; _eltLocation = getLocation(); Item newItem = getStartElement(_item, name); if (newItem == null) { Item parentItem = _itemStack.get(_itemStack.size() - 1); if (parent.getName().equals("#top")) throw new RelaxException(L.l("<{0}> is an unexpected top-level tag.{1}", errorNodeName(name, _item, parentItem), errorMessageDetail(_item, parentItem, null, name))); else throw new RelaxException(L.l("<{0}> is an unexpected tag (parent <{1}> starts at {2}).{3}", errorNodeName(name, _item, parentItem), parent.getName(), parentLocation, errorMessageDetail(_item, parentItem, parent.getName(), name))); } _item = newItem; _itemStack.add(newItem); Item parentItem = newItem; int len = attrs.getLength(); for (int i = 0; i < len; i++) { String attrUri = attrs.getURI(i); String attrQName = attrs.getQName(i); String value = attrs.getValue(i); if (_isLogFinest) log.finest("attribute: " + attrQName + "=\"" + value + "\""); name = new QName(attrQName, attrUri); if (attrQName.startsWith("xml:")) { } else if (! _item.allowAttribute(name, value)) { throw new RelaxException(L.l("{0}=\"{1}\" is an unexpected attribute in <{2}>.{3}", attrQName, value, qName, attributeMessageDetail(_item, parentItem, qName, null))); } else _item = _item.setAttribute(name, value); if (_item == null) _item = EmptyItem.create(); } newItem = _item.attributeEnd(); if (newItem == null) { throw new RelaxException(L.l("<{0}> expects more attributes.{1}", qName, attributeMessageDetail(_item, parentItem, qName, null))); } _item = newItem; } catch (Exception e) { error(e); } } private Item getStartElement(Item item, QName name) throws RelaxException { _startKey.init(item, name); Item newItem = null;//_programCache.get(_startKey); if (newItem != null) { return newItem; } newItem = _item.startElement(name); /* if (newItem != null) _programCache.put(new StartKey(item, name), newItem); */ return newItem; } public void characters(char ch[], int start, int length) throws SAXException { _hasText = true; _text.append(ch, start, length); } public void sendText() throws SAXException { if (! _hasText) return; _hasText = false; try { Item newItem = _item.text(_text); if (newItem == null) { String string = _text.toString(); Item parentItem = _itemStack.get(_itemStack.size() - 1); throw new RelaxException(L.l("The following text is not allowed in this context.\n{0}\n{1}", string, errorMessageDetail(_item, parentItem, _name.getName(), null))); } _text.clear(); _item = newItem; } catch (Exception e) { _text.clear(); error(e); } } /** * Called when an element ends. */ public void endElement(String uri, String localName, String qName) throws SAXException { if (_hasText) sendText(); if (! _isValid) return; if (_isLogFinest) log.finest("element end: " + qName); QName name = _name; QName parent = _nameStack.remove(_nameStack.size() - 1); _name = parent; Item parentItem = _itemStack.remove(_itemStack.size() - 1); String eltOpen = _eltLocation; _eltLocation = _eltLocationStack.remove(_eltLocationStack.size() - 1); try { Item nextItem = getEndElement(_item); if (nextItem == null) throw new RelaxException(L.l("<{0}> closed while expecting more elements (open at {1}).{2}", qName, eltOpen, requiredMessageDetail(_item, parentItem, qName, null))); _item = nextItem; } catch (Exception e) { error(e); } } private Item getEndElement(Item item) throws RelaxException { _endElementKey.init(item); Item newItem = null;//_programCache.get(_endElementKey); if (newItem != null) { return newItem; } newItem = _item.endElement(); /* if (newItem != null) _programCache.put(new EndElementKey(item), newItem); */ return newItem; } /** * Called for errors. */ private void error(SAXException e) throws SAXException { _isValid = false; _verifier.error(new SAXParseException(e.getMessage(), _locator)); } /** * Called for errors. */ private void error(Exception e) throws SAXException { if (e instanceof RuntimeException) throw (RuntimeException) e; else if (e instanceof SAXException) error((SAXException) e); else error(new SAXException(e.getMessage(), e)); } /** * Returns a string containing the allowed values. */ private String errorNodeName(QName name, Item item, Item parentItem) { Item currentItem = item; if (currentItem == null) currentItem = parentItem; if (currentItem == null) return name.toString(); HashSet<QName> values = new LinkedHashSet<QName>(); currentItem.firstSet(values); for (QName value : values) { if (! name.getLocalName().equals(value.getLocalName())) { } else if (name.getPrefix() == null || name.getPrefix().equals("")) { return name.getName() + " xmlns=\"" + name.getNamespaceURI() + "\""; } else { return name.getName() + " xmlns:" + name.getPrefix() + "=\"" + name.getNamespaceURI() + "\""; } } return name.getName(); } /** * Returns a string containing the allowed values. */ private String errorMessageDetail(Item item, Item parentItem, String parentName, QName qName) { Item currentItem = item; if (currentItem == null) currentItem = parentItem; HashSet<QName> values = new LinkedHashSet<QName>(); currentItem.firstSet(values); String expected = null; if (values.size() <= 5) expected = namesToString(values, parentName, qName, currentItem.allowEmpty()); return (getLineContext(getFileName(), getLine()) + syntaxMessage(item, parentItem, parentName, qName, expected)); } /** * Returns a string containing the allowed values. */ private String requiredMessageDetail(Item item, Item parentItem, String parentName, QName qName) { Item currentItem = item; if (currentItem == null) currentItem = parentItem; HashSet<QName> values = new LinkedHashSet<QName>(); currentItem.requiredFirstSet(values); String expected = null; if (values.size() <= 5) { expected = namesToString(values, parentName, qName, currentItem.allowEmpty()); } return (getLineContext(getFileName(), getLine()) + syntaxMessage(item, parentItem, parentName, qName, expected)); } /** * Returns a string containing the allowed values. */ private String attributeMessageDetail(Item item, Item parentItem, String parentName, QName qName) { Item currentItem = item; if (currentItem == null) currentItem = parentItem; String allowed = allowedAttributes(currentItem, qName); return (getLineContext(getFileName(), getLine()) + syntaxMessage(item, parentItem, parentName, qName, allowed)); } /** * Returns a string containing the allowed values. */ private String syntaxMessage(Item item, Item parentItem, String parentName, QName qName, String expected) { String syntaxPrefix; if (parentName == null || parentName.equals("#top")) syntaxPrefix = "Syntax: "; else syntaxPrefix = "<" + parentName + "> syntax: "; String msg = ""; Item topParent = null; for (Item parent = item; parent != null; parent = null) { // parent.getParent()) { if (qName != null && parent.allowsElement(qName)) { msg = "\n Check for duplicate and out-of-order tags."; if (expected != null) msg += expected + "\n"; msg += "\n"; String prefix = "Syntax: "; if (parent == parentItem) prefix = syntaxPrefix; msg += prefix + parent.toSyntaxDescription(prefix.length()); break; } // topParent = parent; } if (topParent == null || topParent instanceof EmptyItem) { topParent = parentItem; if (qName != null && topParent.allowsElement(qName)) { msg = "\n Check for duplicate and out-of-order tags."; if (expected != null) msg += expected + "\n"; msg += "\n"; String prefix = syntaxPrefix; msg += prefix + topParent.toSyntaxDescription(prefix.length()); } } if (msg.equals("")) { msg = ""; if (expected != null) msg += expected + "\n"; msg += "\n"; String prefix = syntaxPrefix; msg += prefix + topParent.toSyntaxDescription(prefix.length()); } return msg; } /** * Returns a string containing the allowed values. */ private String requiredValues(Item item, String parentName, QName qName) { if (item == null) return ""; HashSet<QName> values = new LinkedHashSet<QName>(); item.requiredFirstSet(values); return namesToString(values, parentName, qName, item.allowEmpty()); } private String namesToString(HashSet<QName> values, String parentName, QName qName, boolean allowEmpty) { CharBuffer cb = new CharBuffer(); if (values.size() > 0) { ArrayList<QName> sortedValues = new ArrayList<QName>(values); Collections.sort(sortedValues); for (int i = 0; i < sortedValues.size(); i++) { QName name = sortedValues.get(i); if (i == 0) cb.append("\n\n"); else if (i == sortedValues.size() - 1) cb.append(" or\n"); else cb.append(",\n"); if (name.getName().equals("#text")) { cb.append("text"); } else if (name.getNamespaceURI() == null || qName == null) cb.append("<" + name.getName() + ">"); else if (qName.getNamespaceURI() != name.getNamespaceURI()) { if (name.getPrefix() != null) cb.append("<" + name.getName() + " xmlns:" + name.getPrefix() + "=\"" + name.getNamespaceURI() + "\">"); else cb.append("<" + name.getName() + " xmlns=\"" + name.getNamespaceURI() + "\">"); } else cb.append("<" + name.getName() + ">"); } if (values.size() == 1) cb.append(" is expected"); else cb.append(" are expected"); if (allowEmpty) { if (parentName == null || parentName.equals("#top")) cb.append(",\nor the document may end."); else cb.append(",\nor </" + parentName + "> may close."); } else cb.append("."); } else if (allowEmpty) { if (parentName == null || parentName.equals("#top")) cb.append("\n\nThe document is expected to end."); else cb.append("\n\n</" + parentName + "> is expected to close."); } return cb.toString(); } /** * Returns a string containing the allowed values. */ private String allowedAttributes(Item item, QName qName) { if (item == null) return ""; HashSet<QName> values = new LinkedHashSet<QName>(); item.attributeSet(values); CharBuffer cb = new CharBuffer(); if (values.size() > 0) { ArrayList<QName> sortedValues = new ArrayList<QName>(values); Collections.sort(sortedValues); for (int i = 0; i < sortedValues.size(); i++) { QName name = sortedValues.get(i); if (i == 0) cb.append("\n\n"); else if (i == sortedValues.size() - 1) cb.append(" or "); else cb.append(", "); String uri = name.getNamespaceURI(); if (uri == null || uri.equals("")) cb.append("'" + name.getName() + "'"); else if (qName == null || qName.getName().equals(name.getName())) cb.append("'" + name.getCanonicalName() + "'"); else cb.append("'" + name.getName() + "'"); } if (values.size() == 1) cb.append(" is expected."); else cb.append(" are expected."); } return cb.toString(); } /** * Returns the current location. */ private String getLocation() { if (_locator == null) return ""; else return "" + _locator.getLineNumber(); } /** * Checks if the document was valid. * * <p> * This method can be only called after this handler receives * the <code>endDocument</code> event. * * @return * <b>true</b> if the document was valid, * <b>false</b> if not. * * @exception IllegalStateException * If this method is called before the endDocument event is dispatched. */ public boolean isValid() throws IllegalStateException { return _isValid; } private String getLineContext(String filename, int errorLine) { if (filename == null || errorLine <= 0) return ""; ReadStream is = null; try { Path path = Vfs.lookup().lookup(filename); StringBuilder sb = new StringBuilder("\n\n"); is = path.openRead(); int line = 0; String text; while ((text = is.readLine()) != null) { line++; if (errorLine - 2 <= line && line <= errorLine + 2) { sb.append(line); sb.append(": "); sb.append(text); sb.append("\n"); } } return sb.toString(); } catch (IOException e) { log.log(Level.FINEST, e.toString(), e); return ""; } finally { if (is != null) is.close(); } } static class StartKey { private Item _item; private QName _name; public StartKey(Item item, QName name) { _item = item; _name = name; } public StartKey() { } public void init(Item item, QName name) { _item = item; _name = name; } public int hashCode() { return _name.hashCode() + 137 * System.identityHashCode(_item); } public boolean equals(Object o) { if (o == this) return true; if (o.getClass() != StartKey.class) return false; StartKey key = (StartKey) o; return _name.equals(key._name) && _item == key._item; } } static class EndElementKey { private Item _item; public EndElementKey(Item item) { _item = item; } public EndElementKey() { } public void init(Item item) { _item = item; } public int hashCode() { return 137 + _item.hashCode(); } public boolean equals(Object o) { if (o == this) return true; if (o.getClass() != EndElementKey.class) return false; EndElementKey key = (EndElementKey) o; return _item.equals(key._item); } } }