blob: 5bb6069b04aa372c6de1d90e01db6082773e43b2 [file] [log] [blame]
/*
* 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 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 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) {
LOG.debug("add alias: " + aliasName + " for: " + 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 {
LOG.debug("add alias: " + aliasName + " for: " + 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) {
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) {
LOG.debug("addSortCriterium: " + colRef + " ascending: " + 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) {
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;
}
}
}