View Javadoc

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    * with the License.  You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing,
12   * software distributed under the License is distributed on an
13   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14   * KIND, either express or implied.  See the License for the
15   * specific language governing permissions and limitations
16   * under the License.
17   */
18  package org.apache.chemistry.opencmis.client.runtime;
19  
20  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNotEmpty;
21  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNullOrEmpty;
22  
23  import java.net.URI;
24  import java.net.URL;
25  import java.text.SimpleDateFormat;
26  import java.util.Calendar;
27  import java.util.Collection;
28  import java.util.Date;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  
34  import org.apache.chemistry.opencmis.client.api.ItemIterable;
35  import org.apache.chemistry.opencmis.client.api.ObjectId;
36  import org.apache.chemistry.opencmis.client.api.ObjectType;
37  import org.apache.chemistry.opencmis.client.api.OperationContext;
38  import org.apache.chemistry.opencmis.client.api.QueryResult;
39  import org.apache.chemistry.opencmis.client.api.QueryStatement;
40  import org.apache.chemistry.opencmis.client.api.Session;
41  import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
42  import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
43  import org.apache.chemistry.opencmis.commons.impl.DateTimeHelper;
44  import org.apache.chemistry.opencmis.commons.impl.StringListBuilder;
45  
46  /**
47   * QueryStatement implementation.
48   */
49  public class QueryStatementImpl implements QueryStatement, Cloneable {
50  
51      private final Session session;
52      private final String statement;
53      private final Map<Integer, String> parametersMap = new HashMap<Integer, String>();
54  
55      /**
56       * Creates a QueryStatement object with a given statement.
57       * 
58       * @param session
59       *            the Session object, must not be {@code null}
60       * @param statement
61       *            the query statement with placeholders ('?'), see
62       *            {@link QueryStatement} for details
63       */
64      public QueryStatementImpl(Session session, String statement) {
65          if (session == null) {
66              throw new IllegalArgumentException("Session must be set!");
67          }
68  
69          if (statement == null) {
70              throw new IllegalArgumentException("Statement must be set!");
71          }
72  
73          this.session = session;
74          this.statement = statement.trim();
75      }
76  
77      /**
78       * Creates a QueryStatement object for a query of one primary type joined by
79       * zero or more secondary types.
80       * 
81       * @param session
82       *            the Session object, must not be {@code null}
83       * @param selectPropertyIds
84       *            the property IDs in the SELECT statement, if {@code null} all
85       *            properties are selected
86       * @param fromTypes
87       *            a Map of type aliases (keys) and type IDs (values), the Map
88       *            must contain exactly one primary type and zero or more
89       *            secondary types
90       * @param whereClause
91       *            an optional WHERE clause with placeholders ('?'), see
92       *            {@link QueryStatement} for details
93       * @param orderByPropertyIds
94       *            an optional list of properties IDs for the ORDER BY clause
95       */
96      public QueryStatementImpl(Session session, Collection<String> selectPropertyIds, Map<String, String> fromTypes,
97              String whereClause, List<String> orderByPropertyIds) {
98          if (session == null) {
99              throw new IllegalArgumentException("Session must be set!");
100         }
101 
102         if (isNullOrEmpty(fromTypes)) {
103             throw new IllegalArgumentException("Types must be set!");
104         }
105 
106         this.session = session;
107 
108         StringBuilder stmt = new StringBuilder(1024);
109 
110         // find the primary type and check if all types are queryable
111         ObjectType primaryType = null;
112         String primaryAlias = null;
113 
114         Map<String, ObjectType> types = new HashMap<String, ObjectType>();
115         for (Map.Entry<String, String> fte : fromTypes.entrySet()) {
116             ObjectType type = session.getTypeDefinition(fte.getValue());
117 
118             if (Boolean.FALSE.equals(type.isQueryable())) {
119                 throw new IllegalArgumentException("Type '" + fte.getValue() + "' is not queryable!");
120             }
121 
122             String alias = fte.getKey().trim();
123             if (alias.length() < 1) {
124                 throw new IllegalArgumentException("Invalid alias for type '" + fte.getValue() + "'!");
125             }
126 
127             if (type.getBaseTypeId() != BaseTypeId.CMIS_SECONDARY) {
128                 if (primaryType == null) {
129                     primaryType = type;
130                     primaryAlias = alias;
131                 } else {
132                     throw new IllegalArgumentException(
133                             "Two primary types found: " + primaryType.getId() + " and " + type.getId());
134                 }
135             }
136 
137             // exclude secondary types without properties
138             if (isNotEmpty(type.getPropertyDefinitions())) {
139                 types.put(alias, type);
140             }
141         }
142 
143         if (primaryType == null) {
144             throw new IllegalArgumentException("No primary type found!");
145         }
146 
147         // SELECT
148         stmt.append("SELECT ");
149 
150         StringListBuilder selectList = new StringListBuilder(",", stmt);
151 
152         if (isNullOrEmpty(selectPropertyIds)) {
153             // select all properties
154             for (String alias : types.keySet()) {
155                 selectList.add(alias + ".*");
156             }
157         } else {
158             // select provided properties
159             for (String propertyId : selectPropertyIds) {
160 
161                 propertyId = propertyId.trim();
162 
163                 if (propertyId.equals("*")) {
164                     // found property "*" -> select all properties
165                     for (String alias : types.keySet()) {
166                         selectList.add(alias + ".*");
167                     }
168                     continue;
169                 }
170 
171                 if (propertyId.endsWith(".*")) {
172                     // found property "x.*"
173                     // -> select all properties of the type with alias "x"
174                     String starAlias = propertyId.substring(0, propertyId.length() - 2);
175                     if (types.containsKey(starAlias)) {
176                         selectList.add(starAlias + ".*");
177                         continue;
178                     } else {
179                         throw new IllegalArgumentException("Alias '" + starAlias + "' is not defined!");
180                     }
181                 }
182 
183                 PropertyDefinition<?> propertyDef = null;
184                 String alias = null;
185 
186                 for (Map.Entry<String, ObjectType> te : types.entrySet()) {
187                     propertyDef = te.getValue().getPropertyDefinitions().get(propertyId);
188                     if (propertyDef != null) {
189                         alias = te.getKey();
190                         break;
191                     }
192                 }
193 
194                 if (propertyDef == null) {
195                     throw new IllegalArgumentException(
196                             "Property '" + propertyId + "' is not defined in the provided object types!");
197                 }
198 
199                 if (propertyDef.getQueryName() == null) {
200                     throw new IllegalArgumentException("Property '" + propertyId + "' has no query name!");
201                 }
202 
203                 selectList.add(alias + "." + propertyDef.getQueryName());
204             }
205         }
206 
207         // FROM
208         stmt.append(" FROM ");
209 
210         stmt.append(primaryType.getQueryName());
211         stmt.append(" AS ");
212         stmt.append(primaryAlias);
213 
214         for (Map.Entry<String, ObjectType> te : types.entrySet()) {
215             if (te.getKey().equals(primaryAlias)) {
216                 continue;
217             }
218 
219             stmt.append(" JOIN ");
220             stmt.append(te.getValue().getQueryName());
221             stmt.append(" AS ");
222             stmt.append(te.getKey());
223             stmt.append(" ON ");
224             stmt.append(primaryAlias);
225             stmt.append(".cmis:objectId=");
226             stmt.append(te.getKey());
227             stmt.append(".cmis:objectId");
228         }
229 
230         // WHERE
231         if (whereClause != null && whereClause.trim().length() > 0) {
232             stmt.append(" WHERE ");
233             stmt.append(whereClause.trim());
234         }
235 
236         // ORDER BY
237         if (isNotEmpty(orderByPropertyIds)) {
238             stmt.append(" ORDER BY ");
239 
240             StringListBuilder orderByList = new StringListBuilder(",", stmt);
241 
242             for (String propertyId : orderByPropertyIds) {
243                 String realPropertyId = propertyId.trim();
244                 String realPropertyIdLower = realPropertyId.toLowerCase(Locale.ENGLISH);
245                 boolean desc = false;
246 
247                 if (realPropertyIdLower.endsWith(" asc")) {
248                     // property ends with " asc" -> remove it
249                     realPropertyId = realPropertyId.substring(0, realPropertyId.length() - 4);
250                 }
251 
252                 if (realPropertyIdLower.endsWith(" desc")) {
253                     // property ends with " desc" -> remove it and mark it as
254                     // descending
255                     realPropertyId = realPropertyId.substring(0, realPropertyId.length() - 5);
256                     desc = true;
257                 }
258 
259                 PropertyDefinition<?> propertyDef = null;
260                 String alias = null;
261 
262                 for (Map.Entry<String, ObjectType> te : types.entrySet()) {
263                     propertyDef = te.getValue().getPropertyDefinitions().get(realPropertyId);
264                     if (propertyDef != null) {
265                         alias = te.getKey();
266                         break;
267                     }
268                 }
269 
270                 if (propertyDef == null) {
271                     throw new IllegalArgumentException(
272                             "Property '" + realPropertyId + "' is not defined in the provided object types!");
273                 }
274 
275                 if (propertyDef.getQueryName() == null) {
276                     throw new IllegalArgumentException("Property '" + realPropertyId + "' has no query name!");
277                 }
278 
279                 if (Boolean.FALSE.equals(propertyDef.isOrderable())) {
280                     throw new IllegalArgumentException("Property '" + realPropertyId + "' is not orderable!");
281                 }
282 
283                 orderByList.add(alias + "." + propertyDef.getQueryName() + (desc ? " DESC" : ""));
284             }
285         }
286 
287         this.statement = stmt.toString();
288     }
289 
290     @Override
291     public void setType(int parameterIndex, String typeId) {
292         setType(parameterIndex, session.getTypeDefinition(typeId));
293     }
294 
295     @Override
296     public void setType(int parameterIndex, ObjectType type) {
297         if (type == null) {
298             throw new IllegalArgumentException("Type must be set!");
299         }
300 
301         String queryName = type.getQueryName();
302         if (queryName == null) {
303             throw new IllegalArgumentException("Type has no query name!");
304         }
305 
306         parametersMap.put(parameterIndex, queryName);
307     }
308 
309     @Override
310     public void setProperty(int parameterIndex, String typeId, String propertyId) {
311         ObjectType type = session.getTypeDefinition(typeId);
312 
313         PropertyDefinition<?> propertyDefinition = type.getPropertyDefinitions().get(propertyId);
314         if (propertyDefinition == null) {
315             throw new IllegalArgumentException("Property does not exist!");
316         }
317 
318         setProperty(parameterIndex, propertyDefinition);
319     }
320 
321     @Override
322     public void setProperty(int parameterIndex, PropertyDefinition<?> propertyDefinition) {
323         if (propertyDefinition == null) {
324             throw new IllegalArgumentException("Property must be set!");
325         }
326 
327         String queryName = propertyDefinition.getQueryName();
328         if (queryName == null) {
329             throw new IllegalArgumentException("Property has no query name!");
330         }
331 
332         parametersMap.put(parameterIndex, queryName);
333     }
334 
335     @Override
336     public void setNumber(int parameterIndex, Number... num) {
337         if (num == null || num.length == 0) {
338             throw new IllegalArgumentException("Number must be set!");
339         }
340 
341         StringListBuilder slb = new StringListBuilder(",");
342         for (Number n : num) {
343             if (n == null) {
344                 throw new IllegalArgumentException("Number is null!");
345             }
346 
347             slb.add(n.toString());
348         }
349 
350         parametersMap.put(parameterIndex, slb.toString());
351     }
352 
353     @Override
354     public void setString(int parameterIndex, String... str) {
355         if (str == null || str.length == 0) {
356             throw new IllegalArgumentException("String must be set!");
357         }
358 
359         StringListBuilder slb = new StringListBuilder(",");
360         for (String s : str) {
361             if (s == null) {
362                 throw new IllegalArgumentException("String is null!");
363             }
364 
365             slb.add(escape(s));
366         }
367 
368         parametersMap.put(parameterIndex, slb.toString());
369     }
370 
371     @Override
372     public void setStringContains(int parameterIndex, String str) {
373         if (str == null) {
374             throw new IllegalArgumentException("String must be set!");
375         }
376 
377         parametersMap.put(parameterIndex, escapeContains(str));
378     }
379 
380     @Override
381     public void setStringLike(int parameterIndex, String str) {
382         if (str == null) {
383             throw new IllegalArgumentException("String must be set!");
384         }
385 
386         parametersMap.put(parameterIndex, escapeLike(str));
387     }
388 
389     @Override
390     public void setId(int parameterIndex, ObjectId... id) {
391         if (id == null || id.length == 0) {
392             throw new IllegalArgumentException("Id must be set!");
393         }
394 
395         StringListBuilder slb = new StringListBuilder(",");
396         for (ObjectId oid : id) {
397             if (oid == null || oid.getId() == null) {
398                 throw new IllegalArgumentException("Id is null!");
399             }
400 
401             slb.add(escape(oid.getId()));
402         }
403 
404         parametersMap.put(parameterIndex, slb.toString());
405     }
406 
407     @Override
408     public void setUri(int parameterIndex, URI... uri) {
409         if (uri == null) {
410             throw new IllegalArgumentException("URI must be set!");
411         }
412 
413         StringListBuilder slb = new StringListBuilder(",");
414         for (URI u : uri) {
415             if (u == null) {
416                 throw new IllegalArgumentException("URI is null!");
417             }
418 
419             slb.add(escape(u.toString()));
420         }
421 
422         parametersMap.put(parameterIndex, slb.toString());
423     }
424 
425     @Override
426     public void setUrl(int parameterIndex, URL... url) {
427         if (url == null) {
428             throw new IllegalArgumentException("URL must be set!");
429         }
430 
431         StringListBuilder slb = new StringListBuilder(",");
432         for (URL u : url) {
433             if (u == null) {
434                 throw new IllegalArgumentException("URI is null!");
435             }
436 
437             slb.add(escape(u.toString()));
438         }
439 
440         parametersMap.put(parameterIndex, slb.toString());
441     }
442 
443     @Override
444     public void setBoolean(int parameterIndex, boolean... bool) {
445         if (bool == null || bool.length == 0) {
446             throw new IllegalArgumentException("Boolean must not be set!");
447         }
448 
449         StringListBuilder slb = new StringListBuilder(",");
450         for (boolean b : bool) {
451             slb.add(b ? "TRUE" : "FALSE");
452         }
453 
454         parametersMap.put(parameterIndex, slb.toString());
455     }
456 
457     @Override
458     public void setDateTime(int parameterIndex, Calendar... cal) {
459         setDateTime(parameterIndex, false, cal);
460     }
461 
462     @Override
463     public void setDateTimeTimestamp(int parameterIndex, Calendar... cal) {
464         setDateTime(parameterIndex, true, cal);
465     }
466 
467     protected void setDateTime(int parameterIndex, boolean prefix, Calendar... cal) {
468         if (cal == null || cal.length == 0) {
469             throw new IllegalArgumentException("Calendar must be set!");
470         }
471 
472         StringBuilder sb = new StringBuilder(64);
473         for (Calendar c : cal) {
474             if (c == null) {
475                 throw new IllegalArgumentException("DateTime is null!");
476             }
477 
478             if (sb.length() > 0) {
479                 sb.append(',');
480             }
481 
482             if (prefix) {
483                 sb.append("TIMESTAMP ");
484             }
485 
486             sb.append(convert(c.getTime()));
487         }
488 
489         parametersMap.put(parameterIndex, sb.toString());
490     }
491 
492     @Override
493     public void setDateTime(int parameterIndex, Date... date) {
494         setDateTime(parameterIndex, false, date);
495     }
496 
497     @Override
498     public void setDateTimeTimestamp(int parameterIndex, Date... date) {
499         setDateTime(parameterIndex, true, date);
500     }
501 
502     protected void setDateTime(int parameterIndex, boolean prefix, Date... date) {
503         if (date == null || date.length == 0) {
504             throw new IllegalArgumentException("Date must be set!");
505         }
506 
507         StringListBuilder slb = new StringListBuilder(",");
508         for (Date d : date) {
509             if (d == null) {
510                 throw new IllegalArgumentException("DateTime is null!");
511             }
512 
513             slb.add((prefix ? "TIMESTAMP " : "") + convert(d));
514         }
515 
516         parametersMap.put(parameterIndex, slb.toString());
517     }
518 
519     @Override
520     public void setDateTime(int parameterIndex, long... ms) {
521         setDateTime(parameterIndex, false, ms);
522     }
523 
524     @Override
525     public void setDateTimeTimestamp(int parameterIndex, long... ms) {
526         setDateTime(parameterIndex, true, ms);
527     }
528 
529     protected void setDateTime(int parameterIndex, boolean prefix, long... ms) {
530         if (ms == null || ms.length == 0) {
531             throw new IllegalArgumentException("Timestamp must be set!");
532         }
533 
534         StringListBuilder slb = new StringListBuilder(",");
535         for (long l : ms) {
536             slb.add((prefix ? "TIMESTAMP " : "") + convert(new Date(l)));
537         }
538 
539         parametersMap.put(parameterIndex, slb.toString());
540     }
541 
542     @Override
543     public String toQueryString() {
544         boolean inStr = false;
545         int parameterIndex = 0;
546 
547         StringBuilder sb = new StringBuilder(1024);
548         for (int i = 0; i < statement.length(); i++) {
549             char c = statement.charAt(i);
550 
551             if (c == '\'') {
552                 if (inStr && statement.charAt(i - 1) == '\\') {
553                     inStr = true;
554                 } else {
555                     inStr = !inStr;
556                 }
557                 sb.append(c);
558             } else if (c == '?' && !inStr) {
559                 parameterIndex++;
560                 String s = parametersMap.get(parameterIndex);
561                 if (s == null) {
562                     sb.append(c);
563                 } else {
564                     sb.append(s);
565                 }
566             } else {
567                 sb.append(c);
568             }
569         }
570 
571         return sb.toString();
572     }
573 
574     @Override
575     public ItemIterable<QueryResult> query() {
576         return session.query(toQueryString(), false);
577     }
578 
579     @Override
580     public ItemIterable<QueryResult> query(boolean searchAllVersions) {
581         return session.query(toQueryString(), searchAllVersions);
582     }
583 
584     @Override
585     public ItemIterable<QueryResult> query(boolean searchAllVersions, OperationContext context) {
586         return session.query(toQueryString(), searchAllVersions, context);
587     }
588 
589     @Override
590     protected QueryStatementImpl clone() throws CloneNotSupportedException {
591         QueryStatementImpl qs = new QueryStatementImpl(session, statement);
592         qs.parametersMap.putAll(parametersMap);
593 
594         return qs;
595     }
596 
597     @Override
598     public String toString() {
599         return toQueryString();
600     }
601 
602     // --- internal ---
603 
604     private static String escape(String str) {
605         StringBuilder sb = new StringBuilder(str.length() + 16);
606 
607         sb.append('\'');
608 
609         for (int i = 0; i < str.length(); i++) {
610             char c = str.charAt(i);
611 
612             if (c == '\'' || c == '\\') {
613                 sb.append('\\');
614             }
615 
616             sb.append(c);
617         }
618 
619         sb.append('\'');
620 
621         return sb.toString();
622     }
623 
624     private static String escapeLike(String str) {
625         StringBuilder sb = new StringBuilder(str.length() + 16);
626 
627         sb.append('\'');
628 
629         for (int i = 0; i < str.length(); i++) {
630             char c = str.charAt(i);
631 
632             if (c == '\'') {
633                 sb.append('\\');
634             } else if (c == '\\') {
635                 if (i + 1 < str.length() && (str.charAt(i + 1) == '%' || str.charAt(i + 1) == '_')) {
636                     // no additional back slash
637                 } else {
638                     sb.append('\\');
639                 }
640             }
641 
642             sb.append(c);
643         }
644 
645         sb.append('\'');
646 
647         return sb.toString();
648     }
649 
650     private static String escapeContains(String str) {
651         StringBuilder sb = new StringBuilder(str.length() + 64);
652 
653         for (int i = 0; i < str.length(); i++) {
654             char c = str.charAt(i);
655 
656             if (c == '\\') {
657                 sb.append('\\');
658             } else if (c == '\'' || c == '\"') {
659                 sb.append('\\');
660             } else if (c == '-') {
661                 if (i > 0) {
662                     char cb = str.charAt(i - 1);
663                     if (cb == '\\') {
664                         sb.deleteCharAt(sb.length() - 1);
665                     } else if (cb != ' ') {
666                         sb.append('\\');
667                     }
668                 }
669             }
670 
671             sb.append(c);
672         }
673 
674         return escape(sb.toString());
675     }
676 
677     private static String convert(Date date) {
678         SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
679         sdf.setTimeZone(DateTimeHelper.GMT);
680 
681         return "'" + sdf.format(date) + "'";
682     }
683 }