package dk.brics.xact.analysis.xmlgraph;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import dk.brics.automaton.Automaton;
import dk.brics.automaton.State;
import dk.brics.automaton.Transition;
import dk.brics.xact.XMLXPathException;
import dk.brics.xact.analysis.XMLAnalysisException;
import dk.brics.xact.analysis.dataflow.VariableAnalysisInterface;
import dk.brics.xact.analysis.flowgraph.SchemaType;
import dk.brics.xact.analysis.flowgraph.Statement;
import dk.brics.xact.analysis.flowgraph.statements.CastStm;
import dk.brics.xact.analysis.flowgraph.statements.CheckStm;
import dk.brics.xact.analysis.flowgraph.statements.ConcatStm;
import dk.brics.xact.analysis.flowgraph.statements.ConstStm;
import dk.brics.xact.analysis.flowgraph.statements.CopyStm;
import dk.brics.xact.analysis.flowgraph.statements.EmptyStm;
import dk.brics.xact.analysis.flowgraph.statements.GapifyStm;
import dk.brics.xact.analysis.flowgraph.statements.GetStm;
import dk.brics.xact.analysis.flowgraph.statements.InsertStm;
import dk.brics.xact.analysis.flowgraph.statements.NodeStm;
import dk.brics.xact.analysis.flowgraph.statements.PlugStm;
import dk.brics.xact.analysis.flowgraph.statements.RemoveStm;
import dk.brics.xact.analysis.flowgraph.statements.SetStm;
import dk.brics.xact.analysis.flowgraph.statements.UnknownStm;
import dk.brics.xact.analysis.flowgraph.statements.ValidateStm;
import dk.brics.xact.analysis.flowgraph.statements.GetStm.Kind;
import dk.brics.xmlgraph.AttributeNode;
import dk.brics.xmlgraph.ChoiceNode;
import dk.brics.xmlgraph.ConcreteNode;
import dk.brics.xmlgraph.ElementNode;
import dk.brics.xmlgraph.InterleaveNode;
import dk.brics.xmlgraph.MultiContentNode;
import dk.brics.xmlgraph.Node;
import dk.brics.xmlgraph.NodeProcessor;
import dk.brics.xmlgraph.OneOrMoreNode;
import dk.brics.xmlgraph.ReachableNodesProcessor;
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;
import dk.brics.xpath.evaluator.EvaluationContext;
import dk.brics.xpath.evaluator.StatusMap;
import dk.brics.xpath.evaluator.XPathEvaluator;
/**
* XML graph lattice and transfer functions for dataflow analysis.
*/
public class XMLGraphAnalysis implements VariableAnalysisInterface<XMLGraph> {
/** Every XML graph in the lattice is a copy of this with different edges */
private XMLGraph global_xg;
private ElementNode dummy_root;
private ChoiceNode dummy_root_content;
private StatementNodes stm_nodes;
private XPathEvaluator evaluator; // TODO: use jaxen-based XPath evaluator on XML graphs?
private EvaluationContext evaluation_context;
private Set<Statement> empty_xpath;
private Set<Statement> check_fails;
private Set<Integer> concreteNodes;
/**
* Constructs a new XML graph dataflow analysis.
*/
public XMLGraphAnalysis(XMLGraph global_xg,
ElementNode dummy_root,
ChoiceNode dummy_root_content,
Map<String,String> namespaces,
StatementNodes stm_nodes,
Set<Statement> empty_xpath,
Set<Statement> check_fails) {
this.global_xg = global_xg;
this.dummy_root = dummy_root;
this.dummy_root_content = dummy_root_content;
this.stm_nodes = stm_nodes;
this.empty_xpath = empty_xpath;
this.check_fails = check_fails;
evaluator = new XPathEvaluator();
evaluation_context = new EvaluationContext();
evaluation_context.setDefaultNamespace("");
evaluation_context.addNamespaces(namespaces);
evaluator.setEvaluationContext(evaluation_context);
concreteNodes = new LinkedHashSet<Integer>();
for (Node node : global_xg.getNodes()) {
if (node instanceof ConcreteNode) {
concreteNodes.add(node.getIndex());
}
}
}
/**
* See {@link #restoreEdges(ChoiceNode, XMLGraph)}.
*/
private void restoreEdges(int index, XMLGraph g) {
restoreEdges((ChoiceNode)g.getNode(index), g);
}
/**
* Restores the edges that were added by XMLGraphBuilder, but may have been
* removed by {@link XMLGraph#sharpen()}.
* <p/>
* The choice node will only be replaced if necessary. The caller is NOT allowed
* to modify the set returned by <tt>node.getContents()</tt> after this call.
* @param node a choice node
* @param g graph in which to replace the edges
*/
private void restoreEdges(ChoiceNode node, XMLGraph g) {
int index = node.getIndex();
ChoiceNode gx = (ChoiceNode)global_xg.getNode(index);
if (node.getContents() == gx.getContents())
return;
if (node.getContents().containsAll(gx.getContents()))
return;
if (node.getContents().isEmpty()) {
node.setContent(gx.getContents(), g);
} else {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(node.getContents());
cs.addAll(gx.getContents());
node.setContent(cs, g);
}
}
public XMLGraph newBottomElement() {
return global_xg.clone();
}
public boolean merge(XMLGraph source, XMLGraph dest) {
return dest.merge(source);
}
public void assign(XMLGraph g, Statement s) {
//throw new RuntimeException();
// XMLGraphDebug.dumpXMLGraph(g, s);
}
public XMLGraph transferCast(final CastStm s, final XMLGraph base) {
final XMLGraph g = base.clone();
if (g.isUnknown()) {
return g;
}
final Emptiness emptiness;
switch (s.getKind()) {
case ASATTRIBUTE:
emptiness = new Emptiness(g, null, EPresence.BOTTOM, EPresence.BOTTOM, EPresence.BOTTOM);
break;
case ASELEMENT:
emptiness = new Emptiness(g, EPresence.BOTTOM, null, EPresence.BOTTOM, EPresence.BOTTOM);
break;
case ASTEXT:
emptiness = new Emptiness(g, EPresence.BOTTOM, EPresence.BOTTOM, null, EPresence.BOTTOM);
break;
case ASCOMMENT:
case ASPI:
case ASNSDECL:
default:
return g;
}
final FirstRootAnalysis first = new FirstRootAnalysis(g, emptiness, s.getKind() == CastStm.Kind.ASTEXT);
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ChoiceNode n) {
if (!first.get(n.getIndex()).definitely())
return this;
LinkedList<Integer> remove = new LinkedList<Integer>();
for (int child : n.getContents()) {
if (first.get(child).definitelyNot()) {
remove.add(child);
}
}
if (!remove.isEmpty()) {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(n.getContents());
cs.removeAll(remove);
n.setContent(cs, g);
}
return this;
}
});
LinkedList<Integer> remove = new LinkedList<Integer>();
for (int root : g.getRoots()) {
if (first.get(root).definitelyNot()) {
remove.add(root);
}
}
g.getRoots().removeAll(remove);
return g;
}
public void transferCheck(CheckStm s, XMLGraph base) {
StatusMap sm = evaluateXPathOrRoot(s.getXPath(), base);
switch (s.getKind()) {
case GETNUMBER:
checkXPathEmpty(s, base, sm);
checkGetNumberOrStringFails(s, base, sm, true);
break;
case GETSTRING:
checkXPathEmpty(s, base, sm);
checkGetNumberOrStringFails(s, base, sm, false);
break;
case HAS:
case ISATTRIBUTE:
case ISELEMENT:
case ISTEXT:
checkNodeTestFails(s, base, sm);
break;
case TODOCUMENT:
checkOneRootFails(s, base, sm);
break;
}
}
public XMLGraph transferConcat(ConcatStm s, XMLGraph xmlsource) {
XMLGraph g = xmlsource.clone();
if (g.isUnknown()) {
return g;
}
ChoiceNode cn = stm_nodes.getConcatChoiceNode(s,g);
Set<Integer> cs = new LinkedHashSet<Integer>();
//cs.addAll(cn.getContents());
stm_nodes.getConcatTextNode(s, g).replaceText(s.getStringSource(), g);
if (!s.getStringSource().isEmpty()) {
cs.add(stm_nodes.getConcatTextNode(s, g).getIndex());
}
cs.add(stm_nodes.getConcatSequenceNode(s, g).getIndex());
cs.addAll(g.getRoots());
cn.setContent(cs, g);
g.getRoots().clear();
g.getRoots().add(stm_nodes.getConcatOneOrMoreNode(s, g).getIndex());
//stm_nodes.getConcatTextNode(s,g).replaceText(s.getStringSource(), g);
g.sharpen();
return g;
}
public XMLGraph transferConst(ConstStm s) {
final XMLGraph g = global_xg.clone();
g.useFragment(stm_nodes.getTemplateConstantXMLFragment(s.getConst()));
// open gaps
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ChoiceNode n) {
if (n.isGap() && !n.isOpen()) {
n.setContentAndStatus(true, false, n.getContents(), g);
}
return this;
}
});
g.sharpen();
return g;
}
private static final class AutomatonPresence {
public Automaton auto;
public EPresence presence;
public static final AutomatonPresence NoStringUnknown = new AutomatonPresence(Automaton.makeEmpty(), EPresence.UNKNOWN);
public static final AutomatonPresence NoStringEmpty = new AutomatonPresence(Automaton.makeEmpty(), EPresence.EMPTY);
public AutomatonPresence(Automaton auto, EPresence presence) {
this.auto = auto;
this.presence = presence;
}
}
/**
* Returns the possible names of the first root element in any unfolding of the given XML graph.
* Attributes and text nodes are ignored.
*/
private Automaton getFirstElementNames(XMLGraph g) {
return getFirstElementNames(g, g.getRoots());
}
/**
* Returns the possible names of the first element in some unfolding starting at any of the given
* nodes.
*/
private Automaton getFirstElementNames(final XMLGraph g, Set<Integer> startNodes) {
return applyAutomatonPresence(g, startNodes, new CachedNodeProcessor<AutomatonPresence>() {
@Override
public AutomatonPresence cycle() {
return AutomatonPresence.NoStringUnknown;
}
@Override
public AutomatonPresence process(AttributeNode n) {
return AutomatonPresence.NoStringEmpty;
}
@Override
public AutomatonPresence process(ElementNode n) {
return new AutomatonPresence(n.getName(), EPresence.NONEMPTY);
}
@Override
public AutomatonPresence process(TextNode n) {
return AutomatonPresence.NoStringEmpty;
}
@Override
public AutomatonPresence process(ChoiceNode n) {
return choiceOrInterleave(n);
}
@Override
public AutomatonPresence process(InterleaveNode n) {
return choiceOrInterleave(n);
}
private AutomatonPresence choiceOrInterleave(MultiContentNode n) {
List<Automaton> auto = new ArrayList<Automaton>();
EPresence presence = EPresence.BOTTOM;
for (int child : n.getContents()) {
AutomatonPresence p = g.getNode(child).process(this);
auto.add(p.auto);
presence = presence.leastUpperBound(p.presence);
}
return new AutomatonPresence(Automaton.union(auto), presence);
}
@Override
public AutomatonPresence process(SequenceNode n) {
List<Automaton> auto = new ArrayList<Automaton>();
EPresence presence = EPresence.BOTTOM;
for (int child : n.getContents()) {
AutomatonPresence p = g.getNode(child).process(this);
auto.add(p.auto);
presence = presence.concat(p.presence);
if (presence.definitelyNonEmpty())
break;
}
return new AutomatonPresence(Automaton.union(auto), presence);
}
@Override
public AutomatonPresence process(OneOrMoreNode n) {
return g.getNode(n.getContent()).process(this);
}
}).auto;
}
// private Automaton getAttributeNames(XMLGraph g) {
// return getAttributeNames(g, g.getRoots());
// }
// /**
// * Returns the possible names of the root attributes in some unfolding starting at any of the given
// * nodes.
// */
// private Automaton getAttributeNames(final XMLGraph g, Set<Integer> startNodes) {
// CachedNodeProcessor<Automaton> p = new CachedNodeProcessor<Automaton>() {
// @Override
// public Automaton cycle() {
// return Automaton.makeEmpty();
// }
// @Override
// public Automaton process(AttributeNode n) {
// return n.getName();
// }
// @Override
// public Automaton process(ElementNode n) {
// return Automaton.makeEmpty();
// }
// @Override
// public Automaton process(TextNode n) {
// return Automaton.makeEmpty();
// }
// @Override
// public Automaton process(SingleContentNode n) {
// return g.getNode(n.getContent()).process(this);
// }
// @Override
// public Automaton process(MultiContentNode n) {
// List<Automaton> autos = new ArrayList<Automaton>();
// for (int child : n.getContents()) {
// autos.add(g.getNode(child).process(this));
// }
// return Automaton.union(autos);
// }
// @Override
// public Automaton process(NoContentNode n) {
// return Automaton.makeEmpty();
// }
// };
// List<Automaton> autos = new ArrayList<Automaton>();
// for (int root : startNodes) {
// autos.add(g.getNode(root).process(p));
// }
// return Automaton.union(autos);
// }
/**
* Applies the given processor to all root nodes and returns the union of the
* resulting automatons.
*/
private AutomatonPresence applyAutomatonPresence(XMLGraph g, Set<Integer> startNodes, NodeProcessor<AutomatonPresence> processor) {
List<Automaton> autos = new ArrayList<Automaton>();
EPresence presence = EPresence.BOTTOM;
for (int root : startNodes) {
AutomatonPresence p = g.getNode(root).process(processor);
autos.add(p.auto);
presence = presence.leastUpperBound(p.presence);
}
return new AutomatonPresence(Automaton.union(autos), presence);
}
/**
* Applies the given processor to all root nodes and discards the results.
*/
private void apply(XMLGraph g, NodeProcessor<?> processor) {
for (int root : g.getRoots()) {
g.getNode(root).process(processor);
}
}
private boolean unknown(XMLGraph g) {
return g != null && g.isUnknown();
}
public XMLGraph transferCopy(final CopyStm s, final XMLGraph base, XMLGraph firstattr, XMLGraph firstchild, XMLGraph nextnode) {
XMLGraph g = base.clone();
if (base.isUnknown() || unknown(firstattr) || unknown(firstchild) || unknown(nextnode)) {
g.setUnknown();
return g;
}
restoreEdges(stm_nodes.getCopyLeftChoice(s, g), g);
switch (s.getKind()) {
case ELEMENT:
stm_nodes.getCopyElementNode(s, g).setName(getFirstElementNames(base), g);
restoreEdges(stm_nodes.getCopyElementNode(s, g).getContent(), g);
if (firstattr != null) {
g.merge(firstattr);
stm_nodes.getCopyFirstAttribute(s, g).setContent(firstattr.getRoots(), g);
}
if (firstchild != null) {
g.merge(firstchild);
stm_nodes.getCopyFirstChild(s, g).setContent(firstchild.getRoots(), g);
}
if (nextnode != null) {
g.merge(nextnode);
stm_nodes.getCopyNextNode(s, g).setContent(nextnode.getRoots(), g);
}
g.getRoots().clear();
g.getRoots().add(stm_nodes.getCopyTopNode(s, g).getIndex());
break;
case COMMENT:
case PROCESSINGINSTRUCTION:
g = nextnode.clone();
break;
case ATTRIBUTE:
case ATTRNODE:
case ATTRIBUTEGAP:
case TEMPLATEGAP:
case TEMPNODE:
case TEXT:
ChoiceNode left = stm_nodes.getCopyLeftChoice(s, g);
final LinkedHashSet<Integer> leftContent = new LinkedHashSet<Integer>();
if (nextnode != null) {
g.merge(nextnode);
g.getRoots().clear();
g.getRoots().add(stm_nodes.getCopyTopNode(s, g).getIndex());
stm_nodes.getCopyNextNode(s, g).setContent(nextnode.getRoots(), g);
}
apply(base, new CachedNodeProcessor<EPresence>() {
@Override
public EPresence cycle() {
return EPresence.UNKNOWN;
}
@Override
public EPresence process(AttributeNode n) {
leftContent.add(n.getIndex());
//addChild(left, n.getIndex(), g);
//left.getContents().add(n.getIndex());
return EPresence.NONEMPTY;
}
@Override
public EPresence process(ElementNode n) {
return EPresence.BOTTOM;
}
@Override
public EPresence process(OneOrMoreNode n) {
return base.getNode(n.getContent()).process(this);
}
@Override
public EPresence process(TextNode n) {
if (s.getKind() == CopyStm.Kind.TEXT) {
if (n.getText().isEmpty())
return EPresence.BOTTOM;
leftContent.add(n.getIndex());
//addChild(left, child, g)
//left.getContents().add(n.getIndex());
if (n.getText().isEmptyString())
return EPresence.EMPTY;
else if (n.getText().run(""))
return EPresence.UNKNOWN;
else
return EPresence.NONEMPTY;
}
return EPresence.BOTTOM;
}
@Override
public EPresence process(ChoiceNode n) {
EPresence p = EPresence.BOTTOM;
if (n.isGap() && n.isOpen()) {
if (base.getOpenAttributeGaps().contains(n.getName()) || base.getOpenTemplateGaps().contains(n.getName())) {
//left.getContents().add(n.getIndex());
leftContent.add(n.getIndex());
p = EPresence.UNKNOWN;
}
}
for (int child : n.getContents()) {
p = p.leastUpperBound(base.getNode(child).process(this));
}
return p;
}
@Override
public EPresence process(SequenceNode n) {
EPresence p = EPresence.EMPTY;
for (int child : n.getContents()) {
p = p.concat(base.getNode(child).process(this));
if (p.definitelyNonEmpty())
break;
}
return p;
}
@Override
public EPresence process(InterleaveNode n) {
EPresence p = EPresence.BOTTOM;
for (int child : n.getContents()) {
p = p.leastUpperBound(base.getNode(child).process(this));
}
return p;
}
});
left.setContent(leftContent, g);
//left.getContents().addAll(base.getRoots());
break;
default:
throw new RuntimeException("unexpected copy type " + s.getKind());
}
g.sharpen();
return g;
}
public XMLGraph transferEmpty(EmptyStm s) {
XMLGraph g = global_xg.clone();
return g;
}
public XMLGraph transferGapify(final GapifyStm s, final XMLGraph base) {
final XMLGraph g = base.clone();
if (g.isUnknown()) {
return g;
}
final ChoiceNode gap_node = stm_nodes.getGapifyChoiceNode(s,g);
StatusMap smt;
//try {
//smt = evaluator.evaluate(base, s.getXPath(), dummy_root, dummy_root_content);
smt = evaluateXPathOrRoot(s.getXPath(), base);
//} catch (XPathException e) {
// throw new XMLXPathException(e);
//}
final StatusMap sm = smt;
checkXPathEmpty(s, base, sm);
g.processRoots(new ReachableNodesProcessor(g) {
@Override
public Object process(ChoiceNode n) { // all concrete nodes have a choice node parent
Collection<Integer> cs = new LinkedHashSet<Integer>();
boolean modified = false;
boolean add_gap = false;
for (int i : n.getContents()) {
boolean keep = true;
boolean hit = false;
Node c = g.getNode(i);
if (c instanceof ConcreteNode) {
switch (sm.get(i)) {
case ALL:
case DEFINITE:
hit = true;
keep = false;
break;
case SOME:
case DONTKNOW:
hit = true;
break;
case NEVER:
case NONE:
break;
}
}
if (keep)
cs.add(i);
else
modified = true;
if (hit) {
add_gap = true;
boolean tgap = false;
boolean agap = false;
if (c instanceof ElementNode)
tgap = true;
else if (c instanceof AttributeNode)
agap = true;
else if (c instanceof TextNode) { // TODO: determine whether this is TextNode a tgap or an agap (or both)
tgap = true;
agap = true;
}
String gapname = s.getGapName();
if (tgap)
g.addOpenTemplateGap(gapname);
if (agap)
g.addOpenAttributeGap(gapname);
if (s.getSchema() != null) {
if (g.getGapTypeMap().get(gapname) != null &&
!g.getGapTypeMap().get(gapname).equals(s.getSchema().getType()))
throw new XMLAnalysisException("Different types of gap '" + gapname + "'", s.getOrigin());
g.getGapTypeMap().put(gapname, s.getSchema().getType());
}
}
}
if (add_gap) {
cs.add(gap_node.getIndex());
modified = true;
}
if (modified) {
n.setContent(cs, g);
gap_node.setContentAndStatus(true, gap_node.isRemoved(), gap_node.getContents(), g);
}
return null;
}
});
g.sharpen();
return g;
}
private Set<ElementNode> getFirstRootElements(final XMLGraph g) {
final Set<ElementNode> rootelements = new LinkedHashSet<ElementNode>();
for (int root : g.getRoots()) {
g.getNode(root).process(new CachedNodeProcessor<EPresence>() {
@Override
public EPresence cycle() {
return EPresence.UNKNOWN;
}
@Override
public EPresence process(AttributeNode n) {
return EPresence.EMPTY;
}
@Override
public EPresence process(ElementNode n) {
rootelements.add(n);
return EPresence.NONEMPTY;
}
@Override
public EPresence process(ChoiceNode n) {
EPresence result = EPresence.BOTTOM;
for (int child : n.getContents()) {
result = result.leastUpperBound(g.getNode(child).process(this));
}
return result;
}
@Override
public EPresence process(OneOrMoreNode n) {
return g.getNode(n.getContent()).process(this);
}
@Override
public EPresence process(SequenceNode n) {
EPresence result = EPresence.EMPTY;
for (int child : n.getContents()) {
result = result.concat(g.getNode(child).process(this));
if (result.definitelyNonEmpty())
break;
}
return result;
}
@Override
public EPresence process(InterleaveNode n) {
EPresence result = EPresence.BOTTOM;
for (int child : n.getContents()) {
result = result.leastUpperBound(g.getNode(child).process(this));
}
return result;
}
@Override
public EPresence process(TextNode n) {
return EPresence.EMPTY;
}
});
}
return rootelements;
}
public XMLGraph transferGet(final GetStm s, final XMLGraph base) {
final XMLGraph g = base.clone();
if (g.isUnknown()) {
return g;
}
ChoiceNode cn = stm_nodes.getGetChoiceNode(s, g);
final LinkedHashSet<Integer> c = new LinkedHashSet<Integer>();
cn.setContent(c, g);
switch (s.getKind()) {
case GET:
case GETELEMENT: // TODO: to improve analysis precision, only select the *first* element for getElement
case GETELEMENTS: {
StatusMap sm;
// try {
// sm = evaluator.evaluate(base, s.getXPath(), dummy_root, dummy_root_content);
// } catch (XPathException e) {
// throw new XMLXPathException(e);
// }
sm = evaluateXPathOrRoot(s.getXPath(), base);
checkXPathEmpty(s, base, sm);
for (int i : sm.getNodes()) {
if (g.getNode(i) instanceof ConcreteNode) {
StatusMap.Status status = sm.get(i);
if (status!=StatusMap.Status.NONE && status!=StatusMap.Status.NEVER)
c.add(i);
}
}
c.addAll(cn.getContents());
g.getRoots().clear();
g.addRoot(cn);
// TODO: could improve precision by considering simple XPath *predicates*
break;
}
case GETFIRSTATTR:
case GETFIRSTATTRIBUTE:
case GETFIRSTCHILD:
g.getRoots().clear();
g.addRoot(cn);
Set<ElementNode> rootElements = getFirstRootElements(base);
for (ElementNode node : rootElements) {
g.getNode(node.getContent()).process(new CachedNodeProcessor<EPresence>() {
@Override
public EPresence cycle() {
return EPresence.UNKNOWN;
}
@Override
public EPresence process(AttributeNode n) {
c.add(n.getIndex());
return EPresence.NONEMPTY;
}
@Override
public EPresence process(ElementNode n) {
if (s.getKind() == Kind.GETFIRSTATTR || s.getKind() == Kind.GETFIRSTATTRIBUTE) {
return EPresence.EMPTY;
} else {
c.add(n.getIndex());
return EPresence.NONEMPTY;
}
}
@Override
public EPresence process(ChoiceNode n) {
if (n.isGap() && n.isOpen() && s.getKind() == Kind.GETFIRSTATTR && g.getOpenAttributeGaps().contains(n.getName())) {
c.add(n.getIndex());
return EPresence.UNKNOWN;
} else {
EPresence result = EPresence.BOTTOM;
for (int child : n.getContents()) {
result = result.leastUpperBound(g.getNode(child).process(this));
}
return result;
}
}
@Override
public EPresence process(SequenceNode n) {
EPresence result = EPresence.EMPTY;
for (int child : n.getContents()) {
result = result.concat(g.getNode(child).process(this));
if (result.definitelyNonEmpty())
break;
}
return result;
}
@Override
public EPresence process(OneOrMoreNode n) {
return g.getNode(n.getContent()).process(this);
}
@Override
public EPresence process(TextNode n) {
return EPresence.EMPTY;
}
@Override
public EPresence process(InterleaveNode n) {
EPresence result = EPresence.BOTTOM;
for (int child : n.getContents()) {
result = result.leastUpperBound(g.getNode(child).process(this));
}
return result;
}
});
}
break;
case GETFIRSTELEMENT: {
final Emptiness emptiness = new Emptiness(g, EPresence.EMPTY, EPresence.NONEMPTY, EPresence.EMPTY, EPresence.EMPTY);
final FirstRootAnalysis first = new FirstRootAnalysis(g, emptiness, false);
final RootAnalysis roots = new RootAnalysis(g);
final int emptyIndex = stm_nodes.getGetEmptySequence(s, g).getIndex();
// we model this as "replace every root node that is not the first root element with the empty sequence"
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ChoiceNode n) {
EBooleanLattice r = roots.get(n.getIndex());
if (r.definitelyNot()) {
return this;
}
LinkedList<Integer> removed = new LinkedList<Integer>();
boolean addempty = false;
for (int child : n.getContents()) {
if (roots.get(child).maybe()) {
if (r.definitely()) {
if (first.get(child).definitelyNot() || emptiness.get(child).definitelyEmpty()) {
removed.add(child);
}
}
if (first.get(child).maybeNot() && emptiness.get(child).maybeNonEmpty()) {
addempty = true;
}
}
}
boolean removegap = false;
if (n.isGap() && n.isOpen() && emptiness.get(n.getIndex()).definitelyEmpty() && r.definitely()) {
removegap = true;
}
if (!removed.isEmpty() || addempty || removegap) {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(n.getContents());
cs.removeAll(removed);
cs.add(emptyIndex);
if (removegap) {
n.setContentAndStatus(false, true, cs, g);
} else {
n.setContent(cs, g);
}
}
return this;
}
});
LinkedList<Integer> removed = new LinkedList<Integer>();
for (int child : g.getRoots()) {
switch (emptiness.get(child)) {
case EMPTY:
case BOTTOM:
// if no ELEMENT can occur as root here, then remove the root edge
removed.add(child);
break;
default: ;
}
}
g.getRoots().removeAll(removed);
// note: no need to add an empty root, because getFirstElement() throws
// an exception if none of the roots are elements
break;
}
case GETNEXTATTR:
case GETNEXTATTRIBUTE:
case GETNEXTSIBLING: {
Emptiness emptiness = new Emptiness(g,
null, // allow attributes
s.getKind() == Kind.GETNEXTSIBLING ? null : EPresence.BOTTOM, // allow elements
s.getKind() == Kind.GETNEXTSIBLING ? null : EPresence.BOTTOM, // allow text
s.getKind() != Kind.GETNEXTATTRIBUTE ? null : EPresence.BOTTOM); // allow gaps
final FirstRootAnalysis first = new FirstRootAnalysis(g, emptiness, s.getKind() == Kind.GETNEXTSIBLING);
final int emptyIndex = stm_nodes.getGetEmptySequence(s, g).getIndex();
// We model the "get next sibling" operations as "replace first root with empty sequece".
// If a ChoiceNode has an edge to a ConcreteNode that might be the first root,
// then the ChoiceNode gets an additional edge to the empty sequence.
// If that ConcreteNode can only occur as the first root, then the edge to it is removed.
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ChoiceNode n) {
if (first.get(n.getIndex()).definitelyNot())
return this;
LinkedList<Integer> removed = new LinkedList<Integer>();
boolean addempty = false;
for (int child : n.getContents()) {
if (!(g.getNode(child) instanceof ConcreteNode))
continue;
switch (first.get(child)) {
case YES:
// removed.add(child); // cannot remove due to invisible comments and PIs :(
addempty = true;
break;
case MAYBE:
addempty = true;
break;
default: ;
}
}
if (removed.isEmpty() || addempty) {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(n.getContents());
cs.removeAll(removed);
cs.add(emptyIndex);
n.setContent(cs, g);
}
return removed;
}
});
LinkedList<Integer> removed = new LinkedList<Integer>();
boolean addempty = false;
for (int root : g.getRoots()) {
if (!(g.getNode(root) instanceof ConcreteNode))
continue;
switch (first.get(root)) {
case YES:
// removed.add(root); // cannot remove due to invisible comments and PIs :(
addempty = true;
break;
case MAYBE:
addempty = true;
break;
default: ;
}
}
g.getRoots().removeAll(removed);
if (addempty) {
g.getRoots().add(emptyIndex);
}
break;
}
default:
throw new RuntimeException("Unexpected GET kind: " + s.getKind());
}//switch
g.sharpen();
return g;
}
/**
* Evaluates the given XPath expression if it is non-null, and otherwise it evaluates the "/" XPath
* expression identifying which nodes are always, sometimes, and never roots.
* @param xpath an XPath expression or <tt>null</tt>
* @param g an XML graph
* @return the status map returned by {@link XPathEvaluator#evaluate(XMLGraph, String, ElementNode, ChoiceNode)}
*/
private StatusMap evaluateXPathOrRoot(String xpath, XMLGraph g) {
if (xpath == null) {
xpath = "/node()|/@*";
}
try {
return evaluator.evaluate(g, xpath, dummy_root, dummy_root_content);
} catch (Exception ex) {
throw new XMLXPathException(ex);
}
}
private ChoiceNode makePrivate(ChoiceNode ch, XMLGraph g) {
ch.setContent(new LinkedHashSet<Integer>(ch.getContents()), g);
return (ChoiceNode)g.getNode(ch.getIndex());
}
public XMLGraph transferInsert(final InsertStm s, XMLGraph base, final XMLGraph xmlsrc) {
final XMLGraph g = base.clone();
if (g.isUnknown() || xmlsrc.isUnknown()) {
g.setUnknown();
return g;
}
final SequenceNode seq = stm_nodes.getInsertSequenceNode(s, g);
final ChoiceNode left;
final ChoiceNode right;
final TextNode text = stm_nodes.getInsertTextNode(s, g);
switch (s.getKind()) {
case APPEND:
case APPENDCONTENT:
case INSERTAFTER:
case SETATTRIBUTE:
left = makePrivate(stm_nodes.getInsertLeftSide(s, g), g);
right = makePrivate(stm_nodes.getInsertRightSide(s, g), g);
break;
case PREPEND:
case PREPENDCONTENT:
case INSERTBEFORE:
// SWAP left and right if prepending
left = makePrivate(stm_nodes.getInsertRightSide(s, g), g);
right = makePrivate(stm_nodes.getInsertLeftSide(s, g), g);
break;
default:
throw new RuntimeException("unknown insert kind");
}
text.replaceText(s.getStringSource(), g);
right.getContents().addAll(xmlsrc.getRoots());
right.getContents().add(text.getIndex());
switch (s.getKind()) {
case APPEND:
case PREPEND:
g.merge(xmlsrc);
left.getContents().addAll(base.getRoots());
g.getRoots().clear();
g.getRoots().add(seq.getIndex());
return g;
case SETATTRIBUTE:
case APPENDCONTENT: // this is both appendContent(XML) and appendContent(XPath,XML)
case PREPENDCONTENT:{
final StatusMap stm = evaluateXPathOrRoot(s.getXPath(), g);
boolean empty = checkXPathEmpty(s, g, stm);
if (empty)
return g;
g.merge(xmlsrc);
g.getRoots().retainAll(base.getRoots());
// TODO can we append as root here???
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ElementNode n) {
ChoiceNode ch = (ChoiceNode)g.getNode(n.getContent());
switch (stm.get(n.getIndex())) {
case DEFINITE:
case ALL:
left.getContents().addAll(ch.getContents());
ch = makePrivate(ch, g);
if (g.getGapTypeMap().isEmpty()) {
ch.getContents().clear();
}
ch.getContents().add(seq.getIndex());
break;
case SOME:
case DONTKNOW:
ch = makePrivate(ch, g);
left.getContents().addAll(ch.getContents());
ch.getContents().add(seq.getIndex());
break;
case NONE:
case NEVER:
break;
}
return null;
}
});
g.sharpen();
return g;
}
case INSERTAFTER:
case INSERTBEFORE: {
final StatusMap stm = evaluateXPathOrRoot(s.getXPath(), g);
boolean empty = checkXPathEmpty(s, g, stm);
if (empty)
return g;
g.merge(xmlsrc);
g.getRoots().retainAll(base.getRoots());
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ChoiceNode ch) {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(ch.getContents());
for (int child : ch.getContents()) {
Node node = g.getNode(child);
if (node instanceof ConcreteNode) {
switch (stm.get(child)) {
case ALL:
case DEFINITE:
if (g.getGapTypeMap().isEmpty()) {
cs.remove(child);
}
left.getContents().add(child);
cs.add(seq.getIndex());
break;
case SOME:
case DONTKNOW:
left.getContents().add(child);
cs.add(seq.getIndex());
break;
case NONE:
case NEVER:
break;
}
}
}
ch.setContent(cs, g);
return null;
}
});
g.sharpen();
return g;
}
default:
throw new RuntimeException("unknown insert kind");
}
}
private void mergeContents(ChoiceNode node, Collection<Integer> toadd, XMLGraph g) {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(node.getContents());
cs.addAll(toadd);
node.setContent(cs, g);
}
private void mergeOpen(ChoiceNode node, XMLGraph g) {
node.setContentAndStatus(true, node.isRemoved(), node.getContents(), g);
}
public XMLGraph transferNode(NodeStm s, XMLGraph firstattr, XMLGraph firstchild, XMLGraph nextnode) {
XMLGraph g = global_xg.clone();
if (unknown(firstattr) || unknown(firstchild) || unknown(nextnode)) {
g.setUnknown();
return g;
}
g.getRoots().clear();
g.getRoots().add(stm_nodes.getStmNode(s, g).getIndex());
if (nextnode != null) {
g.merge(nextnode);
mergeContents(stm_nodes.getStmNextNode(s, g), nextnode.getRoots(), g);
}
if (firstattr != null) {
g.merge(firstattr);
mergeContents(stm_nodes.getStmFirstAttribute(s, g), firstattr.getRoots(), g);
}
if (firstchild != null) {
g.merge(firstchild);
mergeContents(stm_nodes.getStmFirstChild(s, g), firstchild.getRoots(), g);
}
g.getRoots().clear();
g.getRoots().add(stm_nodes.getStmNode(s, g).getIndex());
switch (s.getKind()) {
case ATTRIBUTEGAP:
mergeOpen(stm_nodes.getStmGap(s, g), g);
g.addOpenAttributeGap(s.getGap());
break;
case TEMPLATEGAP:
mergeOpen(stm_nodes.getStmGap(s, g), g);
g.addOpenTemplateGap(s.getGap());
break;
default: ;
}
g.sharpen();
return g;
}
public XMLGraph transferPlug(final PlugStm s, XMLGraph base, final XMLGraph xmlsrc) {
/*final XMLGraph g = global_xg.clone();
g.getRoots().addAll(base.getRoots());
g.merge(base);*/
final XMLGraph g = base.clone();
if (base.isUnknown() || unknown(xmlsrc)) {
g.setUnknown();
return g;
}
switch (s.getKind()) {
case PLUG:
case PLUGMULTI:
case PLUGWRAP:
final String gapname = s.getGapName();
// plug roots into the relevant gaps and merge the XML graphs
final boolean skip_str = s.getStringSource().isEmpty();
if (g.getOpenAttributeGaps().contains(gapname) || g.getOpenTemplateGaps().contains(gapname)) {
g.processRoots(new ReachableNodesProcessor(g) {
@Override
public Object process(ChoiceNode n) {
if (n.isGap() && n.isOpen() && n.getName().equals(gapname)) {
Collection<Integer> c = new LinkedHashSet<Integer>();
c.addAll(n.getContents());
c.addAll(xmlsrc.getRoots());
if (!skip_str)
c.add(stm_nodes.getPlugTextNode(s,g).getIndex());
n.setContentAndStatus(false, n.isRemoved(), c, g);
}
return null;
}
});
}
if (!skip_str) {
TextNode tn = stm_nodes.getPlugTextNode(s,g);
ArrayList<Automaton> ss = new ArrayList<Automaton>();
ss.add(s.getStringSource());
ss.add(tn.getText());
tn.replaceText(Automaton.union(ss), g);
}
g.merge(xmlsrc);
if (s.getKind() == PlugStm.Kind.PLUGWRAP) {
g.getRoots().clear();
g.getRoots().add(stm_nodes.getPlugWrapTopNode(s, g).getIndex());
restoreEdges(stm_nodes.getPlugWrapTopNode(s, g), g);
stm_nodes.getPlugWrapContentNode(s, g).setContent(base.getRoots(), g);
} else {
g.getRoots().retainAll(base.getRoots());
}
// mark the gap as closed
if (g.getOpenTemplateGaps().contains(gapname))
g.addClosedTemplateGap(gapname);
g.removeOpenTemplateGap(gapname);
if (g.getOpenAttributeGaps().contains(gapname))
g.addClosedAttributeGap(gapname);
g.removeOpenAttributeGap(gapname);
g.getGapTypeMap().remove(gapname);
// open again if the inserted value contains the gap
if (xmlsrc.getOpenTemplateGaps().contains(gapname)) {
g.addOpenTemplateGap(gapname);
if (!xmlsrc.getClosedTemplateGaps().contains(gapname))
g.removeClosedTemplateGap(gapname);
String gaptype = xmlsrc.getGapTypeMap().get(gapname);
if (gaptype != null)
g.getGapTypeMap().put(gapname, gaptype);
}
if (xmlsrc.getOpenAttributeGaps().contains(gapname)) {
g.addOpenAttributeGap(gapname);
if (!xmlsrc.getClosedAttributeGaps().contains(gapname))
g.removeClosedAttributeGap(gapname);
String gaptype = xmlsrc.getGapTypeMap().get(gapname);
if (gaptype != null)
g.getGapTypeMap().put(gapname, gaptype);
}
// XMLGraphDebug.dumpXMLGraph(g, s);
break;
case CLOSE:
g.processRoots(new ReachableNodesProcessor(g) {
@Override
public Object process(ChoiceNode n) {
if (n.isGap() && n.isOpen()) {
Collection<Integer> c = new LinkedHashSet<Integer>();
c.addAll(n.getContents());
c.add(stm_nodes.getPlugSequenceNode(s,g).getIndex());
n.setContentAndStatus(false, n.isRemoved(), c, g);
}
return null;
}
});
g.getClosedTemplateGaps().addAll(g.getOpenTemplateGaps());
g.getClosedAttributeGaps().addAll(g.getOpenAttributeGaps());
g.getOpenTemplateGaps().clear();
g.getOpenAttributeGaps().clear();
g.getGapTypeMap().clear();
break;
}
g.sharpen();
return g;
}
public XMLGraph transferRemove(final RemoveStm s, final XMLGraph base) {
final XMLGraph g = base.clone();
if (g.isUnknown()) {
return g;
}
StatusMap smt;
smt = evaluateXPathOrRoot(s.getXPath(), base); //evaluator.evaluate(base, s.getXPath(), dummy_root, dummy_root_content);
final StatusMap sm = smt;
checkXPathEmpty(s, base, sm);
g.processRoots(new ReachableNodesProcessor(g) {
@Override
public Object process(ChoiceNode n) { // all concrete nodes have a choice node parent
Collection<Integer> cs = new LinkedHashSet<Integer>();
boolean modified = false;
boolean add_emptyseq = false;
for (int i : n.getContents()) {
boolean keep = true;
if (base.getNode(i) instanceof ConcreteNode) {
switch (sm.get(i)) {
case ALL:
case DEFINITE:
add_emptyseq = true;
if (g.getGapTypeMap().isEmpty()) {
keep = false;
}
modified = true;
break;
case SOME:
case DONTKNOW:
add_emptyseq = true;
modified = true;
break;
case NEVER:
case NONE:
break;
}
}
if (keep)
cs.add(i);
}
if (add_emptyseq)
cs.add(stm_nodes.getRemoveSequenceNode(s,g).getIndex());
if (modified)
n.setContent(cs, g);
return null;
}
});
g.sharpen();
return g;
}
public XMLGraph transferSet(SetStm s, XMLGraph base, final XMLGraph xmlsrc) {
final XMLGraph g = base.clone();
if (g.isUnknown() || xmlsrc.isUnknown()) {
g.setUnknown();
return g;
}
final StatusMap stm = evaluateXPathOrRoot(s.getXPath(), g);
boolean empty = checkXPathEmpty(s, g, stm);
if (empty)
return g;
g.merge(xmlsrc);
g.getRoots().retainAll(base.getRoots());
final TextNode text = stm_nodes.getSetTextNode(s, g);
text.replaceText(s.getStringSource(), g);
// TODO avoid recursive replacement for nodes that will not actually be replaced
// because its ancestor got replaced instead.
switch (s.getKind()) {
case SET:
// set(XPath, Object)
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ChoiceNode n) {
LinkedHashSet<Integer> cs = new LinkedHashSet<Integer>(n.getContents());
for (int child : n.getContents()) {
Node node = g.getNode(child);
if (node instanceof ConcreteNode) {
switch (stm.get(child)) {
case ALL:
case DEFINITE:
if (g.getGapTypeMap().isEmpty()) {
cs.remove(child);
}
cs.addAll(xmlsrc.getRoots());
cs.add(text.getIndex());
break;
case SOME:
case DONTKNOW:
cs.addAll(xmlsrc.getRoots());
cs.add(text.getIndex());
break;
case NONE:
case NEVER:
break;
}
}
}
n.setContent(cs, g);
return null;
}
});
break;
case SETCONTENT:
g.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(ElementNode n) {
ChoiceNode ch = (ChoiceNode)g.getNode(n.getContent());
LinkedHashSet<Integer> c = new LinkedHashSet<Integer>(ch.getContents());
switch (stm.get(n.getIndex())) {
case ALL:
case DEFINITE:
if (g.getGapTypeMap().isEmpty()) {
c.clear();
}
c.addAll(xmlsrc.getRoots());
c.add(text.getIndex());
break;
case SOME:
case DONTKNOW:
c.addAll(xmlsrc.getRoots());
c.add(text.getIndex());
break;
case NONE:
case NEVER:
break;
}
ch.setContent(c, g);
return null;
}
});
break;
default:
throw new RuntimeException("unknown set kind");
}
g.sharpen();
return g;
}
public XMLGraph transferUnknown(UnknownStm s) {
XMLGraph g = global_xg.clone();
g.setUnknown(); // TODO: use an ordinary 'any' schema instead?
return g;
}
public XMLGraph transferValidate(final ValidateStm s, XMLGraph base) {
final SchemaType schema = s.getSchema();
final XMLGraph g = global_xg.clone();
g.useFragment(new XMLGraphFragment(schema.getTypeNode(), schema.getTemplateGaps(), schema.getAttributeGaps(), schema.getGapTypes()));
g.sharpen();
return g;
}
/**
* Returns the XML node corresponding to the given schema type.
*/
// private SequenceNode getNode(String type, Origin origin) {
// SequenceNode n = schema_types.get(type);
// if (n == null) { // TODO: source .rng defines are ncnames, this is just a hack
// String localname;
// int i = type.lastIndexOf('}');
// if (i==-1)
// localname = type;
// else
// localname = type.substring(i+1);
// n = schema_types.get(localname);
// }
// if (n == null)
// throw new XMLAnalysisException("No schema definition found for " + type, origin);
// return n;
// }
/**
* Adds (/removes) 's' to 'empty_xpath' if no (/some) concrete node can be selected.
*/
private boolean checkXPathEmpty(Statement s, XMLGraph xg, StatusMap sm) {
boolean empty;
if (xg.isUnknown()) {
empty = false;
} else {
empty = true;
for (Node n : xg.getNodes()) {
if (n instanceof ConcreteNode) { // TODO: checkXPathEmpty and other checks ignore comment/PI/nsdecl nodes (nonexisting in XML graphs)
if (!sm.get(n.getIndex()).equals(StatusMap.Status.NONE)) {
empty = false;
break;
}
}
}
}
if (empty)
empty_xpath.add(s);
else
empty_xpath.remove(s);
return empty;
}
/**
* Adds 's' to 'check_fails' if the 'CheckStm' node test always fails.
* (Otherwise, removes 's'.)
*/
private void checkNodeTestFails(CheckStm s, XMLGraph xg, StatusMap sm) {
if (sm == null)
return;
boolean empty;
if (xg.isUnknown()) {
empty = false;
} else {
empty = true;
loop:
for (Node n : xg.getNodes()) {
boolean relevant = false;
switch (s.getKind()) {
case HAS:
relevant = n instanceof ConcreteNode;
break;
case ISATTRIBUTE:
relevant = n instanceof AttributeNode;
break;
case ISELEMENT:
relevant = n instanceof ElementNode;
break;
case ISTEXT:
relevant = n instanceof TextNode;
break;
default:
throw new RuntimeException(s.getKind() + " not expected");
}
if (relevant)
switch (sm.get(n.getIndex())) {
case ALL:
case DEFINITE:
case SOME:
case DONTKNOW:
empty = false;
break loop;
default: ;
}
}
}
if (empty)
check_fails.add(s);
else
check_fails.remove(s);
}
/**
* Adds 's' to 'check_fails' if the 'CheckStm' getNumber/getString gives empty result.
* (Otherwise, removes 's'.)
*/
private void checkGetNumberOrStringFails(CheckStm s, XMLGraph xg, StatusMap sm, boolean number) {
if (sm == null)
return;
boolean fails;
if (xg.isUnknown()) {
fails = false;
} else {
fails = true;
Set<TextNode> reachable = getReachableTextNodes(xg, sm);
search: for (TextNode n : reachable) {
for (State st : n.getText().getStates()) {
for (Transition tr : st.getTransitions()) {
if (number) { // look for digits
if (tr.getMin() <= '9' && tr.getMax() >= '0') {
fails = false;
break search;
}
} else { // look for non-whitespace chars
// TODO: check whether tr contains non-whitespace chars
fails = false;
break search;
}
}
}
}
}
if (fails)
check_fails.add(s);
else
check_fails.remove(s);
}
/**
* Finds the set of text nodes that are reachable from nodes that may be selected.
*/
private Set<TextNode> getReachableTextNodes(final XMLGraph xg, final StatusMap sm) {
final Set<TextNode> textnodes = new LinkedHashSet<TextNode>();
xg.processRoots(new CachedNodeProcessor<Object>() {
int depth = 0;
@Override
public Object cycle() {
return this;
}
@Override
public Object process(TextNode n) {
if (depth > 0)
textnodes.add(n);
return null;
}
@Override
public Object process(SingleContentNode n) {
boolean accepted = maybeAccepted(n);
if (accepted)
depth++;
xg.getNode(n.getContent()).process(this);
if (accepted)
depth--;
return null;
}
@Override
public Object process(MultiContentNode n) {
boolean accepted = maybeAccepted(n);
if (accepted)
depth++;
for (int child : n.getContents()) {
xg.getNode(child).process(this);
}
if (accepted)
depth--;
return null;
}
private boolean maybeAccepted(Node node) {
switch (sm.get(node.getIndex())) {
case ALL:
case DEFINITE:
case DONTKNOW:
case SOME:
return true;
default:
return false;
}
}
});
/*xg.processReachableNodes(new NodeProcessor<Object>() {
@Override
public Object process(SingleContentNode n) {
// skip nodes that cannot be selected
switch (sm.get(n.getIndex())) {
case NONE:
case NEVER:
return null;
}
if (xg.getNode(n.getContent()) instanceof TextNode) {
textnodes.add((TextNode)xg.getNode(n.getContent()));
}
return null;
}
@Override
public Object process(MultiContentNode n) {
switch (sm.get(n.getIndex())) {
case NONE:
case NEVER:
return null;
}
for (int i : n.getContents()) {
if (xg.getNode(i) instanceof TextNode)
textnodes.add((TextNode)xg.getNode(i));
}
return null;
}
});*/
return textnodes;
}
/**
* Adds 's' to 'check_fails' if not exactly one root element.
* (Otherwise, removes 's'.)
*/
private void checkOneRootFails(CheckStm s, final XMLGraph xg, StatusMap sm) {
// TODO use fixpoint search to increase precision here
final boolean[] malformed = new boolean[1]; // many roots?
final Set<Node> open = new HashSet<Node>();
NodeProcessor<EPresence> b = new NodeProcessor<EPresence>() {
// TODO this takes worst-case exponential time unless results are cached or a fixpoint search is used
@Override
public EPresence process(SequenceNode n) {
if (!open.add(n)) {
malformed[0] = true; // cycles require a fixpoint search
return EPresence.UNKNOWN;
}
EPresence result = EPresence.EMPTY;
for (int index : n.getContents()) {
Node child = xg.getNode(index);
EPresence p = child.process(this);
if (result.maybeNonEmpty() && p.maybeNonEmpty())
malformed[0] = true; // many roots
result = result.concat(p);
}
open.remove(n);
return result;
}
@Override
public EPresence process(ChoiceNode n) {
if (!open.add(n))
return EPresence.UNKNOWN;
EPresence result = EPresence.BOTTOM;
for (int index : n.getContents()) {
Node child = xg.getNode(index);
EPresence p = child.process(this);
result = result.leastUpperBound(p);
}
open.remove(n);
return result;
}
@Override
public EPresence process(OneOrMoreNode n) {
if (!open.add(n))
return EPresence.UNKNOWN;
EPresence p = xg.getNode(n.getContent()).process(this);
open.remove(n);
if (p.maybeNonEmpty())
malformed[0] = true; // many roots
return p;
}
@Override
public EPresence process(TextNode n) {
if (n.getText().isEmpty()) {
return EPresence.BOTTOM;
} else {
malformed[0] = true; // text at root
return EPresence.NONEMPTY;
}
}
@Override
public EPresence process(ElementNode n) {
return EPresence.NONEMPTY;
}
@Override
public EPresence process(InterleaveNode n) {
return EPresence.UNKNOWN;
}
@Override
public EPresence process(AttributeNode n) {
malformed[0] = true; // attribute at root
return EPresence.UNKNOWN;
}
};
for (int index : xg.getRoots()) {
if (xg.getNode(index).process(b).maybeEmpty()) {
malformed[0] = true;
}
}
// TODO when a text node or attribute is root, a different error message than "non-unique"
// root should be generated
if (malformed[0])
check_fails.add(s);
else
check_fails.remove(s);
}
}