| /* |
| * Copyright (c) 1998, 2020 Oracle and/or its affiliates. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0, |
| * or the Eclipse Distribution License v. 1.0 which is available at |
| * http://www.eclipse.org/org/documents/edl-v10.php. |
| * |
| * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause |
| */ |
| |
| // Contributors: |
| // Oracle - initial API and implementation from Oracle TopLink |
| package org.eclipse.persistence.sdo.helper.extension; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import org.eclipse.persistence.exceptions.ConversionException; |
| import org.eclipse.persistence.sdo.SDODataObject; |
| import org.eclipse.persistence.sdo.SDOProperty; |
| import org.eclipse.persistence.sdo.helper.SDOXMLHelper; |
| import org.eclipse.persistence.sdo.helper.XPathEngine; |
| |
| import commonj.sdo.DataObject; |
| import commonj.sdo.Property; |
| |
| /** |
| * This singleton provides support for querying DataObjects |
| * via xpath expression where the result is a List containing |
| * one or more simple types or DataObjects. |
| */ |
| public class XPathHelper { |
| private static XPathHelper instance; |
| static final int GTE = 4; |
| static final int LTE = 5; |
| static final int GT = 6; |
| static final int LT = 7; |
| static final int EQ = 8; |
| static final int NEQ = 9; |
| static final int AND = 10; |
| static final int OR = 11; |
| private static final String GTE_STR = ">="; |
| private static final String LTE_STR = "<="; |
| private static final String GT_STR = ">"; |
| private static final String LT_STR = "<"; |
| private static final String EQ_STR = "="; |
| private static final String NEQ_STR = "!="; |
| private static final String AND_STR = "and"; |
| private static final String OR_STR = "or"; |
| |
| /** |
| * Returns the one and only instance of this singleton. |
| */ |
| public static XPathHelper getInstance() { |
| if (instance == null) { |
| instance = new XPathHelper(); |
| } |
| return instance; |
| } |
| |
| /** |
| * Create and return an XPathExpression, using the provided |
| * string to create the expression. |
| * |
| * @param expression |
| * @return |
| */ |
| public XPathExpression prepareExpression(String expression) { |
| return new XPathExpression(expression); |
| } |
| |
| /** |
| * Evaluate an XPath expression in the specified context and return a List |
| * containing any types or DataObjects that match the search criteria. |
| * |
| * @param expression |
| * @param dataObject |
| * @return List containing zero or more entries |
| */ |
| public List evaluate(String expression, DataObject dataObject) { |
| List results = new ArrayList(); |
| // call into XPathEngine until all functionality is implemented |
| if (shouldCallXPathEngine(expression)) { |
| return addResultsToList(XPathEngine.getInstance().get(expression, dataObject), results); |
| } |
| |
| return evaluate(expression, dataObject, results); |
| } |
| |
| /** |
| * Evaluate an XPath expression in the specified context and populate |
| * the provided List with any types or DataObjects that match the |
| * search criteria. |
| * |
| * @param expression |
| * @param dataObject |
| * @param results |
| * @return |
| */ |
| private List evaluate(String expression, DataObject dataObject, List results) { |
| Object result; |
| int index = expression.indexOf('/'); |
| if (index > -1) { |
| if (index == (expression.length() - 1)) { |
| return addResultsToList(processFragment(expression.substring(0, index), dataObject), results); |
| } else { |
| result = processFragment(expression.substring(0, index), dataObject); |
| if (result instanceof DataObject) { |
| return evaluate(expression.substring(index + 1, expression.length()), (DataObject)result, results); |
| } else if (result instanceof List) { |
| // loop over each result, executing the remaining portion of the expression |
| for (Iterator resultIt = ((List) result).iterator(); resultIt.hasNext();) { |
| evaluate(expression.substring(index + 1, expression.length()), (DataObject)resultIt.next(), results); |
| } |
| return results; |
| } else { |
| return null; |
| } |
| } |
| } |
| return addResultsToList(processFragment(expression, dataObject), results); |
| } |
| |
| /** |
| * Process an XPath expression fragment. |
| * |
| * @param xpFrag |
| * @param dataObject |
| * @return |
| */ |
| private Object processFragment(String xpFrag, DataObject dataObject) { |
| // handle self expression |
| if (xpFrag.equals(".")) { |
| return dataObject; |
| } |
| // handle containing DataObject expression |
| if (xpFrag.equals("..")) { |
| return dataObject.getContainer(); |
| } |
| |
| // ignore '@' |
| xpFrag = getPathWithAtRemoved(xpFrag); |
| |
| // handle positional '[]' expression |
| int idx = xpFrag.indexOf('['); |
| if (idx > -1) { |
| return processBracket(xpFrag, dataObject, idx); |
| } |
| |
| // handle non-positional expression |
| Property prop = dataObject.getInstanceProperty(xpFrag); |
| try { |
| return dataObject.get(prop); |
| } catch (IllegalArgumentException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Process a positional or query XPath expression fragment. This method |
| * determines if the brackets represent a query or a positional path, |
| * and calls into the appropriate methods accordingly. |
| * |
| * @param xpFrag |
| * @param dataObject |
| * @param idx |
| * @return |
| */ |
| private Object processBracket(String xpFrag, DataObject dataObject, int idx) { |
| int closeIdx = xpFrag.indexOf(']'); |
| if (closeIdx == -1 || closeIdx < idx) { |
| return null; |
| } |
| |
| String contents = xpFrag.substring(idx + 1, closeIdx); |
| // check for integer index |
| if (contents.matches("[1-9][0-9]*")) { |
| return processIndex(xpFrag, dataObject, idx, Integer.parseInt(contents) - 1); |
| } |
| |
| // check for float index |
| if (contents.matches("[1-9].[0-9]*")) { |
| Float num = Float.valueOf(contents); |
| return processIndex(xpFrag, dataObject, idx, num.intValue() - 1); |
| } |
| |
| // check for simple/complex queries |
| String reference = xpFrag.substring(0, idx); |
| if (contents.indexOf(AND_STR) != -1 || contents.indexOf(OR_STR) != -1) { |
| return processComplexQuery(dataObject, reference, contents); |
| } |
| return processSimpleQuery(dataObject, reference, contents); |
| } |
| |
| /** |
| * Process a positional XPath expression fragment. |
| * |
| * @param xpFrag |
| * @param dataObject |
| * @param idx |
| * @param idxValue |
| * @return |
| */ |
| private Object processIndex(String xpFrag, DataObject dataObject, int idx, int idxValue) { |
| try { |
| Property prop = dataObject.getInstanceProperty(xpFrag.substring(0, idx)); |
| List dataObjects = dataObject.getList(prop); |
| if (idxValue < dataObjects.size()) { |
| return dataObjects.get(idxValue); |
| } else { |
| // out of bounds |
| throw new IndexOutOfBoundsException(); |
| } |
| } catch (IllegalArgumentException e) { |
| return null; |
| } |
| } |
| |
| /** |
| * Evaluate the query represented by the XPath Expression fragment against |
| * the DataObject. A complex query contains logical operators. |
| * |
| * @param dataObject |
| * @param reference |
| * @param bracketContents |
| * @return |
| */ |
| private Object processComplexQuery(DataObject dataObject, String reference, String bracketContents) { |
| // convert the expression to postfix notation |
| OPStack opStack = new OPStack(); |
| List expressionParts = opStack.processExpression(bracketContents); |
| |
| ArrayList queryParts = new ArrayList(); |
| // first iteration, create QueryParts from the expression, keeping the |
| // position of any 'and' / 'or' |
| for (int i=0; i<expressionParts.size(); i++) { |
| Token tok = (Token) expressionParts.get(i); |
| if (tok.getName().equals(AND_STR) || tok.getName().equals(OR_STR)) { |
| queryParts.add(tok.getName()); |
| } else { |
| // assume next three entries make up a query part |
| String propertyName = ((Token) expressionParts.get(i)).getName(); |
| String queryValue = ((Token) expressionParts.get(++i)).getName(); |
| int operator = getOperandFromString(((Token) expressionParts.get(++i)).getName()); |
| queryParts.add(new QueryPart(propertyName.trim(), queryValue, operator)); |
| } |
| } |
| |
| // get the DataObject(s) to execute the query against |
| Property prop = dataObject.getInstanceProperty(reference); |
| List objects; |
| if (prop.isMany()) { |
| objects = dataObject.getList(prop); |
| } else { |
| objects = new ArrayList(); |
| DataObject obj = dataObject.getDataObject(prop); |
| if (obj != null) { |
| objects.add(obj); |
| } |
| } |
| |
| List valuesToReturn = new ArrayList(); |
| |
| Iterator iterObjects = objects.iterator(); |
| while (iterObjects.hasNext()) { |
| SDODataObject cur = (SDODataObject)iterObjects.next(); |
| |
| // this iteration, evaluate each QueryPart against the current DataObject |
| ArrayList booleanValues = new ArrayList(); |
| for (int j=0; j<queryParts.size(); j++) { |
| if (queryParts.get(j).equals(AND_STR) || queryParts.get(j).equals(OR_STR)) { |
| // add 'and'/'or' keeping in correct order |
| booleanValues.add(queryParts.get(j)); |
| } else { |
| // assume QueryPart - evaluate and add the result |
| QueryPart qp = (QueryPart) queryParts.get(j); |
| booleanValues.add(qp.evaluate(cur)); |
| } |
| } |
| |
| // at this point we have a list of boolean values with |
| // a mix of 'and'/'or' that need to be applied to get |
| // the final result for this DataObject |
| |
| // iterate L-R, looking for 'and' / 'or' - when one is encountered, |
| // apply it to the previous two booleans, repeating this until a |
| // single result is achieved |
| for (int k=0; k<booleanValues.size(); k++) { |
| if (booleanValues.get(k).equals(AND_STR) || booleanValues.get(k).equals(OR_STR)) { |
| if (k >= 2) { |
| Boolean b1 = (Boolean) booleanValues.get(k-1); |
| Boolean b2 = (Boolean) booleanValues.get(k-2); |
| int logicalOp = getOperandFromString((String) booleanValues.get(k)); |
| booleanValues.remove(k); |
| booleanValues.remove(k-1); |
| booleanValues.set(k-2, evaluate(b1, b2, logicalOp)); |
| k=0; |
| } |
| } |
| } |
| // if there isn't a single result something went wrong... |
| if (booleanValues.size() == 1) { |
| if ((Boolean)booleanValues.get(0)) { |
| valuesToReturn.add(cur); |
| } |
| } |
| } |
| return valuesToReturn; |
| } |
| |
| private boolean evaluate(boolean b1, boolean b2, int op) { |
| switch (op) { |
| case AND: |
| return b1 && b2; |
| case OR: |
| return b1 || b2; |
| } |
| return false; |
| } |
| |
| /** |
| * Evaluate the query represented by the XPath Expression fragment against |
| * the DataObject. A 'simple' query has not logical operators. |
| * |
| * @param dataObject |
| * @param reference |
| * @param query |
| * @return |
| */ |
| private Object processSimpleQuery(DataObject dataObject, String reference, String query) { |
| Property prop = dataObject.getInstanceProperty(reference); |
| |
| List objects; |
| if (prop.isMany()) { |
| objects = dataObject.getList(prop); |
| } else { |
| objects = new ArrayList(); |
| DataObject obj = dataObject.getDataObject(prop); |
| if (obj != null) { |
| objects.add(obj); |
| } |
| } |
| |
| List valuesToReturn = new ArrayList(); |
| QueryPart queryPart = new QueryPart(query); |
| |
| Iterator iterObjects = objects.iterator(); |
| while (iterObjects.hasNext()) { |
| SDODataObject cur = (SDODataObject)iterObjects.next(); |
| |
| if (queryPart.evaluate(cur)) { |
| valuesToReturn.add(cur); |
| } |
| } |
| return valuesToReturn; |
| } |
| |
| // ----------------------------- Convenience Methods ----------------------------- // |
| |
| /** |
| * Convenience method that will add the provided object to the 'results' list |
| * if the object is non-null. If the object represents a list, each non-null |
| * entry will be added to the results list. |
| * |
| * @param obj |
| * @param results |
| * @return |
| */ |
| protected List addResultsToList(Object obj, List results) { |
| if (obj != null) { |
| if (obj instanceof List) { |
| for (Iterator resultIt = ((List) obj).iterator(); resultIt.hasNext();) { |
| Object nextResult = resultIt.next(); |
| if (nextResult != null) { |
| results.add(nextResult); |
| } |
| } |
| } else { |
| results.add(obj); |
| } |
| } |
| return results; |
| } |
| |
| /** |
| * Convenience method that strips off '@' portion, if |
| * one exists. |
| * |
| * @param expression |
| * @return |
| */ |
| protected String getPathWithAtRemoved(String expression) { |
| int index = expression.indexOf('@'); |
| if (index > -1) { |
| if (index > 0) { |
| StringBuffer sbuf = new StringBuffer(expression.substring(0, index)); |
| sbuf.append(expression.substring(index + 1, expression.length())); |
| return sbuf.toString(); |
| } |
| return expression.substring(index + 1, expression.length()); |
| } |
| return expression; |
| } |
| |
| /** |
| * Convenience method that strips off 'ns0:' portion, if |
| * one exists. |
| * |
| * @param expression |
| * @return |
| */ |
| protected String getPathWithPrefixRemoved(String expression) { |
| int index = expression.indexOf(':'); |
| if (index > -1) { |
| return expression.substring(index + 1, expression.length()); |
| } |
| return expression; |
| } |
| |
| private int getOperandFromString(String op) { |
| if (op.equals(EQ_STR)) { |
| return EQ; |
| } |
| if (op.equals(NEQ_STR)) { |
| return NEQ; |
| } |
| if (op.equals(GT_STR)) { |
| return GT; |
| } |
| if (op.equals(LT_STR)) { |
| return LT; |
| } |
| if (op.equals(GTE_STR)) { |
| return GTE; |
| } |
| if (op.equals(LTE_STR)) { |
| return LTE; |
| } |
| if (op.equals(AND_STR)) { |
| return AND; |
| } |
| if (op.equals(OR_STR)) { |
| return OR; |
| } |
| return -1; |
| } |
| |
| private String getStringFromOperand(int op) { |
| switch(op) { |
| case EQ: |
| return EQ_STR; |
| case NEQ: |
| return NEQ_STR; |
| case GTE: |
| return GTE_STR; |
| case LTE: |
| return LTE_STR; |
| case GT: |
| return GT_STR; |
| case LT: |
| return LT_STR; |
| case AND: |
| return AND_STR; |
| case OR: |
| return OR_STR; |
| } |
| return ""; |
| } |
| |
| /** |
| * Convenience method for determining if XPathEngine should be |
| * called, i.e. the XPath expression contains functionality |
| * not yet supported. |
| * |
| * @param expression |
| * @return |
| */ |
| protected boolean shouldCallXPathEngine(String expression) { |
| return false; |
| } |
| |
| // ----------------------------- Inner Classes ----------------------------- // |
| |
| /** |
| * A QueryPart knows the name of the property to be queried against on a |
| * given DataObject, as well as the value to be used in the comparison. |
| */ |
| public class QueryPart { |
| String propertyName, queryValue; |
| int relOperand; |
| int logOperand; |
| |
| /** |
| * This constructor breaks the provided query into |
| * property name and query value parts. |
| * |
| * @param query |
| */ |
| public QueryPart(String query) { |
| processQueryContents(query); |
| } |
| |
| /** |
| * This constructor sets a logical operator and breaks |
| * the provided query into property name and query |
| * value parts. |
| */ |
| public QueryPart(String property, String value, int op) { |
| relOperand = op; |
| propertyName = property; |
| queryValue = formatValue(value); |
| } |
| |
| /** |
| * Breaks the provided query into property name and |
| * query value parts |
| */ |
| private void processQueryContents(String query) { |
| int idx = -1, operandLen = 1; |
| relOperand = 1; |
| |
| if ((idx = query.indexOf(GTE_STR)) != -1) { |
| relOperand = GTE; |
| operandLen = 2; |
| } else if ((idx = query.indexOf(LTE_STR)) != -1) { |
| relOperand = LTE; |
| operandLen = 2; |
| } else if ((idx = query.indexOf(NEQ_STR)) != -1) { |
| relOperand = NEQ; |
| operandLen = 2; |
| } else if ((idx = query.indexOf(GT_STR)) != -1) { |
| relOperand = GT; |
| } else if ((idx = query.indexOf(LT_STR)) != -1) { |
| relOperand = LT; |
| } else if ((idx = query.indexOf(EQ_STR)) != -1) { |
| relOperand = EQ; |
| } |
| |
| propertyName = query.substring(0, idx).trim(); |
| queryValue = formatValue(query.substring(idx + operandLen)); |
| } |
| |
| private String formatValue(String value) { |
| int openQIdx = value.indexOf('\''); |
| int closeQIdx = value.lastIndexOf('\''); |
| if (openQIdx == -1 && closeQIdx == -1) { |
| openQIdx = value.indexOf('\"'); |
| closeQIdx = value.lastIndexOf('\"'); |
| } |
| if (openQIdx != -1 && closeQIdx != -1 && openQIdx < closeQIdx) { |
| value = value.substring(openQIdx + 1, closeQIdx); |
| } else { |
| // if the value is not enclosed on quotes, trim off any whitespace |
| value = value.trim(); |
| } |
| return value; |
| } |
| |
| /** |
| * Indicate if the query represented by this QueryPart evaluates to |
| * true or false when executed on a given DataObject. |
| * |
| * @param dao |
| * @return |
| */ |
| public boolean evaluate(SDODataObject dao) { |
| Object queryVal = queryValue; |
| Object actualVal = null; |
| |
| SDOProperty prop = dao.getInstanceProperty(propertyName); |
| try { |
| SDOXMLHelper sdoXMLHelper = (SDOXMLHelper) dao.getType().getHelperContext().getXMLHelper(); |
| queryVal = sdoXMLHelper.getXmlConversionManager().convertObject(queryValue, prop.getType().getInstanceClass()); |
| } catch (ConversionException e) { |
| // do nothing |
| } |
| |
| List values; |
| if (!prop.isMany()) { |
| values = new ArrayList(); |
| values.add(dao.get(prop)); |
| } else { |
| values = dao.getList(prop); |
| } |
| |
| Iterator iterValues = values.iterator(); |
| while (iterValues.hasNext()) { |
| actualVal = iterValues.next(); |
| if (actualVal == null) { |
| continue; |
| } |
| |
| int resultOfComparison; |
| try { |
| resultOfComparison = ((Comparable)actualVal).compareTo(queryVal) ; |
| } catch (Exception x) { |
| continue; |
| } |
| |
| // check the result against the logical operand |
| switch (relOperand) { |
| case EQ: |
| if (resultOfComparison == 0) { |
| return true; |
| } |
| break; |
| case NEQ: |
| if (resultOfComparison != 0) { |
| return true; |
| } |
| break; |
| case GTE: |
| if (resultOfComparison >= 0) { |
| return true; |
| } |
| break; |
| case LTE: |
| if (resultOfComparison <= 0) { |
| return true; |
| } |
| break; |
| case GT: |
| if (resultOfComparison > 0) { |
| return true; |
| } |
| break; |
| case LT: |
| if (resultOfComparison < 0) { |
| return true; |
| } |
| break; |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public String toString() { |
| return "QueryPart {" + propertyName + " " + getStringFromOperand(relOperand) + " " + queryValue + "}"; |
| } |
| } |
| } |