package com.openMap1.mapper.health.cda;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.Vector;
import com.openMap1.mapper.core.MapperException;
/**
* Represents a node in a tree of connectives AND, OR or NOT
* which the test attribute of a Schematron <assert> element represents,
* as part of an assertion
*
* @author robert
*
*/
public class TestNode {
private boolean tracing = false;
// Possible connectives for a node
public static int AND = 0;
public static int OR = 1;
public static int NOT = 2;
public static int COUNT = 3;
public static int POSITION = 4;
public static int STRING_EQUALITY = 5;
public static int NODE_EQUALITY = 6;
public static int XPATH = 7;
public static int STEP = 8;
public static int CONTAINS = 9;
public static int STRING_LENGTH = 10;
public static int UNKNOWN = 11;
private String[] connective = {"AND","OR","NOT","COUNT",
"POS","STRING=","NODE=","XPATH","STEP",
"CONTAINS","STRING_LENGTH","UNKNOWN"};
// possible axes for a step
public static int UNDEFINED = 0;
public static int SELF = 1;
public static int ANCESTOR = 2;
public static int PARENT = 3;
public static int CHILD = 4;
public static int DESCENDANT = 5;
public static int ANY = 6;
public static int ROOT = 7;
public static int ATTRIBUTE = 8;
private String[] axisText = {"UNDEFINED","SELF","ANCESTOR",
"PARENT","CHILD","DESCENDANT","ANY","ROOT","ATTRIBUTE"};
/**
* @return for a STEP TestNode, the axis of the step
*/
public int axis() {return axis;}
private int axis = UNDEFINED;
/**
* @return for a STEP TestNode, the node test
*/
public String nodeTest() {return nodeTest;}
private String nodeTest = "node()"; // the node test that always passes
/**
* @return for a STEP TestNode,
* true if the name of the node is constrained in this step
*/
public boolean hasNodeTest()
{
if (nodeTest.equals("node()")) return false;
if (nodeTest.equals("*")) return false;
return true;
}
/**
* @return for an EQUALITY TestNode , the String value that the node at the end of the XPath is equal to
*/
public String rhsValue() {return rhsValue;}
private String rhsValue = "";
/**
* @return for a COUNT TestNode, the integer that the count of nodes is compared to
*/
public int integerConstant() {return integerConstant;}
private int integerConstant = 0;
/**
* @return for a COUNT TestNode, the relation used to compare the number of nodes with the integer constant.
* Can be '=', '<=' , '>=' , '<' , '>'
*/
public String relation() {return relation;}
private String relation = "";
// this TestNode has one connector connecting its child TestNodes
public int connector() {return connector;}
private int connector = UNKNOWN; // until proved otherwise
/**
* @return child nodes connected by the connector , or otherwise used (e.g. steps of an XPath)
*/
public Vector<TestNode> childNodes() {return childNodes;}
private Vector<TestNode> childNodes = new Vector<TestNode>();
public int children() {return childNodes().size();}
/**
* @return a child TestNode
*/
public TestNode child(int c) {return childNodes().get(c);}
public String stringForm() {return stringForm;}
private String stringForm;
//-------------------------------------------------------------------------------------------
// Constructors
//-------------------------------------------------------------------------------------------
public TestNode(String test)
throws MapperException
{
trace("Test Node '" + test + "'");
stringForm = test;
// strip off outer brackets and spaces
String stripped = stripBrackets(test);
// deal with outermost 'and' or 'or'
if (handleOuterANDOR(stripped)) {}
// deal with outermost 'not'
else if (handleNOT(stripped)) {}
// specific constructs such as count, contains, '='
else if (handleCondition(stripped)) {}
//parse an XPath with one or more steps
else handleXPath(stripped);
}
/**
* Constructor when the test string is known to contain just a single step of an XPath
* @param test
* @param knownToBeStep should always be true
* @throws MapperException
*/
public TestNode(String test, boolean knownToBeStep) throws MapperException
{
trace ("Single step TestNode '" + test + "'");
stringForm = test;
// strip off outer brackets and spaces
String stripped = stripBrackets(test);
handleSingleStep(stripped);
}
//-------------------------------------------------------------------------------------------
// Access methods testing structure
//-------------------------------------------------------------------------------------------
public boolean isLeaf() {return (childNodes.size() == 0);}
/**
* true if this assertion helps define the set of nodes the rule applies to
* (1) an XPath self::(nodeName), (possibly followed by [] conditions on descendant nodes)
* (2) an XPath parent::(nodeName)
* (3) an XPath ../(nodeName)
* (4) an XPath ancestor::(nodeName)
* (5) an AND of OR of TestNodes , some of which are XPATHs with first axis SELF, PARENT or ANCESTOR
*/
public boolean definesNode()
{
if ((connector == XPATH) && !isLeaf())
{
TestNode step0 = childNodes.get(0);
if (childNodes.size() == 1)
{
if (step0.axis == SELF) return true; // there may be [] conditions after the self test
if (step0.axis == PARENT) return true;
if (step0.axis == ANCESTOR) return true;
}
if (childNodes.size() == 2)
{
TestNode step1 = childNodes.get(0);
if ((step0.axis == PARENT) && step0.isLeaf()&&
(step1.axis == CHILD) && step1.isLeaf()) return true;
}
}
// check for any AND of terms; if one of them is defining ,the combination is defining
else if (connector == AND)
{
for (int c = 0; c < childNodes.size();c++)
{
TestNode child = childNodes.get(c);
if (child.definesNode()) return true;
}
}
// check for any OR of terms; if they are all defining ,the combination is defining
else if (connector == OR)
{
boolean defining = true;
for (int c = 0; c < childNodes.size();c++)
{
TestNode child = childNodes.get(c);
defining = defining && (child.definesNode());
}
return defining;
}
return false;
}
/**
* @return true if this assertion leads to some structural constraints on the subtree
* beneath its context node
* (1) constrains the string values of some nodes found by definite paths
* (2) constrains the min cardinality to be 1 down some definite path
* (3) constrains the max cardinality to be 1 down some definite path
*/
public boolean constrainsSubtree()
{
if (constrainsStringValues()) return true;
if (constrainsMinCardinality()) return true;
if (constrainsMaxCardinality()) return true;
return false;
}
/**
* @return true if this TestNode constrains the string values of a number of
* fully defined nodes in a 'narrow tree' down from the context node. Two cases:
* (1) The main XPath (trunk of the tree) must be made only of CHILD steps.
* Each step may have any number of [] constraints on it. At least one of
* these [] constraints must consist another 'pure child' path,
* leading to a node whose value is equal to a string constant
* (2) the TestNode is a STRING_EQUALITY with a pure child XPsth
*/
public boolean constrainsStringValues()
{
boolean constrainsValues = false;
/* (1) the path must have only CHILD steps, but some of them must have
* constraints which set one value on a descendant node
* (other constraints might allow two or more values; ignore them) */
if ((isPureChildPath(false)) && (!isPureChildPath(true)))
{
constrainsValues = true;
int constrainedValues = 0;
for (int s = 0; s < childNodes.size();s++)
{
TestNode step = childNodes.get(s); // because of isPureChildPath(), known to be a STEP with axis CHILD
for (int c = 0; c < step.childNodes.size(); c++)
{
TestNode constraint = step.childNodes.get(c);
if (constraint.connector() == STRING_EQUALITY)
{
TestNode sidePath = constraint.childNodes.get(0); // path to the constrained node
if (sidePath.isPureChildPath(true)) constrainedValues++;
}
}
}
if (constrainedValues == 0) constrainsValues = false; // no simple constraints have been found
}
// (2) STRING_EQUALITY of a pure child path
else if (connector == STRING_EQUALITY)
{
TestNode xpath = childNodes.get(0);
constrainsValues = xpath.isPureChildPath(true);
}
return constrainsValues;
}
/**
* @return all the constraints that this assertion implies on values of attributes
*/
public Vector<AttributeValueConstraint> getStringValueConstraints()
{
Vector<String> associationPath = new Vector<String>();
return getStringValueConstraints(associationPath);
}
/**
* @return all the constraints that this assertion implies on values of attributes
* @param contextFromTemplate TestNode expressing the context of the rule from the template node;
* this is an XPATH whose first CHILD step checks the templateID element
*/
public Vector<AttributeValueConstraint> getStringValueConstraints(TestNode contextFromTemplate)
{
Vector<String> associationPath = new Vector<String>();
Vector<AttributeValueConstraint> constraints = new Vector<AttributeValueConstraint>();
if (contextFromTemplate.isPureChildPath(false))
{
// ignore the first step, which only checks the template id
for (int s = 1; s < contextFromTemplate.childNodes().size();s++)
{
TestNode step = contextFromTemplate.childNodes().get(s);
associationPath.add(step.nodeTest());
}
constraints = getStringValueConstraints(associationPath);
}
return constraints;
}
/**
* @return all the constraints that this assertion implies on values of attributes
* @param associationPath the path from the template node to the rule node
*/
private Vector<AttributeValueConstraint> getStringValueConstraints(Vector<String> associationPath)
{
Vector<AttributeValueConstraint> constraints = new Vector<AttributeValueConstraint>();
/* Case (1): the path must have only CHILD steps, but some of them must have
* constraints which set one value on a descendant node
* (other constraints might allow two or more values; ignore them) */
if ((isPureChildPath(false)) && (!isPureChildPath(true)))
{
for (int s = 0; s < childNodes.size();s++)
{
TestNode step = childNodes.get(s); // because of isPureChildPath(), known to be a STEP with axis CHILD
associationPath.add(step.nodeTest);
for (int c = 0; c < step.childNodes.size(); c++)
{
TestNode constraint = step.childNodes.get(c);
if (constraint.connector() == STRING_EQUALITY)
{
TestNode sidePath = constraint.childNodes.get(0); // path to the constrained node
if (sidePath.isPureChildPath(true))
{
AttributeValueConstraint avc = new AttributeValueConstraint(associationPath,constraint.rhsValue);
for (int d = 0; d < sidePath.childNodes.size(); d++)
{
TestNode node = sidePath.childNodes.get(d);
if (node.axis == CHILD) avc.addAssociationStep(node.nodeTest());
else if (node.axis == ATTRIBUTE) avc.setAttName(node.nodeTest());
}
constraints.add(avc);
}
}
}
}
}
// Case (2): STRING_EQUALITY of a pure child path
else if (connector == STRING_EQUALITY)
{
TestNode xpath = childNodes.get(0);
if (xpath.isPureChildPath(true))
{
AttributeValueConstraint avc = new AttributeValueConstraint(associationPath,rhsValue);
for (int d = 0; d < xpath.childNodes.size(); d++)
{
TestNode node = xpath.childNodes.get(d);
if (node.axis == CHILD) avc.addAssociationStep(node.nodeTest());
else if (node.axis == ATTRIBUTE) avc.setAttName(node.nodeTest());
}
constraints.add(avc);
}
}
return constraints;
}
/**
* @return true if this assertion constrains the min cardinality to be 1
* down some path from the node
* (1) An XPath made of only child steps, with no substructure on any step, defines that the
* final node of the path must exist
* (2) a COUNT on an XPATH with all child steps, no constraint on any step, which rules out maxOccurs = many
*/
public boolean constrainsMinCardinality()
{
// a pure child XPath implies the final node must exist
if (isPureChildPath(true)) return true;
// a COUNT which rules out zero, based on a pure child XPath (possibly with conditions on the steps)
if (rulesOutZero())
{
TestNode path = childNodes.get(0);
if (path.isPureChildPath(false)) return true;
}
return false;
}
/**
* @param context the full context path to a final node
* @param ruleStep the step number of the context to which the rule applies
* @return true if the rule implies the final node must exist, for certain specialised form of assertion:
* (1) an XPath of all CHILD steps (which may have [] conditions; these do not affect the test)
* (2) a COUNT > 0 of an XPath of all CHILD steps (which may have [] conditions)
*/
public boolean finalNodeMustExist(TemplatedPath context, int ruleStep)
{
// if the rule is on the final step of the context, it cannot imply that node exists
if (ruleStep < context.length() -1)
{
// if the test is an XPath, every step to the final node must match names
if (connector == XPATH)
{
return matchesContext(context, ruleStep);
}
// if a COUNT rules out zero, its XPath must match names on every step to the final node
else if ((connector == COUNT) && (rulesOutZero()))
{
return childNodes().get(0).matchesContext(context, ruleStep);
}
}
return false;
}
/**
* @param context the full context path to a final node
* @param ruleStep the step number of the context to which the rule applies
* @return true if the rule implies the final node must be single, for certain specialised form of assertion:
* (2) a COUNT < N of an XPath of all CHILD steps (which cannot have [] conditions)
*/
public boolean finalNodeMustBeSingle(TemplatedPath context, int ruleStep)
{
// if the rule is on the final step of the context, it cannot imply that node exists
if (ruleStep < context.length() -1)
{
// if a COUNT rules out many, its XPath must match names on every step to the final node
if ((connector == COUNT) && (rulesOutMany()))
{
TestNode xpath = childNodes().get(0);
boolean constrains = xpath.matchesContext(context, ruleStep);
/* If any of the steps of the XPath has [] conditions, the
* XPath does not constrain max cardinality */
for (Iterator<TestNode> ic = xpath.childNodes().iterator();ic.hasNext();)
if (ic.next().childNodes().size() > 0) constrains = false;
return constrains;
}
}
return false;
}
/**
* @param context a CDAContext
* @param ruleStep step at which the rule applies
* @return for an XPATH, true if every step of the XPath as far as the final step of the context
* is a CHILD and matches the name of the context step.
* The XPath may or may not go beyond the context.
*/
public boolean matchesContext(TemplatedPath context, int ruleStep)
{
boolean matches = true;
// if the XPath will run out of steps before the end of the context, it cannot match
if (ruleStep + childNodes().size() < context.length()-1) matches = false;
// iterate over context steps which must be matched, to the final node
else for (int s = ruleStep + 1; s < context.length();s++)
{
ContextStep step = context.step(s);
int assertStep = s - ruleStep -1; // steps 0...N of the XPath
TestNode pathStep = childNodes().get(assertStep);
if (pathStep.axis() != CHILD) matches = false; // must be all CHILD or ATTRIBUTE steps
if (!(pathStep.nodeTest().equals(step.associationName()))) matches = false;
}
return matches;
}
/**
* @param context a CDAContext
* @param ruleStep step at which the rule applies
* @return for an XPATH, true if every step of the XPath as far as the final step of the context
* is a CHILD and matches the name of the context step, and
* EITHER the XPath goes beyond the context to constrain a node in the subtree below it.
* OR the XPath has a [] on the last step of the context
*/
public boolean extendsContext(TemplatedPath context, int ruleStep)
{
/* (ruleStep + childNodes().size() == context.length()-1) is the case
* where the assertion XPath ends on the final node of the context -
* e.g. ruleStep = 0, childNodes().size() = 1, context.length() = 2 */
if ((matchesContext(context,ruleStep)))
{
// if the rule's XPath goes beyond the context
if (ruleStep + childNodes().size() > context.length()-1) return true;
// if the rule's final step (at the end of the context) has a [] condition
else if (childNodes.get(childNodes().size()-1).childNodes().size() > 0) return true;
}
return false;
}
/**
* @return true if this assertion constrains some node or nodes in the subtree
* below the final node of the context. Cases included:
* (1) XPath with all child steps matching the context, and some steps beyond it
* (2) Multiplicity-constraining COUNT of such an XPath
* (3) String equality of some XPath
*/
public boolean constrainsSubtreeBeneath(TemplatedPath context, int ruleStep)
{
if (connector == STRING_EQUALITY)
return childNodes().get(0).extendsContext(context, ruleStep);
else if ((connector == COUNT) && (constrainsMaxCardinality()|constrainsMinCardinality()))
return childNodes().get(0).extendsContext(context, ruleStep);
else if (connector == XPATH)
return extendsContext(context, ruleStep);
else return false;
}
/**
* @return true if this assertion constrains the max cardinality to be 1
* down some path from the node
* (1) a COUNT on an XPATH with all child steps, no constraint on any step, which rules out maxOccurs = many
*/
public boolean constrainsMaxCardinality()
{
// a COUNT which rules out many, based on a pure child XPath
if (rulesOutMany())
{
TestNode path = childNodes.get(0);
if (path.isPureChildPath(true)) return true;
}
return false;
}
/**
* @return true if a COUNT constraint does not allow zero
*/
public boolean rulesOutZero()
{
if (connector == COUNT)
{
if ((relation.equals("=")) && (integerConstant > 0)) return true;
if ((relation.equals(">=")) && (integerConstant > 0)) return true;
if (relation.equals(">")) return true;
}
return false;
}
/**
* @return true if a COUNT constraint does not allow greater than 1
*/
public boolean rulesOutMany()
{
if (connector == COUNT)
{
if ((relation.equals("=")) && (integerConstant < 2)) return true;
if ((relation.equals("<=")) && (integerConstant < 2 )) return true;
if ((relation.equals("<")) && (integerConstant < 3 )) return true;
}
return false;
}
/**
* @param bareSteps if true, each step is required to have no constraints on it
* @return true if this is an XPATH with all steps having axis CHILD,
* except for the first step which may be SELF (as long as there are other steps)
* (a) if bareSteps = true, no step can have any other [] constraints
* (b) if bareSteps = false, any step can have other [] constraints
*/
public boolean isPureChildPath(boolean bareSteps)
{
boolean pureChild = false;
if ((connector== XPATH) && !isLeaf())
{
pureChild = true;
for (int c = 0; c < childNodes.size(); c++)
{
TestNode step = childNodes.get(c);
if (!((step.axis == CHILD)|
(step.axis == ATTRIBUTE)|
((c == 0) && (step.axis == SELF)))) pureChild = false;
if ((bareSteps) && (!step.isLeaf())) pureChild = false;
}
}
return pureChild;
}
//-------------------------------------------------------------------------------------------
// References to other templates
//-------------------------------------------------------------------------------------------
public void addTemplateReferences (Vector<String> templateRefs)
{
String templateIdName = TemplateRule.CDA_PREFIX + ":templateId";
// check this node; find the outer XPATH containing the [] condition on its last step
if ((connector == XPATH) && (childNodes.size() > 0))
{
TestNode lastStep = childNodes.get(childNodes.size() - 1);
// find the last step and check it has some [] conditions
if (lastStep.childNodes.size() > 0) for (int c = 0; c < lastStep.childNodes.size(); c++)
{
TestNode equality = lastStep.childNodes.get(c);
// find the [] condition
if (equality.connector == STRING_EQUALITY)
{
// find the XPath inside the [] and check it has some steps
TestNode innerPath = equality.childNodes.get(0);
if ((innerPath.connector == XPATH) && (innerPath.childNodes.size() > 0))
{
// find the last step and check it is '@root'
TestNode lastInnerStep = innerPath.childNodes.get(innerPath.childNodes.size() - 1);
if ((lastInnerStep.axis == ATTRIBUTE) & (lastInnerStep.nodeTest().equals("root")))
{
boolean templateConstraint = false;
// if there is a previous step on the inner XPath it must be 'templateId'
if (innerPath.childNodes.size() > 1)
{
TestNode nextInnerStep = innerPath.childNodes.get(innerPath.childNodes.size() - 2);
templateConstraint = nextInnerStep.nodeTest().equals(templateIdName);
}
// otherwise the last step of the outer path must be 'templateId'
else
{
templateConstraint = lastStep.nodeTest().equals(templateIdName);
}
if (templateConstraint) templateRefs.add(equality.rhsValue);
}
}
}
}
}
// recursive descent checking all nodes
for (Iterator<TestNode> it = childNodes.iterator(); it.hasNext();)
it.next().addTemplateReferences(templateRefs);
}
//-------------------------------------------------------------------------------------------
// Methods directly supporting constructors
//-------------------------------------------------------------------------------------------
/**
* @param test test string
* @return process the test string, looking for any 'and' or 'or' at the outer level,
* not inside any square or round brackets.
* If any are found and they don't clash with one another, make the child TestNodes
* of this TestNode and return true.
* Otherwise return false, so the test String can be re-analysed.
* @throws MapperException if not 'and and 'or' are found at the outer level.
*/
private boolean handleOuterANDOR(String test)
throws MapperException
{
String segmentText = "";
String previousSegmentText = "";
StringTokenizer st = new StringTokenizer(test," ()[]|",true);
int roundBracketDepth = 0;
int squareBracketDepth = 0;
while (st.hasMoreTokens())
{
previousSegmentText = segmentText;
String token = st.nextToken();
segmentText = segmentText + token;
// keep track of bracket nesting depth; 'and' or 'or' inside brackets don't count
if (token.equals("(")) roundBracketDepth++;
if (token.equals(")")) roundBracketDepth--;
if (token.equals("[")) squareBracketDepth++;
if (token.equals("]")) squareBracketDepth--;
// things to do if you are at the outer level, not just collecting text for some inner level
if ((roundBracketDepth ==0) & (squareBracketDepth == 0))
{
if ((token.equals("or"))|(token.equals("|")))
{
if (!mayBeOR()) throw new MapperException("Mixed AND and OR in expression '" + test + "'");
connector = OR;
addChildNode(previousSegmentText);
segmentText = "";
}
if (token.equals("and"))
{
if (!mayBeAND()) throw new MapperException("Mixed AND and OR in expression '" + test + "'");
connector = AND;
addChildNode(previousSegmentText);
segmentText = "";
}
} // end of if (outer) section
} // end of loop over tokens
// if any outer 'and' or 'or' was found, add the last child TestNode and return true.
boolean andOrFound = ((connector == AND)|(connector == OR));
if (andOrFound)
{
addChildNode(segmentText);
trace(" completed AND_OR '" + test + "'");
}
return andOrFound;
}
/**
* @param test
* @return if the test string is of the form "not()", add the child node for what is negated
* and return true.
* Otherwise return false to the test string can be analysed as an XPath
*/
private boolean handleNOT(String test) throws MapperException
{
String negatedText = "";
// FIXME: what about new lines?
StringTokenizer st = new StringTokenizer(test," ()[]",true);
int roundBracketDepth = 0;
int squareBracketDepth = 0;
while (st.hasMoreTokens())
{
String token = st.nextToken();
negatedText = negatedText + token;
// keep track of bracket nesting depth; 'not' inside brackets doesn't count
if (token.equals("(")) roundBracketDepth++;
if (token.equals(")")) roundBracketDepth--;
if (token.equals("[")) squareBracketDepth++;
if (token.equals("]")) squareBracketDepth--;
// to do if you are at the outer level, not just collecting text for an inner level
if ((roundBracketDepth ==0) && (squareBracketDepth == 0))
{
if (token.equals("not"))
{
connector = NOT;
negatedText = ""; // start the negated text - usually has brackets
}
} // end of outside () section
} // end of loop over tokens
boolean notFound = (connector == NOT);
if (notFound)
{
addChildNode(negatedText);
trace("recognised NOT '" + test + "'");
}
return notFound;
}
/**
* @param stripped a String
* recognise a condition, whose form is either <XPath> = 'String' or
* count(<XPath>) relation integer.
* In either case
* @return
*/
private boolean handleCondition(String stripped) throws MapperException
{
trace("trying condition '" + stripped + "'");
boolean relationFound = false;
// test for initial 'count' or 'position'
String[] intFunction = {"contains","count","position","string-length"};
int[] intConnector = {CONTAINS,COUNT,POSITION,STRING_LENGTH};
for (int i = 0; i < intFunction.length; i++)
{
if (stripped.startsWith(intFunction[i])) // one of the strings above
{
connector = intConnector[i];
if (i == 0) relationFound = true; // 'contains' requires no relation such as '='
StringTokenizer st = new StringTokenizer(stripped.substring(intFunction[i].length())," ()",true);
String inBrackets = "";
String previousInBrackets = "";
int bracketLevel = 0;
int charsRead = intFunction[i].length(); // accounts for 'count', 'position' etc.
while (st.hasMoreTokens())
{
String token = st.nextToken();
previousInBrackets = inBrackets;
inBrackets = inBrackets + token;
charsRead = charsRead + token.length();
// build up and process the expression in brackets
if (token.equals("("))
{
bracketLevel++;
if (bracketLevel == 1) inBrackets = "";
}
if (token.equals(")"))
{
bracketLevel--;
if (bracketLevel == 0)
{
addChildNode(previousInBrackets); // may be empty - if so, no node added
String remainder = stripped.substring(charsRead);
trace("Remainder: " + remainder);
relation = "";
StringTokenizer rm = new StringTokenizer(remainder,"=<> ",true);
while (rm.hasMoreTokens())
{
String bit = rm.nextToken();
if (bit.equals(" ")){}
else if (bit.equals("="))
{
if (relation.equals(">")) relation = ">=";
else if (relation.equals("<")) relation = "<=";
else relation = "=";
}
else if (bit.equals(">")) relation = bit;
else if (bit.equals("<")) relation = bit;
else try
{
trace("Integer: " + bit);
integerConstant = new Integer(bit).intValue();
}
catch (Exception ex)
{
relation=""; // to set relationFound false later
// don't want to throw an exception on a 'count = count' constraint
// throw new MapperException("Found no integer constant in '" + stripped + "'");
}
} // end of loop over remainder tokens
relationFound = ((!relation.equals(""))|(i==0)); // 'contains' needs no relation
// if (!relationFound) {System.out.println("Found no relation in '" + stripped + "'");}
} // end of bracketLevel = 0 section
}// end of token = ')' section
} // end of loop over tokens
} // end of section where string starts with 'contains', 'count' etc
} // end of loop over i to look for 'count' and 'position'
// otherwise, look for an '=' not in square brackets
if (connector == UNKNOWN)
{
StringTokenizer st = new StringTokenizer(stripped,"[]=", true);
int bracketLevel = 0;
String xPath = "";
while (st.hasMoreTokens())
{
String token = st.nextToken();
xPath = xPath + token;
if (token.equals("[")) bracketLevel++;
if (token.equals("]")) bracketLevel--;
if ((bracketLevel ==0) && (token.equals("=")))
{
relationFound = true;
xPath = xPath.substring(0,xPath.length()-1); // strip off final '='
addChildNode(xPath);
String remainder = stripped.substring(xPath.length() + 1);
try {
rhsValue = stripQuotes(remainder);
connector = STRING_EQUALITY;
}
catch(Exception ex) // remainder has no quotes
{
addChildNode(remainder);
connector = NODE_EQUALITY;
}
trace("Equality between '" + xPath + " ' and '" + remainder + "'");
}
}
}
if (!relationFound) trace ("No relation found in '" + stripped + "'");
return relationFound;
}
/**
* @param stripped a String with no outer 'and', 'or' , 'not' or round brackets,
* and which is not an equality or a count relation
* parse it as an XPath, of steps separated by outer '/' (not inside [])
*/
private void handleXPath(String stripped) throws MapperException
{
trace("XPATH '" + stripped + "'");
connector = XPATH;
StringTokenizer st = new StringTokenizer(stripped,"/[]",true);
String stepText = "";
String previousStepText = "";
int squareBracketDepth = 0;
// initial './/' denotes a descendant step from the current node
if (stripped.startsWith(".//"))
{
childNodes.add(new TestNode("descendant::node()",true)); // true mean it must be a step
st.nextToken();st.nextToken();st.nextToken(); // consume the '.' and two '/'
}
// initial '//' denotes a descendant step from the root node
else if (stripped.startsWith("//"))
{
childNodes.add(new TestNode("ROOT",true)); // true means it must be a step
childNodes.add(new TestNode("descendant::node()",true)); // true mean it must be a step
st.nextToken();st.nextToken(); // consume the two '/'
}
// initial '/' not followed by '/' denotes a ROOT step
else if (stripped.startsWith("/"))
{
childNodes.add(new TestNode("ROOT",true)); // true means it must be a step
st.nextToken(); // consume the '/'
}
while (st.hasMoreTokens())
{
String token = st.nextToken();
previousStepText = stepText;
stepText = stepText + token;
// keep track of bracket nesting depth; '/' inside square brackets doesn't count
if (token.equals("[")) squareBracketDepth++;
if (token.equals("]")) squareBracketDepth--;
// to do if you are at the outer level, not just collecting text for an inner level
if ((squareBracketDepth ==0) && (token.equals("/")))
{
// two consecutive '/' are a descendant step
if (previousStepText.equals("")) previousStepText = "descendant::node()";
childNodes.add(new TestNode(previousStepText,true));
stepText = ""; // re-initialise text for the next step
} // end of 'outside any square bracket' section
} // end of loop over tokens
// add the last step (which might be the first)
childNodes.add(new TestNode(stepText,true));
}
/**
* Handle a single step.
* Text is the node test, followed by any number of [].
* Set variables for the axis and node name, with child TestNode objects for each [] test.
* @param stripped
*/
private void handleSingleStep(String stripped) throws MapperException
{
trace("STEP '" + stripped + "'");
connector = STEP;
// special case where '/' was found at the start of the XPath
if (stripped.equals("ROOT"))
{
axis = ROOT;
return;
}
StringTokenizer st = new StringTokenizer(stripped,"[]", true);
int squareBracketDepth = 0;
String preBracket = st.nextToken(); // text before the first '['
handleNodeTest(preBracket);
String bracketText = "";
String previousBracketText = "";
while (st.hasMoreTokens())
{
String token = st.nextToken();
previousBracketText = bracketText;
bracketText = bracketText + token;
if (token.equals("["))
{
squareBracketDepth++;
// start collecting text inside an outer bracket
if (squareBracketDepth == 1) bracketText = "";
}
else if (token.equals("]"))
{
squareBracketDepth--;
// process text inside an outer bracket
if (squareBracketDepth == 0) addChildNode(previousBracketText);
}
}
}
/**
*
* @param preBracket the initial part of an Path step, with any trailing '[]' or '/' removed
*/
private void handleNodeTest(String preBracket)
{
if (preBracket.equals("*")) axis = CHILD; // leave nodeTest as 'node()'
else if (preBracket.startsWith("self::"))
{
axis = SELF;
nodeTest = preBracket.substring(6);
}
else if (preBracket.equals(".")) axis = SELF; // leave nodeTest as 'node()'
else if (preBracket.equals("..")) axis = PARENT; // leave nodeTest as 'node()'
else if (preBracket.startsWith("parent::"))
{
axis = PARENT;
nodeTest = preBracket.substring(8);
}
else if (preBracket.startsWith("ancestor::"))
{
axis = ANCESTOR;
nodeTest = preBracket.substring(10);
}
else if (preBracket.startsWith("descendant::"))
{
axis = DESCENDANT;
nodeTest = preBracket.substring(12);
}
else if (preBracket.startsWith("child::"))
{
axis = CHILD;
nodeTest = preBracket.substring(7);
}
else if (preBracket.startsWith("@"))
{
axis = ATTRIBUTE;
nodeTest = preBracket.substring(1);
}
else
{
axis = CHILD;
nodeTest = preBracket;
}
}
//-------------------------------------------------------------------------------------------
// Utility Methods
//-------------------------------------------------------------------------------------------
/**
* add a child TestNode if there is any test to make it from
*/
private void addChildNode(String text) throws MapperException
{
if (!text.equals("")) childNodes.add(new TestNode(text));
}
private boolean mayBeOR() {return ((connector == UNKNOWN)|(connector == OR));}
private boolean mayBeAND() {return ((connector == UNKNOWN)|(connector == AND));}
/**
* Strip outer round brackets, square brackets, and any spaces outside them from a test string
* For this, it needs to detect whether an initial '(' is matched at any time before
* the final ')'; and only remove the two if they are not matched in between;
* i.e not strip the outer brackets from '(fred) and (joe)'
* @param test
* @return
*/
private String stripBrackets(String test)
{
char first = ' ';
char last = ' ';
int start = 0; // to be the position of first non-blank character
int end = test.length() - 1; // to be the position of last non-blank character
boolean firstNonBlank = false;
boolean lastNonBlank = false;
for (int c = 0; c < test.length(); c++)
{
char early = test.charAt(c);
char late = test.charAt(test.length() - 1 - c);
// find the first non-blank character and its position
if ((!firstNonBlank) && (early != ' '))
{
first = early;
start = c;
firstNonBlank = true;
}
// find the last non-blank character and its position
if ((!lastNonBlank) && (late != ' '))
{
last = late;
end = test.length() - 1 - c;
lastNonBlank = true;
}
}
// at this stage, going from start to end+1 would trim blanks off the outside
// if the first and last non-blank characters are brackets of the same kind, test if they really match
if (((first == '(') && (last == ')'))|
((first == '[') && (last == ']')))
{
boolean matching = true;
int bracketDepth = 0;
// test all characters from the first open bracket to just before the last closing bracket
for (int c = start; c < end; c++)
{
if (test.charAt(c) == first) bracketDepth++;
if (test.charAt(c) == last) bracketDepth--;
if (bracketDepth == 0) matching = false;
}
// if the bracket depth never goes zero between the two end brackets, trim them off.
if (matching) {start++; end--;}
}
String result = test;
if (firstNonBlank) result = test.substring(start,end + 1);
return result;
}
/**
* @param remainder a String that contains two double or single quotes
* @return the string between the two '"'
*/
private String stripQuotes(String remainder) throws MapperException
{
int start = -1;
int end = 0;
for (int i = 0; i < remainder.length();i++)
{
char c = remainder.charAt(i);
if ((c == '"')|(c == '\''))
{
if (start == -1) start = i;
else end = i;
}
}
if ((start == -1)|(end == 0)) throw new MapperException("Cannot find two quotes in '" + remainder + "'");
return remainder.substring(start + 1, end);
}
//-----------------------------------------------------------------------------------------------
// Testing a STEP TestNode against a ContextStep
//-----------------------------------------------------------------------------------------------
public boolean isCompatible(ContextStep contextStep)
{
// this must be a step
if (connector() == STEP)
{
// if its name is defined, it must have the same name as the context step
if ((nodeTest.equals("node()"))|(nodeTest().equals(contextStep.associationName())))
{
boolean compatible = true;
/* if this step has any other [] tests, they can only be String equalities on
* attributes, which are exactly duplicated in the context step */
for (int c = 0; c < childNodes().size(); c++)
{
TestNode child = childNodes().get(c);
boolean childOK = false;
if (child.connector() == STRING_EQUALITY)
{
TestNode path = child.childNodes().get(0);
// the XPATH can have only one step which is an attribute
if (path.childNodes().size() == 1)
{
TestNode pathStep = path.childNodes().get(0);
if (pathStep.axis == ATTRIBUTE)
{
String[] fv = new String[2];
fv[0] = pathStep.nodeTest(); // attribute name
fv[1] = child.rhsValue(); // fixed value it must have
// this step is OK only if the context step requires the same value for the same attribute
childOK = contextStep.hasFixedValue(fv);
}
}
}
if (!childOK) compatible = false;
}
return compatible; // OK if all the tests in this step have been matched in the context step
}
}
return false;
}
//-----------------------------------------------------------------------------------------------
// trivia
//-----------------------------------------------------------------------------------------------
private void trace(String s) {if (tracing) System.out.println(s);}
/**
* @return the structure tree of this TestNode
*/
public String structure()
{
String structure = connective[connector]; // 'AND', 'XPATH', etc.
if (connector == STEP) structure = axisText[axis]; // in stead of STEP, give the axis PARENT, CHILD, etc.
if (childNodes.size() > 0)
{
structure = structure + "[";
for (int i = 0; i < childNodes.size(); i++)
{
structure = structure + childNodes.get(i).structure();
if (i < childNodes.size() - 1) structure = structure + ",";
}
structure = structure + "]";
}
return structure;
}
}