/* * Copyright (c) 2009-2013, 2016 Eike Stepper (Berlin, Germany) and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Andre Dietisheim - initial API and implementation * Eike Stepper - maintenance */ package org.eclipse.emf.cdo.common.internal.db.cache; import org.eclipse.emf.cdo.common.branch.CDOBranch; import org.eclipse.emf.cdo.common.branch.CDOBranchManager; import org.eclipse.emf.cdo.common.branch.CDOBranchPoint; import org.eclipse.emf.cdo.common.branch.CDOBranchVersion; import org.eclipse.emf.cdo.common.commit.CDOCommitInfoManager; import org.eclipse.emf.cdo.common.id.CDOID; import org.eclipse.emf.cdo.common.id.CDOIDProvider; import org.eclipse.emf.cdo.common.internal.db.AbstractQueryStatement; import org.eclipse.emf.cdo.common.internal.db.AbstractUpdateStatement; import org.eclipse.emf.cdo.common.internal.db.DBRevisionCacheUtil; import org.eclipse.emf.cdo.common.lob.CDOLobStore; import org.eclipse.emf.cdo.common.model.CDOPackageRegistry; import org.eclipse.emf.cdo.common.protocol.CDODataInput; import org.eclipse.emf.cdo.common.protocol.CDODataOutput; import org.eclipse.emf.cdo.common.revision.CDOListFactory; import org.eclipse.emf.cdo.common.revision.CDORevision; import org.eclipse.emf.cdo.common.revision.CDORevisionCache; import org.eclipse.emf.cdo.common.revision.CDORevisionFactory; import org.eclipse.emf.cdo.spi.common.protocol.CDODataInputImpl; import org.eclipse.emf.cdo.spi.common.protocol.CDODataOutputImpl; import org.eclipse.emf.cdo.spi.common.revision.InternalCDORevision; import org.eclipse.emf.cdo.spi.common.revision.InternalCDORevisionCache; import org.eclipse.net4j.db.DBException; import org.eclipse.net4j.db.DBUtil; import org.eclipse.net4j.db.IDBAdapter; import org.eclipse.net4j.db.IDBConnectionProvider; import org.eclipse.net4j.util.CheckUtil; import org.eclipse.net4j.util.io.ExtendedDataInputStream; import org.eclipse.net4j.util.io.ExtendedDataOutput; import org.eclipse.net4j.util.io.ExtendedDataOutputStream; import org.eclipse.net4j.util.lifecycle.Lifecycle; import org.eclipse.emf.ecore.EClass; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.sql.Blob; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * A JDBC-based {@link CDORevisionCache}. * * @author Andre Dietisheim */ public class DBRevisionCache extends Lifecycle implements InternalCDORevisionCache { private CDOIDProvider idProvider; private CDOListFactory listFactory; private CDOPackageRegistry packageRegistry; private CDORevisionFactory revisionFactory; private IDBAdapter dbAdapter; private IDBConnectionProvider dbConnectionProvider; public DBRevisionCache() { } public InternalCDORevisionCache instantiate(CDORevision revision) { // TODO: Support branches directly throw new UnsupportedOperationException(); } public CDOIDProvider getIDProvider() { return idProvider; } public void setIDProvider(CDOIDProvider idProvider) { this.idProvider = idProvider; } public CDOListFactory getListFactory() { return listFactory; } public void setListFactory(CDOListFactory listFactory) { this.listFactory = listFactory; } public CDOPackageRegistry getPackageRegistry() { return packageRegistry; } public void setPackageRegistry(CDOPackageRegistry packageRegistry) { this.packageRegistry = packageRegistry; } public CDORevisionFactory getRevisionFactory() { return revisionFactory; } public void setRevisionFactory(CDORevisionFactory revisionFactory) { this.revisionFactory = revisionFactory; } public IDBAdapter getDBAdapter() { return dbAdapter; } public void setDBAdapter(IDBAdapter dbAdapter) { this.dbAdapter = dbAdapter; } public IDBConnectionProvider getDBConnectionProvider() { return dbConnectionProvider; } public void setDBConnectionProvider(IDBConnectionProvider dbConnectionProvider) { this.dbConnectionProvider = dbConnectionProvider; } public EClass getObjectType(CDOID id) { return null; } /** * Gets the revision with the highest version for a given {@link CDOID}. * * @param id * the id to match * @return the revision that was found */ public InternalCDORevision getRevision(final CDOID id) { Connection connection = null; String sql = null; try { connection = getConnection(); AbstractQueryStatement<InternalCDORevision> query = createGetRevisionByIDStatement(id); sql = query.getSQL(); return query.query(connection); } catch (Exception e) { throw new DBException("Error while retrieving the revision from the database", e, sql); //$NON-NLS-1$ } finally { DBUtil.close(connection); } } private AbstractQueryStatement<InternalCDORevision> createGetRevisionByIDStatement(final CDOID id) { return new AbstractQueryStatement<InternalCDORevision>() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("SELECT "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CDOREVISION); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(" FROM "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" WHERE "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_ID); builder.append("=? AND "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append("="); //$NON-NLS-1$ builder.append(CDORevision.UNSPECIFIED_DATE); return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws SQLException { preparedStatement.setString(1, id.toURIFragment()); } @Override protected InternalCDORevision getResult(ResultSet resultSet) throws Exception { long revised = resultSet.getLong(2); Blob blob = resultSet.getBlob(1); return toRevision(blob, revised); } }; } /** * Gets an {@link InternalCDORevision} that matches the given timestamp (it is >= created timestamp AND <= revised * timestamp of the revision). * * @param id * the id * @return the revision by time */ public InternalCDORevision getRevision(final CDOID id, final CDOBranchPoint branchPoint) { Connection connection = null; String sql = null; try { connection = getConnection(); AbstractQueryStatement<InternalCDORevision> statement = new AbstractQueryStatement<InternalCDORevision>() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("SELECT "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CDOREVISION); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(" FROM "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" WHERE "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_ID); builder.append("=? AND "); //$NON-NLS-1$ appendTimestampCondition(builder); return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws SQLException { long timeStamp = branchPoint.getTimeStamp(); preparedStatement.setString(1, id.toURIFragment()); preparedStatement.setLong(2, timeStamp); preparedStatement.setLong(3, timeStamp); } @Override protected InternalCDORevision getResult(ResultSet resultSet) throws Exception { long revised = resultSet.getLong(2); Blob blob = resultSet.getBlob(1); return toRevision(blob, revised); } }; sql = statement.getSQL(); return statement.query(connection); } catch (Exception e) { throw new DBException("Error while retrieving a revision by timestamp from the database", e, sql); //$NON-NLS-1$ } finally { DBUtil.close(connection); } } /** * Gets a {@link InternalCDORevision} by a given id and version. * * @param id * the id to match the revision against * @return the revision by version */ public InternalCDORevision getRevisionByVersion(final CDOID id, final CDOBranchVersion branchVersion) { Connection connection = null; String sql = null; try { connection = getConnection(); AbstractQueryStatement<InternalCDORevision> statement = new AbstractQueryStatement<InternalCDORevision>() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("SELECT "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CDOREVISION); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(" FROM "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" WHERE "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_ID); builder.append("=? AND "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_VERSION); builder.append("=?"); //$NON-NLS-1$ return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws SQLException { preparedStatement.setString(1, id.toURIFragment()); preparedStatement.setInt(2, branchVersion.getVersion()); } @Override protected InternalCDORevision getResult(ResultSet resultSet) throws Exception { long revised = resultSet.getLong(2); Blob blob = resultSet.getBlob(1); return toRevision(blob, revised); } }; sql = statement.getSQL(); return statement.query(connection); } catch (Exception e) { throw new DBException("Error while retrieving a revision by version from the database", e, sql); //$NON-NLS-1$ } finally { DBUtil.close(connection); } } /** * Gets the latest revisions of all persisted model versions. * * @return the revisions */ public List<CDORevision> getCurrentRevisions() { Connection connection = null; String sql = null; try { connection = getConnection(); AbstractQueryStatement<List<CDORevision>> query = new AbstractQueryStatement<List<CDORevision>>() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("SELECT "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CDOREVISION); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(" FROM "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" WHERE "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append("="); //$NON-NLS-1$ builder.append(CDORevision.UNSPECIFIED_DATE); return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws SQLException { } @Override protected List<CDORevision> getResult(ResultSet resultSet) throws Exception { final List<CDORevision> revisionList = new ArrayList<CDORevision>(); do { long revised = resultSet.getLong(2); Blob blob = resultSet.getBlob(1); revisionList.add(toRevision(blob, revised)); } while (resultSet.next()); return revisionList; } }; sql = query.getSQL(); return query.query(connection); } catch (Exception e) { throw new DBException("Error while retrieving a revision by version from the database", e, sql); //$NON-NLS-1$ } finally { DBUtil.close(connection); } } public void getAllRevisions(List<InternalCDORevision> result) { // TODO: implement DBRevisionCache.enclosing_method(enclosing_method_arguments) throw new UnsupportedOperationException(); } /** * Adds a given revision to this cache. It furthermore updates the revised timestamp of the latest (before inserting * the new one) revision * * @param revision * the revision to add to this cache */ public void addRevision(CDORevision revision) { CheckUtil.checkArg(revision, "revision"); Connection connection = null; String sql = null; try { connection = getConnection(); AbstractUpdateStatement update = createAddRevisionStatement((InternalCDORevision)revision); sql = update.getSQL(); update.update(connection); if (revision.getVersion() > CDORevision.FIRST_VERSION) { // Update former latest revision update = createUpdateRevisedStatement((InternalCDORevision)revision); update.update(connection); } } catch (DBException e) { throw e; } catch (Exception e) { throw new DBException("Error while retrieving the revision from the database", e, sql); //$NON-NLS-1$ } finally { DBUtil.close(connection); } } private AbstractUpdateStatement createUpdateRevisedStatement(final InternalCDORevision revision) { return new AbstractUpdateStatement() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("UPDATE "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" SET "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(" =? WHERE "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_ID); builder.append(" =? AND "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_VERSION); builder.append(" =?"); //$NON-NLS-1$ return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws SQLException { preparedStatement.setLong(1, revision.getTimeStamp() - 1); preparedStatement.setString(2, revision.getID().toURIFragment()); preparedStatement.setInt(3, revision.getVersion() - 1); } }; } private AbstractUpdateStatement createAddRevisionStatement(final InternalCDORevision revision) { return new AbstractUpdateStatement() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("INSERT INTO "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" ("); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_ID); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_VERSION); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CREATED); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CDOREVISION); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_RESOURCENODE_NAME); builder.append(", "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_CONTAINERID); builder.append(") "); //$NON-NLS-1$ builder.append(" VALUES (?, ?, ?, ?, ?, ? ,?)"); //$NON-NLS-1$ return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws Exception { preparedStatement.setString(1, revision.getID().toURIFragment()); preparedStatement.setInt(2, revision.getVersion()); preparedStatement.setLong(3, revision.getTimeStamp()); preparedStatement.setLong(4, revision.getRevised()); preparedStatement.setBytes(5, toBytes(revision)); setResourceNodeValues(revision, preparedStatement); } /** * Sets the values in the prepared statment, that are related to the given revision. If the revision is a resource * node, the values are set otherwise the fields are set to <tt>null</tt> * * @param revision * the revision * @param preparedStatement * the prepared statement * @throws SQLException * the SQL exception */ private void setResourceNodeValues(InternalCDORevision revision, PreparedStatement preparedStatement) throws SQLException { if (revision.isResourceNode()) { preparedStatement.setString(6, DBRevisionCacheUtil.getResourceNodeName(revision)); CDOID containerID = (CDOID)revision.getContainerID(); preparedStatement.setString(7, containerID.toURIFragment()); } else { preparedStatement.setNull(6, Types.VARCHAR); preparedStatement.setNull(7, Types.INTEGER); } } }; } /** * Removes a revision by its Id and version. If the given revision does not exist <tt>null</tt> is returned. Otherwise * the {@link InternalCDORevision}, that was removed is returned * * @param id * the id of the revision to remove * @return the {@link InternalCDORevision} that was removed, <tt>null</tt> otherwise */ public InternalCDORevision removeRevision(CDOID id, CDOBranchVersion branchVersion) { Connection connection = null; String sql = null; try { final InternalCDORevision revision = getRevisionByVersion(id, branchVersion); if (revision != null) { connection = getConnection(); AbstractUpdateStatement statement = new AbstractUpdateStatement() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("DELETE FROM "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); builder.append(" WHERE ID =? AND VERSION =?"); //$NON-NLS-1$ return builder.toString(); } @Override protected void setParameters(PreparedStatement preparedStatement) throws Exception { preparedStatement.setString(1, revision.getID().toURIFragment()); preparedStatement.setInt(2, revision.getVersion()); } }; sql = statement.getSQL(); statement.update(connection); } return revision; } catch (Exception e) { throw new DBException("Error while removing a revision from the database", e, sql); //$NON-NLS-1$ } finally { DBUtil.close(connection); } } /** * Removes all revisions from this cache (and its database). */ public void clear() { Connection connection = null; String sql = null; try { connection = getConnection(); AbstractUpdateStatement update = new AbstractUpdateStatement() { @Override public String getSQL() { StringBuilder builder = new StringBuilder(); builder.append("DELETE FROM "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS); return builder.toString(); } }; sql = update.getSQL(); update.update(connection); } catch (Exception e) { throw new DBException("Error while clearing the database", e, sql); } finally { DBUtil.close(connection); } } public Map<CDOBranch, List<CDORevision>> getAllRevisions() { throw new UnsupportedOperationException(); } public List<CDORevision> getRevisions(CDOBranchPoint branchPoint) { throw new UnsupportedOperationException(); } @Override protected void doBeforeActivate() throws Exception { super.doBeforeActivate(); checkState(idProvider, "idProvider"); //$NON-NLS-1$ checkState(listFactory, "listFactory");//$NON-NLS-1$ checkState(packageRegistry, "packageRegistry"); //$NON-NLS-1$ checkState(revisionFactory, "revisionFactory"); //$NON-NLS-1$ checkState(dbAdapter, "dbAdapter"); //$NON-NLS-1$ checkState(dbConnectionProvider, "dbConnectionProvider"); //$NON-NLS-1$ } @Override protected void doActivate() throws Exception { super.doActivate(); createTable(); } /** * Creates the (single) table that's used to store the cached revisions. * * @throws SQLException * Signals that an error has occured while getting the connection or committing the transaction */ private void createTable() throws SQLException { Connection connection = null; try { connection = getConnection(); DBRevisionCacheSchema.INSTANCE.create(dbAdapter, connection); connection.commit(); } finally { DBUtil.close(connection); } } private static StringBuilder appendTimestampCondition(StringBuilder builder) { builder.append(DBRevisionCacheSchema.REVISIONS_CREATED); builder.append("<=? AND ("); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append(">=? OR "); //$NON-NLS-1$ builder.append(DBRevisionCacheSchema.REVISIONS_REVISED); builder.append("="); //$NON-NLS-1$ builder.append(CDORevision.UNSPECIFIED_DATE); builder.append(")"); //$NON-NLS-1$ return builder; } /** * Converts the given Objects to an {@link InternalCDORevision}. The object is deserialized to an instance of the * correct type and the revised timestamp is set separatley. Whe you insert a new revision into this cache, the former * latest revision gets a new revised timestamp. This timestamp's only updated in the database column 'revised', not * in the blob that holds the serialized instance. Therefore the revised timestamp has to be set separately * * @param revisedTimestamp * the revised timestamp to set to the revision * @param blob * the blob that holds the revision * @return the revision * @throws IOException * Signals that an error has occurred while reading the revision from the blob. * @throws SQLException * Signals that an error hass occured while getting the binary stream from the blob */ private InternalCDORevision toRevision(Blob blob, long revisedTimestamp) throws IOException, SQLException { CDODataInput dataInput = getCDODataInput(ExtendedDataInputStream.wrap(blob.getBinaryStream())); InternalCDORevision revision = (InternalCDORevision)dataInput.readCDORevision(); // Revised timestamp's updated in the revised column only (not in the blob) revision.setRevised(revisedTimestamp); return revision; } private CDODataInput getCDODataInput(ExtendedDataInputStream inputStream) throws IOException { return new CDODataInputImpl(inputStream) { public CDOPackageRegistry getPackageRegistry() { return packageRegistry; } @Override protected CDOBranchManager getBranchManager() { return null; } @Override protected CDOCommitInfoManager getCommitInfoManager() { return null; } @Override protected CDORevisionFactory getRevisionFactory() { return revisionFactory; } @Override protected CDOListFactory getListFactory() { return listFactory; } @Override protected CDOLobStore getLobStore() { return null; } }; } /** * Converts a given {@link CDORevision} to a byte array. * * @param revision * the revision * @return the array of bytes for the given revision * @throws IOException * Signals an error has occurred while writing the revision to the byte array. */ private byte[] toBytes(InternalCDORevision revision) throws IOException { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); CDODataOutput dataOutput = getCDODataOutput(ExtendedDataOutputStream.wrap(byteArrayOutputStream)); dataOutput.writeCDORevision(revision, CDORevision.UNCHUNKED); return byteArrayOutputStream.toByteArray(); } private CDODataOutput getCDODataOutput(ExtendedDataOutput extendedDataOutputStream) { return new CDODataOutputImpl(extendedDataOutputStream) { @Override public CDOPackageRegistry getPackageRegistry() { return packageRegistry; } @Override public CDOIDProvider getIDProvider() { return idProvider; } }; } /** * Gets a connection from the {@link IDBConnectionProvider} within this cache. The Connection is set not to auto * commit transactions. * * @return the connection * @throws SQLException * Signals that an error occured while getting the connection from the connection provider */ private Connection getConnection() throws SQLException { Connection connection = dbConnectionProvider.getConnection(); connection.setAutoCommit(false); return connection; } }