/*
* (C) Copyright 2006-2017 Nuxeo (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Florent Guillaume
*/
package org.nuxeo.ecm.core.opencmis.impl.server;
import static org.apache.chemistry.opencmis.commons.enums.BaseTypeId.CMIS_DOCUMENT;
import static org.apache.chemistry.opencmis.commons.enums.BaseTypeId.CMIS_RELATIONSHIP;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.tree.Tree;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.Cardinality;
import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalDefinitionImpl;
import org.apache.chemistry.opencmis.server.support.query.AbstractPredicateWalker;
import org.apache.chemistry.opencmis.server.support.query.CmisSelector;
import org.apache.chemistry.opencmis.server.support.query.ColumnReference;
import org.apache.chemistry.opencmis.server.support.query.FunctionReference;
import org.apache.chemistry.opencmis.server.support.query.FunctionReference.CmisQlFunction;
import org.apache.chemistry.opencmis.server.support.query.QueryObject;
import org.apache.chemistry.opencmis.server.support.query.QueryObject.SortSpec;
import org.apache.chemistry.opencmis.server.support.query.QueryUtilStrict;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.LocalDateTime;
import org.joda.time.format.DateTimeFormatter;
import org.joda.time.format.ISODateTimeFormat;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentRef;
import org.nuxeo.ecm.core.api.IdRef;
import org.nuxeo.ecm.core.api.IterableQueryResult;
import org.nuxeo.ecm.core.api.LifeCycleConstants;
import org.nuxeo.ecm.core.api.PartialList;
import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
import org.nuxeo.ecm.core.query.QueryParseException;
import org.nuxeo.ecm.core.query.sql.NXQL;
import org.nuxeo.runtime.api.Framework;
import org.nuxeo.runtime.services.config.ConfigurationService;
/**
* Transformer of CMISQL queries into NXQL queries.
*/
public class CMISQLtoNXQL {
private static final Log log = LogFactory.getLog(CMISQLtoNXQL.class);
protected static final String CMIS_PREFIX = "cmis:";
protected static final String NX_PREFIX = "nuxeo:";
protected static final String NXQL_DOCUMENT = "Document";
protected static final String NXQL_RELATION = "Relation";
protected static final String NXQL_DC_TITLE = "dc:title";
protected static final String NXQL_DC_DESCRIPTION = "dc:description";
protected static final String NXQL_DC_CREATOR = "dc:creator";
protected static final String NXQL_DC_CREATED = "dc:created";
protected static final String NXQL_DC_MODIFIED = "dc:modified";
protected static final String NXQL_DC_LAST_CONTRIBUTOR = "dc:lastContributor";
protected static final String NXQL_REL_SOURCE = "relation:source";
protected static final String NXQL_REL_TARGET = "relation:target";
protected static final DateTimeFormatter ISO_DATE_TIME_FORMAT = ISODateTimeFormat.dateTime();
private static final char QUOTE = '\'';
private static final String SPACE_ASC = " asc";
private static final String SPACE_DESC = " desc";
// list of SQL column where NULL (missing value) should be treated as
// Boolean.FALSE in a query result
protected static final Set<String> NULL_IS_FALSE_COLUMNS = new HashSet<>(Arrays.asList(NXQL.ECM_ISVERSION,
NXQL.ECM_ISLATESTVERSION, NXQL.ECM_ISLATESTMAJORVERSION, NXQL.ECM_ISCHECKEDIN));
protected final boolean supportsProxies;
protected Map<String, PropertyDefinition<?>> typeInfo;
protected CoreSession coreSession;
// ----- filled during walks of the clauses -----
protected QueryUtilStrict queryUtil;
protected QueryObject query;
protected TypeDefinition fromType;
protected boolean skipDeleted = true;
// ----- passed to IterableQueryResult -----
/** The real columns, CMIS name mapped to NXQL. */
protected Map<String, String> realColumns = new LinkedHashMap<>();
/** The non-real-columns we'll return as well. */
protected Map<String, ColumnReference> virtualColumns = new LinkedHashMap<>();
public CMISQLtoNXQL(boolean supportsProxies) {
this.supportsProxies = supportsProxies;
}
/**
* Gets the NXQL from a CMISQL query.
*/
public String getNXQL(String cmisql, NuxeoCmisService service, Map<String, PropertyDefinition<?>> typeInfo,
boolean searchAllVersions) throws QueryParseException {
this.typeInfo = typeInfo;
boolean searchLatestVersion = !searchAllVersions;
TypeManagerImpl typeManager = service.getTypeManager();
coreSession = service.coreSession;
try {
queryUtil = new QueryUtilStrict(cmisql, typeManager, new AnalyzingWalker(), false);
queryUtil.processStatement();
query = queryUtil.getQueryObject();
} catch (RecognitionException e) {
throw new QueryParseException(queryUtil.getErrorMessage(e), e);
}
if (query.getTypes().size() != 1 && query.getJoinedSecondaryTypes() == null) {
throw new QueryParseException("JOINs not supported in query: " + cmisql);
}
fromType = query.getMainFromName();
BaseTypeId fromBaseTypeId = fromType.getBaseTypeId();
// now resolve column selectors to actual database columns
for (CmisSelector sel : query.getSelectReferences()) {
recordSelectSelector(sel);
}
for (CmisSelector sel : query.getJoinReferences()) {
ColumnReference col = ((ColumnReference) sel);
if (col.getTypeDefinition().getBaseTypeId() == BaseTypeId.CMIS_SECONDARY) {
// ignore reference to ON FACET.cmis:objectId
continue;
}
recordSelector(sel, JOIN);
}
for (CmisSelector sel : query.getWhereReferences()) {
recordSelector(sel, WHERE);
}
for (SortSpec spec : query.getOrderBys()) {
recordSelector(spec.getSelector(), ORDER_BY);
}
addSystemColumns();
List<String> whereClauses = new ArrayList<>();
// what to select (result columns)
String what = StringUtils.join(realColumns.values(), ", ");
// determine relevant primary types
String nxqlFrom;
if (fromBaseTypeId == CMIS_RELATIONSHIP) {
if (fromType.getParentTypeId() == null) {
nxqlFrom = NXQL_RELATION;
} else {
nxqlFrom = fromType.getId();
}
} else {
nxqlFrom = NXQL_DOCUMENT;
List<String> types = new ArrayList<>();
if (fromType.getParentTypeId() != null) {
// don't add abstract root types
types.add(fromType.getId());
}
LinkedList<TypeDefinitionContainer> typesTodo = new LinkedList<>();
typesTodo.addAll(typeManager.getTypeDescendants(fromType.getId(), -1, Boolean.TRUE));
// recurse to get all subtypes
TypeDefinitionContainer tc;
while ((tc = typesTodo.poll()) != null) {
types.add(tc.getTypeDefinition().getId());
typesTodo.addAll(tc.getChildren());
}
if (types.isEmpty()) {
// shoudn't happen
types = Collections.singletonList("__NOSUCHTYPE__");
}
// build clause
StringBuilder pt = new StringBuilder();
pt.append(NXQL.ECM_PRIMARYTYPE);
pt.append(" IN (");
for (Iterator<String> it = types.iterator(); it.hasNext();) {
pt.append(QUOTE);
pt.append(it.next());
pt.append(QUOTE);
if (it.hasNext()) {
pt.append(", ");
}
}
pt.append(")");
whereClauses.add(pt.toString());
}
// lifecycle not deleted filter
if (skipDeleted) {
whereClauses.add(String.format("%s <> '%s'", NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE));
}
// searchAllVersions filter
if (searchLatestVersion && fromBaseTypeId == CMIS_DOCUMENT) {
whereClauses.add(String.format("%s = 1", NXQL.ECM_ISLATESTVERSION));
}
// no proxies
if (!supportsProxies) {
whereClauses.add(NXQL.ECM_ISPROXY + " = 0");
}
// WHERE clause
Tree whereNode = queryUtil.getWalker().getWherePredicateTree();
if (whereNode != null) {
GeneratingWalker generator = new GeneratingWalker();
generator.walkPredicate(whereNode);
whereClauses.add(generator.buf.toString());
}
// ORDER BY clause
List<String> orderbys = new ArrayList<>();
for (SortSpec spec : query.getOrderBys()) {
String orderby;
CmisSelector sel = spec.getSelector();
if (sel instanceof ColumnReference) {
orderby = (String) sel.getInfo();
} else {
orderby = NXQL.ECM_FULLTEXT_SCORE;
}
if (!spec.ascending) {
orderby += " DESC";
}
orderbys.add(orderby);
}
// create the whole select
String where = StringUtils.join(whereClauses, " AND ");
String nxql = "SELECT " + what + " FROM " + nxqlFrom + " WHERE " + where;
if (!orderbys.isEmpty()) {
nxql += " ORDER BY " + StringUtils.join(orderbys, ", ");
}
// System.err.println("CMIS: " + statement);
// System.err.println("NXQL: " + nxql);
return nxql;
}
public IterableQueryResult getIterableQueryResult(IterableQueryResult it, NuxeoCmisService service) {
return new NXQLtoCMISIterableQueryResult(it, realColumns, virtualColumns, service);
}
public PartialList<Map<String, Serializable>> convertToCMIS(PartialList<Map<String, Serializable>> pl,
NuxeoCmisService service) {
return pl.list.stream().map(map -> convertToCMISMap(map, realColumns, virtualColumns, service)).collect(
Collectors.collectingAndThen(Collectors.toList(), result -> new PartialList<>(result, pl.totalSize)));
}
protected boolean isFacetsColumn(String name) {
return PropertyIds.SECONDARY_OBJECT_TYPE_IDS.equals(name) || NuxeoTypeHelper.NX_FACETS.equals(name);
}
protected void addSystemColumns() {
// additional references to cmis:objectId and cmis:objectTypeId
for (String propertyId : Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID)) {
if (!realColumns.containsKey(propertyId)) {
ColumnReference col = new ColumnReference(propertyId);
col.setTypeDefinition(propertyId, fromType);
recordSelectSelector(col);
}
}
}
/**
* Records a SELECT selector, and associates it to a database column.
*/
protected void recordSelectSelector(CmisSelector sel) {
if (sel instanceof FunctionReference) {
FunctionReference fr = (FunctionReference) sel;
if (fr.getFunction() != CmisQlFunction.SCORE) {
throw new CmisRuntimeException("Unknown function: " + fr.getFunction());
}
String key = fr.getAliasName();
if (key == null) {
key = "SEARCH_SCORE"; // default, from spec
}
realColumns.put(key, NXQL.ECM_FULLTEXT_SCORE);
if (typeInfo != null) {
PropertyDecimalDefinitionImpl pd = new PropertyDecimalDefinitionImpl();
pd.setId(key);
pd.setQueryName(key);
pd.setCardinality(Cardinality.SINGLE);
pd.setDisplayName("Score");
pd.setLocalName("score");
typeInfo.put(key, pd);
}
} else { // sel instanceof ColumnReference
ColumnReference col = (ColumnReference) sel;
if (col.getPropertyQueryName().equals("*")) {
for (PropertyDefinition<?> pd : fromType.getPropertyDefinitions().values()) {
String id = pd.getId();
if ((pd.getCardinality() == Cardinality.SINGLE //
&& Boolean.TRUE.equals(pd.isQueryable())) //
|| id.equals(PropertyIds.BASE_TYPE_ID)) {
ColumnReference c = new ColumnReference(null, id);
c.setTypeDefinition(id, fromType);
recordSelectSelector(c);
}
}
return;
}
String key = col.getPropertyQueryName();
PropertyDefinition<?> pd = col.getPropertyDefinition();
String nxqlCol = getColumn(col);
String id = pd.getId();
if (nxqlCol != null && pd.getCardinality() == Cardinality.SINGLE && (Boolean.TRUE.equals(pd.isQueryable())
|| id.equals(PropertyIds.BASE_TYPE_ID) || id.equals(PropertyIds.OBJECT_TYPE_ID))) {
col.setInfo(nxqlCol);
realColumns.put(key, nxqlCol);
} else {
virtualColumns.put(key, col);
}
if (typeInfo != null) {
typeInfo.put(key, pd);
}
}
}
protected static final String JOIN = "JOIN";
protected static final String WHERE = "WHERE";
protected static final String ORDER_BY = "ORDER BY";
/**
* Records a JOIN / WHERE / ORDER BY selector, and associates it to a database column.
*/
protected void recordSelector(CmisSelector sel, String clauseType) {
if (sel instanceof FunctionReference) {
FunctionReference fr = (FunctionReference) sel;
if (clauseType != ORDER_BY) { // == ok
throw new QueryParseException("Cannot use function in " + clauseType + " clause: " + fr.getFunction());
}
// ORDER BY SCORE, nothing further to record
return;
}
ColumnReference col = (ColumnReference) sel;
// fetch column and associate it to the selector
String column = getColumn(col);
if (!isFacetsColumn(col.getPropertyId()) && column == null) {
throw new QueryParseException(
"Cannot use column in " + clauseType + " clause: " + col.getPropertyQueryName());
}
col.setInfo(column);
if (clauseType == WHERE && NuxeoTypeHelper.NX_LIFECYCLE_STATE.equals(col.getPropertyId())) {
// explicit lifecycle query: do not include the 'deleted' lifecycle
// filter
skipDeleted = false;
}
}
/**
* Finds a NXQL column from a CMIS reference.
*/
protected String getColumn(ColumnReference col) {
return getColumn(col.getPropertyId());
}
/**
* Finds a NXQL column from a CMIS reference.
*/
protected String getColumn(String propertyId) {
if (propertyId.startsWith(CMIS_PREFIX) || propertyId.startsWith(NX_PREFIX)) {
return getSystemColumn(propertyId);
} else {
// CMIS property names are identical to NXQL ones
// for non-system properties
return propertyId;
}
}
/**
* Finds a NXQL system column from a CMIS system property id.
*/
protected String getSystemColumn(String propertyId) {
switch (propertyId) {
case PropertyIds.OBJECT_ID:
return NXQL.ECM_UUID;
case PropertyIds.PARENT_ID:
case NuxeoTypeHelper.NX_PARENT_ID:
return NXQL.ECM_PARENTID;
case NuxeoTypeHelper.NX_PATH_SEGMENT:
return NXQL.ECM_NAME;
case NuxeoTypeHelper.NX_POS:
return NXQL.ECM_POS;
case PropertyIds.OBJECT_TYPE_ID:
return NXQL.ECM_PRIMARYTYPE;
case PropertyIds.SECONDARY_OBJECT_TYPE_IDS:
case NuxeoTypeHelper.NX_FACETS:
return NXQL.ECM_MIXINTYPE;
case PropertyIds.VERSION_LABEL:
return NXQL.ECM_VERSIONLABEL;
case PropertyIds.IS_LATEST_MAJOR_VERSION:
return NXQL.ECM_ISLATESTMAJORVERSION;
case PropertyIds.IS_LATEST_VERSION:
return NXQL.ECM_ISLATESTVERSION;
case NuxeoTypeHelper.NX_ISVERSION:
return NXQL.ECM_ISVERSION;
case NuxeoTypeHelper.NX_ISCHECKEDIN:
return NXQL.ECM_ISCHECKEDIN;
case NuxeoTypeHelper.NX_LIFECYCLE_STATE:
return NXQL.ECM_LIFECYCLESTATE;
case PropertyIds.NAME:
return NXQL_DC_TITLE;
case PropertyIds.DESCRIPTION:
return NXQL_DC_DESCRIPTION;
case PropertyIds.CREATED_BY:
return NXQL_DC_CREATOR;
case PropertyIds.CREATION_DATE:
return NXQL_DC_CREATED;
case PropertyIds.LAST_MODIFICATION_DATE:
return NXQL_DC_MODIFIED;
case PropertyIds.LAST_MODIFIED_BY:
return NXQL_DC_LAST_CONTRIBUTOR;
case PropertyIds.SOURCE_ID:
return NXQL_REL_SOURCE;
case PropertyIds.TARGET_ID:
return NXQL_REL_TARGET;
}
return null;
}
protected static String cmisToNxqlFulltextQuery(String statement) {
// NXQL syntax has implicit AND
statement = statement.replace(" and ", " ");
statement = statement.replace(" AND ", " ");
return statement;
}
/**
* Convert an ORDER BY part from CMISQL to NXQL.
*
* @since 6.0
*/
protected String convertOrderBy(String orderBy, TypeManagerImpl typeManager) {
List<String> list = new ArrayList<>(1);
for (String order : orderBy.split(",")) {
order = order.trim();
String lower = order.toLowerCase();
String prop;
boolean asc;
if (lower.endsWith(SPACE_ASC)) {
prop = order.substring(0, order.length() - SPACE_ASC.length()).trim();
asc = true;
} else if (lower.endsWith(SPACE_DESC)) {
prop = order.substring(0, order.length() - SPACE_DESC.length()).trim();
asc = false;
} else {
prop = order;
asc = true; // default is repository-specific
}
// assume query name is same as property id
String propId = typeManager.getPropertyIdForQueryName(prop);
if (propId == null) {
throw new CmisInvalidArgumentException("Invalid orderBy: " + orderBy);
}
String col = getColumn(propId);
list.add(asc ? col : (col + " DESC"));
}
return StringUtils.join(list, ", ");
}
/**
* Walker of the WHERE clause that doesn't parse fulltext expressions.
*/
public class AnalyzingWalker extends AbstractPredicateWalker {
public boolean hasContains;
@Override
public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
if (hasContains && Framework.getService(ConfigurationService.class)
.isBooleanPropertyFalse(NuxeoRepository.RELAX_CMIS_SPEC)) {
throw new QueryParseException("At most one CONTAINS() is allowed");
}
hasContains = true;
return null;
}
}
/**
* Walker of the WHERE clause that generates NXQL.
*/
public class GeneratingWalker extends AbstractPredicateWalker {
public static final String NX_FULLTEXT_INDEX_PREFIX = "nx:";
public StringBuilder buf = new StringBuilder();
@Override
public Boolean walkNot(Tree opNode, Tree node) {
buf.append("NOT ");
walkPredicate(node);
return null;
}
@Override
public Boolean walkAnd(Tree opNode, Tree leftNode, Tree rightNode) {
buf.append("(");
walkPredicate(leftNode);
buf.append(" AND ");
walkPredicate(rightNode);
buf.append(")");
return null;
}
@Override
public Boolean walkOr(Tree opNode, Tree leftNode, Tree rightNode) {
buf.append("(");
walkPredicate(leftNode);
buf.append(" OR ");
walkPredicate(rightNode);
buf.append(")");
return null;
}
@Override
public Boolean walkEquals(Tree opNode, Tree leftNode, Tree rightNode) {
walkExpr(leftNode);
buf.append(" = ");
walkExpr(rightNode);
return null;
}
@Override
public Boolean walkNotEquals(Tree opNode, Tree leftNode, Tree rightNode) {
walkExpr(leftNode);
buf.append(" <> ");
walkExpr(rightNode);
return null;
}
@Override
public Boolean walkGreaterThan(Tree opNode, Tree leftNode, Tree rightNode) {
walkExpr(leftNode);
buf.append(" > ");
walkExpr(rightNode);
return null;
}
@Override
public Boolean walkGreaterOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
walkExpr(leftNode);
buf.append(" >= ");
walkExpr(rightNode);
return null;
}
@Override
public Boolean walkLessThan(Tree opNode, Tree leftNode, Tree rightNode) {
walkExpr(leftNode);
buf.append(" < ");
walkExpr(rightNode);
return null;
}
@Override
public Boolean walkLessOrEquals(Tree opNode, Tree leftNode, Tree rightNode) {
walkExpr(leftNode);
buf.append(" <= ");
walkExpr(rightNode);
return null;
}
@Override
public Boolean walkIn(Tree opNode, Tree colNode, Tree listNode) {
walkExpr(colNode);
buf.append(" IN ");
walkExpr(listNode);
return null;
}
@Override
public Boolean walkNotIn(Tree opNode, Tree colNode, Tree listNode) {
walkExpr(colNode);
buf.append(" NOT IN ");
walkExpr(listNode);
return null;
}
@Override
public Boolean walkInAny(Tree opNode, Tree colNode, Tree listNode) {
walkAny(colNode, "IN", listNode);
return null;
}
@Override
public Boolean walkNotInAny(Tree opNode, Tree colNode, Tree listNode) {
walkAny(colNode, "NOT IN", listNode);
return null;
}
@Override
public Boolean walkEqAny(Tree opNode, Tree literalNode, Tree colNode) {
// note that argument order is reversed
walkAny(colNode, "=", literalNode);
return null;
}
protected void walkAny(Tree colNode, String op, Tree exprNode) {
ColumnReference col = getColumnReference(colNode);
if (col.getPropertyDefinition().getCardinality() != Cardinality.MULTI) {
throw new QueryParseException(
"Cannot use " + op + " ANY with single-valued property: " + col.getPropertyQueryName());
}
String nxqlCol = (String) col.getInfo();
buf.append(nxqlCol);
if (!NXQL.ECM_MIXINTYPE.equals(nxqlCol)) {
buf.append("/*");
}
buf.append(' ');
buf.append(op);
buf.append(' ');
walkExpr(exprNode);
}
@Override
public Boolean walkIsNull(Tree opNode, Tree colNode) {
return walkIsNullOrIsNotNull(colNode, true);
}
@Override
public Boolean walkIsNotNull(Tree opNode, Tree colNode) {
return walkIsNullOrIsNotNull(colNode, false);
}
protected Boolean walkIsNullOrIsNotNull(Tree colNode, boolean isNull) {
ColumnReference col = getColumnReference(colNode);
boolean multi = col.getPropertyDefinition().getCardinality() == Cardinality.MULTI;
walkExpr(colNode);
if (multi) {
buf.append("/*");
}
buf.append(isNull ? " IS NULL" : " IS NOT NULL");
return null;
}
@Override
public Boolean walkLike(Tree opNode, Tree colNode, Tree stringNode) {
walkExpr(colNode);
buf.append(" LIKE ");
walkExpr(stringNode);
return null;
}
@Override
public Boolean walkNotLike(Tree opNode, Tree colNode, Tree stringNode) {
walkExpr(colNode);
buf.append(" NOT LIKE ");
walkExpr(stringNode);
return null;
}
@Override
public Boolean walkContains(Tree opNode, Tree qualNode, Tree queryNode) {
String statement = (String) super.walkString(queryNode);
String indexName = NXQL.ECM_FULLTEXT;
// micro parsing of the fulltext statement to perform fulltext
// search on a non default index
if (statement.startsWith(NX_FULLTEXT_INDEX_PREFIX)) {
statement = statement.substring(NX_FULLTEXT_INDEX_PREFIX.length());
int firstColumnIdx = statement.indexOf(':');
if (firstColumnIdx > 0 && firstColumnIdx < statement.length() - 1) {
indexName += '_' + statement.substring(0, firstColumnIdx);
statement = statement.substring(firstColumnIdx + 1);
} else {
log.warn(String.format("fail to microparse custom fulltext index:" + " fallback to '%s'",
indexName));
}
}
// CMIS syntax to NXQL syntax
statement = cmisToNxqlFulltextQuery(statement);
buf.append(indexName);
buf.append(" = ");
buf.append(NXQL.escapeString(statement));
return null;
}
@Override
public Boolean walkInFolder(Tree opNode, Tree qualNode, Tree paramNode) {
String id = (String) super.walkString(paramNode);
buf.append(NXQL.ECM_PARENTID);
buf.append(" = ");
buf.append(NXQL.escapeString(id));
return null;
}
@Override
public Boolean walkInTree(Tree opNode, Tree qualNode, Tree paramNode) {
String id = (String) super.walkString(paramNode);
// don't use ecm:ancestorId because the Elasticsearch converter doesn't understand it
// buf.append(NXQL.ECM_ANCESTORID);
// buf.append(" = ");
// buf.append(NXQL.escapeString(id));
String path;
DocumentRef docRef = new IdRef(id);
if (coreSession.exists(docRef)) {
path = coreSession.getDocument(docRef).getPathAsString();
} else {
// TODO better removal
path = "/__NOSUCHPATH__";
}
buf.append(NXQL.ECM_PATH);
buf.append(" STARTSWITH ");
buf.append(NXQL.escapeString(path));
return null;
}
@Override
public Object walkList(Tree node) {
buf.append("(");
for (int i = 0; i < node.getChildCount(); i++) {
if (i != 0) {
buf.append(", ");
}
Tree child = node.getChild(i);
walkExpr(child);
}
buf.append(")");
return null;
}
@Override
public Object walkBoolean(Tree node) {
Object value = super.walkBoolean(node);
buf.append(Boolean.FALSE.equals(value) ? "0" : "1");
return null;
}
@Override
public Object walkNumber(Tree node) {
// Double or Long
Number value = (Number) super.walkNumber(node);
buf.append(value.toString());
return null;
}
@Override
public Object walkString(Tree node) {
String value = (String) super.walkString(node);
buf.append(NXQL.escapeString(value));
return null;
}
@Override
public Object walkTimestamp(Tree node) {
Calendar value = (Calendar) super.walkTimestamp(node);
buf.append("TIMESTAMP ");
buf.append(QUOTE);
buf.append(ISO_DATE_TIME_FORMAT.print(LocalDateTime.fromCalendarFields(value)));
buf.append(QUOTE);
return null;
}
@Override
public Object walkCol(Tree node) {
String nxqlCol = (String) getColumnReference(node).getInfo();
buf.append(nxqlCol);
return null;
}
protected ColumnReference getColumnReference(Tree node) {
CmisSelector sel = query.getColumnReference(Integer.valueOf(node.getTokenStartIndex()));
if (sel instanceof ColumnReference) {
return (ColumnReference) sel;
} else {
throw new QueryParseException("Cannot use column in WHERE clause: " + sel.getName());
}
}
}
/**
* IterableQueryResult wrapping the one from the NXQL query to turn values into CMIS ones.
*/
// static to avoid keeping the whole QueryMaker in the returned object
public static class NXQLtoCMISIterableQueryResult
implements IterableQueryResult, Iterator<Map<String, Serializable>> {
protected IterableQueryResult it;
protected Iterator<Map<String, Serializable>> iter;
protected Map<String, String> realColumns;
protected Map<String, ColumnReference> virtualColumns;
protected NuxeoCmisService service;
public NXQLtoCMISIterableQueryResult(IterableQueryResult it, Map<String, String> realColumns,
Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) {
this.it = it;
iter = it.iterator();
this.realColumns = realColumns;
this.virtualColumns = virtualColumns;
this.service = service;
}
@Override
public Iterator<Map<String, Serializable>> iterator() {
return this;
}
@Override
public void close() {
it.close();
}
@SuppressWarnings("deprecation")
@Override
public boolean isLife() {
return it.isLife();
}
@Override
public boolean mustBeClosed() {
return it.mustBeClosed();
}
@Override
public long size() {
return it.size();
}
@Override
public long pos() {
return it.pos();
}
@Override
public void skipTo(long pos) {
it.skipTo(pos);
}
@Override
public boolean hasNext() {
return iter.hasNext();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Map<String, Serializable> next() {
// map of NXQL to value
Map<String, Serializable> nxqlMap = iter.next();
return convertToCMISMap(nxqlMap, realColumns, virtualColumns, service);
}
}
protected static Map<String, Serializable> convertToCMISMap(Map<String, Serializable> nxqlMap,
Map<String, String> realColumns, Map<String, ColumnReference> virtualColumns, NuxeoCmisService service) {
// find the CMIS keys and values
Map<String, Serializable> cmisMap = new HashMap<>();
for (Entry<String, String> en : realColumns.entrySet()) {
String cmisCol = en.getKey();
String nxqlCol = en.getValue();
Serializable value = nxqlMap.get(nxqlCol);
// type conversion to CMIS values
if (value instanceof Long) {
value = BigInteger.valueOf(((Long) value).longValue());
} else if (value instanceof Integer) {
value = BigInteger.valueOf(((Integer) value).intValue());
} else if (value instanceof Double) {
value = BigDecimal.valueOf(((Double) value).doubleValue());
} else if (value == null) {
// special handling of some columns where NULL means FALSE
if (NULL_IS_FALSE_COLUMNS.contains(nxqlCol)) {
value = Boolean.FALSE;
}
}
cmisMap.put(cmisCol, value);
}
// virtual values
// map to store actual data for each qualifier
Map<String, NuxeoObjectData> datas = null;
TypeManagerImpl typeManager = service.getTypeManager();
for (Entry<String, ColumnReference> vc : virtualColumns.entrySet()) {
String key = vc.getKey();
ColumnReference col = vc.getValue();
String qual = col.getQualifier();
if (col.getPropertyId().equals(PropertyIds.BASE_TYPE_ID)) {
// special case, no need to get full Nuxeo Document
String typeId = (String) cmisMap.get(PropertyIds.OBJECT_TYPE_ID);
if (typeId == null) {
throw new NullPointerException();
}
TypeDefinitionContainer type = typeManager.getTypeById(typeId);
String baseTypeId = type.getTypeDefinition().getBaseTypeId().value();
cmisMap.put(key, baseTypeId);
continue;
}
if (datas == null) {
datas = new HashMap<>(2);
}
NuxeoObjectData data = datas.get(qual);
if (data == null) {
// find main id for this qualifier in the result set
// (main id always included in joins)
// TODO check what happens if cmis:objectId is aliased
String id = (String) cmisMap.get(PropertyIds.OBJECT_ID);
try {
// reentrant call to the same session, but the MapMaker
// is only called from the IterableQueryResult in
// queryAndFetch which manipulates no session state
// TODO constructing the DocumentModel (in
// NuxeoObjectData) is expensive, try to get value
// directly
data = service.getObject(service.getNuxeoRepository().getId(), id, null, null, null, null, null,
null, null);
} catch (CmisRuntimeException e) {
log.error("Cannot get document: " + id, e);
}
datas.put(qual, data);
}
Serializable v;
if (data == null) {
// could not fetch
v = null;
} else {
NuxeoPropertyDataBase<?> pd = data.getProperty(col.getPropertyId());
if (pd == null) {
v = null;
} else {
if (pd.getPropertyDefinition().getCardinality() == Cardinality.SINGLE) {
v = (Serializable) pd.getFirstValue();
} else {
v = (Serializable) pd.getValues();
}
}
}
cmisMap.put(key, v);
}
return cmisMap;
}
}