GenericTableHandler.java revision 5c6fc9459842796234027c8bb8f58886d69ebc8f
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright © 2011-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
* 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 {
/**
* Maximum length of searchable properties.
* This is used to trim values due to database index size limitations.
*/
protected static final int SEARCHABLE_LENGTH = 2000;
final String mainTableName;
final String propTableName;
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 {
}
}
/**
* Create a generic table handler using a QueryFilterVisitor that uses generic object property tables to process
* query filters.
*
* @param tableConfig the table config
* @param dbSchemaName the schem name
* @param queriesConfig a map of named queries
* @param commandsConfig a map of named commands
* @param maxBatchSize the maximum batch size
* @param sqlExceptionHandler a handler for SQLExceptions
*/
int maxBatchSize,
this.dbSchemaName = dbSchemaName;
if (maxBatchSize < 1) {
this.maxBatchSize = 1;
} else {
this.maxBatchSize = maxBatchSize;
}
if (sqlExceptionHandler == null) {
this.sqlExceptionHandler = new DefaultSQLExceptionHandler();
} else {
}
queries = new TableQueries(this, mainTableName, propTableName, dbSchemaName, getSearchableLength(), new GenericQueryResultMapper());
// 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 {
}
}
/**
* Get the length of the searchable index.
*/
int getSearchableLength() {
return SEARCHABLE_LENGTH;
}
// 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.hasPossibleSearchableProperties()) {
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: {}", 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: {}", 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 {} {} {} {} {}", 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 {} {} {}", deletePropStatement, fullId, type, localId);
} finally {
// Ensure associated statement also is closed
}
}
}
/**
* @see org.forgerock.openidm.repo.jdbc.impl.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 {} {} {} {}", 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 {
}
public Integer command(String type, Map<String, Object> params, Connection connection) throws SQLException, ResourceException {
}
}
protected PreparedStatement getPreparedStatement(Connection connection, QueryDefinition queryDefinition) throws SQLException {
}
/**
* Render and SQL SELECT statement with placeholders for the given query filter.
*
* @param filter the query filter
* @param replacementTokens a map to store any replacement tokens
* @param params a map containing query parameters
* @return an SQL SELECT statement
*/
public String renderQueryFilter(QueryFilter filter, Map<String, Object> replacementTokens, Map<String, Object> params) {
+ getFromClause().toSQL()
+ getWhereClause().toSQL()
+ getOrderByClause().toSQL()
+ " LIMIT " + pageSizeParam
+ " OFFSET " + offsetParam;
}
};
// "SELECT obj.* FROM mainTable obj..."
.from("${_dbSchema}.${_mainTable} obj")
// join objecttypes to fix OPENIDM-2773
.and("objecttypes.objecttype = ${otype}"))
// construct where clause by visiting filter
.where(filter.accept(new GenericSQLQueryFilterVisitor(SEARCHABLE_LENGTH, builder), replacementTokens));
// other half of OPENIDM-2773 fix
// JsonValue-cheat to avoid an unchecked cast
// Check for sort keys and build up order-by syntax
}
/**
* Loops through sort keys constructing the inner join and key statements.
*
* @param builder the SQL builder
* @param sortKeys a {@link java.util.List} of sort keys
* @param replacementTokens a {@link java.util.Map} containing replacement tokens for the {@link java.sql.PreparedStatement}
*/
protected void prepareSortKeyStatements(SQLBuilder builder, List<SortKey> sortKeys, Map<String, Object> replacementTokens) {
return;
}
.on(where(tableAlias + ".${_mainTable}_id = obj.id").and(tableAlias + ".propkey = ${" + tokenName + "}"))
}
}
}
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;
boolean hasTotal = 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) {
}
if (hasTotal) {
}
// 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;
// More specific configuration takes precedence
}
return explicit.booleanValue();
} else {
return searchableDefault;
}
}
/**
* @return Approximation on whether this may have searchable properties
* It is only an approximation as we do not have an exhaustive list of possible properties
* to consider against a default setting of searchable.
*/
public boolean hasPossibleSearchableProperties() {
}
return cfg;
}
}
class GenericPropertiesConfig {
public String mainTableName;
public String propertiesTableName;
public boolean searchableDefault;
public GenericPropertiesConfig properties;
// Whether there are any properties explicitly set to searchable true
public boolean explicitSearchableProperties;
if (!propsConfig.isNull()) {
if (propSearchable) {
cfg.explicitSearchableProperties = true;
}
}
}
return cfg;
}
}