package dk.brics.xact.analysis;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import dk.brics.misc.Origin;
import dk.brics.relaxng.Grammar;
import dk.brics.relaxng.converter.ParseException;
import dk.brics.relaxng.converter.RNGParser;
import dk.brics.relaxng.converter.RestrRelaxNG2XMLGraph;
import dk.brics.relaxng.converter.StandardDatatypes;
import dk.brics.relaxng.converter.dtd.DTD2RestrRelaxNG;
import dk.brics.relaxng.converter.xmlschema.XMLSchema2RestrRelaxNG;
import dk.brics.xact.XML;
import dk.brics.xact.XMLValidationException;
import dk.brics.xact.operations.XMLGraphConverter;
import dk.brics.xmlgraph.ElementNode;
import dk.brics.xmlgraph.SequenceNode;
import dk.brics.xmlgraph.XMLGraph;
import dk.brics.xmlgraph.validator.ValidationErrorHandler;
import dk.brics.xmlgraph.validator.Validator;
/**
* Schema validation for XML templates.
* Supported schema languages: DTD, XML Schema, and Restricted RELAX NG.
*/
public class XMLAnalysisValidator { // TODO: runtime validation is slow!
private final static Collection<ConvertedSchema> schemas = new ArrayList<ConvertedSchema>();;
private final static StandardDatatypes datatypes = new StandardDatatypes();
private XMLAnalysisValidator() {}
/**
* Checks that the given template is valid according to the given schema type.
* @throws XMLValidationException if the template is invalid
*/
public static void validate(XML x, String type, Origin origin) throws XMLValidationException {
XMLGraph xg = XMLGraphConverter.convert(x, XMLGraphConverter.GapConversion.IGNORE, false); // TODO: make variant of validate that treats gaps optimistically instead of just ignoring them
//new dk.brics.xmlgraph.converter.XMLGraph2Dot(System.out).print(xg);
XMLGraph t = null;
int i = 0;
String expanded = expandQName(type, origin);
for (ConvertedSchema s : schemas) {
SequenceNode n = s.types.get(expanded);
if (n != null) {
t = s.xg;
i = n.getIndex();
break;
}
}
if (t == null)
throw new XMLValidationException("No schema found for type " + type, origin);
LocalValidationErrorHandler handler = new LocalValidationErrorHandler();
new Validator(handler).validate(xg, t, i);
if (!handler.errors.isEmpty()) {
XMLValidationException ex = null;
while (!handler.errors.isEmpty()) {
XMLValidationException e = handler.errors.pop();
e.setNext(ex);
ex = e;
}
throw ex;
}
}
private static String expandQName(String type, Origin origin) {
return expandQName(type, XML.getThreadNamespaceMap(), XML.getNamespaceMap(), origin);
}
/**
* Converts a QName into an expanded name using the given namespace maps.
* @throws XMLValidationException if the prefix is not declared in any of the given maps
*/
public static String expandQName(String type, Map<String,String> map1, Map<String,String> map2, Origin origin) {
if (type.startsWith("{")) // already expanded
return type;
String prefix;
String localname;
int i = type.indexOf(':');
if (i == -1) {
prefix = "";
localname = type;
} else {
prefix = type.substring(0, i);
localname = type.substring(i + 1);
}
String ns = null;
if (map1 != null)
ns = map1.get(prefix);
if (ns == null && map2 != null)
ns = map2.get(prefix);
if (ns == null && prefix.length() == 0)
ns = "";
if (ns == null)
throw new XMLValidationException("Undeclared namespace prefix " + prefix, origin);
if (ns.length() == 0)
return localname;
else
return "{" + ns + "}" + localname;
}
/**
* Loads an XML schema.
* @throws ParseException if a parse error occurs
*/
public static void loadXMLSchema(URL url) throws ParseException {
loadXMLSchema(url, null);
}
/**
* Loads an XML schema into an existing XML graph.
* @throws ParseException if a parse error occurs
*/
public static Map<String,SequenceNode> loadXMLSchema(URL url, XMLGraph xg) throws ParseException {
boolean extend = xg != null;
String u = url.toString();
Map<String,SequenceNode> types = new HashMap<String,SequenceNode>();
RNGParser rngparser = new RNGParser();
RestrRelaxNG2XMLGraph rrng2xg = new RestrRelaxNG2XMLGraph(xg, datatypes);
if (u.endsWith(".xsd")) {
XMLSchema2RestrRelaxNG xsd2rrng = new XMLSchema2RestrRelaxNG(datatypes);
if (extend)
rrng2xg.extend(rngparser.parse(xsd2rrng.convert(url), url));
else
xg = rrng2xg.convert(rngparser.parse(xsd2rrng.convert(url), url));
Map<String,String> m1 = xsd2rrng.getNameMap();
Map<String,String> m2 = rngparser.getTopLevelNewNames();
Map<String,SequenceNode> m3 = rrng2xg.getDefineNodes();
for (Map.Entry<String,String> e : m1.entrySet())
types.put(e.getKey(), m3.get(m2.get(e.getValue())));
} else if (u.endsWith(".dtd")) {
DTD2RestrRelaxNG dtd2rrng = new DTD2RestrRelaxNG();
if (extend)
rrng2xg.extend(rngparser.parse(dtd2rrng.convert(url), url));
else
xg = rrng2xg.convert(rngparser.parse(dtd2rrng.convert(url), url));
Map<String,String> m1 = dtd2rrng.getNameMap();
Map<String,String> m2 = rngparser.getTopLevelNewNames();
Map<String,SequenceNode> m3 = rrng2xg.getDefineNodes();
for (Map.Entry<String,String> e : m1.entrySet())
types.put(e.getKey(), m3.get(m2.get(e.getValue())));
} else if (u.endsWith(".rrng") || u.endsWith(".rng")) {
Grammar rrng = rngparser.parse(url);
if (!rrng.check(System.err))
throw new ParseException("Schema is not Restricted RELAX NG " + url);
if (extend)
rrng2xg.extend(rrng);
else
xg = rrng2xg.convert(rrng);
Map<String,String> m1 = rngparser.getTopLevelNewNames();
Map<String,SequenceNode> m2 = rrng2xg.getDefineNodes();
for (Map.Entry<String,String> e : m1.entrySet())
types.put(e.getKey(), m2.get(e.getValue()));
} else
throw new ParseException("Unrecognized schema type " + url);
if (extend)
return types;
else {
schemas.add(new ConvertedSchema(xg, types));
return null;
}
}
private static class ConvertedSchema {
final XMLGraph xg;
final Map<String,SequenceNode> types;
ConvertedSchema(XMLGraph xg, Map<String,SequenceNode> types) {
this.xg = xg;
this.types = types;
}
}
private static class LocalValidationErrorHandler implements ValidationErrorHandler {
final Stack<XMLValidationException> errors = new Stack<XMLValidationException>();
public boolean error(ElementNode n, Origin origin, String msg, String example, Origin schema) {
StringBuilder b = new StringBuilder(msg);
Origin or;
if (n != null) {
String name = n.getName().getShortestExample(true);
int i1 = name.indexOf('@');
name = (i1 < 0) ? name : name.substring(0, i1);
int i2 = name.indexOf('%');
name = (i2 < 0) ? name : name.substring(0, i2);
b.append(" at element " + name);
or = n.getOrigin();
} else {
b.append(" at root");
or = origin;
}
if (example != null)
b.append(": ").append(example.length() > 0 ? example : "[empty contents]");
errors.push(new XMLValidationException(b.toString(), or));
return true;
}
}
}