Skip to content

Commit

Permalink
Add support for COUNT queries and NUMBER and BOOLEAN property mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelhoral committed Feb 6, 2024
1 parent 2c50423 commit ab36537
Show file tree
Hide file tree
Showing 25 changed files with 270 additions and 187 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright © 2011-2013 ForgeRock AS. All rights reserved.
* Portions Copyright Wren Security 2024
*
* The contents of this file are subject to the terms
* of the Common Development and Distribution License
Expand All @@ -27,5 +28,5 @@
* Supported database types.
*/
public enum DatabaseType {
SQLSERVER, MYSQL, POSTGRESQL, ORACLE, DB2, H2, ANSI_SQL99, ODBC;
SQLSERVER, MYSQL, POSTGRESQL, ORACLE, DB2, H2, ANSI_SQL99;
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,13 @@
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.InternalServerErrorException;
import org.forgerock.json.resource.NotFoundException;
import org.forgerock.json.resource.PreconditionFailedException;
import org.forgerock.json.resource.ResourceException;
import org.forgerock.json.resource.ResourceResponse;
import org.forgerock.openidm.repo.QueryConstants;
import org.forgerock.util.query.QueryFilter;

/**
* Handler responsible for performing SQL operations on the underlying data source.
Expand Down Expand Up @@ -187,10 +185,8 @@ List<Map<String, Object>> query(String type, Map<String, Object> params, Connect
* @throws InternalServerErrorException if the operation failed because of a (possibly transient) failure
* @throws SQLException if a DB failure is reported
*/
default Integer queryCount(String type, Map<String, Object> params, Connection connection)
throws SQLException, ResourceException {
throw new UnsupportedOperationException(); // TODO remove default after dropping legacy handlers
}
Integer queryCount(String type, Map<String, Object> params, Connection connection)
throws SQLException, ResourceException;

/**
* Perform the command on the specified target and return the number of affected objects.
Expand All @@ -211,32 +207,6 @@ default Integer queryCount(String type, Map<String, Object> params, Connection c
Integer command(String type, Map<String, Object> params, Connection connection)
throws SQLException, ResourceException;

/**
* 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
* @return the raw query string
*/
@Deprecated
default String renderQueryFilter(QueryFilter<JsonPointer> filter, Map<String, Object> replacementTokens,
Map<String, Object> params) {
throw new UnsupportedOperationException();
}

/**
* Check if a given queryId exists in our set of known queries
*
* @param queryId Identifier for the query
* @return true if queryId is available
*/
@Deprecated
default boolean queryIdExists(final String queryId) {
throw new UnsupportedOperationException();
}

/**
* Check if a given exception signifies a well known error type.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ private String getDbDirname(JsonValue repoConfig) {
case H2:
return databaseType.toString().toLowerCase();
case ANSI_SQL99:
case ODBC:
default:
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import static org.forgerock.json.JsonValue.json;
import static org.forgerock.json.JsonValue.object;
import static org.forgerock.json.JsonValueFunctions.enumConstant;
import static org.forgerock.json.resource.QueryResponse.NO_COUNT;
import static org.forgerock.json.resource.ResourceException.newResourceException;
import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_ID;
import static org.forgerock.json.resource.ResourceResponse.FIELD_CONTENT_REVISION;
Expand All @@ -33,7 +32,6 @@
import static org.forgerock.openidm.repo.QueryConstants.QUERY_FILTER;
import static org.forgerock.openidm.repo.QueryConstants.QUERY_ID;
import static org.forgerock.openidm.repo.QueryConstants.SORT_KEYS;
import static org.wrensecurity.guava.common.base.Strings.isNullOrEmpty;

import java.io.IOException;
import java.sql.Connection;
Expand Down Expand Up @@ -583,105 +581,37 @@ public Promise<ResourceResponse, ResourceException> handlePatch(Context context,
@Override
public Promise<QueryResponse, ResourceException> handleQuery(Context context, QueryRequest request, QueryResourceHandler handler) {
try {

// If paged results are requested then decode the cookie in order to determine
// the index of the first result to be returned.
final int requestPageSize = request.getPageSize();

// Cookie containing offset of last request
final String pagedResultsCookie = request.getPagedResultsCookie();

final boolean pagedResultsRequested = requestPageSize > 0;

// index of first record (used for SKIP/OFFSET)
final int firstResultIndex;

if (pagedResultsRequested) {
if (!isNullOrEmpty(pagedResultsCookie)) {
try {
firstResultIndex = Integer.parseInt(pagedResultsCookie);
} catch (final NumberFormatException e) {
throw new BadRequestException("Invalid paged results cookie");
}
} else {
firstResultIndex = Math.max(0, request.getPagedResultsOffset());
}
} else {
firstResultIndex = 0;
}

// Once cookie is processed Queries.query() can rely on the offset.
request.setPagedResultsOffset(firstResultIndex);
String type = trimStartingSlash(request.getResourcePath());
Map<String, Object> params = createQueryParams(request);

List<ResourceResponse> results = query(request);
for (ResourceResponse result : results) {
handler.handleResource(result);
}

/*
* Execute additional -count query if we are paging
*/
final String nextCookie;

// The number of results (if known)
final int resultCount;

if (pagedResultsRequested) {
TableHandler tableHandler = getTableHandler(trimStartingSlash(request.getResourcePath()));

// count if requested
switch (request.getTotalPagedResultsPolicy()) {
case ESTIMATE:
case EXACT:
// 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<ResourceResponse> 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;
}
break;
case NONE:
default:
resultCount = NO_COUNT;
break;
}
if (request.getPageSize() == 0) {
return newQueryResponse(null).asPromise();
}

if (results.size() < requestPageSize) {
nextCookie = null;
} else {
final int remainingResults = resultCount - (firstResultIndex + results.size());
if (remainingResults == 0) {
nextCookie = null;
} else {
nextCookie = String.valueOf(firstResultIndex + requestPageSize);
}
}
} else {
nextCookie = null;
resultCount = NO_COUNT;
var tableHandler = getTableHandler(type);

Integer totalCount = null;
if (request.getTotalPagedResultsPolicy() != CountPolicy.NONE) {
try (var connection = getConnection()) {
totalCount = tableHandler.queryCount(type, params, connection);
}
}

if (resultCount == NO_COUNT) {
return newQueryResponse(nextCookie).asPromise();
int nextOffset = ((Integer) params.get(PAGED_RESULTS_OFFSET)) + results.size();

if (totalCount != null) {
return newQueryResponse(
totalCount > nextOffset ? String.valueOf(nextOffset) : null,
CountPolicy.EXACT,
totalCount).asPromise();
} else {
return newQueryResponse(nextCookie, CountPolicy.EXACT, resultCount).asPromise();
return newQueryResponse(
results.size() >= request.getPageSize() ? String.valueOf(nextOffset) : null).asPromise();
}
} catch (final ResourceException e) {
return e.asPromise();
Expand All @@ -690,20 +620,49 @@ public Promise<QueryResponse, ResourceException> handleQuery(Context context, Qu
}
}

@Override
public List<ResourceResponse> query(QueryRequest request) throws ResourceException {
String fullId = request.getResourcePath();
String type = trimStartingSlash(fullId);
logger.trace("Full id: {} Extracted type: {}", fullId, type);
/**
* Create query parameters for calling {@link TableHandler} implementation.
*
* @param request the original query request
* @return table handler's query parameters
*/
private Map<String, Object> createQueryParams(QueryRequest request) throws BadRequestException {
Map<String, Object> params = new HashMap<>();
params.putAll(request.getAdditionalParameters());

// query parameters
params.put(QUERY_ID, request.getQueryId());
params.put(QUERY_EXPRESSION, request.getQueryExpression());
params.put(QUERY_FILTER, request.getQueryFilter());

// paging parameters
params.put(PAGE_SIZE, request.getPageSize());
params.put(PAGED_RESULTS_OFFSET, request.getPagedResultsOffset());
final String pagedResultsCookie = request.getPagedResultsCookie();
if (pagedResultsCookie != null && !pagedResultsCookie.isEmpty()) {
try {
params.put(PAGED_RESULTS_OFFSET, Math.max(0, Integer.parseInt(pagedResultsCookie)));
} catch (NumberFormatException e) {
throw new BadRequestException("Invalid paged results cookie");
}
} else {
params.put(PAGED_RESULTS_OFFSET, request.getPagedResultsOffset());
}

// sorting parameters
params.put(SORT_KEYS, request.getSortKeys());

// additional named parameters
params.putAll(request.getAdditionalParameters());

return params;
}

@Override
public List<ResourceResponse> query(QueryRequest request) throws ResourceException {
String fullId = request.getResourcePath();
String type = trimStartingSlash(fullId);
logger.trace("Full id: {} Extracted type: {}", fullId, type);
var params = createQueryParams(request);

Connection connection = null;
try {
TableHandler tableHandler = getTableHandler(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,19 @@ public enum ValueType {

public static final String COLUMN_NAME = "column";
public static final String VALUE_TYPE = "valueType";
public static final String JAVA_TYPE = "javaType";

public final JsonPointer propertyName;
public final ValueType valueType;
public final Class<?> javaType;
public final String columnName;

public MappedColumnConfig(JsonPointer propertyName, String columnName, ValueType valueType) {
public MappedColumnConfig(JsonPointer propertyName, String columnName,
ValueType valueType, Class<?> javaType) {
this.propertyName = propertyName;
this.columnName = columnName;
this.valueType = valueType;
this.javaType = javaType;
}

/**
Expand Down Expand Up @@ -76,7 +80,30 @@ public static MappedColumnConfig parse(String name, JsonValue columnConfig) {
return new MappedColumnConfig(
new JsonPointer(name),
columnConfig.required().asString(),
ValueType.STRING);
ValueType.STRING,
String.class);
}
}

/**
* Parse java value class name (used as a type hint when mapping JDBC types).
*
* @param className java class name
* @return java class that for the stored value
*/
private static Class<?> parseClass(String className) {
// intentionally avoid Class#forName
switch (className) {
case "java.lang.Integer":
return Integer.class;
case "java.lang.Long":
return Long.class;
case "java.lang.Double":
return Double.class;
case "java.lang.String":
return String.class;
default:
throw new InvalidException("Unsupported java class name " + className);
}
}

Expand All @@ -87,25 +114,28 @@ public static MappedColumnConfig parse(String name, JsonValue columnConfig) {
*
* <pre>
* "propertyPointer": ["columnName", "valueType"],
* "propertyPointer": ["columnName", "valueType", "javaType"]
* </pre>
*
* Example:
*
* <pre>
* "foo": ["foo", "STRING"]
* "foo": ["foo", "STRING"],
* "bar": ["bar", "NUMBER", "java.lang.Double"]
* </pre>
*/
private static MappedColumnConfig parseList(String name, JsonValue columnConfig) {
int size = columnConfig.asList().size();
if (size < 2 || size > 3) {
throw new InvalidException("Explicit table mapping has invalid entry for "
+ name + ", expecting [column name, value type, stored type] but contains "
+ name + ", expecting [column name, value type, java type] but contains "
+ columnConfig.asList());
}
return new MappedColumnConfig(
new JsonPointer(name),
columnConfig.get(0).required().asString(),
size > 1 ? ValueType.valueOf(columnConfig.get(1).asString()) : ValueType.STRING);
size > 1 ? ValueType.valueOf(columnConfig.get(1).asString()) : ValueType.STRING,
size > 2 ? parseClass(columnConfig.get(2).asString()) : null);
}

/**
Expand All @@ -117,13 +147,18 @@ private static MappedColumnConfig parseList(String name, JsonValue columnConfig)
* "propertyPointer": {
* "type": "VALUE_TYPE",
* },
* "propertyPointer": {
* "type": "VALUE_TYPE",
* "javaType": "JAVA_CLASS"
* },
* </pre>
*
* Example:
*
* <pre>
* "foo": {
* "type": "NUMBER"
* "type": "NUMBER",
* "javaType": "java.lang.Double"
* }
* </pre>
*/
Expand All @@ -135,7 +170,8 @@ private static MappedColumnConfig parseMap(String name, JsonValue columnConfig)
return new MappedColumnConfig(
new JsonPointer(name),
columnConfig.get(COLUMN_NAME).required().asString(),
valueType != null ? ValueType.valueOf(valueType) : ValueType.STRING);
valueType != null ? ValueType.valueOf(valueType) : ValueType.STRING,
columnConfig.isDefined(JAVA_TYPE) ? parseClass(columnConfig.get(JAVA_TYPE).asString()) : null);
}

}
Loading

0 comments on commit ab36537

Please sign in to comment.