/*
* 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);
}
}
}