package com.openMap1.mapper.mapping; import javax.xml.xpath.XPathExpressionException; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import java.util.Vector; import org.eclipse.emf.ecore.EObject; import com.openMap1.mapper.core.Xpth; import com.openMap1.mapper.core.NamespaceSet; import com.openMap1.mapper.core.MapperException; import com.openMap1.mapper.util.GenUtil; import com.openMap1.mapper.util.ModelUtil; import com.openMap1.mapper.util.XMLUtil; import com.openMap1.mapper.util.XPathAPI; import com.openMap1.mapper.AssocEndMapping; import com.openMap1.mapper.AssocMapping; import com.openMap1.mapper.CrossCondition; import com.openMap1.mapper.Mapping; import com.openMap1.mapper.MappingCondition; import com.openMap1.mapper.ObjMapping; import com.openMap1.mapper.PropMapping; import com.openMap1.mapper.ValueCondition; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; //-------------------------------------------------------------------------------------- // Inner class for conditions involved in mappings //-------------------------------------------------------------------------------------- /* There are five types of condition: 'when' conditions (a) if a node represents objects of class conditionally, this is defined by e.g.: <me:when objectToLeftValue = '@age' test = 'gt' rightValue = '30'/> (b) if a node represents a property conditionally, this is defined by e.g.: <me:when propertyToLeftValue = '../@pName' rightValue = 'surname' > (c) if a node represents an association conditionally, this is defined by e.g.: <me:when associationToLeftValue = '../@aName' rightValue = 'godFather' > link conditions (d) when the representation of a property has value-sharing link conditions, e.g: <me:link propertyToLeftValue = "." objectToRightValue ="@name"/> (e) when the representation of an association has value-sharing link conditions, e.g: <me:link associationToLeftValue = "." objectToRightValue ="@name"/> These all represent a test in which a left-hand side (LHS) value is got from a node and compared with a right-hand side (RHS)value, which is either a constant or got from a node. {object/property/association}ToLeftValue is the path from the current node to the node containing the left value ('current node' = node representing the object, property or association) test is the test to apply. It can be '=', 'contains', 'contained', 'gt', 'le', etc. The default for test is '='. rightValue is the constant value to compare leftValue with, or right Path is the path from some other node (i.e. for associations, it is one of the two object-representing nodes) to get the right-hand value. */ /** * superclass of the two types of condition on a mapping - * value conditions and cross conditions. * * This is a wrapper class for the Mapping Model class MappingCondition */ public abstract class Condition { /** the Mapper model object which this is a wrapper for */ public MappingCondition mappingCondition() {return mappingCondition;} protected MappingCondition mappingCondition; /** @return absolute path to the node at the start of a relative path; the * value of the node at the end of that relative path is tested. * Every condition filters a set of nodes. For all when conditions, * and most uses of link conditions, his is the path from the root * to that set of nodes. */ public Xpth rootToLHSNode() {return rootToLHSNode;} protected Xpth rootToLHSNode; /** relative path from the node of the mapping to the node holding the LHS value */ public Xpth lhsEndToLeftValue() {return lhsEndToLeftValue;} protected Xpth lhsEndToLeftValue; /* relative path from node representing /** absolute path from the root to the node holding the LHS value */ public Xpth rootToLeftValue() {return rootToLeftValue;} protected Xpth rootToLeftValue; // path from root to node carrying LHS value /** the namespace set of the set of mappings */ public NamespaceSet NSSet() {return NSSet;} protected NamespaceSet NSSet; /** the test to be applied between LHS and RHS values, eg LHS contains RHS */ public String test() {return test;} protected String test; protected Mapping map; protected int type; // 1,2,3 for object, property, association conditions /** further when-conditions on the node reached at the end of the path (LHS) */ protected Vector<whenCondition> leftPathWhenConditions = new Vector<whenCondition>(); /** further link-conditions between the node reached at the end of the LHS path * (LHS of nested condition) and the node at the start of the LHS path * (RHS of nested condition) */ protected Vector<linkCondition> leftPathLinkConditions = new Vector<linkCondition>(); public String getLeftFunction() {return mappingCondition.getLeftFunction();} //------------------------------------------------------------------------------------------ // Constructors //------------------------------------------------------------------------------------------ /** * constructor from mapping model objects, for a Condition nested immediately * inside a mapping * @param mc the mapping condition */ public Condition(MappingCondition mc) throws MapperException { this.mappingCondition = mc; // this constructor is for conditions nested inside mappings EObject cont = mc.eContainer(); if (!(cont instanceof Mapping)) throw new MapperException("Container of a condition is a " + cont.getClass().getName()); map = (Mapping)cont; if (map instanceof ObjMapping) type = 1; else if (map instanceof PropMapping) type = 2; else if (map instanceof AssocMapping) type = 3; else if (map instanceof AssocEndMapping) type = 3; NSSet = ModelUtil.getGlobalNamespaceSet(map); // this condition filters the nodes picked out by the root path of the mapping rootToLHSNode = map.getRootXPath(); // the relative path from them to some value-providing node is provided from the mapper editor lhsEndToLeftValue = new Xpth(NSSet,mc.getLeftPath()); // The absolute path to the value-providing node is computed from these two rootToLeftValue = lhsEndToLeftValue.crossToRootPath(rootToLHSNode); test = mc.getTest().getLiteral(); makeLeftPathConditions(); } /** * constructor for a Condition nested inside a Condition * @param mc the mapper model condition that this is a wrapper for * @param parentCondition: this condition is a filter on the nodes used to provide values when * testing the parent condition * @param rootToFilteredNodes the XPath from the root to the set of nodes which this * condition filters */ public Condition(MappingCondition mc, Condition parentCondition, Xpth rootToFilteredNodes) throws MapperException { this.mappingCondition = mc; // this constructor is for conditions nested inside mappings EObject cont = mc.eContainer(); if (!(cont instanceof MappingCondition)) throw new MapperException("Container of a nested condition is a " + cont.getClass().getName()); type = parentCondition.type; NSSet = parentCondition.NSSet; rootToLHSNode = rootToFilteredNodes; lhsEndToLeftValue = new Xpth(NSSet,mc.getLeftPath()); rootToLeftValue = lhsEndToLeftValue.crossToRootPath(rootToLHSNode); test = mc.getTest().getLiteral(); makeLeftPathConditions(); } /** * Make the nested whenConditions and linkConditions on the LHS path. * These conditions are used to filter the LHS value-providing nodes. */ private void makeLeftPathConditions() throws MapperException { for (Iterator<MappingCondition> it = mappingCondition.getLeftPathConditions().iterator();it.hasNext();) { MappingCondition mapCon = it.next(); /* a child condition filters the nodes that provide the LHS values of this condition, * by taking some relative path from them and comparing the value to a constant */ if (mapCon instanceof ValueCondition) leftPathWhenConditions.add(new whenCondition((ValueCondition)mapCon,this,rootToLeftValue)); /* a child condition filters the nodes that provide the LHS values of this condition, * by taking some relative path from them and comparing the value to a value * got by taking some other relative path from the filtered nodes of this condition */ if (mapCon instanceof CrossCondition) leftPathLinkConditions.add(new linkCondition((CrossCondition)mapCon,this,rootToLeftValue,rootToLHSNode)); } } //------------------------------------------------------------------------------------------- // Following paths to get values to compare //------------------------------------------------------------------------------------------- /** * Follow an XPath from a node to get a string value to compare * with a constant (value condition) or with the value from some other node (cross condition). * Apply any conditions on the end node of the path to filter the set of end nodes * * The following rules apply: * If no node is found, the value returned is "" * If more than one node is found, and one of those is the start node, then the start * node is removed from the set * (the case of two nodes including the start node arises in XMI) * If the remaining set has more than one node, the first is chosen * * @param start the start node * @param path the XPath to follow * @param context the namespace context for the XPath to use * @param pathConditions conditions to apply to the end nodes to reduce the * set of end nodes (hopefully to one) * @return the string value for comparison */ protected String getStringValueAtEndOfPath(Node start, Xpth path, List<whenCondition> pathWhenConditions, List<linkCondition> pathLinkConditions, NamespaceSet context) throws MapperException,XPathExpressionException { // get the initial list of nodes by following the XPath Vector<Node> nl = XPathAPI.selectNodeVector(start, path.stringForm(), context); // filter the list by applying path conditions Vector<Node> nodes = new Vector<Node>(); for (int i = 0; i < nl.size(); i++) { Node node = nl.get(i); boolean nodeOK = true; for (Iterator<whenCondition> iw = pathWhenConditions.iterator();iw.hasNext();) nodeOK = nodeOK && (iw.next().eval1(node, context)); for (Iterator<linkCondition> il = pathLinkConditions.iterator();il.hasNext();) nodeOK = nodeOK && (il.next().eval2(start,node,context)); if (nodeOK) nodes.add(node); } // apply rules above to the filtered list if (nodes.size() == 0) return ""; if (nodes.size() == 1) return XMLUtil.textValue(nodes.get(0)); if (nodes.size() > 1) for (int i = 0; i < nodes.size(); i++) { Node n = nodes.get(i); if (!(start.equals(n)))return XMLUtil.textValue(n); } return ""; } //--------------------------------------------------------------------------------------- // testing conditions, with possible function evaluation //--------------------------------------------------------------------------------------- /** * Test a general condition, which may or may not involve a function on either node, * as well as the value of that node. * For a ValueCondition, rightNode is null, rightFunction is "" * and rightValue is some constant * @param leftFunction the function expression on the left node * @param leftValue the String value of the left node * @param leftNode the left node * @param test the test used to compare the two sides * @param rightFunction the function expression on the right node * @param rightValue the String value of the right node * @param rightNode the right node * @return the result of the test; false if any test cannot be done */ boolean testCondition(String leftFunction, String leftValue, Node leftNode, String test, String rightFunction, String rightValue, Node rightNode) { boolean passes = false; // default if anything goes wrong // if the test can only be done as an integer test, do it as one if ((isOnlyIntegerTest(test)) && (isIntegerSide(leftFunction)) && (isIntegerSide(rightFunction))) { int iLeft = evalAsInteger(leftFunction, leftValue, leftNode); int iRight = evalAsInteger(rightFunction, rightValue, rightNode); passes = tryIntegerValues(iLeft,test,iRight); } /* if the test is equality and there are no functions, do it as a string test * (that will work for node values which are integers as well) */ else if ((test.equals("=")) && (noFunction(leftFunction)) && (noFunction(rightFunction))) { passes = tryStringValues(leftValue,test,rightValue); } /* if the test is equality and there are some functions, * do it as an integer test if possible; otherwise do it as a string test */ else if (test.equals("=")) { if ((isIntegerSide(leftFunction)) && (isIntegerSide(rightFunction))) { int iLeft = evalAsInteger(leftFunction, leftValue, leftNode); int iRight = evalAsInteger(rightFunction, rightValue, rightNode); passes = tryIntegerValues(iLeft,"=",iRight); } else { String vLeft = evalAsString(leftFunction, leftValue, leftNode); String vRight = evalAsString(rightFunction, rightValue, rightNode); passes = tryStringValues(vLeft,"=", vRight); } } // other tests can only be done as a string test else if ((isStringSide(leftFunction)) && (isStringSide(rightFunction))) { String vLeft = evalAsString(leftFunction, leftValue, leftNode); String vRight = evalAsString(rightFunction, rightValue, rightNode); passes = tryStringValues(vLeft,test, vRight); } return passes; } /** * compare two integer values * @param iLeft the LHS of the comparison * @param test the test to apply * @param iRight the RHS of the comparison * @return the result of the test */ boolean tryIntegerValues(int iLeft, String test, int iRight) { boolean res = false; if (test.equals("=")) res = (iLeft == iRight); if (test.equals(">")) res = (iLeft > iRight); if (test.equals("<")) res = (iLeft < iRight); return res; } boolean tryStringValues(String vLeft, String test, String vRight) { boolean res = false; if ((vLeft != null) && (vRight != null)) { if (test.equals("=")) {res = (vLeft.equals(vRight));} else if (test.equals("!=")) {res = !(vLeft.equals(vRight));} else if (test.equals("contains")) {res = GenUtil.contains(vLeft, vRight);} else if (test.equals("contained")) {res = GenUtil.contains(vRight, vLeft);} else if (test.equals("containsAsWord")) {res = GenUtil.containsAsWord(vLeft, vRight);} else if (test.equals("containedAsWord")) {res = GenUtil.containsAsWord(vRight, vLeft);} else if (test.equals("ignoreHash")) {res = ignoreInitial(vRight, vLeft,'#');} else GenUtil.message("Test '" + test + "' is not supported yet."); } return res; } /* Test two strings for equality, ignoring a specified initial character in either of them. */ boolean ignoreInitial(String v1, String v2, char c) { boolean res = false; String w1,w2; if ((v1 != null) && (v2 !=null)) { w1 = "x"; w2 = "y"; // keep the compiler happy if (v1.charAt(0) == c) {w1 = v1.substring(1);} else {w1 = v1;} if (v2.charAt(0) == c) {w2 = v2.substring(1);} else {w2 = v2;} res = (w1.equals(w2)); } return res; } //-------------------------------------------------------------------------------------- // Functions in conditions //-------------------------------------------------------------------------------------- private String[] integerTest = {">","<"}; protected boolean isOnlyIntegerTest(String test) {return GenUtil.inArray(test, integerTest);} private String[] stringTest = {"=","contains","containedBy"}; protected boolean isStringTest(String test) {return GenUtil.inArray(test, stringTest);} /** * @param expression the expression in the LHS or RHS function of a condition * @return true if the expression can be taken as one side of an integer comparison */ protected boolean isIntegerSide(String expression) { boolean isInteger = true; // if there is no function expression,assume the node values might be cast to integers if (expression == null) return true; if (expression.equals("")) return true; StringTokenizer st = new StringTokenizer(expression," +-"); while (st.hasMoreTokens()) { String token = st.nextToken(); if (!isValidToken(token)) isInteger = false; if (isPropertyValueToken(token)) isInteger = false; if (isNodeValueToken(token)) isInteger = false; } return isInteger; } /** * @param expression * @return true if the expression implies no function - just using the node value */ boolean noFunction(String expression) { if (expression == null) return true; if (expression.equals("")) return true; return false; } /** * @param expression the expression given in a 'function' slot of a value or cross-condition; * it has been checked that this can be evaluated an an integer expression, i.e. as a set of terms * separated by '+' and '-' * @param value the value on the node, which may or may not be used in the expression * @param node the node on which the expression is being evaluated * @return the integer value of the expression */ protected int evalAsInteger(String expression, String value, Node node) { int val = 0; // no problem if the node value is not an integer - it may not be used try {val = new Integer(value).intValue();} catch (Exception ex){}; if (expression == null) return val; if (expression.equals("")) return val; if (!isIntegerSide(expression)) return val; // start evaluating a non-trivial expression int exVal = 0; int sign = 1; // sign of first term is positive, if '+' and '-' have not been encountered StringTokenizer st = new StringTokenizer(expression," +-",true); while (st.hasMoreTokens()) { String token = st.nextToken(); if (!token.equals(" ")) // ignore all spaces { // set the sign multiplier for the next token after this one if (token.equals("-")) sign = -1; else if (token.equals("+")) sign = 1; // tokens that need to be added in with the correct sign else if (isNodeValueToken(token)) exVal = exVal + sign*val; else if (isIntegerToken(token)) exVal = exVal + sign*(new Integer(token).intValue()); else if (isPositionToken(token)) exVal = exVal + sign*findPosition(token,node); else if (isLengthToken(token)) {if (value != null) exVal = exVal + sign*(value.length());} else {System.out.println("Invalid token for integer expression: '" + token + "' at node '" + node.getNodeName() + "'");} } } return exVal; } /** * @param expression the expression given in a 'function' slot of a value or cross-condition; * it has been checked that this can be evaluated an a String expression * @param value the value on the node, which may or may not be used in the expression * @param node the node on which the expression is being evaluated * @return the String value of the expression */ protected String evalAsString(String expression, String value, Node node) { if (expression == null) return value; if (expression.equals("")) return value; if (!isStringSide(expression)) return value; // start evaluating a non-trivial expression String exVal = ""; StringTokenizer st = new StringTokenizer(expression," +"); // ignore space and + while (st.hasMoreTokens()) { String token = st.nextToken(); // concatenate the text value of the node on the String if (isNodeValueToken(token)) exVal = exVal + value; // value of a property or pseudo-property, starting at the node else if (isPropertyValueToken(token)) exVal = exVal + getPropertyValue(token, node); // treat the integer length of a String as a String else if (isLengthToken(token)) {if (value != null) exVal = exVal + value.length();} // concatenate a String constant on the String else exVal = exVal + token; } return exVal; } /** * @param expression the function expression on one side of a condition * @return true if it can be used n s String comparison, i.e * - not if it contains any invalid tokens * - not if it contains '-' as a separator */ protected boolean isStringSide(String expression) { boolean isStringSide = true; if (noFunction(expression)) return true; StringTokenizer st = new StringTokenizer(expression," +-",true); while (st.hasMoreTokens()) { String token = st.nextToken(); if (!isValidToken(token)) isStringSide = false; if (token.equals("-")) isStringSide = false; } return isStringSide; } /** * Most strings are valid tokens, but: * - if the string starts with '$', it must be $class.property * - if the string starts with 'position' it must be position(nodeName). * Otherwise anything goes * @param token * @return */ private boolean isValidToken(String token) { if (token.startsWith("$")) return isPropertyValueToken(token); if (token.startsWith("position(")) return isPositionToken(token); return true; } /** * @param token * @return true if the token is of the form '$class.property' */ private boolean isPropertyValueToken(String token) { if (!token.startsWith("$")) return false; StringTokenizer st = new StringTokenizer(token.substring(1),"."); return (st.countTokens()== 2); } /** * @param token * @return true if the token is of the form 'position(nodeName)' * Node names may contain '.' but not any of (), or ' ' */ private boolean isPositionToken(String token) { if (!token.startsWith("position(")) return false; if (!token.endsWith(")")) return false; String nodeNameClose = token.substring("position(".length()); String nodeName = nodeNameClose.substring(0,nodeNameClose.length()-1); StringTokenizer parts = new StringTokenizer(nodeName,"(), "); return (parts.countTokens() == 1); } /** * @param token of the form 'position(<nodeName>)' (previously checked) * @return the node name */ private String soughtNodeName(String token) { String nodeNameClose = token.substring("position(".length()); String nodeName = nodeNameClose.substring(0,nodeNameClose.length()-1); return nodeName; } /** * @param token * @return true if the token is 'value' */ private boolean isNodeValueToken(String token) { return (token.equals("value")); } /** * @param token * @return true if the token is 'length' */ private boolean isLengthToken(String token) { return (token.equals("length")); } /** * @param token * @return true if the token can be read as an integer */ private boolean isIntegerToken(String token) { try {new Integer(token); return true;} catch (Exception ex) {return false;} } //----------------------------------------------------------------------------------------- // Evaluating the position function on a node //----------------------------------------------------------------------------------------- /** * Find the first element with name <nodeName> amongst this element and * its ancestors (ascending order) and return the position of that * element amongst its siblings of the same name * @param token a String of the form 'position(<nodeName>)' * @param el an Element * @return the position of that element amongst its siblings of the same name; * or -1 if an ancestor of the right name cannot be found * */ private int findPosition(String token, Node node) { int pos = -1; // duff return value if the node name cannot be found boolean found = false; Node current = node; /* If this node does not have the right name, ascend through ancestors. * Stop after finding the first ancestor node of the right name. * The document root element has position -1. */ while ((getParentElement(current) != null) && (!found)) { if (XMLUtil.getLocalName(current).equals(soughtNodeName(token))) { if (current instanceof Attr) pos = 1; // there can be only one attribute of given name else if (current instanceof Element) pos = siblingPosition((Element)current); found = true; } current = getParentElement(current); } return pos; } /** * * @param node * @return the parent element of an Element or attribute; or null * if this is the Document Root Element */ private Element getParentElement(Node node) { Element parent = null; if (node instanceof Attr) parent = ((Attr)node).getOwnerElement(); if (node instanceof Element) { Node parentNode = ((Element)node).getParentNode(); // if this is the document root, its parent is a Document; so leave parent null if (parentNode instanceof Element) parent = (Element)parentNode; } return parent; } /** * @param node * @return the position of an Element amongst its siblings of the same local name * positions are returned in the convention 1...N, not the java convention 0..N-1 */ private int siblingPosition(Element el) { int pos = -1; Element parent = (Element)el.getParentNode(); // use the same (local) name as XMLUtil uses to get the children Vector<Element> siblings = XMLUtil.namedChildElements(parent, XMLUtil.getLocalName(el)); for (int i = 0; i < siblings.size();i++) if (siblings.get(i).equals(el)) pos = i + 1; return pos; } //----------------------------------------------------------------------------------------- // Evaluating a property or pseudo-property on a node //----------------------------------------------------------------------------------------- /** * TODO: not implemented yet */ private String getPropertyValue(String token, Node node) { StringTokenizer st = new StringTokenizer(token.substring(1),"."); if (st.countTokens() != 2) return ""; // String className = st.nextToken(); // String propName = st.nextToken(); return ""; } }