GenericTableHandler.java revision 37f9df7d5b474a12668813f98992dceb7c7feacb
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright © 2011 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
* 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
* 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]"
*/
/**
* Handling of tables in a generic (not object specific) layout
*
* @author aegloff
*/
public class GenericTableHandler implements TableHandler {
final String mainTableName;
final String dbSchemaName;
// Jackson parser
// Type information for the Jackson parser
final TypeReference<LinkedHashMap<String,Object>> typeRef = new TypeReference<LinkedHashMap<String,Object>>() {};
final TableQueries queries;
final boolean enableBatching; // Whether to use JDBC statement batching.
int maxBatchSize; // The maximum number of statements to batch together. If max batch size is 1, do not use batching.
public enum QueryDefinition {
}
public GenericTableHandler(JsonValue tableConfig, String dbSchemaName, JsonValue queriesConfig, int maxBatchSize, SQLExceptionHandler sqlExceptionHandler) {
this.dbSchemaName = dbSchemaName;
if (maxBatchSize < 1) {
this.maxBatchSize = 1;
} else {
this.maxBatchSize = maxBatchSize;
}
if (sqlExceptionHandler == null) {
this.sqlExceptionHandler = new DefaultSQLExceptionHandler();
} else {
}
// TODO: Consider taking into account DB meta-data rather than just configuration
//DatabaseMetaData metadata = connection.getMetaData();
//boolean isBatchingSupported = metadata.supportsBatchUpdates();
//if (!isBatchingSupported) {
// maxBatchSize = 1;
//}
if (enableBatching) {
} else {
}
}
// objecttypes table
result.put(QueryDefinition.CREATETYPEQUERYSTR, "INSERT INTO " + typeTable + " (objecttype) VALUES (?)");
result.put(QueryDefinition.READTYPEQUERYSTR, "SELECT id FROM " + typeTable + " objtype WHERE objtype.objecttype = ?");
// Main object table
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 = ? FOR UPDATE");
result.put(QueryDefinition.READQUERYSTR, "SELECT obj.rev, obj.fullobject FROM " + typeTable + " objtype, " + mainTable + " obj WHERE obj.objecttypes_id = objtype.id AND objtype.objecttype = ? AND obj.objectid = ?");
result.put(QueryDefinition.CREATEQUERYSTR, "INSERT INTO " + mainTable + " (objecttypes_id, objectid, rev, fullobject) VALUES (?,?,?,?)");
result.put(QueryDefinition.UPDATEQUERYSTR, "UPDATE " + mainTable + " obj SET obj.objectid = ?, obj.rev = ?, obj.fullobject = ? WHERE obj.id = ?");
result.put(QueryDefinition.DELETEQUERYSTR, "DELETE obj FROM " + mainTable + " obj INNER JOIN " + typeTable + " objtype ON obj.objecttypes_id = objtype.id AND objtype.objecttype = ? WHERE obj.objectid = ? AND obj.rev = ?");
/* DB2 Script
deleteQueryStr = "DELETE FROM " + dbSchemaName + "." + mainTableName + " obj WHERE EXISTS (SELECT 1 FROM " + dbSchemaName + ".objecttypes objtype WHERE obj.objecttypes_id = objtype.id AND objtype.objecttype = ?) AND obj.objectid = ? AND obj.rev = ?";
*/
// Object properties table
result.put(QueryDefinition.PROPCREATEQUERYSTR, "INSERT INTO " + propertyTable + " ( " + mainTableName + "_id, propkey, proptype, propvalue) VALUES (?,?,?,?)");
result.put(QueryDefinition.PROPDELETEQUERYSTR, "DELETE prop FROM " + propertyTable + " prop INNER JOIN " + mainTable + " obj ON prop." + mainTableName + "_id = obj.id INNER JOIN " + typeTable + " objtype ON obj.objecttypes_id = objtype.id WHERE objtype.objecttype = ? AND obj.objectid = ?");
// Default object queries
result.put(QueryDefinition.QUERYALLIDS, "SELECT obj.objectid FROM " + tableVariable + " obj INNER JOIN " + typeTable + " objtype ON obj.objecttypes_id = objtype.id WHERE objtype.objecttype = ${_resource}");
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)
*/
try {
} else {
}
} finally {
}
return result;
}
/* (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)
*/
public void create(String fullId, String type, String localId, Map<String, Object> obj, Connection connection)
long typeId = getTypeId(type, connection); // Note this call can commit and start a new transaction in some cases
try {
createStatement = queries.getPreparedStatement(connection, queryMap.get(QueryDefinition.CREATEQUERYSTR), true);
if (!validKeyEntry) {
throw new InternalServerErrorException("Object creation for " + fullId + " failed to retrieve an assigned ID from the DB.");
}
} finally {
}
}
/**
* Writes all properties of a given resource to the properties table and links them to the main table record.
*
* @param fullId the full URI of the resource the belongs to
* @param dbId the generated identifier to link the properties table with the main table (foreign key)
* @param localId the local identifier of the resource these properties belong to
* @param value the JSON value with the properties to write
* @param connection the DB connection
* @throws SQLException if the insert failed
*/
void writeValueProperties(String fullId, long dbId, String localId, JsonValue value, Connection connection) throws SQLException {
if (cfg.searchableDefault) {
PreparedStatement propCreateStatement = getPreparedStatement(connection, QueryDefinition.PROPCREATEQUERYSTR);
try {
batchingCount = writeValueProperties(fullId, dbId, localId, value, connection, propCreateStatement, batchingCount);
if (logger.isDebugEnabled()) {
}
}
} finally {
}
}
}
/**
* If batching is enabled, prepared statements are added to the batch and only executed if they hit the max limit.
* After completion returns the number of properties that have only been added to the batch but not yet executed.
* The caller is responsible for executing the batch on remaining items when it deems the batch complete.
*
* If batching is not enabled, prepared statements are immediately executed.
*
* @param fullId the full URI of the resource the belongs to
* @param dbId the generated identifier to link the properties table with the main table (foreign key)
* @param localId the local identifier of the resource these properties belong to
* @param value the JSON value with the properties to write
* @param connection the DB connection
* @param propCreateStatement the prepared properties insert statement
* @param batchingCount the current number of statements that have been batched and not yet executed on the prepared statement
* @return status of the current batchingCount, i.e. how many statements are not yet executed in the PreparedStatement
* @throws SQLException if the insert failed
*/
private int writeValueProperties(String fullId, long dbId, String localId, JsonValue value, Connection connection,
batchingCount = writeValueProperties(fullId, dbId, localId, entry, connection, propCreateStatement, batchingCount);
} else {
}
}
if (logger.isTraceEnabled()) {
}
if (enableBatching) {
} else {
}
if (logger.isTraceEnabled()) {
logger.trace("Inserting objectproperty id: {} propkey: {} proptype: {}, propvalue: {}", new Object[]{fullId, propkey, proptype, propvalue});
}
}
if (logger.isDebugEnabled()) {
logger.debug("Batch limit reached, update of objectproperties updated: {}", Arrays.asList(numUpdates));
}
batchingCount = 0;
}
}
}
return batchingCount;
}
/**
* @inheritDoc
*/
}
/**
* InheritDoc
*/
}
// Ensure type is in objecttypes table and get its assigned id
// Callers should note that this may commit a transaction and start a new one if a new type gets added
long getTypeId(String type, Connection connection) throws SQLException, InternalServerErrorException {
if (typeId < 0) {
connection.setAutoCommit(true); // Commit the new type right away, and have no transaction isolation for read
try {
} catch (SQLException ex) {
// Rather than relying on DB specific ignore if exists functionality handle it here
// Could extend this in the future to more explicitly check for duplicate key error codes, but these again can be DB specific
detectedEx = ex;
}
if (typeId < 0) {
throw new InternalServerErrorException("Failed to populate and look up objecttypes table, no id could be retrieved for " + type, detectedEx);
}
}
return typeId;
}
/**
* @param type the object type URI
* @param connection the DB connection
* @return the typeId for the given type if exists, or -1 if does not exist
* @throws java.sql.SQLException
*/
long typeId = -1;
try {
}
} finally {
}
return typeId;
}
/**
* @param type the object type URI
* @param connection the DB connection
* @return true if a type was inserted
* @throws SQLException if the insert failed (e.g. concurrent insert by another thread)
*/
PreparedStatement createTypeStatement = getPreparedStatement(connection, QueryDefinition.CREATETYPEQUERYSTR);
try {
return (val == 1);
} finally {
}
}
/**
* Reads an object with for update locking applied
*
* Note: statement associated with the returned resultset
* is not closed upon return.
* Aside from taking care to close the resultset it also is
* the responsibility of the caller to close the associated
* should close the statement automatically, not all do this reliably.
*
* @param fullId qualified id of component type and id
* @param type the component type
* @param localId the id of the object within the component type
* @param connection the connection to use
* @return the row for the requested object, selected FOR UPDATE
* @throws NotFoundException if the requested object was not found in the DB
* @throws java.sql.SQLException for general DB issues
*/
throws NotFoundException, SQLException {
try {
return rs;
} else {
}
} catch (SQLException ex) {
throw ex;
}
}
/* (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)
*/
public void update(String fullId, String type, String localId, String rev, Map<String, Object> obj, Connection connection)
throws SQLException, IOException, PreconditionFailedException, NotFoundException, InternalServerErrorException {
++revInt;
try {
logger.debug("Update existing object {} rev: {} db id: {}, object type db id: {}", new Object[]{fullId, existingRev, dbId, objectTypeDbId});
throw new PreconditionFailedException("Update rejected as current Object revision " + existingRev + " is different than expected by caller (" + rev + "), the object has changed since retrieval.");
}
// Support changing object identifier
} else {
}
logger.trace("Populating prepared statement {} for {} {} {} {} {}", new Object[]{updateStatement, fullId, newLocalId, newRev, objString, dbId});
if (updateCount != 1) {
throw new InternalServerErrorException("Update execution did not result in updating 1 row as expected. Updated rows: " + updateCount);
}
// TODO: only update what changed?
logger.trace("Populating prepared statement {} for {} {} {}", new Object[]{deletePropStatement, fullId, type, localId});
} finally {
// Ensure associated statement also is closed
}
}
}
/**
* @see org.forgerock.openidm.repo.jdbc.internal.GenericTableHandler#delete(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.sql.Connection)
*/
throws PreconditionFailedException, InternalServerErrorException, NotFoundException, SQLException, IOException {
// First check if the revision matches and select it for UPDATE
try {
try {
} catch (NotFoundException ex) {
}
throw new PreconditionFailedException("Delete rejected as current Object revision " + existingRev + " is different than "
}
// Proceed with the valid delete
logger.trace("Populating prepared statement {} for {} {} {} {}", new Object[]{deleteStatement, fullId, type, localId, rev});
// Rely on ON DELETE CASCADE for connected object properties to be deleted
if (deletedRows < 1) {
throw new InternalServerErrorException("Deleting object for " + fullId + " failed, DB reported " + deletedRows + " rows deleted");
} else {
}
} finally {
// Ensure associated statement also is closed
}
}
}
/* (non-Javadoc)
* @see org.forgerock.openidm.repo.jdbc.impl.TableHandler#delete(java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.sql.Connection)
*/
public List<Map<String, Object>> query(String type, Map<String, Object> params, Connection connection)
throws ResourceException {
}
}
protected PreparedStatement getPreparedStatement(Connection connection, QueryDefinition queryDefinition) throws SQLException {
}
}
class GenericQueryResultMapper implements QueryResultMapper {
// Jackson parser
// Type information for the Jackson parser
TypeReference<LinkedHashMap<String,Object>> typeRef = new TypeReference<LinkedHashMap<String,Object>>() {};
public List<Map<String, Object>> mapQueryToObject(ResultSet rs, String queryId, String type, Map<String, Object> params, TableQueries tableQueries)
throws SQLException, IOException {
boolean hasId = false;
boolean hasRev = false;
boolean hasPropKey = false;
boolean hasPropValue = false;
if (!hasFullObject) {
}
if (hasFullObject) {
// TODO: remove data logging
logger.trace("Query result for queryId: {} type: {} converted obj: {}", new Object[] {queryId, type, obj});
} else {
if (hasId) {
}
if (hasRev) {
}
// Results from query on individual searchable property
if (hasPropKey && hasPropValue) {
}
}
}
return result;
}
}
class GenericTableConfig {
public String mainTableName;
public String propertiesTableName;
public boolean searchableDefault;
public GenericPropertiesConfig properties;
return explicit.booleanValue();
} else {
return searchableDefault;
}
}
cfg.searchableDefault = tableConfig.get("searchableDefault").defaultTo(Boolean.TRUE).asBoolean().booleanValue();
return cfg;
}
}
class GenericPropertiesConfig {
public String mainTableName;
public String propertiesTableName;
public boolean searchableDefault;
public GenericPropertiesConfig properties;
if (!propsConfig.isNull()) {
}
}
return cfg;
}
}