/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright © 2012-2015 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]" * $Id$ */ package org.forgerock.openidm.repo.jdbc.impl; import static org.forgerock.openidm.repo.QueryConstants.PAGED_RESULTS_OFFSET; import static org.forgerock.openidm.repo.QueryConstants.PAGE_SIZE; import static org.forgerock.openidm.repo.QueryConstants.SORT_KEYS; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; import org.forgerock.guava.common.base.Function; import org.forgerock.guava.common.collect.FluentIterable; import org.forgerock.json.JsonPointer; import org.forgerock.json.JsonValue; import org.forgerock.json.resource.SortKey; import org.forgerock.openidm.repo.jdbc.SQLExceptionHandler; import org.forgerock.openidm.repo.util.StringSQLQueryFilterVisitor; import org.forgerock.openidm.repo.util.StringSQLRenderer; import org.forgerock.openidm.util.ResourceUtil; import org.forgerock.util.query.QueryFilter; /** * Postgres-specific generic table handler. */ public class PostgreSQLTableHandler extends GenericTableHandler { private class JsonExtractPathQueryFilterVisitor extends StringSQLQueryFilterVisitor<Map<String, Object>> { // value number for each value placeholder int objectNumber = 0; /** * Convenience method to generate the json_extract_path_text fragment: * * <pre><blockquote> * json_extract_path_text(obj.fullobject, ${p1}, ${p2}, {$p3} ...) * </blockquote></pre> * * where ${pn} are placeholders for the JsonPointer path elements. */ private StringSQLRenderer jsonExtractPathOnField(JsonPointer field, final Map<String, Object> objects) { return new StringSQLRenderer("json_extract_path_text(obj.fullobject, ") .append(StringUtils.join( FluentIterable.from(Arrays.asList(field.toArray())) .transform(new Function<String, String>() { @Override public String apply(String jsonPath) { ++objectNumber; String placeholder = "p" + objectNumber; objects.put(placeholder, jsonPath); return "${" + placeholder + "}"; } }), ", ")) .append(")"); } @Override public StringSQLRenderer visitValueAssertion(Map<String, Object> objects, String operand, JsonPointer field, Object valueAssertion) { ++objectNumber; String value = "v"+objectNumber; objects.put(value, valueAssertion); if (ResourceUtil.RESOURCE_FIELD_CONTENT_ID_POINTER.equals(field)) { return new StringSQLRenderer("(obj.objectid " + operand + " ${" + value + "})"); } else { // cast to numeric for numeric types String cast = (valueAssertion instanceof Integer || valueAssertion instanceof Long || valueAssertion instanceof Float || valueAssertion instanceof Double) ? "::numeric" : ""; return new StringSQLRenderer("(") .append(jsonExtractPathOnField(field, objects).toSQL()) .append(cast) .append(" ") .append(operand) .append(" (${") .append(value) .append("})") .append(cast) .append(")"); } } @Override public StringSQLRenderer visitPresentFilter(Map<String, Object> objects, JsonPointer field) { if (ResourceUtil.RESOURCE_FIELD_CONTENT_ID_POINTER.equals(field)) { // NOT NULL enforced by the schema return new StringSQLRenderer("(obj.objectid IS NOT NULL)"); } else { return new StringSQLRenderer("(" + jsonExtractPathOnField(field, objects).toSQL() + " IS NOT NULL)"); } } } /** * Construct a table handler for Postgres using Postgres-specific json-handling * * {@inheritDoc} */ public PostgreSQLTableHandler(JsonValue tableConfig, String dbSchemaName, JsonValue queriesConfig, JsonValue commandsConfig, int maxBatchSize, SQLExceptionHandler sqlExceptionHandler) { super(tableConfig, dbSchemaName, queriesConfig, commandsConfig, maxBatchSize, sqlExceptionHandler); } protected Map<QueryDefinition, String> initializeQueryMap() { Map<QueryDefinition, String> result = super.initializeQueryMap(); String typeTable = dbSchemaName == null ? "objecttypes" : dbSchemaName + ".objecttypes"; String mainTable = dbSchemaName == null ? mainTableName : dbSchemaName + "." + mainTableName; String propertyTable = dbSchemaName == null ? propTableName : dbSchemaName + "." + propTableName; result.put(QueryDefinition.UPDATEQUERYSTR, "UPDATE " + mainTable + " SET objectid = ?, rev = ?, fullobject = ?::json WHERE id = ?"); result.put(QueryDefinition.CREATEQUERYSTR, "INSERT INTO " + mainTable + " (objecttypes_id, objectid, rev, fullobject) VALUES (?,?,?,?::json)"); result.put(QueryDefinition.DELETEQUERYSTR, "DELETE FROM " + mainTable + " obj USING " + typeTable + " objtype WHERE obj.objecttypes_id = objtype.id AND objtype.objecttype = ? AND obj.objectid = ? AND obj.rev = ?"); result.put(QueryDefinition.PROPDELETEQUERYSTR, "DELETE FROM " + propertyTable + " WHERE " + mainTableName + "_id IN (SELECT obj.id FROM " + mainTable + " obj INNER JOIN " + typeTable + " objtype ON obj.objecttypes_id = objtype.id WHERE objtype.objecttype = ? AND obj.objectid = ?)"); return result; } @Override public String renderQueryFilter(QueryFilter<JsonPointer> filter, Map<String, Object> replacementTokens, Map<String, Object> params) { final String offsetParam = (String) params.get(PAGED_RESULTS_OFFSET); final String pageSizeParam = (String) params.get(PAGE_SIZE); String pageClause = " LIMIT " + pageSizeParam + " OFFSET " + offsetParam; // JsonValue-cheat to avoid an unchecked cast final List<SortKey> sortKeys = new JsonValue(params).get(SORT_KEYS).asList(SortKey.class); // Check for sort keys and build up order-by syntax if (sortKeys != null && sortKeys.size() > 0) { List<String> keys = new ArrayList<String>(); for (int i = 0; i < sortKeys.size(); i++) { final SortKey sortKey = sortKeys.get(i); final String tokenName = "sortKey" + i; keys.add("json_extract_path_text(fullobject, ${" + tokenName + (sortKey.isAscendingOrder() ? "}) ASC" : "}) DESC")); replacementTokens.put(tokenName, sortKey.getField().toString().substring(1)); } pageClause = " ORDER BY " + StringUtils.join(keys, ", ") + pageClause; } replacementTokens.put("otype", params.get("_resource")); return "SELECT fullobject::text" + " FROM ${_dbSchema}.${_mainTable} obj" + " INNER JOIN ${_dbSchema}.objecttypes objtype ON objtype.id = obj.objecttypes_id AND objtype.objecttype = ${otype}" + " WHERE " + filter.accept(new JsonExtractPathQueryFilterVisitor(), replacementTokens).toSQL() + pageClause; } }