| /* |
| * 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 |
| * 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.apache.chemistry.opencmis.client.runtime; |
| |
| import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNotEmpty; |
| import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNullOrEmpty; |
| |
| import java.net.URI; |
| import java.net.URL; |
| import java.text.SimpleDateFormat; |
| import java.util.Calendar; |
| import java.util.Collection; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.TimeZone; |
| |
| import org.apache.chemistry.opencmis.client.api.ItemIterable; |
| import org.apache.chemistry.opencmis.client.api.ObjectId; |
| import org.apache.chemistry.opencmis.client.api.ObjectType; |
| import org.apache.chemistry.opencmis.client.api.OperationContext; |
| import org.apache.chemistry.opencmis.client.api.QueryResult; |
| import org.apache.chemistry.opencmis.client.api.QueryStatement; |
| import org.apache.chemistry.opencmis.client.api.Session; |
| import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition; |
| import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; |
| import org.apache.chemistry.opencmis.commons.impl.StringListBuilder; |
| |
| /** |
| * QueryStatement implementation. |
| */ |
| public class QueryStatementImpl implements QueryStatement, Cloneable { |
| |
| private final Session session; |
| private final String statement; |
| private final Map<Integer, String> parametersMap = new HashMap<Integer, String>(); |
| |
| /** |
| * Creates a QueryStatement object with a given statement. |
| * |
| * @param session |
| * the Session object, must not be {@code null} |
| * @param statement |
| * the query statement with placeholders ('?'), see |
| * {@link QueryStatement} for details |
| */ |
| public QueryStatementImpl(Session session, String statement) { |
| if (session == null) { |
| throw new IllegalArgumentException("Session must be set!"); |
| } |
| |
| if (statement == null) { |
| throw new IllegalArgumentException("Statement must be set!"); |
| } |
| |
| this.session = session; |
| this.statement = statement.trim(); |
| } |
| |
| /** |
| * Creates a QueryStatement object for a query of one primary type joined by |
| * zero or more secondary types. |
| * |
| * @param session |
| * the Session object, must not be {@code null} |
| * @param selectPropertyIds |
| * the property IDs in the SELECT statement, if {@code null} all |
| * properties are selected |
| * @param fromTypes |
| * a Map of type aliases (keys) and type IDs (values), the Map |
| * must contain exactly one primary type and zero or more |
| * secondary types |
| * @param whereClause |
| * an optional WHERE clause with placeholders ('?'), see |
| * {@link QueryStatement} for details |
| * @param orderByPropertyIds |
| * an optional list of properties IDs for the ORDER BY clause |
| */ |
| public QueryStatementImpl(Session session, Collection<String> selectPropertyIds, Map<String, String> fromTypes, |
| String whereClause, List<String> orderByPropertyIds) { |
| if (session == null) { |
| throw new IllegalArgumentException("Session must be set!"); |
| } |
| |
| if (isNullOrEmpty(fromTypes)) { |
| throw new IllegalArgumentException("Types must be set!"); |
| } |
| |
| this.session = session; |
| |
| StringBuilder stmt = new StringBuilder(1024); |
| |
| // find the primary type and check if all types are queryable |
| ObjectType primaryType = null; |
| String primaryAlias = null; |
| |
| Map<String, ObjectType> types = new HashMap<String, ObjectType>(); |
| for (Map.Entry<String, String> fte : fromTypes.entrySet()) { |
| ObjectType type = session.getTypeDefinition(fte.getValue()); |
| |
| if (Boolean.FALSE.equals(type.isQueryable())) { |
| throw new IllegalArgumentException("Type '" + fte.getValue() + "' is not queryable!"); |
| } |
| |
| String alias = fte.getKey().trim(); |
| if (alias.length() < 1) { |
| throw new IllegalArgumentException("Invalid alias for type '" + fte.getValue() + "'!"); |
| } |
| |
| if (type.getBaseTypeId() != BaseTypeId.CMIS_SECONDARY) { |
| if (primaryType == null) { |
| primaryType = type; |
| primaryAlias = alias; |
| } else { |
| throw new IllegalArgumentException("Two primary types found: " + primaryType.getId() + " and " |
| + type.getId()); |
| } |
| } |
| |
| // exclude secondary types without properties |
| if (isNotEmpty(type.getPropertyDefinitions())) { |
| types.put(alias, type); |
| } |
| } |
| |
| if (primaryType == null) { |
| throw new IllegalArgumentException("No primary type found!"); |
| } |
| |
| // SELECT |
| stmt.append("SELECT "); |
| |
| StringListBuilder selectList = new StringListBuilder(",", stmt); |
| |
| if (isNullOrEmpty(selectPropertyIds)) { |
| // select all properties |
| for (String alias : types.keySet()) { |
| selectList.add(alias + ".*"); |
| } |
| } else { |
| // select provided properties |
| for (String propertyId : selectPropertyIds) { |
| |
| propertyId = propertyId.trim(); |
| |
| if (propertyId.equals("*")) { |
| // found property "*" -> select all properties |
| for (String alias : types.keySet()) { |
| selectList.add(alias + ".*"); |
| } |
| continue; |
| } |
| |
| if (propertyId.endsWith(".*")) { |
| // found property "x.*" |
| // -> select all properties of the type with alias "x" |
| String starAlias = propertyId.substring(0, propertyId.length() - 2); |
| if (types.containsKey(starAlias)) { |
| selectList.add(starAlias + ".*"); |
| continue; |
| } else { |
| throw new IllegalArgumentException("Alias '" + starAlias + "' is not defined!"); |
| } |
| } |
| |
| PropertyDefinition<?> propertyDef = null; |
| String alias = null; |
| |
| for (Map.Entry<String, ObjectType> te : types.entrySet()) { |
| propertyDef = te.getValue().getPropertyDefinitions().get(propertyId); |
| if (propertyDef != null) { |
| alias = te.getKey(); |
| break; |
| } |
| } |
| |
| if (propertyDef == null) { |
| throw new IllegalArgumentException("Property '" + propertyId |
| + "' is not defined in the provided object types!"); |
| } |
| |
| if (propertyDef.getQueryName() == null) { |
| throw new IllegalArgumentException("Property '" + propertyId + "' has no query name!"); |
| } |
| |
| selectList.add(alias + "." + propertyDef.getQueryName()); |
| } |
| } |
| |
| // FROM |
| stmt.append(" FROM "); |
| |
| stmt.append(primaryType.getQueryName()); |
| stmt.append(" AS "); |
| stmt.append(primaryAlias); |
| |
| for (Map.Entry<String, ObjectType> te : types.entrySet()) { |
| if (te.getKey().equals(primaryAlias)) { |
| continue; |
| } |
| |
| stmt.append(" JOIN "); |
| stmt.append(te.getValue().getQueryName()); |
| stmt.append(" AS "); |
| stmt.append(te.getKey()); |
| stmt.append(" ON "); |
| stmt.append(primaryAlias); |
| stmt.append(".cmis:objectId="); |
| stmt.append(te.getKey()); |
| stmt.append(".cmis:objectId"); |
| } |
| |
| // WHERE |
| if (whereClause != null && whereClause.trim().length() > 0) { |
| stmt.append(" WHERE "); |
| stmt.append(whereClause.trim()); |
| } |
| |
| // ORDER BY |
| if (isNotEmpty(orderByPropertyIds)) { |
| stmt.append(" ORDER BY "); |
| |
| StringListBuilder orderByList = new StringListBuilder(",", stmt); |
| |
| for (String propertyId : orderByPropertyIds) { |
| String realPropertyId = propertyId.trim(); |
| String realPropertyIdLower = realPropertyId.toLowerCase(Locale.ENGLISH); |
| boolean desc = false; |
| |
| if (realPropertyIdLower.endsWith(" asc")) { |
| // property ends with " asc" -> remove it |
| realPropertyId = realPropertyId.substring(0, realPropertyId.length() - 4); |
| } |
| |
| if (realPropertyIdLower.endsWith(" desc")) { |
| // property ends with " desc" -> remove it and mark it as |
| // descending |
| realPropertyId = realPropertyId.substring(0, realPropertyId.length() - 5); |
| desc = true; |
| } |
| |
| PropertyDefinition<?> propertyDef = null; |
| String alias = null; |
| |
| for (Map.Entry<String, ObjectType> te : types.entrySet()) { |
| propertyDef = te.getValue().getPropertyDefinitions().get(realPropertyId); |
| if (propertyDef != null) { |
| alias = te.getKey(); |
| break; |
| } |
| } |
| |
| if (propertyDef == null) { |
| throw new IllegalArgumentException("Property '" + realPropertyId |
| + "' is not defined in the provided object types!"); |
| } |
| |
| if (propertyDef.getQueryName() == null) { |
| throw new IllegalArgumentException("Property '" + realPropertyId + "' has no query name!"); |
| } |
| |
| if (Boolean.FALSE.equals(propertyDef.isOrderable())) { |
| throw new IllegalArgumentException("Property '" + realPropertyId + "' is not orderable!"); |
| } |
| |
| orderByList.add(alias + "." + propertyDef.getQueryName() + (desc ? " DESC" : "")); |
| } |
| } |
| |
| this.statement = stmt.toString(); |
| } |
| |
| @Override |
| public void setType(int parameterIndex, String typeId) { |
| setType(parameterIndex, session.getTypeDefinition(typeId)); |
| } |
| |
| @Override |
| public void setType(int parameterIndex, ObjectType type) { |
| if (type == null) { |
| throw new IllegalArgumentException("Type must be set!"); |
| } |
| |
| String queryName = type.getQueryName(); |
| if (queryName == null) { |
| throw new IllegalArgumentException("Type has no query name!"); |
| } |
| |
| parametersMap.put(parameterIndex, queryName); |
| } |
| |
| @Override |
| public void setProperty(int parameterIndex, String typeId, String propertyId) { |
| ObjectType type = session.getTypeDefinition(typeId); |
| |
| PropertyDefinition<?> propertyDefinition = type.getPropertyDefinitions().get(propertyId); |
| if (propertyDefinition == null) { |
| throw new IllegalArgumentException("Property does not exist!"); |
| } |
| |
| setProperty(parameterIndex, propertyDefinition); |
| } |
| |
| @Override |
| public void setProperty(int parameterIndex, PropertyDefinition<?> propertyDefinition) { |
| if (propertyDefinition == null) { |
| throw new IllegalArgumentException("Property must be set!"); |
| } |
| |
| String queryName = propertyDefinition.getQueryName(); |
| if (queryName == null) { |
| throw new IllegalArgumentException("Property has no query name!"); |
| } |
| |
| parametersMap.put(parameterIndex, queryName); |
| } |
| |
| @Override |
| public void setNumber(int parameterIndex, Number... num) { |
| if (num == null || num.length == 0) { |
| throw new IllegalArgumentException("Number must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (Number n : num) { |
| if (n == null) { |
| throw new IllegalArgumentException("Number is null!"); |
| } |
| |
| slb.add(n.toString()); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setString(int parameterIndex, String... str) { |
| if (str == null || str.length == 0) { |
| throw new IllegalArgumentException("String must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (String s : str) { |
| if (s == null) { |
| throw new IllegalArgumentException("String is null!"); |
| } |
| |
| slb.add(escape(s)); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setStringContains(int parameterIndex, String str) { |
| if (str == null) { |
| throw new IllegalArgumentException("String must be set!"); |
| } |
| |
| parametersMap.put(parameterIndex, escapeContains(str)); |
| } |
| |
| @Override |
| public void setStringLike(int parameterIndex, String str) { |
| if (str == null) { |
| throw new IllegalArgumentException("String must be set!"); |
| } |
| |
| parametersMap.put(parameterIndex, escapeLike(str)); |
| } |
| |
| @Override |
| public void setId(int parameterIndex, ObjectId... id) { |
| if (id == null || id.length == 0) { |
| throw new IllegalArgumentException("Id must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (ObjectId oid : id) { |
| if (oid == null || oid.getId() == null) { |
| throw new IllegalArgumentException("Id is null!"); |
| } |
| |
| slb.add(escape(oid.getId())); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setUri(int parameterIndex, URI... uri) { |
| if (uri == null) { |
| throw new IllegalArgumentException("URI must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (URI u : uri) { |
| if (u == null) { |
| throw new IllegalArgumentException("URI is null!"); |
| } |
| |
| slb.add(escape(u.toString())); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setUrl(int parameterIndex, URL... url) { |
| if (url == null) { |
| throw new IllegalArgumentException("URL must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (URL u : url) { |
| if (u == null) { |
| throw new IllegalArgumentException("URI is null!"); |
| } |
| |
| slb.add(escape(u.toString())); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setBoolean(int parameterIndex, boolean... bool) { |
| if (bool == null || bool.length == 0) { |
| throw new IllegalArgumentException("Boolean must not be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (boolean b : bool) { |
| slb.add(b ? "TRUE" : "FALSE"); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setDateTime(int parameterIndex, Calendar... cal) { |
| setDateTime(parameterIndex, false, cal); |
| } |
| |
| @Override |
| public void setDateTimeTimestamp(int parameterIndex, Calendar... cal) { |
| setDateTime(parameterIndex, true, cal); |
| } |
| |
| protected void setDateTime(int parameterIndex, boolean prefix, Calendar... cal) { |
| if (cal == null || cal.length == 0) { |
| throw new IllegalArgumentException("Calendar must be set!"); |
| } |
| |
| StringBuilder sb = new StringBuilder(64); |
| for (Calendar c : cal) { |
| if (c == null) { |
| throw new IllegalArgumentException("DateTime is null!"); |
| } |
| |
| if (sb.length() > 0) { |
| sb.append(','); |
| } |
| |
| if (prefix) { |
| sb.append("TIMESTAMP "); |
| } |
| |
| sb.append(convert(c.getTime())); |
| } |
| |
| parametersMap.put(parameterIndex, sb.toString()); |
| } |
| |
| @Override |
| public void setDateTime(int parameterIndex, Date... date) { |
| setDateTime(parameterIndex, false, date); |
| } |
| |
| @Override |
| public void setDateTimeTimestamp(int parameterIndex, Date... date) { |
| setDateTime(parameterIndex, true, date); |
| } |
| |
| protected void setDateTime(int parameterIndex, boolean prefix, Date... date) { |
| if (date == null || date.length == 0) { |
| throw new IllegalArgumentException("Date must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (Date d : date) { |
| if (d == null) { |
| throw new IllegalArgumentException("DateTime is null!"); |
| } |
| |
| slb.add((prefix ? "TIMESTAMP " : "") + convert(d)); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public void setDateTime(int parameterIndex, long... ms) { |
| setDateTime(parameterIndex, false, ms); |
| } |
| |
| @Override |
| public void setDateTimeTimestamp(int parameterIndex, long... ms) { |
| setDateTime(parameterIndex, true, ms); |
| } |
| |
| protected void setDateTime(int parameterIndex, boolean prefix, long... ms) { |
| if (ms == null || ms.length == 0) { |
| throw new IllegalArgumentException("Timestamp must be set!"); |
| } |
| |
| StringListBuilder slb = new StringListBuilder(","); |
| for (long l : ms) { |
| slb.add((prefix ? "TIMESTAMP " : "") + convert(new Date(l))); |
| } |
| |
| parametersMap.put(parameterIndex, slb.toString()); |
| } |
| |
| @Override |
| public String toQueryString() { |
| boolean inStr = false; |
| int parameterIndex = 0; |
| |
| StringBuilder sb = new StringBuilder(1024); |
| for (int i = 0; i < statement.length(); i++) { |
| char c = statement.charAt(i); |
| |
| if (c == '\'') { |
| if (inStr && statement.charAt(i - 1) == '\\') { |
| inStr = true; |
| } else { |
| inStr = !inStr; |
| } |
| sb.append(c); |
| } else if (c == '?' && !inStr) { |
| parameterIndex++; |
| String s = parametersMap.get(parameterIndex); |
| if (s == null) { |
| sb.append(c); |
| } else { |
| sb.append(s); |
| } |
| } else { |
| sb.append(c); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| @Override |
| public ItemIterable<QueryResult> query(boolean searchAllVersions) { |
| return session.query(toQueryString(), searchAllVersions); |
| } |
| |
| @Override |
| public ItemIterable<QueryResult> query(boolean searchAllVersions, OperationContext context) { |
| return session.query(toQueryString(), searchAllVersions, context); |
| } |
| |
| @Override |
| protected QueryStatementImpl clone() throws CloneNotSupportedException { |
| QueryStatementImpl qs = new QueryStatementImpl(session, statement); |
| qs.parametersMap.putAll(parametersMap); |
| |
| return qs; |
| } |
| |
| @Override |
| public String toString() { |
| return toQueryString(); |
| } |
| |
| // --- internal --- |
| |
| private static String escape(String str) { |
| StringBuilder sb = new StringBuilder(str.length() + 16); |
| |
| sb.append('\''); |
| |
| for (int i = 0; i < str.length(); i++) { |
| char c = str.charAt(i); |
| |
| if (c == '\'' || c == '\\') { |
| sb.append('\\'); |
| } |
| |
| sb.append(c); |
| } |
| |
| sb.append('\''); |
| |
| return sb.toString(); |
| } |
| |
| private static String escapeLike(String str) { |
| StringBuilder sb = new StringBuilder(str.length() + 16); |
| |
| sb.append('\''); |
| |
| for (int i = 0; i < str.length(); i++) { |
| char c = str.charAt(i); |
| |
| if (c == '\'') { |
| sb.append('\\'); |
| } else if (c == '\\') { |
| if (i + 1 < str.length() && (str.charAt(i + 1) == '%' || str.charAt(i + 1) == '_')) { |
| // no additional back slash |
| } else { |
| sb.append('\\'); |
| } |
| } |
| |
| sb.append(c); |
| } |
| |
| sb.append('\''); |
| |
| return sb.toString(); |
| } |
| |
| private static String escapeContains(String str) { |
| StringBuilder sb = new StringBuilder(str.length() + 64); |
| |
| for (int i = 0; i < str.length(); i++) { |
| char c = str.charAt(i); |
| |
| if (c == '\\') { |
| sb.append('\\'); |
| } else if (c == '\'' || c == '\"') { |
| sb.append('\\'); |
| } else if (c == '-') { |
| if (i > 0) { |
| char cb = str.charAt(i - 1); |
| if (cb == '\\') { |
| sb.deleteCharAt(sb.length() - 1); |
| } else if (cb != ' ') { |
| sb.append('\\'); |
| } |
| } |
| } |
| |
| sb.append(c); |
| } |
| |
| return escape(sb.toString()); |
| } |
| |
| private static String convert(Date date) { |
| SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); |
| sdf.setTimeZone(TimeZone.getTimeZone("GMT")); |
| |
| return "'" + sdf.format(date) + "'"; |
| } |
| } |