/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.exoplatform.services.jcr.impl.core.query.xpath; import org.exoplatform.commons.utils.ISO8601; import org.exoplatform.commons.utils.Tools; import org.exoplatform.services.jcr.datamodel.InternalQName; import org.exoplatform.services.jcr.datamodel.QPath; import org.exoplatform.services.jcr.datamodel.QPathEntry; import org.exoplatform.services.jcr.impl.core.LocationFactory; import org.exoplatform.services.jcr.impl.core.query.AndQueryNode; import org.exoplatform.services.jcr.impl.core.query.DefaultQueryNodeVisitor; import org.exoplatform.services.jcr.impl.core.query.DerefQueryNode; import org.exoplatform.services.jcr.impl.core.query.ExactQueryNode; import org.exoplatform.services.jcr.impl.core.query.LocationStepQueryNode; import org.exoplatform.services.jcr.impl.core.query.NodeTypeQueryNode; import org.exoplatform.services.jcr.impl.core.query.NotQueryNode; import org.exoplatform.services.jcr.impl.core.query.OrQueryNode; import org.exoplatform.services.jcr.impl.core.query.OrderQueryNode; import org.exoplatform.services.jcr.impl.core.query.PathQueryNode; import org.exoplatform.services.jcr.impl.core.query.PropertyFunctionQueryNode; import org.exoplatform.services.jcr.impl.core.query.QueryConstants; import org.exoplatform.services.jcr.impl.core.query.QueryNode; import org.exoplatform.services.jcr.impl.core.query.QueryNodeVisitor; import org.exoplatform.services.jcr.impl.core.query.QueryRootNode; import org.exoplatform.services.jcr.impl.core.query.RelationQueryNode; import org.exoplatform.services.jcr.impl.core.query.TextsearchQueryNode; import org.exoplatform.services.jcr.impl.util.ISO9075; import java.util.ArrayList; import java.util.Calendar; import java.util.List; import javax.jcr.NamespaceException; import javax.jcr.RepositoryException; import javax.jcr.query.InvalidQueryException; /** * Implements the query node tree serialization into a String. */ class QueryFormat implements QueryNodeVisitor, QueryConstants { /** * Will be used to resolve QNames */ private final LocationFactory resolver; /** * The String representation of the query node tree */ private final String statement; /** * List of exception objects created while creating the XPath string */ private final List<Exception> exceptions = new ArrayList<Exception>(); private QueryFormat(QueryRootNode root, LocationFactory resolver) throws RepositoryException { this.resolver = resolver; statement = root.accept(this, new StringBuilder()).toString(); if (exceptions.size() > 0) { Exception e = (Exception) exceptions.get(0); throw new InvalidQueryException(e.getMessage(), e); } } /** * Creates a XPath <code>String</code> representation of the QueryNode tree * argument <code>root</code>. * * @param root the query node tree. * @param resolver to resolve QNames. * @return the XPath string representation of the QueryNode tree. * @throws InvalidQueryException the query node tree cannot be represented * as a XPath <code>String</code>. */ public static String toString(QueryRootNode root, LocationFactory resolver) throws InvalidQueryException { try { return new QueryFormat(root, resolver).toString(); } catch (RepositoryException e) { throw new InvalidQueryException(e); } } /** * Returns the string representation. * * @return the string representation. */ public String toString() { return statement; } //-------------< QueryNodeVisitor interface >------------------------------- public Object visit(QueryRootNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; node.getLocationNode().accept(this, data); InternalQName[] selectProps = node.getSelectProperties(); if (selectProps.length > 0) { sb.append('/'); boolean union = selectProps.length > 1; if (union) { sb.append('('); } String pipe = ""; for (int i = 0; i < selectProps.length; i++) { try { sb.append(pipe); sb.append('@'); sb.append(resolver.createJCRName(encode(selectProps[i])).getAsString()); pipe = "|"; } catch (NamespaceException e) { exceptions.add(e); } } if (union) { sb.append(')'); } } if (node.getOrderNode() != null) { node.getOrderNode().accept(this, data); } return data; } public Object visit(OrQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; boolean bracket = false; if (node.getParent() instanceof AndQueryNode) { bracket = true; } if (bracket) { sb.append("("); } String or = ""; QueryNode[] operands = node.getOperands(); for (int i = 0; i < operands.length; i++) { sb.append(or); operands[i].accept(this, sb); or = " or "; } if (bracket) { sb.append(")"); } return sb; } public Object visit(AndQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; String and = ""; QueryNode[] operands = node.getOperands(); for (int i = 0; i < operands.length; i++) { sb.append(and); operands[i].accept(this, sb); and = " and "; } return sb; } public Object visit(NotQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; QueryNode[] operands = node.getOperands(); if (operands.length > 0) { try { sb.append(resolver.createJCRName(XPathQueryBuilder.FN_NOT_10).getAsString()); sb.append("("); operands[0].accept(this, sb); sb.append(")"); } catch (NamespaceException e) { exceptions.add(e); } } return sb; } public Object visit(ExactQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; sb.append("@"); try { InternalQName name = encode(node.getPropertyName()); sb.append(resolver.createJCRName(name).getAsString()); sb.append("='"); sb.append(resolver.createJCRName(node.getValue()).getAsString()); } catch (NamespaceException e) { exceptions.add(e); } sb.append("'"); return sb; } public Object visit(NodeTypeQueryNode node, Object data) { // handled in location step visit return data; } public Object visit(TextsearchQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; try { sb.append(resolver.createJCRName(XPathQueryBuilder.JCR_CONTAINS).getAsString()); sb.append("("); QPath relPath = node.getRelativePath(); if (relPath == null) { sb.append("."); } else { QPathEntry[] elements = relPath.getEntries(); String slash = ""; for (int i = 0; i < elements.length; i++) { sb.append(slash); slash = "/"; if (node.getReferencesProperty() && i == elements.length - 1) { sb.append("@"); } if (elements[i].equals(RelationQueryNode.STAR_NAME_TEST)) { sb.append("*"); } else { InternalQName n = encode(elements[i]); sb.append(resolver.createJCRName(n).getAsString()); } if (elements[i].getIndex() != 0) { sb.append("[").append(elements[i].getIndex()).append("]"); } } } sb.append(", '"); sb.append(node.getQuery().replaceAll("'", "''")); sb.append("')"); } catch (NamespaceException e) { exceptions.add(e); } return sb; } public Object visit(PathQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; if (node.isAbsolute()) { sb.append("/"); } LocationStepQueryNode[] steps = node.getPathSteps(); String slash = ""; for (int i = 0; i < steps.length; i++) { sb.append(slash); steps[i].accept(this, sb); slash = "/"; } return sb; } public Object visit(LocationStepQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; if (node.getIncludeDescendants()) { sb.append('/'); } final InternalQName[] nodeType = new InternalQName[1]; node.acceptOperands(new DefaultQueryNodeVisitor() { public Object visit(NodeTypeQueryNode node, Object data) { nodeType[0] = node.getValue(); return data; } }, null); if (nodeType[0] != null) { sb.append("element("); } if (node.getNameTest() == null) { sb.append("*"); } else { try { if (node.getNameTest().getName().length() == 0) { sb.append(resolver.createJCRName(XPathQueryBuilder.JCR_ROOT).getAsString()); } else { sb.append(resolver.createJCRName(encode(node.getNameTest())).getAsString()); } } catch (NamespaceException e) { exceptions.add(e); } } if (nodeType[0] != null) { sb.append(", "); try { sb.append(resolver.createJCRName(encode(nodeType[0])).getAsString()); } catch (NamespaceException e) { exceptions.add(e); } sb.append(")"); } if (node.getIndex() != LocationStepQueryNode.NONE) { sb.append('[').append(node.getIndex()).append(']'); } QueryNode[] predicates = node.getPredicates(); for (int i = 0; i < predicates.length; i++) { // ignore node type query nodes if (predicates[i].getType() == QueryNode.TYPE_NODETYPE) { continue; } sb.append('['); predicates[i].accept(this, sb); sb.append(']'); } return sb; } public Object visit(DerefQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; try { sb.append(resolver.createJCRName(XPathQueryBuilder.JCR_DEREF).getAsString()); sb.append("(@"); sb.append(resolver.createJCRName(encode(node.getRefProperty())).getAsString()); sb.append(", '"); if (node.getNameTest() == null) { sb.append("*"); } else { sb.append(resolver.createJCRName(encode(node.getNameTest())).getAsString()); } sb.append("')"); } catch (NamespaceException e) { exceptions.add(e); } return sb; } public Object visit(RelationQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; try { StringBuilder propPath = new StringBuilder(); // only encode if not position function QPath relPath = node.getRelativePath(); if (relPath == null) { propPath.append("."); } else if (relPath.getName().equals(XPathQueryBuilder.FN_POSITION_FULL)) { propPath.append(resolver.createJCRName(XPathQueryBuilder.FN_POSITION_FULL).getAsString()); } else { QPathEntry[] elements = relPath.getEntries(); String slash = ""; for (int i = 0; i < elements.length; i++) { propPath.append(slash); slash = "/"; if (i == elements.length - 1 && node.getOperation() != OPERATION_SIMILAR) { propPath.append("@"); } if (elements[i].equals(RelationQueryNode.STAR_NAME_TEST)) { propPath.append("*"); } else { propPath.append(resolver.createJCRName(encode(elements[i])).getAsString()); } if (elements[i].getIndex() != 0) { propPath.append("[").append(elements[i].getIndex()).append("]"); } } } // surround name with property function node.acceptOperands(this, propPath); if (node.getOperation() == OPERATION_EQ_VALUE) { sb.append(propPath).append(" eq "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_EQ_GENERAL) { sb.append(propPath).append(" = "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_GE_GENERAL) { sb.append(propPath).append(" >= "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_GE_VALUE) { sb.append(propPath).append(" ge "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_GT_GENERAL) { sb.append(propPath).append(" > "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_GT_VALUE) { sb.append(propPath).append(" gt "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_LE_GENERAL) { sb.append(propPath).append(" <= "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_LE_VALUE) { sb.append(propPath).append(" le "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_LIKE) { sb.append(resolver.createJCRName(XPathQueryBuilder.JCR_LIKE).getAsString()); sb.append("(").append(propPath).append(", "); appendValue(node, sb); sb.append(")"); } else if (node.getOperation() == OPERATION_LT_GENERAL) { sb.append(propPath).append(" < "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_LT_VALUE) { sb.append(propPath).append(" lt "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_NE_GENERAL) { sb.append(propPath).append(" != "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_NE_VALUE) { sb.append(propPath).append(" ne "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_NULL) { sb.append(resolver.createJCRName(XPathQueryBuilder.FN_NOT).getAsString()); sb.append("(").append(propPath).append(")"); } else if (node.getOperation() == OPERATION_NOT_NULL) { sb.append(propPath); } else if (node.getOperation() == OPERATION_SIMILAR) { sb.append(resolver.createJCRName(XPathQueryBuilder.REP_SIMILAR).getAsString()); sb.append("(").append(propPath).append(", "); appendValue(node, sb); } else if (node.getOperation() == OPERATION_SPELLCHECK) { sb.append(resolver.createJCRName(XPathQueryBuilder.REP_SPELLCHECK).getAsString()); sb.append("("); appendValue(node, sb); sb.append(")"); } else { exceptions.add(new InvalidQueryException("Invalid operation: " + node.getOperation())); } } catch (NamespaceException e) { exceptions.add(e); } return sb; } public Object visit(OrderQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; sb.append(" order by"); OrderQueryNode.OrderSpec[] specs = node.getOrderSpecs(); String comma = ""; try { for (int i = 0; i < specs.length; i++) { sb.append(comma); QPath propPath = specs[i].getPropertyPath(); QPathEntry[] elements = propPath.getEntries(); sb.append(" "); String slash = ""; for (int j = 0; j < elements.length; j++) { sb.append(slash); slash = "/"; QPathEntry element = elements[j]; InternalQName name = encode(element); if (j == elements.length - 1) { // last sb.append("@"); } sb.append(resolver.createJCRName(name).getAsString()); } if (!specs[i].isAscending()) { sb.append(" descending"); } comma = ","; } } catch (NamespaceException e) { exceptions.add(e); } return data; } public Object visit(PropertyFunctionQueryNode node, Object data) throws RepositoryException { StringBuilder sb = (StringBuilder) data; String functionName = node.getFunctionName(); try { if (functionName.equals(PropertyFunctionQueryNode.LOWER_CASE)) { sb.insert(0, resolver.createJCRName(XPathQueryBuilder.FN_LOWER_CASE).getAsString() + "("); sb.append(")"); } else if (functionName.equals(PropertyFunctionQueryNode.UPPER_CASE)) { sb.insert(0, resolver.createJCRName(XPathQueryBuilder.FN_UPPER_CASE).getAsString() + "("); sb.append(")"); } else { exceptions.add(new InvalidQueryException("Unsupported function: " + functionName)); } } catch (NamespaceException e) { exceptions.add(e); } return sb; } //----------------------------< internal >---------------------------------- /** * Appends the value of a relation node to the <code>StringBuilder</code> * <code>sb</code>. * * @param node the relation node. * @param b where to append the value. * @throws RepositoryException */ private void appendValue(RelationQueryNode node, StringBuilder b) throws RepositoryException { if (node.getValueType() == TYPE_LONG) { b.append(node.getLongValue()); } else if (node.getValueType() == TYPE_DOUBLE) { b.append(node.getDoubleValue()); } else if (node.getValueType() == TYPE_STRING) { b.append("'").append(node.getStringValue().replaceAll("'", "''")).append("'"); } else if (node.getValueType() == TYPE_DATE || node.getValueType() == TYPE_TIMESTAMP) { Calendar cal = Calendar.getInstance(Tools.getTimeZone("UTC")); cal.setTime(node.getDateValue()); b.append(resolver.createJCRName(XPathQueryBuilder.XS_DATETIME).getAsString()); b.append("('").append(ISO8601.format(cal)).append("')"); } else if (node.getValueType() == TYPE_POSITION) { if (node.getPositionValue() == LocationStepQueryNode.LAST) { b.append("last()"); } else { b.append(node.getPositionValue()); } } else { exceptions.add(new InvalidQueryException("Invalid type: " + node.getValueType())); } } private static InternalQName encode(InternalQName name) { String encoded = ISO9075.encode(name.getName()); if (encoded.equals(name.getName())) { return name; } else { return new InternalQName(name.getNamespace(),encoded); } } }