diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/TableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/TableHandler.java index 966279032f..ab083e74c0 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/TableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/TableHandler.java @@ -2,6 +2,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright © 2011-2015 ForgeRock AS. All rights reserved. + * Portions Copyright 2023 Wren Security. * * The contents of this file are subject to the terms * of the Common Development and Distribution License @@ -23,197 +24,208 @@ */ package org.forgerock.openidm.repo.jdbc; -import java.io.IOException; import java.sql.Connection; import java.sql.SQLException; import java.util.List; import java.util.Map; - import org.forgerock.json.JsonPointer; import org.forgerock.json.resource.BadRequestException; -import org.forgerock.json.resource.ConflictException; -import org.forgerock.json.resource.ForbiddenException; import org.forgerock.json.resource.InternalServerErrorException; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.PreconditionFailedException; -import org.forgerock.json.resource.ResourceResponse; import org.forgerock.json.resource.ResourceException; +import org.forgerock.json.resource.ResourceResponse; import org.forgerock.util.query.QueryFilter; +/** + * Handler responsible for performing SQL operations on the underlying data source. + * + *

There are two different strategies represented by the respective implementations: + * + *

+ */ public interface TableHandler { /** - * Gets an object from the repository by identifier. The returned object is not validated + * Get an object from the repository by its identifier. The returned object is not validated * against the current schema and may need processing to conform to an updated schema. + * *

* The object will contain metadata properties, including object identifier {@code _id}, - * and object version {@code _rev} to enable optimistic concurrency + * and object version {@code _rev} to enable optimistic concurrency. * - * @param fullId the qualified identifier of the object to retrieve from the object set. - * @param type is the qualifier of the object to retrieve - * @param localId the identifier without the qualifier of the object to retrieve - * @param connection + * @param fullId The qualified identifier of the object to retrieve from the object set. + * @param type The qualifier of the object to retrieve. + * @param localId The identifier without the qualifier of the object to retrieve. + * @param connection Database connection to use. * @throws NotFoundException if the specified object could not be found. - * @throws SQLException if a DB failure was reported - * @throws IOException if a failure to convert the JSON model was reported - * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure - * @return the requested object. + * @throws SQLException if a DB failure was reported. + * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure. + * @return The requested object. */ - public abstract ResourceResponse read(String fullId, String type, - String localId, Connection connection) - throws SQLException, IOException, ResourceException; + ResourceResponse read(String fullId, String type, String localId, Connection connection) + throws SQLException, ResourceException; /** - * Creates a new object in the object set. + * Create a new object in the object set. + * *

- * This method sets the {@code _id} property to the assigned identifier for the object, - * and the {@code _rev} property to the revised object version (For optimistic concurrency) - * - * @param fullId the client-generated identifier to use, or {@code null} if server-generated identifier is requested. - * @param type - * @param localId - * @param obj the contents of the object to create in the object set. - * @param connection + * This method mutates the provided object by setting the {@code _id} property to the + * assigned identifier for the object and the {@code _rev} property to the revised object + * version (for optimistic concurrency). + * + * @param fullId The client-generated identifier to use, or {@code null} if server-generated + * identifier is requested. + * @param type The qualifier of the object to create. + * @param localId The identifier without the qualifier (if specified in {@code fullId} parameter). + * @param obj The contents of the object to create in the object set. + * @param connection Database connection to use. * @throws NotFoundException if the specified id could not be resolved. - * @throws ForbiddenException if access to the object or object set is forbidden. * @throws PreconditionFailedException if an object with the same ID already exists. - * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure - * @throws java.io.IOException - * @throws java.sql.SQLException + * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure. + * @throws java.io.IOException in case of JSON processing error. + * @throws java.sql.SQLException if a DB failure is reported. */ - public abstract void create(String fullId, String type, String localId, - Map obj, Connection connection) - throws SQLException, IOException, ResourceException; + void create(String fullId, String type, String localId, Map obj, Connection connection) + throws SQLException, ResourceException; /** - * Updates the specified object in the object set. + * Update the specified object in the object set. + * *

- * This implementation requires MVCC and hence enforces that clients state what revision they expect - * to be updating - * - * If successful, this method updates metadata properties within the passed object, - * including: a new {@code _rev} value for the revised object's version - * - * @param fullId the identifier of the object to be put, or {@code null} to request a generated identifier. - * @param type - * @param localId - * @param rev the version of the object to update; or {@code null} if not provided. - * @param obj the contents of the object to put in the object set. - * @param connection - * @throws ConflictException if version is required but is {@code null}. - * @throws ForbiddenException if access to the object is forbidden. - * @throws NotFoundException if the specified object could not be found. + * This implementation requires MVCC and hence enforces that clients state what revision they expect + * to be updating. + * + *

+ * This method mutates the provided object by updating {@code _rev} property value for the revised + * object's version. + * + * @param fullId The identifier of the object to be updated. + * @param type The qualifier of the object to update. + * @param localId The identifier without the qualifier. + * @param rev The version of the object to update. + * @param obj The contents of the object to put in the object set. + * @param connection Database connection to use. + * @throws NotFoundException if the specified object could not be found. * @throws PreconditionFailedException if version did not match the existing object in the set. - * @throws BadRequestException if the passed identifier is invalid - * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure - * @throws java.io.IOException - * @throws java.sql.SQLException + * @throws BadRequestException if the passed identifier is invalid. + * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure. + * @throws java.sql.SQLException if a DB failure is reported. */ - public abstract void update(String fullId, String type, String localId, - String rev, Map obj, Connection connection) - throws SQLException, IOException, ResourceException; + void update(String fullId, String type, String localId, String rev, Map obj, + Connection connection) throws SQLException, ResourceException; /** - * Deletes the specified object from the object set. + * Delete the specified object from the object set. * - * @param fullId the identifier of the object to be deleted. - * @param type - * @param localId - * @param rev the version of the object to delete or {@code null} if not provided. - * @param connection + * @param fullId The identifier of the object to be deleted. + * @param type The qualifier of the object to delete. + * @param localId The identifier without the qualifier. + * @param rev The version of the object to delete or {@code *} to match any version. + * @param connection Database connection to use. * @throws NotFoundException if the specified object could not be found. - * @throws ForbiddenException if access to the object is forbidden. - * @throws ConflictException if version is required but is {@code null}. * @throws PreconditionFailedException if version did not match the existing object in the set. * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure - * @throws java.io.IOException - * @throws java.sql.SQLException + * @throws java.sql.SQLException if a DB failure is reported. */ - public abstract void delete(String fullId, String type, String localId, - String rev, Connection connection) - throws SQLException, IOException, ResourceException; + void delete(String fullId, String type, String localId, String rev, Connection connection) + throws SQLException, ResourceException; /** - * Performs the query on the specified object and returns the associated results. + * Perform a query on the specified object set and return the associated results. + * *

* Queries are parametric; a set of named parameters is provided as the query criteria. - * The query result is a JSON object structure composed of basic Java types. - * - * The returned map is structured as follow: - * - The top level map contains meta-data about the query, plus an entry with the actual result records. - * - The QueryConstants defines the map keys, including the result records (QUERY_RESULT) - * - * @param type identifies the object to query. - * @param params the parameters of the query to perform. - * @param connection - * @return the query results, which includes meta-data and the result records in JSON object structure format. - * @throws NotFoundException if the specified object could not be found. - * @throws BadRequestException if the specified params contain invalid arguments, e.g. a query id that is not - * configured, a query expression that is invalid, or missing query substitution tokens. - * @throws ForbiddenException if access to the object or specified query is forbidden. + * The query result is a JSON object structure composed of basic Java types. + * + *

+ * The query parameters map is a simple shallow map that consists of two types + * of key-value pairs: + * + *

+ * + * @param type Identifies the object type (qualifier) to query. + * @param params The parameters for the query to perform. + * @param connection Database connection to use. + * @return List of matched records in JSON object structure format. + * @throws NotFoundException if the specified object could not be found. + * @throws BadRequestException if the specified params contain invalid arguments, e.g. a query id that + * is not configured, a query expression that is invalid, or missing query substitution tokens. * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure - * @throws java.sql.SQLException + * @throws java.sql.SQLException if a DB failure is reported. */ - public List> query(String type, Map params, Connection connection) + List> query(String type, Map params, Connection connection) throws SQLException, ResourceException; /** - * Performs the command on the specified target and returns the number of affected objects + * Perform the command on the specified target and return the number of affected objects. + * *

* Commands are parametric; a set of named parameters is provided as the query criteria. * The command returns the number of records altered/updated/deleted. * - * @param type identifies the object to query. - * @param params the parameters of the query to perform. - * @param connection - * @return the number of records affected. - * @throws BadRequestException if the specified params contain invalid arguments, e.g. a query id that is not - * configured, a query expression that is invalid, or missing query substitution tokens. - * @throws ForbiddenException if access to the object or specified query is forbidden. + * @param type Identifies the object set to query. + * @param params The parameters of the query to perform. + * @param connection Database connection to use. + * @return The number of records affected or {@code null} if unknown. + * @throws BadRequestException if the specified params contain invalid arguments, e.g. a query id that + * is not configured, a query expression that is invalid, or missing query substitution tokens. * @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure - * @throws java.sql.SQLException + * @throws java.sql.SQLException if a DB failure is reported. */ - - public Integer command(String type, Map params, Connection connection) + Integer command(String type, Map params, Connection connection) throws SQLException, ResourceException; /** - * Check if a given queryId exists in our set of known queries - * - * @param queryId Identifier for the query + * Check if the given queryId exists in our set of known queries * - * @return true if queryId is available + * @param queryId Identifier for the query. + * @return {@code true} if queryId is available, false if not. */ - public boolean queryIdExists(final String queryId); - + boolean queryIdExists(final String queryId); + /** - * Builds a raw query from the supplied filter. - * - * @param filter the query filter + * Build a raw query from the supplied filter. + * + * @param filter The query filter. * @param replacementTokens a map to store any replacement tokens * @param params a map containing query parameters - * @param count whether to render a query for total number of matched rows + * @param count whether to render a query for total number of matched rows * @return the raw query String */ - public String renderQueryFilter(QueryFilter filter, Map replacementTokens, - Map params, boolean count); + // XXX What is replacementTokens used for? + // XXX How to handle COUNT query rendering? + String renderQueryFilter(QueryFilter filter, Map replacementTokens, + Map params, boolean count); /** - * Query if a given exception signifies a well known error type - * + * Check if a given exception signifies a well known error type. + * + *

* Allows table handlers to abstract database specific differences in reporting errors. - * - * @param ex The exception thrown by the database - * @param errorType the error type to test against - * @return true if the exception matches the error type passed + * + * @param ex The exception thrown by the database. + * @param errorType The error type to test against. + * @return true if the exception matches the error type passed. */ - public boolean isErrorType(SQLException ex, ErrorType errorType); - + // XXX This is a strange method design... Wouldn't it be better to simply return ErrorType? + boolean isErrorType(SQLException ex, ErrorType errorType); + /** - * As whether a given exception should be retried - * @param ex the exception thrown by the database - * @param connection where the failure occured, used for additional context - * @return + * Determine whether a given exception can be followed up by a operation retry. + * @param ex The exception thrown by the database. + * @param connection Database connection where the failure occured (used for additional context). + * @return true if the operation that lead to the error should be retried. */ - public boolean isRetryable(SQLException ex, Connection connection); -} \ No newline at end of file + boolean isRetryable(SQLException ex, Connection connection); + +} diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java index 47507ca894..b487cf1ae6 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/GenericTableHandler.java @@ -200,9 +200,6 @@ protected Map initializeQueryMap() { return result; } - /* (non-Javadoc) - * @see org.forgerock.openidm.repo.jdbc.impl.TableHandler#read(java.lang.String, java.lang.String, java.lang.String, java.sql.Connection) - */ @Override public ResourceResponse read(String fullId, String type, String localId, Connection connection) throws ResourceException, SQLException, IOException { @@ -231,9 +228,6 @@ public ResourceResponse read(String fullId, String type, String localId, Connect } } - /* (non-Javadoc) - * @see org.forgerock.openidm.repo.jdbc.impl.TableHandler#create(java.lang.String, java.lang.String, java.lang.String, java.util.Map, java.sql.Connection) - */ @Override public void create(String fullId, String type, String localId, Map obj, Connection connection) throws SQLException, IOException, InternalServerErrorException { @@ -499,9 +493,6 @@ public Map readForUpdate(String fullId, String type, String loca } } - /* (non-Javadoc) - * @see org.forgerock.openidm.repo.jdbc.impl.TableHandler#update(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.util.Map, java.sql.Connection) - */ @Override public void update(String fullId, String type, String localId, String rev, Map obj, Connection connection) throws SQLException, IOException, PreconditionFailedException, NotFoundException, InternalServerErrorException { @@ -564,12 +555,9 @@ public void update(String fullId, String type, String localId, String rev, Map> query(String type, Map params, Connection connection) throws ResourceException { diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/JDBCRepoService.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/JDBCRepoService.java index 238864b7c2..a181b00278 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/JDBCRepoService.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/JDBCRepoService.java @@ -571,6 +571,7 @@ public Promise handlePatch(Context context, @Override public Promise handleQuery(Context context, QueryRequest request, QueryResourceHandler handler) { + // TODO XXX FIXME bordel nize try { // If paged results are requested then decode the cookie in order to determine @@ -622,31 +623,32 @@ public Promise handleQuery(Context context, Qu switch (request.getTotalPagedResultsPolicy()) { case ESTIMATE: case EXACT: - if (request.getQueryId() != null) { - // Get total if -count query is available - final String countQueryId = request.getQueryId() + "-count"; - if (tableHandler.queryIdExists(countQueryId)) { - QueryRequest countRequest = Requests.copyOfQueryRequest(request); - countRequest.setQueryId(countQueryId); - - // Strip pagination parameters - countRequest.setPageSize(0); - countRequest.setPagedResultsOffset(0); - countRequest.setPagedResultsCookie(null); - - List countResult = query(countRequest); - - if (countResult != null && !countResult.isEmpty()) { - resultCount = countResult.get(0).getContent().get("total").asInteger(); - } else { - logger.debug("Count query {} failed", countQueryId); - resultCount = NO_COUNT; - } - } else { - logger.debug("Count query with id {} not found", countQueryId); - resultCount = NO_COUNT; - } - } else if (request.getQueryFilter() != null) { + if (request.getQueryId() != null) { + // Get total if -count query is available + final String countQueryId = request.getQueryId() + "-count"; + if (tableHandler.queryIdExists(countQueryId)) { + QueryRequest countRequest = Requests.copyOfQueryRequest(request); + countRequest.setQueryId(countQueryId); + + // Strip pagination parameters + countRequest.setPageSize(0); + countRequest.setPagedResultsOffset(0); + countRequest.setPagedResultsCookie(null); + + List countResult = query(countRequest); + + if (countResult != null && !countResult.isEmpty()) { + resultCount = countResult.get(0).getContent().get("total").asInteger(); + } else { + logger.debug("Count query {} failed", countQueryId); + resultCount = NO_COUNT; + } + } else { + logger.debug("Count query with id {} not found", countQueryId); + resultCount = NO_COUNT; + } + } else if (request.getQueryFilter() != null) { + // TODO XXX pouzit COUNT POLICY jako parametr Map params = new HashMap<>(); params.putAll(request.getAdditionalParameters()); params.put(QUERY_FILTER, request.getQueryFilter()); @@ -657,7 +659,7 @@ public Promise handleQuery(Context context, Qu logger.debug("Count query {} failed", QUERY_FILTER); resultCount = NO_COUNT; } - } + } break; case NONE: default: diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java index 9bfb20b397..7a65998d6d 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/MappedTableHandler.java @@ -173,10 +173,6 @@ protected void initializeQueries() { } - /** - * @see org.forgerock.openidm.repo.jdbc.TableHandler#read(java.lang.String, - * java.lang.String, java.lang.String, java.sql.Connection) - */ @Override public ResourceResponse read(String fullId, String type, String localId, Connection connection) throws NotFoundException, SQLException, InternalServerErrorException { @@ -247,11 +243,6 @@ Map readForUpdate(String fullId, String type, String localId, Co } } - /** - * @see org.forgerock.openidm.repo.jdbc.TableHandler#create(java.lang.String, - * java.lang.String, java.lang.String, java.util.Map, - * java.sql.Connection) - */ @Override public void create(String fullId, String type, String localId, Map obj, Connection connection) throws SQLException, IOException { @@ -356,11 +347,6 @@ int populatePrepStatementColumns(String type, PreparedStatement prepStatement, J return colPos; } - /** - * @see org.forgerock.openidm.repo.jdbc.TableHandler#update(java.lang.String, - * java.lang.String, java.lang.String, java.lang.String, java.util.Map, - * java.sql.Connection) - */ @Override public void update(String fullId, String type, String localId, String rev, Map obj, Connection connection) throws SQLException, IOException, @@ -415,11 +401,6 @@ public void update(String fullId, String type, String localId, String rev, } } - /** - * @see org.forgerock.openidm.repo.jdbc.TableHandler#delete(java.lang.String, - * java.lang.String, java.lang.String, java.lang.String, - * java.sql.Connection) - */ @Override public void delete(String fullId, String type, String localId, String rev, Connection connection) throws PreconditionFailedException, InternalServerErrorException, NotFoundException, @@ -463,11 +444,6 @@ public void delete(String fullId, String type, String localId, String rev, Conne } } - /** - * @see org.forgerock.openidm.repo.jdbc.TableHandler#delete(java.lang.String, - * java.lang.String, java.lang.String, java.lang.String, - * java.sql.Connection) - */ @Override public List> query(String type, Map params, Connection connection) throws ResourceException { diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/OracleTableHandler.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/OracleTableHandler.java index d980154a30..232da67b1f 100755 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/OracleTableHandler.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/OracleTableHandler.java @@ -179,6 +179,6 @@ Clause buildNumericValueClause(String propTable, String operand, String placehol builder.orderBy("obj.id", false); } - return builder.toSQL(count); + return count ? builder.toCountSQL() : builder.toSQL(); } } diff --git a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/SQLBuilder.java b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/SQLBuilder.java index 02a9d2f96e..09fb5beb50 100644 --- a/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/SQLBuilder.java +++ b/openidm-repo-jdbc/src/main/java/org/forgerock/openidm/repo/jdbc/impl/SQLBuilder.java @@ -65,6 +65,7 @@ private class Column implements SQLRenderer { this.column = column; } + @Override public String toSQL() { return column; } @@ -82,6 +83,7 @@ private class Table implements SQLRenderer { this.alias = alias; } + @Override public String toSQL() { return table + (alias != null ? " " + alias : ""); } @@ -178,6 +180,7 @@ private class OrderBy implements SQLRenderer { this.ascending = ascending; } + @Override public String toSQL() { return order + " " + (ascending ? "ASC" : "DESC"); } @@ -429,19 +432,17 @@ public String toSQL() { public abstract String toSQL(); /** - * Render the SQL string. - * - * @param count whether the query should return row count only - * @return rendered SQL string + * Render the SQL string for counting number of matched rows. + * + * @return rendered COUNT SQL string */ - public String toSQL(boolean count) { - return !count ? toSQL() : - "SELECT COUNT(*) as total " - + getFromClause() - + getJoinClause() - + getWhereClause(); + public String toCountSQL() { + return "SELECT COUNT(*) as total " + + getFromClause() + + getJoinClause() + + getWhereClause(); } - + /** * Return a string representation of this builder. * diff --git a/openidm-repo/src/main/java/org/forgerock/openidm/repo/util/TokenHandler.java b/openidm-repo/src/main/java/org/forgerock/openidm/repo/util/TokenHandler.java index ffc1edbaa2..bf94ec3a29 100644 --- a/openidm-repo/src/main/java/org/forgerock/openidm/repo/util/TokenHandler.java +++ b/openidm-repo/src/main/java/org/forgerock/openidm/repo/util/TokenHandler.java @@ -2,6 +2,7 @@ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright © 2011 ForgeRock AS. All rights reserved. + * Portions Copyright 2023 Wren Security. * * The contents of this file are subject to the terms * of the Common Development and Distribution License @@ -29,207 +30,101 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.forgerock.json.resource.BadRequestException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +/** + * Utility class for resolving and replacing named parameters (substitution tokens) in query strings. + * + *

+ * Substitution tokens are in the format of ${token-name}. + */ public class TokenHandler { - final static Logger logger = LoggerFactory.getLogger(TokenHandler.class); - - // The OpenIDM query token is of format ${token-name} - Pattern tokenPattern = Pattern.compile("\\$\\{(.+?)\\}"); + private static final Pattern TOKEN_PATTERN = Pattern.compile("\\$\\{(.+?)\\}"); /** - * Replaces a query string with tokens of format ${token-name} with the values from the - * passed in map, where the token-name must be the key in the map + * Extracts all the token names in the query string of format ${token-name} * - * @param queryString the query with tokens - * @param params the parameters to replace the tokens. Values can be String or List. - * @return the query with all tokens replace with their found values - * @throws BadRequestException if token in the query is not in the passed parameters - */ - public String replaceTokensWithValues(String queryString, Map params) - throws BadRequestException { - java.util.regex.Matcher matcher = tokenPattern.matcher(queryString); - StringBuffer buffer = new StringBuffer(); - while (matcher.find()) { - String tokenKey = matcher.group(1); - if (!params.containsKey(tokenKey)) { - // fail with an exception if token not found - throw new BadRequestException("Missing entry in params passed to query for token " + tokenKey); - } else { - Object replacement = params.get(tokenKey); - if (replacement instanceof List) { - StringBuffer commaSeparated = new StringBuffer(); - boolean first = true; - for (Object entry : ((List) replacement)) { - if (!first) { - commaSeparated.append(","); - } else { - first = false; - } - commaSeparated.append(entry.toString()); - } - replacement = commaSeparated.toString(); - } - if (replacement == null) { - replacement = ""; - } - matcher.appendReplacement(buffer, ""); - buffer.append(replacement); - } - } - matcher.appendTail(buffer); - return buffer.toString(); - } - - /** - * Replaces a query string with tokens of format ${token-name} with the - * specified replacement string for all tokens. - * - * @param queryString the query with tokens to replace - * @param replacement the replacement string - * @return the query with all tokens replaced - */ - public String replaceTokens(String queryString, String replacement) { - return replaceTokens(queryString, replacement, new String[] {}); - } - - /** - * Replaces a query string with tokens of format ${token-name} with the - * specified replacement string for all tokens. - * - * @param queryString the query with tokens to replace - * @param replacement the replacement string - * @param nonReplacementTokenPrefixes optional array of prefixes that, if found as part of a token, - * will not be replaced - * @return the query with all tokens replaced - */ - public String replaceTokens(String queryString, String replacement, String... nonReplacementTokenPrefixes) { - Matcher matcher = tokenPattern.matcher(queryString); - StringBuffer buf = new StringBuffer(); - while (matcher.find()) { - String origToken = matcher.group(1); - //TODO: the size check seems invalid - if (origToken != null) { - // OrientDB token is of format :token-name - matcher.appendReplacement(buf, ""); - // if token has one of the "non-replacement" prefixes, leave it alone - if (tokenStartsWithPrefix(origToken, nonReplacementTokenPrefixes)) { - buf.append("${" + origToken + "}"); - } - else { - buf.append(replacement); - } - } - } - matcher.appendTail(buf); - return buf.toString(); - } - - /** - * Returns whether the token starts with one of the prefixes passed. - * - * @param token the token to interrogate - * @param prefixes a list of prefixes - * @return whether the passed token starts with one of the prefixes - */ - private boolean tokenStartsWithPrefix(String token, String... prefixes) { - String[] tokenParts = token.split(":", 2); - if (tokenParts.length == 2) { - for (String prefix : prefixes) { - if (prefix.equals(tokenParts[0])) { - return true; - } - } - } - return false; - } - - /** - * Extracts all the token names in the query string of format ${token-name} - * - * @param queryString the query with tokens - * @return the list of token names, in the order they appear in the queryString + * @param queryString The query with tokens. + * @return The list of token names in the order they appear in the {@code queryString}. */ public List extractTokens(String queryString) { - List tokens = new ArrayList(); - Matcher matcher = tokenPattern.matcher(queryString); + List tokens = new ArrayList<>(); + Matcher matcher = TOKEN_PATTERN.matcher(queryString); while (matcher.find()) { - String origToken = matcher.group(1); - tokens.add(origToken); + tokens.add(matcher.group(1)); } return tokens; } /** - * Replaces some tokens in a query string with tokens of format ${token-name} + * Replace some tokens in a query string with tokens of format ${token-name} * with the given replacements, which may again be tokens (e.g. in another format) * or values. Tokens that have no replacement defined stay in the original token format. * + *

+ * CAUTION: This method does not do any escaping or format checking and it is the + * responsibility of the caller to provide safe and sanitized replacement values. * - * @param queryString the query with OpenIDM format tokens ${token} - * @param replacements the replacement values/tokens, where the key is the token name in the query string, - * and the value is the String to replace it with. - * @return the query with any defined replacement values/tokens replaced, and the remaining tokens - * left in the original format + * @param queryString The query with tokens of format ${token-name}. + * @param replacements The replacement strings, where the key is the token name in the query string, + * and the value is the string to replace it with. + * @return The query with any defined replacement values/tokens replaced, and the remaining tokens + * left in the original format. */ public String replaceSomeTokens(String queryString, Map replacements) { - Matcher matcher = tokenPattern.matcher(queryString); - StringBuffer buf = new StringBuffer(); + Matcher matcher = TOKEN_PATTERN.matcher(queryString); + StringBuffer result = new StringBuffer(); while (matcher.find()) { - String origToken = matcher.group(1); - if (origToken != null) { - String replacement = replacements.get(origToken); + String tokenName = matcher.group(1); + if (tokenName != null) { + String replacement = replacements.get(tokenName); if (replacement == null) { - // if not replacement specified, keep the original token. - replacement = "${" + origToken + "}"; + // if replacement not specified, keep the original token + replacement = "${" + tokenName + "}"; } - matcher.appendReplacement(buf, ""); - buf.append(replacement); + matcher.appendReplacement(result, ""); + result.append(replacement); } } - matcher.appendTail(buf); - return buf.toString(); + matcher.appendTail(result); + return result.toString(); } /** - * Replaces some tokens in a query string with tokens of format ${token-name} - * where token-name represents a list of values. The numberOfReplacements Map tells + * Replaces some tokens in a query string with tokens of format ${token-name} + * where token-name represents a list of values. The ${code numberOfReplacements} map tells * how many replacements to produce (comma-separated) for each token. The replacement * (for all tokens) is provided. Tokens that have no replacement defined stay in the * original token format. * - * @param queryString the query with OpenIDM format tokens ${token} - * @param numberOfReplacements the number of replacements to replace a ${token} with - * @param replacement the replacement values/tokens - * @return the query with any defined replacement values/tokens replaced, and the remaining tokens - * left in the original format + * @param queryString The query with tokens of format tokens ${token-name} + * @param numberOfReplacements The number of replacements to replace a ${token-name} with. + * @param replacement The replacement string that will be repeated as specified by the number of + * replacements. + * @return The query with any defined replacement values replaced, and the remaining tokens + * left in the original format. */ public String replaceListTokens(String queryString, Map numberOfReplacements, String replacement) { - Matcher matcher = tokenPattern.matcher(queryString); - StringBuffer buf = new StringBuffer(); + Matcher matcher = TOKEN_PATTERN.matcher(queryString); + StringBuffer result = new StringBuffer(); while (matcher.find()) { - String origToken = matcher.group(1); - if (origToken != null) { - matcher.appendReplacement(buf, ""); - Integer length = numberOfReplacements.get(origToken); + String tokenName = matcher.group(1); + if (tokenName != null) { + matcher.appendReplacement(result, ""); + Integer length = numberOfReplacements.get(tokenName); if (length != null) { for (int i = 0; i < length; i++) { - buf.append(replacement); + result.append(replacement); if (i != length - 1) { - buf.append(", "); + result.append(", "); } } - } - else { - buf.append("${" + origToken + "}"); + } else { + result.append("${" + tokenName + "}"); } } } - matcher.appendTail(buf); - return buf.toString(); + matcher.appendTail(result); + return result.toString(); } } diff --git a/openidm-repo/src/test/java/org/forgerock/openidm/repo/util/TokenHandlerTest.java b/openidm-repo/src/test/java/org/forgerock/openidm/repo/util/TokenHandlerTest.java new file mode 100644 index 0000000000..8820afeda4 --- /dev/null +++ b/openidm-repo/src/test/java/org/forgerock/openidm/repo/util/TokenHandlerTest.java @@ -0,0 +1,69 @@ +/* + * 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 legal/CDDLv1.1.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.1.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2023 Wren Security. All rights reserved. + */ +package org.forgerock.openidm.repo.util; + +import static org.testng.Assert.assertEquals; + +import java.util.List; +import java.util.Map; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +public class TokenHandlerTest { + + @DataProvider + public Object[][] extractTokensData() { + return new Object[][] { + { "HELLO ${WORLD}", List.of("WORLD") } + }; + } + + @Test(dataProvider = "extractTokensData") + public void testExtractTokens(String statement, List expected) { + TokenHandler tokenHandler = new TokenHandler(); + assertEquals(tokenHandler.extractTokens("HELLO ${WORLD}"), expected); + } + + @DataProvider + public Object[][] replaceSomeTokensData() { + return new Object[][] { + { "HELLO ${WORLD}", Map.of("WORLD", "UNIVERSE"), "HELLO UNIVERSE" }, + { "HELLO ${WORLD}", Map.of("FOO", "BAR"), "HELLO ${WORLD}" } + }; + } + + @Test(dataProvider = "replaceSomeTokensData") + public void testReplaceSomeTokens(String statement, Map replacements, String expected) { + TokenHandler tokenHandler = new TokenHandler(); + assertEquals(tokenHandler.replaceSomeTokens(statement, replacements), expected); + } + + @DataProvider + public Object[][] replaceListTokensData() { + return new Object[][] { + { "foo IN (${bar})", Map.of("bar", 1), "foo IN (?)" }, + { "foo IN (${bar})", Map.of("bar", 2), "foo IN (?, ?)" }, + { "foo IN (${bar})", Map.of(), "foo IN (${bar})" } + }; + } + + @Test(dataProvider = "replaceListTokensData") + public void testReplaceListTokens(String statement, Map counts, String expected) { + TokenHandler tokenHandler = new TokenHandler(); + assertEquals(tokenHandler.replaceListTokens(statement, counts, "?"), expected); + } + +}