package dk.brics.xact.analysis.transformations;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import dk.brics.misc.Origin;
import dk.brics.relaxng.converter.ParseException;
import dk.brics.xact.XMLException;
import dk.brics.xact.analysis.config.Configuration;
import dk.brics.xact.analysis.flowgraph.FlowGraph;
import dk.brics.xact.analysis.flowgraph.SchemaType;
import dk.brics.xact.analysis.flowgraph.Statement;
import dk.brics.xact.analysis.flowgraph.TemplateConstant;
import dk.brics.xact.analysis.flowgraph.statements.AnalyzeStm;
import dk.brics.xact.analysis.flowgraph.statements.BasicStatementVisitor;
import dk.brics.xact.analysis.flowgraph.statements.ConstStm;
import dk.brics.xact.analysis.flowgraph.statements.GapifyStm;
import dk.brics.xact.analysis.flowgraph.statements.ValidateStm;
import dk.brics.xact.analysis.xmlgraph.XMLSchemaDatatypes;
import dk.brics.xact.operations.XMLGraphConverter;
import dk.brics.xact.operations.XMLValidator;
import dk.brics.xmlgraph.AttributeNode;
import dk.brics.xmlgraph.ChoiceNode;
import dk.brics.xmlgraph.ElementNode;
import dk.brics.xmlgraph.MultiContentNode;
import dk.brics.xmlgraph.Node;
import dk.brics.xmlgraph.OneOrMoreNode;
import dk.brics.xmlgraph.SequenceNode;
import dk.brics.xmlgraph.SingleContentNode;
import dk.brics.xmlgraph.TextNode;
import dk.brics.xmlgraph.XMLGraph;
import dk.brics.xmlgraph.XMLGraphFragment;
/**
* Loads all XML schemas used by the program and then builds an XML graph containing:
* <ul>
* <li>All types defined in loaded XML schemas.
* <li>The necessary types from the XMLSchema-datatypes namespace.
* <li>The necessary quantified versions of those types (suffixed ?,+, or * accordingly)
* </ul>
* Additionally, only <tt>ChoiceNode</tt>s can have element and attribute nodes as content,
* and the contents of each element node will be a <tt>ChoiceNode</tt>.
* <p/>
* A <em>quantified</em> type is the name of a type suffixed with ?, + or *, corresponding
* to the zero-or-one, one-or-more and zero-or-more operators. Quantifiers can appear both
* inside and outside of gap annotations.
* For example, these are all types that use quantifiers:
* <pre>
* foo+
* foo[bar* X]
* foo*[bar+ Z]
* foo[bar? X, baz? Y]
* </pre>
*/
public class SchemaTypeLinking {
enum Quantifier {
One,
OneOrMore,
ZeroOrOne,
ZeroOrMore,
}
public void run(final FlowGraph g, Configuration config) {
final XMLGraph xg = new XMLGraph();
g.setXMLGraph(xg);
// load XML Schemas
for (Map.Entry<String,Origin> entry : g.getSchemas().entrySet()) {
try {
Map<String,SequenceNode> map = XMLValidator.loadXMLSchema(new URL(entry.getKey()), xg);
g.getTypemap().putAll(map);
} catch (MalformedURLException ex) {
throw new XMLException("Schema URL is malformed", ex, entry.getValue());
} catch (ParseException ex) {
throw new XMLException("Could not parse schema: " + ex.getMessage(), ex, entry.getValue());
}
}
for (URL url : config.getAdditionalSchemas()) {
try {
Map<String, SequenceNode> map = XMLValidator.loadXMLSchema(url, xg);
g.getTypemap().putAll(map);
} catch (ParseException ex) {
throw new XMLException("Could not parse schema: " + ex.getMessage(), ex);
}
}
// link SchemaTypes to their nodes
final class Visitor extends BasicStatementVisitor {
Origin origin;
@Override
public void visitAnalyzeStm(AnalyzeStm s) {
if (s.getKind() != AnalyzeStm.Kind.HOTSPOT) {
linkSchemaType(s, s.getSchema());
}
}
@Override
public void visitValidateStm(ValidateStm s) {
linkSchemaType(s, s.getSchema());
}
@Override
public void visitGapifyStm(GapifyStm s) {
if (s.getSchema() != null)
linkSchemaType(s, s.getSchema());
}
@Override
public void visitConstStm(ConstStm s) {
TemplateConstant t = s.getConst();
if (t.getFragment() != null)
return;
// handle empty XML specially to avoid "" text nodes
if (t.getXML().isText() && t.getXML().asText().getString().isEmpty()) {
SequenceNode n = new SequenceNode(Collections.<Integer>emptyList(), s.getOrigin());
xg.addNode(n);
XMLGraphFragment f = new XMLGraphFragment(n, null, null, null);
t.setFragment(f);
} else {
XMLGraphFragment f = XMLGraphConverter.extend(xg, t.getXML(), XMLGraphConverter.GapConversion.CLOSED, true);
for (Map.Entry<String,String> en : f.getGapTypeMap().entrySet()) {
String gaptype = en.getValue();
// add gap types to the xml graph, and ensure that they are valid
resolveTypename(gaptype);
}
t.setFragment(f);
}
}
void linkSchemaType(Statement s, SchemaType type) {
type.setTypeNode(resolveTypename(type.getType()));
for (Map.Entry<String,String> gap : type.getGapTypes().entrySet()) {
String gapname = gap.getKey();
String gaptype = gap.getValue();
type.getGapTypeNodes().put(gapname, resolveTypename(gaptype));
}
}
Node resolveTypename(String name) {
if (g.getTypemap().containsKey(name)) {
return g.getTypemap().get(name);
}
String schemaName;
Quantifier quantifier;
if (name.endsWith("?")) {
schemaName = name.substring(0, name.length() - 1);
quantifier = Quantifier.ZeroOrOne;
} else if (name.endsWith("+")) {
schemaName = name.substring(0, name.length() - 1);
quantifier = Quantifier.OneOrMore;
} else if (name.endsWith("*")) {
schemaName = name.substring(0, name.length() - 1);
quantifier = Quantifier.ZeroOrMore;
} else {
schemaName = name;
quantifier = Quantifier.One;
}
Node base = g.getTypemap().get(schemaName);
// add datatypes on-the-fly when needed
if (base == null) {
if (XMLSchemaDatatypes.isDatatype(schemaName)) {
base = new TextNode(XMLSchemaDatatypes.getDatatype(schemaName, origin), new Origin("", -1, -1));
xg.addNode(base);
g.getTypemap().put(schemaName, base);
}
}
// if still not found, it does not exist
if (base == null) {
throw new XMLException("Could not find schema for type " + schemaName + " referred to at " + origin, origin);
}
Node result;
OneOrMoreNode on;
SequenceNode empty;
switch (quantifier) {
case One:
result = base;
break;
case OneOrMore:
result = new OneOrMoreNode(base.getIndex(), origin);
xg.addNode(result);
break;
case ZeroOrMore:
empty = new SequenceNode(Collections.<Integer>emptyList(), origin);
on = new OneOrMoreNode(base.getIndex(), origin);
xg.addNode(empty);
xg.addNode(on);
result = new ChoiceNode(set(empty.getIndex(), on.getIndex()), origin);
xg.addNode(result);
break;
case ZeroOrOne:
default:
empty = new SequenceNode(Collections.<Integer>emptyList(), origin);
xg.addNode(empty);
result = new ChoiceNode(set(base.getIndex(), empty.getIndex()), origin);
xg.addNode(result);
break;
}
g.getTypemap().put(name, result);
return result;
}
}
Visitor visitor = new Visitor();
for (Statement stm : g.getNodes()) {
visitor.origin = stm.getOrigin();
stm.visitBy(visitor);
}
wrapNodesInChoices(xg);
}
private Set<Integer> set(Integer ... contents) {
return new LinkedHashSet<Integer>(Arrays.asList(contents));
}
/** Puts a choice node around each element node, and before each attribute node */
void wrapNodesInChoices(XMLGraph xg) {
Map<Node, ChoiceNode> pred = new HashMap<Node, ChoiceNode>();
Map<Node, ChoiceNode> succ = new HashMap<Node, ChoiceNode>();
for (Node node : new ArrayList<Node>(xg.getNodes())) {
if (node instanceof ElementNode) {
ElementNode en = (ElementNode)node;
LinkedHashSet<Integer> cs1 = new LinkedHashSet<Integer>();
cs1.add(en.getIndex());
ChoiceNode ch1 = new ChoiceNode(cs1, node.getOrigin());
pred.put(en, ch1);
xg.addNode(ch1);
LinkedHashSet<Integer> cs2 = new LinkedHashSet<Integer>();
cs2.add(en.getContent());
ChoiceNode ch2 = new ChoiceNode(cs2, node.getOrigin());
succ.put(en, ch2);
xg.addNode(ch2);
en.setContent(ch2.getIndex());
}
else if (node instanceof AttributeNode) {
AttributeNode an = (AttributeNode)node;
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>();
cs.add(an.getIndex());
ChoiceNode ch = new ChoiceNode(cs, node.getOrigin());
pred.put(an, ch);
xg.addNode(ch);
}
}
// update ingoing edges
for (Node node : xg.getNodes()) {
if (node instanceof SingleContentNode) {
SingleContentNode sc = (SingleContentNode)node;
Node content = xg.getNode(sc.getContent());
if (content instanceof ElementNode || content instanceof AttributeNode) {
sc.setContent(pred.get(content).getIndex());
}
}
else if (node instanceof MultiContentNode) {
MultiContentNode mc = (MultiContentNode)node;
LinkedList<Integer> newcontent = new LinkedList<Integer>();
LinkedList<Integer> removedcontent = new LinkedList<Integer>();
for (int child : mc.getContents()) {
Node content = xg.getNode(child);
if (content instanceof ElementNode || content instanceof AttributeNode) {
Node p = pred.get(content);
if (p != mc) { // predecessor might be this node -- don't change that
newcontent.add(p.getIndex());
removedcontent.add(child);
}
}
}
mc.getContents().removeAll(removedcontent);
mc.getContents().addAll(newcontent);
}
}
}
}