blob: 5e52e36bd23a94dd6ee74eee5408593689f8a7a7 [file] [log] [blame]
/*
* 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 + "}";
}
}
}