// This software is released into the Public Domain. See copying.txt for details.
package org.openstreetmap.osmosis.apidb.v0_6.impl;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openstreetmap.osmosis.core.container.v0_6.ChangeContainer;
import org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;
import org.openstreetmap.osmosis.core.container.v0_6.EntityContainerFactory;
import org.openstreetmap.osmosis.core.database.DbFeature;
import org.openstreetmap.osmosis.core.database.DbFeatureHistory;
import org.openstreetmap.osmosis.core.database.DbFeatureHistoryComparator;
import org.openstreetmap.osmosis.core.database.DbFeatureHistoryRowMapper;
import org.openstreetmap.osmosis.core.database.DbFeatureRowMapper;
import org.openstreetmap.osmosis.core.database.RowMapperListener;
import org.openstreetmap.osmosis.core.database.SortingStoreRowMapperListener;
import org.openstreetmap.osmosis.core.domain.v0_6.CommonEntityData;
import org.openstreetmap.osmosis.core.domain.v0_6.Entity;
import org.openstreetmap.osmosis.core.domain.v0_6.Tag;
import org.openstreetmap.osmosis.core.lifecycle.ReleasableContainer;
import org.openstreetmap.osmosis.core.lifecycle.ReleasableIterator;
import org.openstreetmap.osmosis.core.sort.common.FileBasedSort;
import org.openstreetmap.osmosis.core.store.SingleClassObjectSerializationFactory;
import org.openstreetmap.osmosis.core.store.StoreReleasingIterator;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
/**
* Provides functionality common to all top level entity daos.
*
* @param <T>
* The entity type to be supported.
*/
public abstract class EntityDao<T extends Entity> {
private static final Logger LOG = Logger.getLogger(EntityDao.class.getName());
private JdbcTemplate jdbcTemplate;
private NamedParameterJdbcTemplate namedParamJdbcTemplate;
private String entityName;
/**
* Creates a new instance.
*
* @param jdbcTemplate
* Used to access the database.
* @param entityName
* The name of the entity. Used for building dynamic sql queries.
*/
protected EntityDao(JdbcTemplate jdbcTemplate, String entityName) {
this.jdbcTemplate = jdbcTemplate;
this.namedParamJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
this.entityName = entityName;
}
/**
* Provides access to the named parameter jdbc template.
*
* @return The jdbc template.
*/
protected NamedParameterJdbcTemplate getNamedParamJdbcTemplate() {
return namedParamJdbcTemplate;
}
/**
* Produces an array of additional column names specific to this entity type to be returned by
* entity queries.
*
* @return The column names.
*/
protected abstract String[] getTypeSpecificFieldNames();
/**
* Creates a row mapper that receives common entity data objects and produces full entity
* objects.
*
* @param entityListener
* The full entity object listener.
* @return The full entity row mapper.
*/
protected abstract RowMapperListener<CommonEntityData> getEntityRowMapper(RowMapperListener<T> entityListener);
/**
* Gets the entity container factory for the entity type.
*
* @return The factory.
*/
protected abstract EntityContainerFactory<T> getContainerFactory();
/**
* Gets the history feature populators for the entity type.
*
* @param selectedEntityStatement
* The statement for obtaining the id and version pairs of entity records selected.
* @param parameterSource
* The parameters required to execute the selected entity statement.
* @return The history feature populators.
*/
protected abstract List<FeatureHistoryPopulator<T, ?, ?>> getFeatureHistoryPopulators(
String selectedEntityStatement, MapSqlParameterSource parameterSource);
private String buildFeaturelessEntityHistoryQuery(String selectedEntityStatement) {
StringBuilder sql;
sql = new StringBuilder();
sql.append("SELECT e.");
sql.append(entityName);
sql.append("_id AS id, e.version, e.timestamp, e.visible, u.data_public,");
sql.append(" u.id AS user_id, u.display_name, e.changeset_id");
for (String fieldName : getTypeSpecificFieldNames()) {
sql.append(", e.");
sql.append(fieldName);
}
sql.append(" FROM ");
sql.append(entityName);
sql.append("s e");
sql.append(" INNER JOIN ");
sql.append(selectedEntityStatement);
sql.append(" t ON e.");
sql.append(entityName);
sql.append("_id = t.");
sql.append(entityName);
sql.append("_id AND e.version = t.version");
sql.append(" INNER JOIN changesets c ON e.changeset_id = c.id INNER JOIN users u ON c.user_id = u.id");
LOG.log(Level.FINER, "Entity history query: " + sql);
return sql.toString();
}
private ReleasableIterator<EntityHistory<T>> getFeaturelessEntityHistory(
String selectedEntityStatement, MapSqlParameterSource parameterSource) {
FileBasedSort<EntityHistory<T>> sortingStore =
new FileBasedSort<EntityHistory<T>>(
new SingleClassObjectSerializationFactory(EntityHistory.class),
new EntityHistoryComparator<T>(), true);
try {
String sql;
SortingStoreRowMapperListener<EntityHistory<T>> storeListener;
EntityHistoryRowMapper<T> entityHistoryRowMapper;
RowMapperListener<CommonEntityData> entityRowMapper;
EntityDataRowMapper entityDataRowMapper;
ReleasableIterator<EntityHistory<T>> resultIterator;
sql = buildFeaturelessEntityHistoryQuery(selectedEntityStatement);
// Sends all received data into the object store.
storeListener = new SortingStoreRowMapperListener<EntityHistory<T>>(sortingStore);
// Retrieves the visible attribute allowing modifies to be distinguished
// from deletes.
entityHistoryRowMapper = new EntityHistoryRowMapper<T>(storeListener);
// Retrieves the entity type specific columns and builds the entity objects.
entityRowMapper = getEntityRowMapper(entityHistoryRowMapper);
// Retrieves the common entity information.
entityDataRowMapper = new EntityDataRowMapper(entityRowMapper, false);
// Perform the query passing the row mapper chain to process rows in a streamy fashion.
namedParamJdbcTemplate.query(sql, parameterSource, entityDataRowMapper);
// Open a iterator on the store that will release the store upon completion.
resultIterator = new StoreReleasingIterator<EntityHistory<T>>(sortingStore.iterate(), sortingStore);
// The store itself shouldn't be released now that it has been attached to the iterator.
sortingStore = null;
return resultIterator;
} finally {
if (sortingStore != null) {
sortingStore.release();
}
}
}
private ReleasableIterator<DbFeatureHistory<DbFeature<Tag>>> getTagHistory(String selectedEntityStatement,
MapSqlParameterSource parameterSource) {
FileBasedSort<DbFeatureHistory<DbFeature<Tag>>> sortingStore =
new FileBasedSort<DbFeatureHistory<DbFeature<Tag>>>(
new SingleClassObjectSerializationFactory(DbFeatureHistory.class),
new DbFeatureHistoryComparator<Tag>(), true);
try {
String sql;
SortingStoreRowMapperListener<DbFeatureHistory<DbFeature<Tag>>> storeListener;
DbFeatureHistoryRowMapper<DbFeature<Tag>> dbFeatureHistoryRowMapper;
DbFeatureRowMapper<Tag> dbFeatureRowMapper;
TagRowMapper tagRowMapper;
ReleasableIterator<DbFeatureHistory<DbFeature<Tag>>> resultIterator;
sql =
"SELECT et."
+ entityName
+ "_id AS id, et.k, et.v, et.version"
+ " FROM "
+ entityName
+ "_tags et"
+ " INNER JOIN "
+ selectedEntityStatement
+ " t ON et."
+ entityName
+ "_id = t."
+ entityName
+ "_id AND et.version = t.version";
LOG.log(Level.FINER, "Tag history query: " + sql);
// Sends all received data into the object store.
storeListener = new SortingStoreRowMapperListener<DbFeatureHistory<DbFeature<Tag>>>(sortingStore);
// Retrieves the version information associated with the tag.
dbFeatureHistoryRowMapper = new DbFeatureHistoryRowMapper<DbFeature<Tag>>(storeListener);
// Retrieves the entity information associated with the tag.
dbFeatureRowMapper = new DbFeatureRowMapper<Tag>(dbFeatureHistoryRowMapper);
// Retrieves the basic tag information.
tagRowMapper = new TagRowMapper(dbFeatureRowMapper);
// Perform the query passing the row mapper chain to process rows in a streamy fashion.
namedParamJdbcTemplate.query(sql, parameterSource, tagRowMapper);
// Open a iterator on the store that will release the store upon completion.
resultIterator = new StoreReleasingIterator<DbFeatureHistory<DbFeature<Tag>>>(sortingStore.iterate(),
sortingStore);
// The store itself shouldn't be released now that it has been attached to the iterator.
sortingStore = null;
return resultIterator;
} finally {
if (sortingStore != null) {
sortingStore.release();
}
}
}
private ReleasableIterator<EntityHistory<T>> getEntityHistory(
String selectedEntityStatement, MapSqlParameterSource parameterSource) {
ReleasableContainer releasableContainer;
releasableContainer = new ReleasableContainer();
try {
ReleasableIterator<EntityHistory<T>> entityIterator;
ReleasableIterator<DbFeatureHistory<DbFeature<Tag>>> tagIterator;
List<FeatureHistoryPopulator<T, ?, ?>> featurePopulators;
EntityHistoryReader<T> entityHistoryReader;
entityIterator = releasableContainer.add(
getFeaturelessEntityHistory(selectedEntityStatement, parameterSource));
tagIterator = releasableContainer.add(
getTagHistory(selectedEntityStatement, parameterSource));
featurePopulators = getFeatureHistoryPopulators(selectedEntityStatement, parameterSource);
for (FeatureHistoryPopulator<T, ?, ?> featurePopulator : featurePopulators) {
releasableContainer.add(featurePopulator);
}
entityHistoryReader = new EntityHistoryReader<T>(entityIterator, tagIterator, featurePopulators);
// The sources are now all attached to the history reader so we don't want to release
// them in the finally block.
releasableContainer.clear();
return entityHistoryReader;
} finally {
releasableContainer.release();
}
}
private ReleasableIterator<ChangeContainer> getChangeHistory(
String selectedEntityStatement, MapSqlParameterSource parameterSource) {
return new ChangeReader<T>(getEntityHistory(selectedEntityStatement, parameterSource), getContainerFactory());
}
private List<Integer> buildTransactionRanges(long bottomTransactionId, long topTransactionId) {
List<Integer> rangeValues;
int currentXid;
int finishXid;
// Begin building the values to use in the WHERE clause transaction ranges. Each pair of ids
// in this list will become a range selection.
rangeValues = new ArrayList<Integer>();
// If the bottom and top values are identical then no ranges are required. If we don't add
// this check here we could end up querying for all 4 billion transaction ids or all data in
// the database.
if (bottomTransactionId == topTransactionId) {
return rangeValues;
}
// We must now convert the top and bottom long representations of xids into their integer
// format because that is how they are indexed in the database.
// The bottom id is the first transaction id that has not been read yet, so we begin reading from it.
currentXid = (int) bottomTransactionId;
rangeValues.add(currentXid);
// The top transaction id is the first unassigned transaction id, so we must stop reading one short of that.
finishXid = ((int) topTransactionId) - 1;
// Process until we have enough ranges to reach the finish xid.
do {
// Determine how to terminate the current transaction range.
if (currentXid <= 2 && finishXid >= 0) {
// The range overlaps special ids 0-2 which should never be queried on.
// Terminate the current range before the special values.
rangeValues.add(-1);
// Begin the new range after the special values.
rangeValues.add(3);
currentXid = 3;
} else if (finishXid < currentXid) {
// The range crosses the integer overflow point. Only do this check once we are
// past 2 because the xid special values 0-2 may need to be crossed first.
// Terminate the current range at the maximum int value.
rangeValues.add(Integer.MAX_VALUE);
// Begin a new range at the minimum int value.
rangeValues.add(Integer.MIN_VALUE);
currentXid = Integer.MIN_VALUE;
} else {
// There are no problematic transaction id values between the current value and
// the top transaction id so terminate the current range at the top transaction
// id.
rangeValues.add(finishXid);
currentXid = finishXid;
}
} while (currentXid != finishXid);
return rangeValues;
}
private void buildTransactionRangeWhereClause(StringBuilder sql, MapSqlParameterSource parameters,
long bottomTransactionId, long topTransactionId) {
List<Integer> rangeValues;
// Determine the transaction ranges required to select all transactions between the bottom
// and top values. This takes into account transaction id wrapping issues and reserved ids.
rangeValues = buildTransactionRanges(bottomTransactionId, topTransactionId);
// Create a range clause for each range pair.
for (int i = 0; i < rangeValues.size(); i = i + 2) {
if (i > 0) {
sql.append(" OR ");
}
sql.append("(");
sql.append("xid_to_int4(xmin) >= :rangeValue").append(i);
sql.append(" AND ");
sql.append("xid_to_int4(xmin) <= :rangeValue").append(i + 1);
sql.append(")");
parameters.addValue("rangeValue" + i, rangeValues.get(i), Types.INTEGER);
parameters.addValue("rangeValue" + (i + 1), rangeValues.get(i + 1), Types.INTEGER);
}
}
private void buildTransactionIdListWhereClause(StringBuilder sql, List<Long> transactionIdList) {
for (int i = 0; i < transactionIdList.size(); i++) {
if (i > 0) {
sql.append(",");
}
// Must cast to int to allow the integer based xmin index to be used correctly.
sql.append((int) transactionIdList.get(i).longValue());
}
}
/**
* Retrieves the changes that have were made by a set of transactions.
*
* @param predicates
* Contains the predicates defining the transactions to be queried.
* @return An iterator pointing at the identified records.
*/
public ReleasableIterator<ChangeContainer> getHistory(ReplicationQueryPredicates predicates) {
String selectedEntityTableName;
StringBuilder sql;
MapSqlParameterSource parameterSource;
ReleasableIterator<ChangeContainer> historyIterator;
// PostgreSQL sometimes incorrectly chooses to perform full table scans, these options
// prevent this. Note that this is not recommended practice according to documentation
// but fixing this would require modifying the table statistics gathering
// configuration on the production database to produce better plans.
jdbcTemplate.execute(
"set local enable_seqscan = false;"
+ "set local enable_mergejoin = false;"
+ "set local enable_hashjoin = false");
parameterSource = new MapSqlParameterSource();
selectedEntityTableName = "tmp_" + entityName + "s";
sql = new StringBuilder();
sql.append("CREATE TEMPORARY TABLE ");
sql.append(selectedEntityTableName);
sql.append(" ON COMMIT DROP");
sql.append(" AS SELECT ");
sql.append(entityName);
sql.append("_id, version FROM ");
sql.append(entityName);
sql.append("s WHERE ((");
// Add the main transaction ranges to the where clause.
buildTransactionRangeWhereClause(sql, parameterSource, predicates.getBottomTransactionId(), predicates
.getTopTransactionId());
sql.append(")");
// If previously active transactions have become ready since the last invocation we include those as well.
if (predicates.getReadyList().size() > 0) {
sql.append(" OR xid_to_int4(xmin) IN (");
buildTransactionIdListWhereClause(sql, predicates.getReadyList());
sql.append(")");
}
sql.append(")");
// Any active transactions must be explicitly excluded.
if (predicates.getActiveList().size() > 0) {
sql.append(" AND xid_to_int4(xmin) NOT IN (");
buildTransactionIdListWhereClause(sql, predicates.getActiveList());
sql.append(")");
}
sql.append(" AND redaction_id IS NULL");
LOG.log(Level.FINER, "Entity identification query: " + sql);
namedParamJdbcTemplate.update(sql.toString(), parameterSource);
jdbcTemplate.update("ALTER TABLE ONLY " + selectedEntityTableName
+ " ADD CONSTRAINT pk_" + selectedEntityTableName
+ " PRIMARY KEY (" + entityName + "_id, version)");
jdbcTemplate.update("ANALYZE " + selectedEntityTableName);
if (LOG.isLoggable(Level.FINER)) {
LOG.log(Level.FINER,
jdbcTemplate.queryForObject("SELECT Count(" + entityName + "_id) FROM " + selectedEntityTableName,
Integer.class) + " " + entityName + " records located.");
}
// Extract the data and obtain an iterator for the results.
historyIterator = getChangeHistory(selectedEntityTableName, new MapSqlParameterSource());
// The temp table is no longer required and can be deleted.
jdbcTemplate.execute("DROP TABLE " + selectedEntityTableName);
return historyIterator;
}
/**
* Retrieves the changes that have were made between two points in time.
*
* @param intervalBegin
* Marks the beginning (inclusive) of the time interval to be checked.
* @param intervalEnd
* Marks the end (exclusive) of the time interval to be checked.
* @return An iterator pointing at the identified records.
*/
public ReleasableIterator<ChangeContainer> getHistory(Date intervalBegin, Date intervalEnd) {
String selectedEntityTableName;
StringBuilder sql;
MapSqlParameterSource parameterSource;
ReleasableIterator<ChangeContainer> historyIterator;
// PostgreSQL sometimes incorrectly chooses to perform full table scans, these options
// prevent this. Note that this is not recommended practice according to documentation
// but fixing this would require modifying the table statistics gathering
// configuration on the production database to produce better plans.
jdbcTemplate.execute(
"set local enable_seqscan = false;"
+ "set local enable_mergejoin = false;"
+ "set local enable_hashjoin = false");
selectedEntityTableName = "tmp_" + entityName + "s";
sql = new StringBuilder();
sql.append("CREATE TEMPORARY TABLE ");
sql.append(selectedEntityTableName);
sql.append(" ON COMMIT DROP");
sql.append(" AS SELECT ");
sql.append(entityName);
sql.append("_id, version FROM ");
sql.append(entityName);
sql.append("s WHERE timestamp > :intervalBegin AND timestamp <= :intervalEnd");
LOG.log(Level.FINER, "Entity identification query: " + sql);
parameterSource = new MapSqlParameterSource();
parameterSource.addValue("intervalBegin", intervalBegin, Types.TIMESTAMP);
parameterSource.addValue("intervalEnd", intervalEnd, Types.TIMESTAMP);
namedParamJdbcTemplate.update(sql.toString(), parameterSource);
jdbcTemplate.update("ANALYZE " + selectedEntityTableName);
// Extract the data and obtain an iterator for the results.
historyIterator = getChangeHistory(selectedEntityTableName, new MapSqlParameterSource());
// The temp table is no longer required and can be deleted.
jdbcTemplate.execute("DROP TABLE " + selectedEntityTableName);
return historyIterator;
}
/**
* Retrieves all changes in the database.
*
* @return An iterator pointing at the identified records.
*/
public ReleasableIterator<ChangeContainer> getHistory() {
// Join the entity table to itself which will return all records.
return getChangeHistory(entityName + "s", new MapSqlParameterSource());
}
/**
* Retrieves all current data in the database.
*
* @return An iterator pointing at the current records.
*/
public ReleasableIterator<EntityContainer> getCurrent() {
// Join the entity table to the current version of itself which will return all current
// records.
return new EntityContainerReader<T>(
getEntityHistory("(SELECT id AS " + entityName + "_id, version FROM current_"
+ entityName + "s WHERE visible = TRUE)",
new MapSqlParameterSource()), getContainerFactory());
}
}