1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.chemistry.opencmis.jcr.query;
21  
22  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
23  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
24  import org.apache.chemistry.opencmis.jcr.JcrTypeManager;
25  import org.apache.chemistry.opencmis.server.support.query.CmisQueryWalker;
26  import org.apache.chemistry.opencmis.server.support.query.QueryObject;
27  import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
28  import org.apache.chemistry.opencmis.server.support.query.QueryUtil;
29  
30  import java.util.List;
31  
32  /**
33   * Abstract base class for translating a CMIS query statement to a JCR XPath
34   * query statement.
35   * Overriding class need to implement methods for mapping CMIS ids to JCR paths,
36   * CMIS property names to JCR property names, CMIS type names to JCR type name and
37   * in addition a method for adding further constraints to a query based on a CMIS
38   * type. 
39   */
40  public abstract class QueryTranslator {
41      private final JcrTypeManager typeManager;
42      private final EvaluatorXPath evaluator;
43      private QueryObject queryObject;
44  
45      /**
46       * Create a new query translator which uses the provided <code>typeManager</code>
47       * to resolve CMIS type names to CMIS types.
48       * 
49       * @param typeManager
50       */
51      protected QueryTranslator(JcrTypeManager typeManager) {
52          this.typeManager = typeManager;
53          evaluator = new EvaluatorXPath() {
54  
55              @Override
56              protected String jcrPathFromId(String id) {
57                  return QueryTranslator.this.jcrPathFromId(id);
58              }
59  
60              @Override
61              protected String jcrPathFromCol(String name) {
62                  return QueryTranslator.this.jcrPathFromCol(queryObject.getMainFromName(), name);
63              }
64          };
65      }
66  
67      /**
68       * @return  the {@link QueryObject} from the last translation performed through
69       *      {@link QueryTranslator#translateToXPath(String)}.
70       */
71      public QueryObject getQueryObject() {
72          return queryObject;
73      }
74  
75      /**
76       * Translate a CMIS query statement to a JCR XPath query statement.
77       * 
78       * @param statement
79       * @return
80       */
81      public String translateToXPath(String statement) {
82          QueryUtil queryUtil = new QueryUtil();
83          queryObject = new QueryObject(typeManager);
84          ParseTreeWalker<XPathBuilder> parseTreeWalker = new ParseTreeWalker<XPathBuilder>(evaluator);
85          CmisQueryWalker walker = queryUtil.traverseStatementAndCatchExc(statement, queryObject, parseTreeWalker);
86          walker.setDoFullTextParse(false);
87          XPathBuilder parseResult = parseTreeWalker.getResult();
88          TypeDefinition fromType = getFromName(queryObject);
89  
90          String pathExpression = buildPathExpression(fromType, getFolderPredicate(parseResult));
91          String elementTest = buildElementTest(fromType);
92          String predicates = buildPredicates(fromType, getCondition(parseResult));
93          String orderByClause = buildOrderByClause(fromType, queryObject.getOrderBys());
94          return "/jcr:root" + pathExpression + elementTest + predicates + orderByClause;
95      }
96  
97      //------------------------------------------< protected >---
98  
99      /**
100      * Map a CMIS objectId to an absolute JCR path. This method is called to
101      * resolve the folder if of folder predicates (i.e. IN_FOLDER, IN_TREE).
102      *
103      * @param id  objectId
104      * @return  absolute JCR path corresponding to <code>id</code>.
105      */
106     protected abstract String jcrPathFromId(String id);
107 
108     /**
109      * Map a column name in the CMIS query to the corresponding relative JCR path.
110      * The path must be relative to the context node.
111      *
112      * @param fromType  Type on which the CMIS query is performed
113      * @param name  column name
114      * @return  relative JCR path 
115      */
116     protected abstract String jcrPathFromCol(TypeDefinition fromType, String name);
117 
118     /**
119      * Map a CMIS type to the corresponding JCR type name.
120      * @see #jcrTypeCondition(TypeDefinition)
121      *
122      * @param fromType  CMIS type
123      * @return  name of the JCR type corresponding to <code>fromType</code>
124      */
125     protected abstract String jcrTypeName(TypeDefinition fromType);
126 
127     /**
128      * Create and additional condition in order for the query to only return nodes
129      * of the right type. This condition and-ed to the condition determined by the
130      * CMIS query's where clause.
131      * <p/>
132      * A CMIS query for non versionable documents should for example result in the
133      * following XPath query:
134      * <p/>
135      * <pre>
136      *   element(*, nt:file)[not(@jcr:mixinTypes = 'mix:simpleVersionable')]
137      * </pre>
138      * Here the element test is covered by {@link #jcrTypeName(TypeDefinition)}
139      * while the predicate is covered by this method.  
140      *
141      * @see #jcrTypeName(TypeDefinition)
142      *
143      * @param fromType
144      * @return  Additional condition or <code>null</code> if none. 
145      */
146     protected abstract String jcrTypeCondition(TypeDefinition fromType);  
147 
148     /**
149      * Build a XPath path expression for the CMIS type queried for and a folder predicate.
150      *
151      * @param fromType  CMIS type queried for
152      * @param folderPredicate  folder predicate
153      * @return  a valid XPath path expression or <code>null</code> if none.
154      */
155     protected String buildPathExpression(TypeDefinition fromType, String folderPredicate) {
156         return folderPredicate == null ? "//" : folderPredicate;
157     }
158 
159     /**
160      * Build a XPath element test for the given CMIS type.
161      *
162      * @param fromType  CMIS type queried for
163      * @return  a valid XPath element test. 
164      */
165     protected String buildElementTest(TypeDefinition fromType) {
166         return "element(*," + jcrTypeName(fromType) + ")";
167     }
168 
169     /**
170      * Build a XPath predicate for the given CMIS type and an additional condition.
171      * The additional condition should be and-ed to the condition resulting from
172      * evaluating <code>fromType</code>.
173      *
174      * @param fromType  CMIS type queried for
175      * @param condition  additional condition.
176      * @return  a valid XPath predicate or <code>null</code> if none. 
177      */
178     protected String buildPredicates(TypeDefinition fromType, String condition) {
179         String typeCondition = jcrTypeCondition(fromType);
180 
181         if (typeCondition == null) {
182             return condition == null ? "" : "[" + condition + "]";
183         }
184         else if (condition == null) {
185             return "[" + typeCondition + "]";
186         }
187         else {
188             return "[" + typeCondition + " and " + condition + "]"; 
189         }
190     }
191 
192     /**
193      * Build a XPath order by clause for the given CMIS type and a list of {@link SortSpec}s.
194      *
195      * @param fromType  CMIS type queried for
196      * @param orderBys  <code>SortSpec</code>s
197      * @return  a valid XPath order by clause 
198      */
199     protected String buildOrderByClause(TypeDefinition fromType, List<SortSpec> orderBys) {
200         StringBuilder orderSpecs = new StringBuilder();
201 
202         for (SortSpec orderBy : orderBys) {
203             String selector = jcrPathFromCol(fromType, orderBy.getSelector().getName());  
204             boolean ascending = orderBy.isAscending();
205 
206             if (orderSpecs.length() > 0) {
207                 orderSpecs.append(',');
208             }
209 
210             orderSpecs
211                 .append(selector)
212                 .append(' ')
213                 .append(ascending ? "ascending" : "descending");
214         }
215 
216         return orderSpecs.length() > 0
217                 ? "order by " + orderSpecs
218                 : "";
219     }
220 
221     //------------------------------------------< private >---
222 
223     private static String getFolderPredicate(XPathBuilder parseResult) {
224         if (parseResult == null) {
225             return null;
226         }
227         
228         String folderPredicate = null;
229         for (XPathBuilder p : parseResult.folderPredicates()) {
230             if (folderPredicate == null) {
231                 folderPredicate = p.xPath();
232             }
233             else {
234                 throw new CmisInvalidArgumentException("Query may only contain a single folder predicate");
235             }
236         }
237 
238         // See the class comment on XPathBuilder for details on affirmative literals
239         if (folderPredicate != null &&                             // IF has single folder predicate
240             !Boolean.FALSE.equals(parseResult.eval(false))) {      // AND folder predicate is not affirmative
241             throw new CmisInvalidArgumentException("Folder predicate " + folderPredicate + " is not affirmative.");
242         }
243 
244         return folderPredicate;
245     }
246 
247     private static TypeDefinition getFromName(QueryObject queryObject) {
248         if (queryObject.getTypes().size() != 1) {
249             throw new CmisInvalidArgumentException("From must contain one single reference");
250         }
251         return queryObject.getMainFromName();
252     }
253 
254     private static String getCondition(XPathBuilder parseResult) {
255         // No condition if either parseResult is null or when it evaluates to true under
256         // the valuation which assigns true to the folder predicate.
257         return parseResult == null || Boolean.TRUE.equals(parseResult.eval(true)) ? null : parseResult.xPath();
258     }
259     
260 }