/* * Copyright 2007 T-Rank AS * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package no.trank.openpipe.jdbc.store; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Collection; import java.util.Iterator; import javax.sql.DataSource; import org.apache.ws.jaxme.sqls.BooleanConstraint; import org.apache.ws.jaxme.sqls.Column; import static org.apache.ws.jaxme.sqls.Column.Type.TIMESTAMP; import static org.apache.ws.jaxme.sqls.Column.Type.VARCHAR; import org.apache.ws.jaxme.sqls.ColumnReference; import org.apache.ws.jaxme.sqls.ConstrainedStatement; import org.apache.ws.jaxme.sqls.DeleteStatement; import org.apache.ws.jaxme.sqls.InsertStatement; import org.apache.ws.jaxme.sqls.SQLFactory; import org.apache.ws.jaxme.sqls.SQLGenerator; import org.apache.ws.jaxme.sqls.Schema; import org.apache.ws.jaxme.sqls.SelectStatement; import org.apache.ws.jaxme.sqls.StringColumn; import org.apache.ws.jaxme.sqls.Table; import org.apache.ws.jaxme.sqls.UpdateStatement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.TypeMismatchDataAccessException; import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.PreparedStatementCallback; import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.DefaultTransactionDefinition; /** * A class that tracks an object by its id with a backing database. * * @version $Revision$ */ public class IdStateHolder { private static final Logger log = LoggerFactory.getLogger(IdStateHolder.class); private static final StringRowMapper STRING_ROWMAPPER = new StringRowMapper(); private final SimpleJdbcTemplate jdbcTemplate; private final DataSourceTransactionManager transactionManager; private final SQLFactory sqlFactory; private SqlPSCreator update; private SqlPSCreator insert; private String delete; private String selectDel; private Timestamp startTime; private TransactionStatus status; /** * Creates an id state holder with the given configuration.<br/> * A {@link SimpleJdbcTemplate} and {@link DataSourceTransactionManager} is created from the given * <tt>dataSource</tt>.<br/> * A table matching <tt>desc</tt> is created using SQL's created by <tt>sqlFactory</tt> if one doesn't already * exists. If a table matching {@link TableDescription#getTableName() desc.getTableName()} already exists, it is * validated against <tt>desc</tt>. * * @param dataSource the datasource to use. * @param sqlFactory the factory to use for creating SQL's. * @param desc the description of the table to use. */ public IdStateHolder(DataSource dataSource, SQLFactory sqlFactory, TableDescription desc) { jdbcTemplate = new SimpleJdbcTemplate(dataSource); transactionManager = new DataSourceTransactionManager(dataSource); this.sqlFactory = sqlFactory; final JdbcOperations op = jdbcTemplate.getJdbcOperations(); final Schema schema = (Schema) op.execute(new SchemaCallback(sqlFactory)); Table table = schema.getTable(desc.getTableName()); final SQLGenerator generator = sqlFactory.newSQLGenerator(); final StringColumn colId; final Column colUpd; if (table != null) { final Column id = table.getColumn(desc.getIdColumnName()); colUpd = table.getColumn(desc.getUpdColumnName()); validateTable(id, colUpd, desc.getIdMaxLength()); colId = (StringColumn) id; } else { table = schema.newTable(desc.getTableName()); colId = (StringColumn) table.newColumn(desc.getIdColumnName(), VARCHAR); colUpd = table.newColumn(desc.getUpdColumnName(), TIMESTAMP); colId.setNullable(false); colId.setLength(desc.getIdMaxLength()); table.newPrimaryKey().addColumn(colId); colUpd.setNullable(false); createTable(op, table, generator); } createSqls(table, generator, colId, colUpd); } private void createSqls(Table table, SQLGenerator generator, StringColumn colId, Column colUpd) { update = createUpdate(generator, colId, colUpd, table); insert = createInsert(generator, colId, colUpd, table); delete = createDelete(generator, colUpd, table); selectDel = createSelectDeleted(generator, colId, colUpd, table); log.debug(update.getSql()); log.debug(insert.getSql()); log.debug(delete); log.debug(selectDel); } /** * Prepares for a set of objects. Updates {@link #getStartTime() startTime} and starts a transaction with the * database. */ public void prepare() { startTime = new Timestamp(System.currentTimeMillis()); status = transactionManager.getTransaction(new DefaultTransactionDefinition()); } /** * Commits the transaction started in {@link #prepare()} after deleted ids has been deleted from the database. * * @see #findDeletedIds() */ public void commit() { jdbcTemplate.update(delete, startTime); try { transactionManager.commit(status); } finally { status = null; } } /** * Rolls back the transaction started in {@link #prepare()}. */ public void rollback() { try { transactionManager.rollback(status); } finally { status = null; } } @SuppressWarnings({"unchecked"}) private static void createTable(JdbcOperations op, Table table, SQLGenerator generator) { log.debug("Creating table: {}", table.getName()); final Collection<String> create = generator.getCreate(table, true); for (String sql : create) { log.debug(sql); op.execute(sql); } } private static void validateTable(Column colId, Column colUpd, int idMaxLength) { if (!colId.isStringColumn()) { throw new TypeMismatchDataAccessException("Type for " + colId.getName() + " was: '" + colId.getType().getName() + "' should be '" + VARCHAR.getName() + '\''); } final StringColumn id = (StringColumn) colId; if (id.getLength() < idMaxLength) { throw new TypeMismatchDataAccessException("Length for " + colId.getName() + " was: " + id.getLength() + " should be at least " + idMaxLength); } if (!TIMESTAMP.equals(colUpd.getType())) { throw new TypeMismatchDataAccessException("Type for " + colUpd.getName() + " was: '" + colUpd.getType().getName() + "' should be '" + TIMESTAMP.getName() + '\''); } } private SqlPSCreator createUpdate(SQLGenerator generator, StringColumn colId, Column colUpd, Table table) { final UpdateStatement update = sqlFactory.newUpdateStatement(); update.setTable(table); update.addSet(colUpd); createConstraintId(colId, update); return new SqlPSCreator(generator.getQuery(update)); } private static void createConstraintId(StringColumn colId, ConstrainedStatement statement) { final BooleanConstraint constraintId = statement.getWhere().createEQ(); constraintId.addPart(statement.getTableReference().newColumnReference(colId)); constraintId.addPlaceholder(); } private String createDelete(SQLGenerator generator, Column colUpd, Table table) { final DeleteStatement delete = sqlFactory.newDeleteStatement(); delete.setTable(table); createConstraintUpd(colUpd, delete); return generator.getQuery(delete); } private String createSelectDeleted(SQLGenerator generator, StringColumn colId, Column colUpd, Table table) { final SelectStatement select = sqlFactory.newSelectStatement(); select.setTable(table); select.addResultColumn(select.getSelectTableReference().newColumnReference(colId)); createConstraintUpd(colUpd, select); return generator.getQuery(select); } private static void createConstraintUpd(Column colUpd, ConstrainedStatement statement) { final ColumnReference colRefId = statement.getTableReference().newColumnReference(colUpd); final BooleanConstraint constraintId = statement.getWhere().createLT(); constraintId.addPart(colRefId); constraintId.addPlaceholder(); } private SqlPSCreator createInsert(SQLGenerator generator, StringColumn colId, Column colUpd, Table table) { final InsertStatement insert = sqlFactory.newInsertStatement(); insert.setTable(table); insert.addSet(colUpd); insert.addSet(colId); return new SqlPSCreator(generator.getQuery(insert)); } /** * Gets the timestamp created in {@link #prepare()}. * * @return the timestamp created in {@link #prepare()}. */ public Timestamp getStartTime() { return startTime; } /** * Finds the ids of objects in the database, that was not updated with {@link #isUpdate(String) isUpdate(id)} since * last call to {@link #prepare()}. * * @return the ids of deleted documents. */ public Iterator<String> findDeletedIds() { return jdbcTemplate.query(selectDel, STRING_ROWMAPPER, startTime).iterator(); } /** * Checks if an object with the given id is already in the database.<br/> * If no such id existed, then a new record is inserted. * If id exists, then the record is updated with current timestamp. * * @param id the id of the object to check. * * @return <tt>true</tt> if id exists in database. */ public boolean isUpdate(final String id) { final UpdatePSCallback callback = new UpdatePSCallback(startTime, id); final boolean wasUpdate = (Boolean) jdbcTemplate.getJdbcOperations().execute(update, callback); if (!wasUpdate) { jdbcTemplate.getJdbcOperations().execute(insert, callback); } return wasUpdate; } private static class SchemaCallback implements ConnectionCallback { private final SQLFactory sqlFactory; public SchemaCallback(SQLFactory sqlFactory) { this.sqlFactory = sqlFactory; } @Override public Object doInConnection(Connection con) throws SQLException, DataAccessException { return sqlFactory.getSchema(con, (Schema.Name) null); } } private static final class SqlPSCreator implements PreparedStatementCreator { private final String sql; public SqlPSCreator(String sql) { this.sql = sql; } @Override public PreparedStatement createPreparedStatement(Connection con) throws SQLException { return con.prepareStatement(sql); } public String getSql() { return sql; } } private static final class UpdatePSCallback implements PreparedStatementCallback { private final Timestamp startTime; private final String id; public UpdatePSCallback(Timestamp startTime, String id) { this.startTime = startTime; this.id = id; } @Override public Object doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { ps.setTimestamp(1, startTime); ps.setString(2, id); return ps.executeUpdate() == 1; } } }