/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright © 2011-2014 ForgeRock AS. All rights reserved.
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* http://forgerock.org/license/CDDLv1.0.html
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at http://forgerock.org/license/CDDLv1.0.html
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*/
package org.forgerock.openidm.repo.orientdb.impl.query;
import com.orientechnologies.orient.core.command.OCommandRequest;
import com.orientechnologies.orient.core.db.document.ODatabaseDocumentTx;
import java.util.Map;
import org.forgerock.json.resource.BadRequestException;
import org.forgerock.json.resource.Request;
import org.forgerock.openidm.smartevent.Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Configured and ad-hoc query support on OrientDB
*
* Queries can contain tokens of the format ${token-name}
*
* @param <Q> The query object type
* @param <R> The request type
* @param <U> The result type
*
*/
public abstract class ConfiguredQueries<Q extends OCommandRequest, R extends Request, U> {
final static Logger logger = LoggerFactory.getLogger(ConfiguredQueries.class);
// Monitoring event name prefix
static final String EVENT_RAW_QUERY_PREFIX = "openidm/internal/repo/orientdb/raw/query/";
private final TokenHandler tokenHandler = new TokenHandler();
// Pre-configured queries, key is query id
private final Map<String, QueryInfo<Q>> configuredQueries;
ConfiguredQueries(Map<String, QueryInfo<Q>> configuredQueries) {
this.configuredQueries = configuredQueries;
}
/**
* Set the pre-configured queries, which are identified by a query identifier and can be
* invoked using this identifier
*
* Success to set the queries does not mean they are valid as some can only be validated at
* query execution time.
*
* @param queries the complete list of configured queries, mapping from query id to the
* query expression which may optionally contain tokens in the form ${token-name}.
*/
public void setConfiguredQueries(Map<String, String> queries) {
configuredQueries.clear();
// Populate/Override with Queries configured
if (queries != null) {
for (Map.Entry<String, String> entry : queries.entrySet()) {
String queryId = entry.getKey();
String queryString = entry.getValue();
QueryInfo<Q> queryInfo = prepareQuery(queryString);
configuredQueries.put(queryId, queryInfo);
}
}
}
/**
* Check if a {@code queryId} is present in the set of configured queries.
*
* @param queryId Id of the query to check for
*
* @return true if the queryId is present in the set of configured queries.
*/
public boolean queryIdExists(final String queryId) {
return configuredQueries.containsKey(queryId);
}
/**
* Resolve the query string which can contain %{token} tokens to a fully resolved query
* Doing the resolution ourselves means it can not be a prepared statement with tokens
* that gets re-used, but it allows us to replace more parts of the query than just
* the where clause.
*
* @param queryString The query with tokens
* @param params THe parameters to replace the tokens with
* @return the query with any found tokens replaced
* @throws BadRequestException if the queryString contains token missing from params
*/
private Q resolveQuery(String queryString, Map<String, String> params)
throws BadRequestException {
String resolvedQueryString = tokenHandler.replaceTokensWithValues(queryString, params);
return createQueryObject(resolvedQueryString);
}
/**
* Populate and prepare the query information with the query expression passed in the parameters
*
* @param type the relative/local resource name
* @param queryExpression the parameters with the query expression and token replacement key/values
* @return the populated query info
*/
private QueryInfo<Q> resolveInlineQuery(final String type, String queryExpression) {
// TODO: LRU cache
return prepareQuery(queryExpression);
}
/**
* Construct a prepared statement from the query String
*
* The constructed prepared query can only be verified at query execution time
* and hence the query execution may later fall back onto non-prepared execution
*
* @param queryString the query expression, including tokens to replace
* @return the constructed (but not validated) prepared statement
*/
private QueryInfo<Q> prepareQuery(String queryString) {
try {
String replacedQueryString = tokenHandler.replaceTokensWithOrientToken(queryString);
Q query = createQueryObject(replacedQueryString);
return new QueryInfo<Q>(true, query, queryString);
} catch (PrepareNotSupported ex) {
// Statement not in a format that it can be converted into prepared statement
return new QueryInfo<Q>(false, null, queryString);
} catch (com.orientechnologies.orient.core.exception.OQueryParsingException ex) {
// With current OrientDB impl parsing will actually only fail on first use,
// hence unless the implementation changes this is unlikely to trigger
return new QueryInfo<Q>(false, null, queryString);
}
}
/**
* Create the prepared query object type according to the implementation.
*
* @param queryString the query expression, including tokens to replace
* @return the prepared query object
*/
protected abstract Q createQueryObject(String queryString);
/**
* @return the smartevent Name for a given query
*/
Name getEventName(String queryId, String queryExpression) {
if (queryId == null) {
return Name.get(EVENT_RAW_QUERY_PREFIX + "_query_expression");
} else {
return Name.get(EVENT_RAW_QUERY_PREFIX + queryId);
}
}
/**
* Find the QueryInfo according to the commandId or commandExpression.
*
* @param type the type/resource to query
* @param queryId the queryId parameter
* @param queryExpression the queryExpression parameter
* @return
* @throws NullPointerException if neither queryId or queryExpression are provided
* @throws IllegalArgumentException if the queryId is not known/configured
*/
QueryInfo<Q> findQueryInfo(String type, String queryId, String queryExpression) {
if (queryId == null && queryExpression == null) {
throw new NullPointerException();
}
if (queryExpression != null) {
return resolveInlineQuery(type, queryExpression);
}
if (queryIdExists(queryId)) {
return configuredQueries.get(queryId);
}
throw new IllegalArgumentException();
}
/**
* Execute a query, either a pre-configured query by using the query ID, or a query expression passed as
* part of the params.
*
* The keys for the input parameters as well as the return map entries are in QueryConstants.
*
* @param type the relative/local resource name, which needs to be converted to match the OrientDB document class name
* @param request the query request, including parameters which include the query id, or the query expression, as well as the
* token key/value pairs to replace in the query
* @param database a handle to a database connection instance for exclusive use by the query method whilst it is executing.
* @return The query result, which includes meta-data about the query, and the result set itself.
* @throws BadRequestException if the passed request parameters are invalid, e.g. missing query id or query expression or tokens.
*/
public abstract U query(final String type, R request, ODatabaseDocumentTx database) throws BadRequestException;
U doTokenSubsitutionQuery(QueryInfo<Q> queryInfo, Map<String, String> params, ODatabaseDocumentTx database)
throws BadRequestException {
// Substitute tokens manually, which supports replacing any part of the query
Q query = resolveQuery(queryInfo.getQueryString(), params);
logger.debug("Manual token substitution for {} resulted in {}", queryInfo.getQueryString(), query);
return database.command(query).execute(params);
}
U doPreparedQuery(QueryInfo<Q> queryInfo, Map<String, String> params, ODatabaseDocumentTx database)
throws BadRequestException {
// Try to use the prepared statement, which supports token substitution of where clause
Q query = queryInfo.getPreparedQuery();
logger.debug("Prepared query {} ", query);
return database.command(query).execute(params);
}
}