/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 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]"
*/
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 static org.forgerock.openidm.repo.util.Clauses.where;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
import java.util.Map;
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.Clause;
import org.forgerock.util.query.QueryFilter;
/**
* TableHandler appropriate for MSSQL-specific query syntax.
*/
public class MSSQLTableHandler extends GenericTableHandler {
/**
* Max length of searchable properties for MSSQL.
* Anything larger than 195 will overflow the max index size and error.
*/
private static final int SEARCHABLE_LENGTH = 195;
public MSSQLTableHandler(JsonValue tableConfig, String dbSchemaName, JsonValue queriesConfig, JsonValue commandsConfig,
int maxBatchSize, SQLExceptionHandler sqlExceptionHandler) {
super(tableConfig, dbSchemaName, queriesConfig, commandsConfig, maxBatchSize, sqlExceptionHandler);
}
@Override
int getSearchableLength() {
return SEARCHABLE_LENGTH;
}
@Override
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.READFORUPDATEQUERYSTR,
"SELECT obj.* FROM "
+ mainTable
+ " obj INNER JOIN "
+ typeTable
+ " objtype ON obj.objecttypes_id = objtype.id"
+ " AND objtype.objecttype = ? WHERE obj.objectid = ?");
result.put(QueryDefinition.UPDATEQUERYSTR,
"UPDATE obj SET obj.objectid = ?, obj.rev = ?, obj.fullobject = ? FROM "
+ mainTable + " obj WHERE obj.id = ? AND obj.rev = ?");
return result;
}
/* (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<String, Object> obj, Connection connection)
throws SQLException, IOException, org.forgerock.json.resource.PreconditionFailedException, org.forgerock.json.resource.NotFoundException, org.forgerock.json.resource.InternalServerErrorException {
logger.debug("Update with fullid {}", fullId);
int revInt = Integer.parseInt(rev);
++revInt;
String newRev = Integer.toString(revInt);
obj.put("_rev", newRev); // Save the rev in the object, and return the changed rev from the create.
ResultSet rs = null;
PreparedStatement updateStatement = null;
PreparedStatement deletePropStatement = null;
try {
rs = readForUpdate(fullId, type, localId, connection);
String existingRev = rs.getString("rev");
long dbId = rs.getLong("id");
long objectTypeDbId = rs.getLong("objecttypes_id");
logger.debug("Update existing object {} rev: {} db id: {}, object type db id: {}",
fullId, existingRev, dbId, objectTypeDbId);
if (!existingRev.equals(rev)) {
throw new org.forgerock.json.resource.PreconditionFailedException(
"Update rejected as current Object revision " + existingRev
+ " is different than expected by caller (" + rev + "), "
+ "the object has changed since retrieval.");
}
updateStatement = getPreparedStatement(connection, QueryDefinition.UPDATEQUERYSTR);
deletePropStatement = getPreparedStatement(connection, QueryDefinition.PROPDELETEQUERYSTR);
// Support changing object identifier
String newLocalId = (String) obj.get("_id");
if (newLocalId != null && !localId.equals(newLocalId)) {
logger.debug("Object identifier is changing from " + localId + " to " + newLocalId);
} else {
newLocalId = localId; // If it hasn't changed, use the existing ID
obj.put("_id", newLocalId); // Ensure the ID is saved in the object
}
String objString = mapper.writeValueAsString(obj);
logger.trace("Populating prepared statement {} for {} {} {} {} {} {}",
updateStatement, fullId, newLocalId, newRev, objString, dbId, existingRev);
updateStatement.setString(1, newLocalId);
updateStatement.setString(2, newRev);
updateStatement.setString(3, objString);
updateStatement.setLong(4, dbId);
updateStatement.setString(5, existingRev);
logger.debug("Update statement: {}", updateStatement);
int updateCount = updateStatement.executeUpdate();
logger.trace("Updated rows: {} for {}", updateCount, fullId);
if (updateCount == 0) {
throw new org.forgerock.json.resource.PreconditionFailedException("Update rejected as current Object revision " + existingRev + ", has changed since retrieval.");
} else if (updateCount > 1) {
throw new org.forgerock.json.resource.InternalServerErrorException("Update execution did not result in updating 1 row as expected. Updated rows: " + updateCount);
}
JsonValue jv = new JsonValue(obj);
// TODO: only update what changed?
logger.trace("Populating prepared statement {} for {} {} {}", deletePropStatement, fullId, type, localId);
deletePropStatement.setString(1, type);
deletePropStatement.setString(2, localId);
logger.debug("Update properties del statement: {}", deletePropStatement);
int deleteCount = deletePropStatement.executeUpdate();
logger.trace("Deleted child rows: {} for: {}", deleteCount, fullId);
writeValueProperties(fullId, dbId, localId, jv, connection);
} finally {
if (rs != null) {
// Ensure associated statement also is closed
Statement rsStatement = rs.getStatement();
CleanupHelper.loggedClose(rs);
CleanupHelper.loggedClose(rsStatement);
}
CleanupHelper.loggedClose(updateStatement);
CleanupHelper.loggedClose(deletePropStatement);
}
}
/**
* @inheritDoc
*/
@Override
public String renderQueryFilter(QueryFilter<JsonPointer> filter, Map<String, Object> replacementTokens, Map<String, Object> params) {
final int offsetParam = Integer.parseInt((String) params.get(PAGED_RESULTS_OFFSET));
final int pageSizeParam = Integer.parseInt((String) params.get(PAGE_SIZE));
// Create custom builder which overrides SQL output syntax
// note enclosing offsetParam and pageSizeParam - we don't bother passing these to the builder to deal with
final SQLBuilder builder =
new SQLBuilder() {
@Override
public String toSQL() {
return "WITH results AS ( SELECT rowNo = ROW_NUMBER() OVER ("
+ getOrderByClause().toSQL()
+ "), obj.fullobject "
+ getFromClause().toSQL()
+ getJoinClause().toSQL()
+ getWhereClause().toSQL()
+ ") SELECT * FROM results WHERE rowNo BETWEEN "
+ (offsetParam + 1)
+ " AND "
+ (offsetParam + pageSizeParam);
}
};
// "SELECT obj.* FROM mainTable obj..."
builder.from("${_dbSchema}.${_mainTable} obj")
// join objecttypes to fix OPENIDM-2773
.join("${_dbSchema}.objecttypes", "objecttypes")
.on(where("obj.objecttypes_id = objecttypes.id")
.and("objecttypes.objecttype = ${otype}"))
.where(filter.accept(
// override numeric value clause generation to cast propvalue to a number
new GenericSQLQueryFilterVisitor(SEARCHABLE_LENGTH, builder) {
@Override
Clause buildNumericValueClause(String propTable, String operand, String placeholder) {
return where(propTable + ".proptype = 'java.lang.Integer'")
.or(propTable + ".proptype = 'java.lang.Double'")
.and("(CASE ISNUMERIC(propvalue) WHEN 1 THEN CAST(propvalue AS FLOAT) ELSE null END) " + operand + " ${" + placeholder + "}");
}
},
replacementTokens));
// other half of OPENIDM-2773 fix
replacementTokens.put("otype", params.get("_resource"));
// 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) {
prepareSortKeyStatements(builder, sortKeys, replacementTokens);
} else {
builder.orderBy("obj.id", false);
}
return builder.toSQL();
}
}