/* * 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.apache.chemistry.opencmis.server.support.query; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.antlr.runtime.tree.Tree; import org.apache.chemistry.opencmis.commons.PropertyIds; import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition; import org.apache.chemistry.opencmis.commons.enums.BaseTypeId; import org.apache.chemistry.opencmis.server.support.TypeManager; import org.apache.chemistry.opencmis.server.support.TypeValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * QueryObject is a class used to encapsulate a CMIS query. It is created from * an ANTLR parser on an incoming query string. During parsing various * informations are collected and stored in objects suitable for evaluating the * query (like selected properties, effected types and order statements. A query * evaluator can use this information to perform the query and build the result. */ public class QueryObject { private static final Logger LOG = LoggerFactory.getLogger(QueryObject.class); // For error handling see: // http://www.antlr.org/pipermail/antlr-interest/2008-April/027600.html // select part protected TypeManager typeMgr; protected final List<CmisSelector> selectReferences = new ArrayList<CmisSelector>(); protected final List<CmisSelector> whereReferences = new ArrayList<CmisSelector>(); protected final List<CmisSelector> joinReferences = new ArrayList<CmisSelector>(); // --> Join not implemented yet protected final Map<String, CmisSelector> colOrFuncAlias = new HashMap<String, CmisSelector>(); // from part /** map from alias name to type query name */ protected final Map<String, String> froms = new LinkedHashMap<String, String>(); /** main from alias name */ protected String from = null; protected final List<JoinSpec> joinSpecs = new ArrayList<JoinSpec>(); // where part protected final Map<Integer, CmisSelector> columnReferences = new HashMap<Integer, CmisSelector>(); protected final Map<Integer, String> typeReferences = new HashMap<Integer, String>(); // order by part protected final List<SortSpec> sortSpecs = new ArrayList<SortSpec>(); @SuppressWarnings("serial") protected List<String> predefinedQueryNames = new ArrayList<String>() { { add("SEARCH_SCORE"); } }; private String errorMessage; public enum ParserMode {MODE_STRICT, MODE_ALLOW_RELAXED_SELECT}; private ParserMode selectMode = ParserMode.MODE_STRICT; public static class JoinSpec { /** INNER / LEFT / RIGHT */ public final String kind; /** Alias or full table type */ public final String alias; public ColumnReference onLeft; public ColumnReference onRight; public JoinSpec(String kind, String alias) { this.kind = kind; this.alias = alias; } public void setSelectors(ColumnReference onLeft, ColumnReference onRight) { this.onLeft = onLeft; this.onRight = onRight; } @Override public String toString() { return "JoinReference(" + kind + "," + alias + "," + onLeft + "," + onRight + ")"; } } public class SortSpec { public final boolean ascending; public final Integer colRefKey; // key in columnReferencesMap point to // column // descriptions public SortSpec(Integer key, boolean ascending) { this.colRefKey = key; this.ascending = ascending; } public CmisSelector getSelector() { return columnReferences.get(colRefKey); } public boolean isAscending() { return ascending; } } public QueryObject() { } public QueryObject(TypeManager tm) { typeMgr = tm; } public void setSelectMode(ParserMode mode) { selectMode = mode; } public Map<Integer, CmisSelector> getColumnReferences() { return Collections.unmodifiableMap(columnReferences); } public CmisSelector getColumnReference(Integer token) { return columnReferences.get(token); } public String getTypeReference(Integer token) { return typeReferences.get(token); } public String getErrorMessage() { return errorMessage; } // /////////////////////////////////////////////////////// // SELECT part // public accessor methods public List<CmisSelector> getSelectReferences() { return selectReferences; } public void addSelectReference(Tree node, CmisSelector selRef) { selectReferences.add(selRef); columnReferences.put(node.getTokenStartIndex(), selRef); } public void addAlias(String aliasName, CmisSelector aliasRef) { if (LOG.isDebugEnabled()) { LOG.debug("add alias: {} for: {}", aliasName, aliasRef); } if (colOrFuncAlias.containsKey(aliasName)) { throw new CmisQueryException("You cannot use name " + aliasName + " more than once as alias in a select."); } else { aliasRef.setAliasName(aliasName); colOrFuncAlias.put(aliasName, aliasRef); } } public CmisSelector getSelectAlias(String aliasName) { return colOrFuncAlias.get(aliasName); } // /////////////////////////////////////////////////////// // FROM part public String addType(String aliasName, String typeQueryName) { try { if (LOG.isDebugEnabled()) { LOG.debug("add alias: {} for: {}", aliasName, typeQueryName); } if (froms.containsKey(aliasName)) { throw new CmisQueryException("You cannot use name " + aliasName + " more than once as alias in a from part."); } if (aliasName == null) { aliasName = typeQueryName; } froms.put(aliasName, typeQueryName); if (from == null) { from = aliasName; } return aliasName; } catch (CmisQueryException cqe) { errorMessage = cqe.getMessage(); // preserve message return null; // indicate an error to ANTLR so that it generates // FailedPredicateException } } public String getMainTypeAlias() { return from; } public Map<String, String> getTypes() { return Collections.unmodifiableMap(froms); } public String getTypeQueryName(String qualifier) { return froms.get(qualifier); } public TypeDefinition getTypeDefinitionFromQueryName(String queryName) { return typeMgr.getTypeByQueryName(queryName); } public TypeDefinition getParentType(TypeDefinition td) { String parentType = td.getParentTypeId(); return parentType == null ? null : typeMgr.getTypeById(parentType).getTypeDefinition(); } public TypeDefinition getParentType(String typeId) { TypeDefinition td = typeMgr.getTypeById(typeId).getTypeDefinition(); String parentType = td == null ? null : td.getParentTypeId(); return parentType == null ? null : typeMgr.getTypeById(parentType).getTypeDefinition(); } public TypeDefinition getMainFromName() { // as we don't support JOINS take first type String queryName = froms.values().iterator().next(); TypeDefinition td = getTypeDefinitionFromQueryName(queryName); return td; } /** * return a map of all columns that have been requested in the SELECT part * of the statement. * * @return a map with a String as a key and value. key is the alias if an * alias was given or the query name otherwise. value is the query * name of the property. */ public Map<String, String> getRequestedPropertiesByAlias() { return getRequestedProperties(true); } private Map<String, String> getRequestedProperties(boolean byAlias) { Map<String, String> res = new HashMap<String, String>(); for (CmisSelector sel : selectReferences) { if (sel instanceof ColumnReference) { ColumnReference colRef = (ColumnReference) sel; String key = colRef.getPropertyId(); if (null == key) { key = colRef.getPropertyQueryName(); // happens for * } String propDescr = colRef.getAliasName() == null ? colRef.getPropertyQueryName() : colRef .getAliasName(); if (byAlias) { res.put(propDescr, key); } else { res.put(key, propDescr); } } } return res; } /** * return a map of all functions that have been requested in the SELECT part * of the statement. * * @return a map with a String as a key and value. key is the alias if an * alias was given or the function name otherwise, value is the a * name of the property. */ public Map<String, String> getRequestedFuncsByAlias() { return getRequestedFuncs(true); } private Map<String, String> getRequestedFuncs(boolean byAlias) { Map<String, String> res = new HashMap<String, String>(); for (CmisSelector sel : selectReferences) { if (sel instanceof FunctionReference) { FunctionReference funcRef = (FunctionReference) sel; String propDescr = funcRef.getAliasName() == null ? funcRef.getName() : funcRef.getAliasName(); if (byAlias) { res.put(propDescr, funcRef.getName()); } else { res.put(funcRef.getName(), propDescr); } } } return res; } // /////////////////////////////////////////////////////// // JOINS public void addJoinReference(Tree node, CmisSelector reference) { columnReferences.put(node.getTokenStartIndex(), reference); joinReferences.add(reference); } public List<CmisSelector> getJoinReferences() { return Collections.unmodifiableList(joinReferences); } public void addJoin(String kind, String alias, boolean hasSpec) { JoinSpec join = new JoinSpec(kind, alias); if (hasSpec) { // get columns from last added references int n = joinReferences.size(); ColumnReference onLeft = (ColumnReference) joinReferences.get(n - 2); ColumnReference onRight = (ColumnReference) joinReferences.get(n - 1); join.setSelectors(onLeft, onRight); } joinSpecs.add(join); } public List<JoinSpec> getJoins() { return joinSpecs; } // /////////////////////////////////////////////////////// // WHERE part public void addWhereReference(Tree node, CmisSelector reference) { if (LOG.isDebugEnabled()) { LOG.debug("add node to where: {}", System.identityHashCode(node)); } columnReferences.put(node.getTokenStartIndex(), reference); whereReferences.add(reference); } public List<CmisSelector> getWhereReferences() { return Collections.unmodifiableList(whereReferences); } public void addWhereTypeReference(Tree node, String qualifier) { if (node != null) { typeReferences.put(node.getTokenStartIndex(), qualifier); } } // /////////////////////////////////////////////////////// // ORDER_BY part public List<SortSpec> getOrderBys() { return Collections.unmodifiableList(sortSpecs); } public void addSortCriterium(Tree node, ColumnReference colRef, boolean ascending) { if (LOG.isDebugEnabled()) { LOG.debug("addSortCriterium: {} ascending: {}", colRef, ascending); } columnReferences.put(node.getTokenStartIndex(), colRef); sortSpecs.add(new SortSpec(node.getTokenStartIndex(), ascending)); } /** * Tests if the query has a JOIN from one primary type to only secondary * types (This JOIN does not require a JOIN capability in CMIS). * * @return list of secondary type ids that are joined or null if the query * has no JOINs or has joins to primary types */ public List<TypeDefinition> getJoinedSecondaryTypes() { List<TypeDefinition> secondaryTypeIds = new ArrayList<TypeDefinition>(); Map<String, String> froms = getTypes(); if (froms.size() == 1) { return null; // no JOIN in query } String mainTypeQueryName = froms.get(getMainTypeAlias()); for (String queryName : froms.values()) { TypeDefinition td = getTypeDefinitionFromQueryName(queryName); if (queryName.equals(mainTypeQueryName)) { continue; } if (td.getBaseTypeId() == BaseTypeId.CMIS_SECONDARY) { secondaryTypeIds.add(td); } else { return null; } } for (JoinSpec join : getJoins()) { if (!(null != join.onLeft && null != join.onRight && ((join.onRight.getPropertyId() == null && join.onRight.getTypeDefinition().getBaseTypeId() == BaseTypeId.CMIS_SECONDARY && join.onLeft .getPropertyId().equals(PropertyIds.OBJECT_ID)) || (join.onLeft.getPropertyId() == null && join.onLeft.getTypeDefinition().getBaseTypeId() == BaseTypeId.CMIS_SECONDARY && join.onRight .getPropertyId().equals(PropertyIds.OBJECT_ID))))) { return null; } } return secondaryTypeIds; } // /////////////////////////////////////////////////////// // resolve types after first pass traversing the AST is complete public boolean resolveTypes() { try { LOG.debug("First pass of query traversal is complete, resolving types"); if (null == typeMgr) { return true; } // First resolve all alias names defined in SELECT: for (CmisSelector alias : colOrFuncAlias.values()) { if (alias instanceof ColumnReference) { ColumnReference colRef = ((ColumnReference) alias); resolveTypeForAlias(colRef); } } // Then replace all aliases used somewhere by their resolved column // reference: for (Integer obj : columnReferences.keySet()) { CmisSelector selector = columnReferences.get(obj); String key = selector.getName(); if (colOrFuncAlias.containsKey(key)) { // it is an alias CmisSelector resolvedReference = colOrFuncAlias.get(key); columnReferences.put(obj, resolvedReference); // Note: ^ This may replace the value in the map with the // same // value, but this does not harm. // Otherwise we need to check if it is resolved or not which // causes two more ifs: // if (selector instanceof ColumnReference) { // ColumnReference colRef = ((ColumnReference) selector); // if (colRef.getTypeDefinition() == null) // it is not yet // resolved // // replace unresolved column reference by resolved on // from // alias map // columnReferences.put(obj, // colOrFuncAlias.get(selector.getAliasName())); // } else // columnReferences.put(obj, // colOrFuncAlias.get(selector.getAliasName())); if (whereReferences.remove(selector)) { // replace unresolved by resolved reference whereReferences.add(resolvedReference); } if (joinReferences.remove(selector)) { // replace unresolved by resolved reference joinReferences.add(resolvedReference); } } } // The replace all remaining column references not using an alias for (CmisSelector select : columnReferences.values()) { // ignore functions here if (select instanceof ColumnReference) { ColumnReference colRef = ((ColumnReference) select); if (colRef.getTypeDefinition() == null) { // not yet // resolved if (colRef.getQualifier() == null) { // unqualified select: SELECT p FROM resolveTypeForColumnReference(colRef); } else { // qualified select: SELECT t.p FROM validateColumnReferenceAndResolveType(colRef); } } } } // Replace types used as qualifiers (IN_TREE, IN_FOLDER, // CONTAINS) by their corresponding alias (correlation name) for (Entry<Integer, String> en : typeReferences.entrySet()) { Integer obj = en.getKey(); String qualifier = en.getValue(); String typeQueryName = getReferencedTypeQueryName(qualifier); if (typeQueryName == null) { throw new CmisQueryException(qualifier + " is neither a type query name nor an alias."); } if (typeQueryName.equals(qualifier)) { // try to find an alias for it String alias = null; for (Entry<String, String> e : froms.entrySet()) { String q = e.getKey(); String tqn = e.getValue(); if (!tqn.equals(q) && typeQueryName.equals(tqn)) { alias = q; break; } } if (alias != null) { typeReferences.put(obj, alias); } } } return true; } catch (CmisQueryException cqe) { errorMessage = cqe.getMessage(); // preserve message return false; // indicate an error to ANTLR so that it generates // FailedPredicateException } } protected void resolveTypeForAlias(ColumnReference colRef) { String aliasName = colRef.getAliasName(); if (colOrFuncAlias.containsKey(aliasName)) { CmisSelector selector = colOrFuncAlias.get(aliasName); if (selector instanceof ColumnReference) { colRef = (ColumnReference) selector; // alias target if (colRef.getQualifier() == null) { // unqualified select: SELECT p FROM resolveTypeForColumnReference(colRef); } else { // qualified select: SELECT t.p FROM validateColumnReferenceAndResolveType(colRef); } } // else --> ignore FunctionReference } } // for a select x from y, z ... find the type in type manager for x protected void resolveTypeForColumnReference(ColumnReference colRef) { String propName = colRef.getPropertyQueryName(); boolean isStar = propName.equals("*"); // it is property query name without a type, so find type int noFound = 0; TypeDefinition tdFound = null; if (isPredfinedQueryName(propName)) { return; } for (String typeQueryName : froms.values()) { TypeDefinition td = typeMgr.getTypeByQueryName(typeQueryName); if (null == td) { throw new CmisQueryException(typeQueryName + " is neither a type query name nor an alias."); } else if (isStar) { ++noFound; tdFound = null; } else if (TypeValidator.typeContainsPropertyWithQueryName(td, propName)) { ++noFound; tdFound = td; } } if (noFound == 0 && selectMode == ParserMode.MODE_STRICT) { throw new CmisQueryException(propName + " is not a property query name in any of the types in from ..."); } else if (noFound > 1 && !isStar) { throw new CmisQueryException(propName + " is not a unique property query name within the types in from ..."); } else if (null != tdFound) { validateColumnReferenceAndResolveType(tdFound, colRef); } } public boolean isPredfinedQueryName(String name) { return predefinedQueryNames.contains(name); } // for a select x.y from x ... check that x has property y and that x is in // from protected void validateColumnReferenceAndResolveType(ColumnReference colRef) { // either same name or mapped alias String typeQueryName = getReferencedTypeQueryName(colRef.getQualifier()); TypeDefinition td = typeMgr.getTypeByQueryName(typeQueryName); if (null == td) { throw new CmisQueryException(colRef.getQualifier() + " is neither a type query name nor an alias."); } validateColumnReferenceAndResolveType(td, colRef); } protected void validateColumnReferenceAndResolveType(TypeDefinition td, ColumnReference colRef) { // type found, check if property exists boolean hasProp; if (colRef.getPropertyQueryName().equals("*")) { hasProp = true; } else { hasProp = TypeValidator.typeContainsPropertyWithQueryName(td, colRef.getPropertyQueryName()); if (!hasProp && td.getBaseTypeId() == BaseTypeId.CMIS_SECONDARY && colRef.getPropertyQueryName().equals(PropertyIds.OBJECT_ID)) { hasProp = true; // special handling for object id on secondary // types which are required for JOINS } } if (!hasProp) { throw new CmisQueryException(colRef.getPropertyQueryName() + " is not a valid property query name in type " + td.getId() + "."); } colRef.setTypeDefinition(typeMgr.getPropertyIdForQueryName(td, colRef.getPropertyQueryName()), td); } // return type query name for a referenced column (which can be the name // itself or an alias protected String getReferencedTypeQueryName(String qualifier) { String typeQueryName = froms.get(qualifier); if (null == typeQueryName) { // if an alias was defined but still the original is used we have to // search case: SELECT T.p FROM T AS TAlias String q = null; for (String tqn : froms.values()) { if (qualifier.equals(tqn)) { if (q != null) { throw new CmisQueryException(qualifier + " is an ambiguous type query name."); } q = tqn; } } return q; } else { return typeQueryName; } } }