| /* |
| * Copyright (c) 2009-2014 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: |
| * Eike Stepper - initial API and implementation |
| * Stefan Winkler - 271444: [DB] Multiple refactorings bug 271444 |
| * Stefan Winkler - Bug 329025: [DB] Support branching for range-based mapping strategy |
| */ |
| package org.eclipse.emf.cdo.server.internal.db.mapping.horizontal; |
| |
| import org.eclipse.emf.cdo.common.branch.CDOBranch; |
| import org.eclipse.emf.cdo.common.id.CDOID; |
| import org.eclipse.emf.cdo.common.revision.CDORevision; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOAddFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOClearFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOContainerFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOFeatureDeltaVisitor; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOListFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOMoveFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDORemoveFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOSetFeatureDelta; |
| import org.eclipse.emf.cdo.common.revision.delta.CDOUnsetFeatureDelta; |
| import org.eclipse.emf.cdo.server.db.IDBStoreAccessor; |
| import org.eclipse.emf.cdo.server.db.IIDHandler; |
| import org.eclipse.emf.cdo.server.db.mapping.IListMappingDeltaSupport; |
| import org.eclipse.emf.cdo.server.db.mapping.IMappingStrategy; |
| import org.eclipse.emf.cdo.server.internal.db.bundle.OM; |
| |
| import org.eclipse.net4j.db.DBException; |
| import org.eclipse.net4j.db.DBUtil; |
| import org.eclipse.net4j.db.IDBPreparedStatement; |
| import org.eclipse.net4j.db.IDBPreparedStatement.ReuseProbability; |
| import org.eclipse.net4j.db.ddl.IDBTable; |
| import org.eclipse.net4j.util.om.trace.ContextTracer; |
| |
| import org.eclipse.emf.ecore.EClass; |
| import org.eclipse.emf.ecore.EStructuralFeature; |
| |
| import org.eclipse.core.runtime.Assert; |
| |
| import java.sql.PreparedStatement; |
| import java.sql.ResultSet; |
| import java.sql.SQLException; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.ListIterator; |
| |
| /** |
| * This is a list-to-table mapping optimized for non-audit-mode. It doesn't care about version and has delta support. |
| * |
| * @author Eike Stepper |
| * @since 2.0 |
| */ |
| public class NonAuditListTableMapping extends AbstractListTableMapping implements IListMappingDeltaSupport |
| { |
| private static final ContextTracer TRACER = new ContextTracer(OM.DEBUG, NonAuditListTableMapping.class); |
| |
| private static final int UNBOUNDED_SHIFT = -1; |
| |
| private String sqlClear; |
| |
| private String sqlUpdateValue; |
| |
| private String sqlUpdateIndex; |
| |
| private String sqlInsertValue; |
| |
| private String sqlDeleteItem; |
| |
| private String sqlShiftDownIndex; |
| |
| private String sqlReadCurrentIndexOffset; |
| |
| private String sqlShiftUpIndex; |
| |
| public NonAuditListTableMapping(IMappingStrategy mappingStrategy, EClass eClass, EStructuralFeature feature) |
| { |
| super(mappingStrategy, eClass, feature); |
| initSQLStrings(); |
| } |
| |
| private void initSQLStrings() |
| { |
| IDBTable table = getTable(); |
| |
| // ----------- clear list ------------------------- |
| StringBuilder builder = new StringBuilder(); |
| builder.append("DELETE FROM "); //$NON-NLS-1$ |
| builder.append(table); |
| builder.append(" WHERE "); //$NON-NLS-1$ |
| builder.append(LIST_REVISION_ID); |
| builder.append("=?"); //$NON-NLS-1$ |
| sqlClear = builder.toString(); |
| |
| builder.append(" AND "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append("=?"); //$NON-NLS-1$ |
| sqlDeleteItem = builder.toString(); |
| |
| // ----------- update one item -------------------- |
| builder = new StringBuilder(); |
| builder.append("UPDATE "); //$NON-NLS-1$ |
| builder.append(getTable()); |
| builder.append(" SET "); //$NON-NLS-1$ |
| builder.append(LIST_VALUE); |
| builder.append("=? "); //$NON-NLS-1$ |
| builder.append(" WHERE "); //$NON-NLS-1$ |
| builder.append(LIST_REVISION_ID); |
| builder.append("=? AND "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append("=?"); //$NON-NLS-1$ |
| sqlUpdateValue = builder.toString(); |
| |
| // ----------- insert one item -------------------- |
| builder = new StringBuilder(); |
| builder.append("INSERT INTO "); //$NON-NLS-1$ |
| builder.append(getTable()); |
| builder.append(" ("); //$NON-NLS-1$ |
| builder.append(LIST_REVISION_ID); |
| builder.append(", "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append(", "); //$NON-NLS-1$ |
| builder.append(LIST_VALUE); |
| builder.append(") VALUES(?, ?, ?)"); //$NON-NLS-1$ |
| sqlInsertValue = builder.toString(); |
| |
| // ----------- update one item index -------------- |
| builder = new StringBuilder(); |
| builder.append("UPDATE "); //$NON-NLS-1$ |
| builder.append(getTable()); |
| builder.append(" SET "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append("=? "); //$NON-NLS-1$ |
| builder.append(" WHERE "); //$NON-NLS-1$ |
| builder.append(LIST_REVISION_ID); |
| builder.append("=? AND "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append("=?"); //$NON-NLS-1$ |
| sqlUpdateIndex = builder.toString(); |
| |
| // ----------- mass update item indexes -------------- |
| builder = new StringBuilder(); |
| builder.append("UPDATE "); //$NON-NLS-1$ |
| builder.append(getTable()); |
| builder.append(" SET "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append("="); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append("+? WHERE "); //$NON-NLS-1$ |
| builder.append(LIST_REVISION_ID); |
| builder.append("=? AND "); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append(" BETWEEN ? AND ?"); //$NON-NLS-1$ |
| |
| // needed because of MySQL: |
| builder.append(" /*! ORDER BY "); //$NON-NLS-1$ / |
| builder.append(LIST_IDX); |
| sqlShiftDownIndex = builder.toString() + " */"; //$NON-NLS-1$ |
| |
| builder.append(" DESC"); //$NON-NLS-1$ |
| sqlShiftUpIndex = builder.toString() + " */"; //$NON-NLS-1$ |
| |
| // ----------- read current index offset -------------- |
| builder = new StringBuilder(); |
| builder.append("SELECT MIN("); //$NON-NLS-1$ |
| builder.append(LIST_IDX); |
| builder.append(") FROM "); //$NON-NLS-1$ |
| builder.append(getTable()); |
| builder.append(" WHERE "); //$NON-NLS-1$ |
| builder.append(LIST_REVISION_ID); |
| builder.append("=?"); //$NON-NLS-1$ |
| sqlReadCurrentIndexOffset = builder.toString(); |
| } |
| |
| @Override |
| public void addSimpleChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int index) |
| { |
| int offset = getCurrentIndexOffset(accessor, cdoid); |
| super.addSimpleChunkWhere(accessor, cdoid, builder, index + offset); |
| } |
| |
| @Override |
| public void addRangedChunkWhere(IDBStoreAccessor accessor, CDOID cdoid, StringBuilder builder, int fromIndex, |
| int toIndex) |
| { |
| int offset = getCurrentIndexOffset(accessor, cdoid); |
| super.addRangedChunkWhere(accessor, cdoid, builder, fromIndex + offset, toIndex + offset); |
| } |
| |
| @Override |
| protected void addKeyFields(List<FieldInfo> list) |
| { |
| // Do nothing |
| } |
| |
| @Override |
| protected void setKeyFields(PreparedStatement stmt, CDORevision revision) throws SQLException |
| { |
| IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); |
| idHandler.setCDOID(stmt, 1, revision.getID()); |
| } |
| |
| public void objectDetached(IDBStoreAccessor accessor, CDOID id, long revised) |
| { |
| clearList(accessor, id); |
| } |
| |
| /** |
| * Clear a list of a given revision. |
| * |
| * @param accessor |
| * the accessor to use |
| * @param id |
| * the id of the revision from which to remove all items |
| */ |
| public void clearList(IDBStoreAccessor accessor, CDOID id) |
| { |
| IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); |
| IDBPreparedStatement stmt = accessor.getDBConnection().prepareStatement(sqlClear, ReuseProbability.HIGH); |
| |
| try |
| { |
| idHandler.setCDOID(stmt, 1, id); |
| DBUtil.update(stmt, false); |
| } |
| catch (SQLException e) |
| { |
| throw new DBException(e); |
| } |
| finally |
| { |
| DBUtil.close(stmt); |
| } |
| } |
| |
| @Override |
| public void rawDeleted(IDBStoreAccessor accessor, CDOID id, CDOBranch branch, int version) |
| { |
| clearList(accessor, id); |
| } |
| |
| public int getCurrentIndexOffset(IDBStoreAccessor accessor, CDOID id) |
| { |
| IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); |
| IDBPreparedStatement stmt = accessor.getDBConnection().prepareStatement(sqlReadCurrentIndexOffset, |
| ReuseProbability.HIGH); |
| ResultSet rset = null; |
| |
| try |
| { |
| idHandler.setCDOID(stmt, 1, id); |
| rset = stmt.executeQuery(); |
| if (!rset.next()) |
| { |
| // list is empty. Return the default offset of 0. |
| return 0; |
| } |
| |
| // return the minimum index which is equal to the current offset. |
| return rset.getInt(1); |
| } |
| catch (SQLException e) |
| { |
| throw new DBException(e); |
| } |
| finally |
| { |
| DBUtil.close(rset); |
| close(stmt); |
| } |
| } |
| |
| public void processDelta(final IDBStoreAccessor accessor, final CDOID id, int branchId, int oldVersion, |
| final int newVersion, long created, CDOListFeatureDelta delta) |
| { |
| int oldListSize = delta.getOriginSize(); |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format("ListTableMapping.processDelta for object {0} - original list size: {1}", id, //$NON-NLS-1$ |
| oldListSize); |
| } |
| |
| // let the visitor collect the changes |
| ListDeltaVisitor visitor = new ListDeltaVisitor(oldListSize); |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.trace("Processing deltas..."); //$NON-NLS-1$ |
| } |
| |
| for (CDOFeatureDelta listDelta : delta.getListChanges()) |
| { |
| listDelta.accept(visitor); |
| } |
| |
| visitor.postProcess(accessor, id); |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.trace("Result to be written to DB:"); |
| for (ManipulationElement e : visitor.manipulations) |
| { |
| TRACER.trace(e.toString()); |
| } |
| } |
| |
| // finally, write results to the database |
| visitor.writeResultToDatabase(accessor, id); |
| |
| throw new NewListSizeResult(visitor.getNewListSize()); |
| } |
| |
| private void close(PreparedStatement... stmts) |
| { |
| Throwable t = null; |
| |
| for (PreparedStatement stmt : stmts) |
| { |
| try |
| { |
| if (stmt != null) |
| { |
| try |
| { |
| stmt.clearBatch(); |
| } |
| catch (SQLException e) |
| { |
| throw new DBException(e); |
| } |
| finally |
| { |
| DBUtil.close(stmt); |
| } |
| } |
| } |
| catch (Throwable th) |
| { |
| if (t == null) |
| { |
| // remember first exception |
| t = th; |
| } |
| |
| // more exceptions go to the log |
| OM.LOG.error(t); |
| } |
| } |
| |
| if (t != null) |
| { |
| throw new DBException(t); |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| static final class NewListSizeResult extends RuntimeException |
| { |
| private static final long serialVersionUID = 1L; |
| |
| private final int newListSize; |
| |
| public NewListSizeResult(int newListSize) |
| { |
| this.newListSize = newListSize; |
| } |
| |
| public int getNewListSize() |
| { |
| return newListSize; |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private final class ListDeltaVisitor implements CDOFeatureDeltaVisitor |
| { |
| private boolean clearFirst; |
| |
| private ArrayList<ManipulationElement> manipulations; |
| |
| /** |
| * Start of a range [tempIndex, tempIndex-1, ...] which lies outside of the normal list indexes and which serve as |
| * temporary space to move items temporarily to get them out of the way of other operations. |
| */ |
| private int tempIndex = -1; |
| |
| private int newListSize; |
| |
| public ListDeltaVisitor(int oldListSize) |
| { |
| // reset the clear-flag |
| clearFirst = false; |
| manipulations = new ArrayList<ManipulationElement>(oldListSize); |
| |
| // create list and initialize with original indexes |
| for (int i = 0; i < oldListSize; i++) |
| { |
| manipulations.add(ManipulationElement.createOriginalElement(i)); |
| } |
| |
| newListSize = oldListSize; |
| } |
| |
| public int getNewListSize() |
| { |
| return newListSize; |
| } |
| |
| public void visit(CDOAddFeatureDelta delta) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - insert at {0} value {1}", delta.getIndex(), delta.getValue()); //$NON-NLS-1$ |
| } |
| |
| // make room for the new item |
| shiftIndexes(delta.getIndex(), UNBOUNDED_SHIFT, +1); |
| |
| // create the item |
| manipulations.add(ManipulationElement.createInsertedElement(delta.getIndex(), delta.getValue())); |
| ++newListSize; |
| } |
| |
| public void visit(CDORemoveFeatureDelta delta) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - remove at {0}", delta.getIndex()); //$NON-NLS-1$ |
| } |
| |
| ManipulationElement e = findElement(delta.getIndex()); |
| deleteItem(e); |
| |
| // fill the gap by shifting all subsequent items down |
| shiftIndexes(delta.getIndex() + 1, UNBOUNDED_SHIFT, -1); |
| --newListSize; |
| } |
| |
| public void visit(CDOSetFeatureDelta delta) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - set at {0} value {1}", delta.getIndex(), delta.getValue()); //$NON-NLS-1$ |
| } |
| |
| ManipulationElement e = findElement(delta.getIndex()); |
| // set the new value |
| e.value = delta.getValue(); |
| |
| // if the item is freshly inserted we do not set the SET-mark. |
| // setting the value of a new item results in inserting with the |
| // new value at once. |
| if (!e.is(ManipulationConstants.INSERT)) |
| { |
| // else mark the existing item to be set to a new value |
| e.addType(ManipulationConstants.SET_VALUE); |
| } |
| } |
| |
| public void visit(CDOUnsetFeatureDelta delta) |
| { |
| if (delta.getFeature().isUnsettable()) |
| { |
| Assert.isTrue(false); |
| } |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - unset list"); //$NON-NLS-1$ |
| } |
| |
| // set the clear-flag |
| clearFirst = true; |
| |
| // and also clear all manipulation items |
| manipulations.clear(); |
| newListSize = 0; |
| } |
| |
| public void visit(CDOClearFeatureDelta delta) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - clear list"); //$NON-NLS-1$ |
| } |
| |
| // set the clear-flag |
| clearFirst = true; |
| |
| // and also clear all manipulation items |
| manipulations.clear(); |
| newListSize = 0; |
| } |
| |
| public void visit(CDOMoveFeatureDelta delta) |
| { |
| int fromIdx = delta.getOldPosition(); |
| int toIdx = delta.getNewPosition(); |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - move {0} -> {1}", fromIdx, toIdx); //$NON-NLS-1$ |
| } |
| |
| // ignore the trivial case |
| if (fromIdx == toIdx) |
| { |
| return; |
| } |
| |
| ManipulationElement e = findElement(fromIdx); |
| |
| // adjust indexes and shift either up or down |
| if (fromIdx < toIdx) |
| { |
| shiftIndexes(fromIdx + 1, toIdx, -1); |
| } |
| else |
| { // fromIdx > toIdx here |
| shiftIndexes(toIdx, fromIdx - 1, +1); |
| } |
| |
| // set the new index |
| e.destinationIndex = toIdx; |
| |
| // if it is a new element, no MOVE mark needed, because we insert it |
| // at the new position |
| if (!e.is(ManipulationConstants.INSERT)) |
| { |
| // else we need to handle the move of an existing item |
| e.addType(ManipulationConstants.MOVE); |
| } |
| } |
| |
| public void visit(CDOListFeatureDelta delta) |
| { |
| // never called |
| Assert.isTrue(false); |
| } |
| |
| public void visit(CDOContainerFeatureDelta delta) |
| { |
| // never called |
| Assert.isTrue(false); |
| } |
| |
| /** |
| * Helper method: shift all (destination) indexes in the interval [from,to] (inclusive at both ends) by offset |
| * (positive or negative). |
| */ |
| private void shiftIndexes(int from, int to, int offset) |
| { |
| for (ManipulationElement e : manipulations) |
| { |
| if (e.destinationIndex >= from && (to == UNBOUNDED_SHIFT || e.destinationIndex <= to)) |
| { |
| e.destinationIndex += offset; |
| } |
| } |
| } |
| |
| /** |
| * Find a manipulation item by destination index). |
| */ |
| private ManipulationElement findElement(int index) |
| { |
| for (ManipulationElement e : manipulations) |
| { |
| if (e.destinationIndex == index) |
| { |
| return e; |
| } |
| } |
| |
| // never reached |
| Assert.isTrue(false); |
| return null; |
| } |
| |
| /** |
| * Delete an element (used in remove and clear) |
| */ |
| private void deleteItem(ManipulationElement e) |
| { |
| if (e.is(ManipulationConstants.INSERT)) |
| { |
| // newly inserted items are simply removed, as |
| // removing inserted items is equal to no change at all. |
| manipulations.remove(e); |
| } |
| else |
| { |
| // mark the existing item as to be deleted. |
| // (previous MOVE and SET conditions are overridden by setting |
| // the exclusive DELETE type). |
| e.type = ManipulationConstants.DELETE; |
| e.destinationIndex = ManipulationConstants.NO_INDEX; |
| } |
| } |
| |
| /** |
| * Called after all deltas are applied an before the results are written to the database. This method post-processes |
| * the manipulation elements in order to minimize database access. |
| */ |
| public void postProcess(IDBStoreAccessor accessor, CDOID id) |
| { |
| if (!((HorizontalNonAuditMappingStrategy)getMappingStrategy()).shallForceZeroBasedIndex()) |
| { |
| /* |
| * this is an optimization which reduces the amount of modifications on the database to maintain list indexes. |
| * For the optimization, we let go of the assumption that indexes are zero-based. Instead, we work with an |
| * offset at the database level which can change with every change to the list (e.g. if the second element is |
| * removed from a list with 1000 elements, instead of shifting down indexes 2 to 1000 by 1, we shift up index 0 |
| * by 1 and have now a list with indexes starting at 1 instead of 0. This optimization is applied by modifying |
| * the list of ManipulationElements, which can be seen as the database modification plan. |
| */ |
| |
| // first, get the current offset |
| int offsetBefore = getCurrentIndexOffset(accessor, id); |
| if (TRACER.isEnabled()) |
| { |
| TRACER.trace("Offset optimization."); //$NON-NLS-1$ |
| TRACER.trace("Current offset = " + offsetBefore); //$NON-NLS-1$ |
| } |
| |
| applyOffsetToSourceIndexes(offsetBefore); |
| |
| int offsetAfter; |
| |
| if ((long)Math.abs(offsetBefore) + (long)manipulations.size() > Integer.MAX_VALUE) |
| { |
| // security belt for really huge collections or for collections that have been manipulated lots of times |
| // -> do not optimize after this border is crossed. Instead, reset offset for the whole list to a zero-based |
| // index. |
| offsetAfter = 0; |
| } |
| else |
| { |
| offsetAfter = calculateOptimalOffset(); |
| } |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.trace("New offset = " + offsetAfter); //$NON-NLS-1$ |
| } |
| |
| applyOffsetToDestinationIndexes(offsetAfter); |
| |
| // make sure temporary indexes do not get in the way of the other operations |
| tempIndex = Math.min(offsetBefore, offsetAfter) - 1; |
| } |
| } |
| |
| /** |
| * Calculate the optimal offset wrt the manipulations planned. The optimal offset is the offset which occurs the |
| * most in the manipulations (because letting this offset be neutral leads to the least manipulations. Note: the |
| * zero offset is also regarded as an offset as any other, because selecting an offset != 0 would also lead to |
| * elements with original offset 0 to be moved. |
| */ |
| private int calculateOptimalOffset() |
| { |
| HashMap<Integer, Integer> occurrences = new HashMap<Integer, Integer>(); |
| int bestOffset = 0; |
| int bestOffsetOccurrence = 0; |
| |
| for (ManipulationElement element : manipulations) |
| { |
| int srcIdx = element.sourceIndex; |
| int destIdx = element.destinationIndex; |
| if (srcIdx != ManipulationConstants.NO_INDEX && destIdx != ManipulationConstants.NO_INDEX) |
| { |
| int offset = destIdx - srcIdx; |
| Integer oldOccurrence = occurrences.get(offset); |
| int newOccurrence; |
| if (oldOccurrence == null) |
| { |
| newOccurrence = 1; |
| } |
| else |
| { |
| newOccurrence = oldOccurrence + 1; |
| } |
| occurrences.put(offset, newOccurrence); |
| |
| // remember maximum along the way |
| if (newOccurrence > bestOffsetOccurrence) |
| { |
| bestOffsetOccurrence = newOccurrence; |
| bestOffset = offset; |
| } |
| } |
| } |
| |
| // the offset which has occurred the most has to be applied negatively to normalize the list |
| // therefore return the negative offset as the new offset to be applied |
| return -bestOffset; |
| } |
| |
| private void applyOffsetToSourceIndexes(int offsetBefore) |
| { |
| if (offsetBefore != 0) |
| { |
| for (ManipulationElement element : manipulations) |
| { |
| if (element.sourceIndex != ManipulationConstants.NO_INDEX) |
| { |
| element.sourceIndex += offsetBefore; |
| } |
| } |
| } |
| } |
| |
| private void applyOffsetToDestinationIndexes(int offsetAfter) |
| { |
| if (offsetAfter != 0) |
| { |
| for (ManipulationElement element : manipulations) |
| { |
| if (element.destinationIndex != ManipulationConstants.NO_INDEX) |
| { |
| // apply the offset to all indices to make them relative to the new offset |
| element.destinationIndex += offsetAfter; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Write calculated changes to the database |
| * |
| * @param accessor |
| */ |
| private void writeResultToDatabase(IDBStoreAccessor accessor, CDOID id) |
| { |
| IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); |
| IDBPreparedStatement deleteStmt = null; |
| IDBPreparedStatement moveStmt = null; |
| IDBPreparedStatement setValueStmt = null; |
| IDBPreparedStatement insertStmt = null; |
| |
| int deleteCounter = 0; |
| int moveCounter = 0; |
| int setValueCounter = 0; |
| int insertCounter = 0; |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.trace("Writing to database:"); //$NON-NLS-1$ |
| } |
| |
| if (clearFirst) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.trace(" - clear list"); //$NON-NLS-1$ |
| } |
| |
| clearList(accessor, id); |
| } |
| |
| try |
| { |
| for (ManipulationElement element : manipulations) |
| { |
| if (element.is(ManipulationConstants.DELETE)) |
| { |
| /* |
| * Step 1: DELETE all elements e which have e.is(REMOVE) by e.sourceIdx |
| */ |
| |
| if (deleteStmt == null) |
| { |
| deleteStmt = accessor.getDBConnection().prepareStatement(sqlDeleteItem, ReuseProbability.HIGH); |
| idHandler.setCDOID(deleteStmt, 1, id); |
| } |
| |
| deleteStmt.setInt(2, element.sourceIndex); |
| deleteStmt.addBatch(); |
| deleteCounter++; |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - delete at {0} ", element.sourceIndex); //$NON-NLS-1$ |
| } |
| } |
| |
| if (element.is(ManipulationConstants.MOVE)) |
| { |
| /* |
| * Step 2: MOVE all elements e (by e.sourceIdx) which have e.is(MOVE) to temporary idx (-1, -2, -3, -4, ...) |
| * and store temporary idx in e.tempIndex |
| */ |
| if (moveStmt == null) |
| { |
| moveStmt = accessor.getDBConnection().prepareStatement(sqlUpdateIndex, ReuseProbability.HIGH); |
| idHandler.setCDOID(moveStmt, 2, id); |
| } |
| |
| moveStmt.setInt(3, element.sourceIndex); // from index |
| moveStmt.setInt(1, --tempIndex); // to index |
| element.tempIndex = tempIndex; |
| moveStmt.addBatch(); |
| moveCounter++; |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - move {0} -> {1} ", element.sourceIndex, element.tempIndex); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| /* now perform deletes and moves ... */ |
| if (deleteCounter > 0) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format("Performing {0} delete operations", deleteCounter); //$NON-NLS-1$ |
| } |
| |
| DBUtil.executeBatch(deleteStmt, deleteCounter); |
| } |
| |
| if (moveCounter > 0) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format("Performing {0} move operations", moveCounter); //$NON-NLS-1$ |
| } |
| |
| DBUtil.executeBatch(moveStmt, moveCounter); |
| moveStmt.clearBatch(); |
| moveCounter = 0; |
| } |
| |
| writeShiftOperations(accessor, id); |
| |
| for (ManipulationElement element : manipulations) |
| { |
| if (element.is(ManipulationConstants.MOVE)) |
| { |
| /* |
| * Step 4: MOVE all elements e have e.is(MOVE) from e.tempIdx to e.destinationIdx (because we have moved |
| * them before, moveStmt is always initialized |
| */ |
| moveStmt.setInt(3, element.tempIndex); // from index |
| moveStmt.setInt(1, element.destinationIndex); // to index |
| moveStmt.addBatch(); |
| moveCounter++; |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - move {0} -> {1} ", element.tempIndex, element.destinationIndex); //$NON-NLS-1$ |
| } |
| } |
| |
| if (element.is(ManipulationConstants.SET_VALUE)) |
| { |
| /* |
| * Step 5: SET all elements which have e.type == SET_VALUE by index == e.destinationIdx |
| */ |
| if (setValueStmt == null) |
| { |
| setValueStmt = accessor.getDBConnection().prepareStatement(sqlUpdateValue, ReuseProbability.HIGH); |
| idHandler.setCDOID(setValueStmt, 2, id); |
| } |
| |
| setValueStmt.setInt(3, element.destinationIndex); |
| getTypeMapping().setValue(setValueStmt, 1, element.value); |
| setValueStmt.addBatch(); |
| setValueCounter++; |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - set value at {0} to {1} ", element.destinationIndex, element.value); //$NON-NLS-1$ |
| } |
| } |
| |
| if (element.is(ManipulationConstants.INSERT)) |
| { |
| /* |
| * Step 6: INSERT all elements which have e.type == INSERT. |
| */ |
| if (insertStmt == null) |
| { |
| insertStmt = accessor.getDBConnection().prepareStatement(sqlInsertValue, ReuseProbability.HIGH); |
| idHandler.setCDOID(insertStmt, 1, id); |
| } |
| |
| insertStmt.setInt(2, element.destinationIndex); |
| getTypeMapping().setValue(insertStmt, 3, element.value); |
| insertStmt.addBatch(); |
| insertCounter++; |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - insert value at {0} : value {1} ", element.destinationIndex, element.value); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| if (moveCounter > 0) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format("Performing {0} move operations", moveCounter); //$NON-NLS-1$ |
| } |
| |
| DBUtil.executeBatch(moveStmt, moveCounter); |
| } |
| |
| if (insertCounter > 0) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format("Performing {0} insert operations", insertCounter); //$NON-NLS-1$ |
| } |
| |
| DBUtil.executeBatch(insertStmt, insertCounter); |
| } |
| |
| if (setValueCounter > 0) |
| { |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format("Performing {0} set operations", setValueCounter); //$NON-NLS-1$ |
| } |
| |
| DBUtil.executeBatch(setValueStmt, setValueCounter); |
| } |
| } |
| catch (SQLException e) |
| { |
| throw new DBException(e); |
| } |
| finally |
| { |
| close(deleteStmt, moveStmt, insertStmt, setValueStmt); |
| } |
| } |
| |
| /** |
| * Perform the shift operations to adjust indexes resulting from remove, insert, and move operations. |
| * |
| * @see #writeResultToDatabase(IDBStoreAccessor, CDOID) |
| * @throws SQLException |
| */ |
| private void writeShiftOperations(IDBStoreAccessor accessor, CDOID id) throws SQLException |
| { |
| /* |
| * Step 3: shift all elements which have to be shifted up or down because of add, remove or move of other elements |
| * to their proper position. This has to be done in two phases to avoid collisions, as the index has to be unique |
| * and shift up operations have to be executed in top to bottom order. |
| */ |
| IIDHandler idHandler = getMappingStrategy().getStore().getIDHandler(); |
| int size = manipulations.size(); |
| |
| LinkedList<ShiftOperation> shiftOperations = new LinkedList<ShiftOperation>(); |
| |
| /* |
| * If a necessary shift is detected (source and destination indices differ), firstIndex is set to the current |
| * index and currentOffset is set to the offset of the shift operation. When a new offset is detected or the range |
| * is interrupted, we record the range and start a new one if needed. |
| */ |
| int rangeStartIndex = ManipulationConstants.NO_INDEX; |
| int rangeOffset = 0; |
| int lastElementIndex = ManipulationConstants.NO_INDEX; |
| |
| // iterate through the manipulationElements and collect the necessary operations |
| for (int i = 0; i < size; i++) |
| { |
| ManipulationElement element = manipulations.get(i); |
| |
| /* |
| * shift applies only to elements which are not moved, inserted or deleted (i.e. only plain SET_VALUE and NONE |
| * are affected) |
| */ |
| if (element.type == ManipulationConstants.NONE || element.type == ManipulationConstants.SET_VALUE) |
| { |
| int elementOffset = element.destinationIndex - element.sourceIndex; |
| |
| /* |
| * first make sure if we have to close a previous range. This is the case, if the current element's offset |
| * differs from the rangeOffset and a range is open. |
| */ |
| if (elementOffset != rangeOffset && rangeStartIndex != ManipulationConstants.NO_INDEX) |
| { |
| // there is an open range but the rangeOffset differs. We have to close the open range |
| shiftOperations.add(new ShiftOperation(rangeStartIndex, lastElementIndex, rangeOffset)); |
| // and reset the state |
| rangeStartIndex = ManipulationConstants.NO_INDEX; |
| rangeOffset = 0; |
| } |
| |
| /* |
| * at this point, either a range is open, which means that the current element also fits in the range (i.e. |
| * the offsets match) or no range is open. In the latter case, we have to open one if the current element's |
| * offset is not 0. |
| */ |
| if (elementOffset != 0 && rangeStartIndex == ManipulationConstants.NO_INDEX) |
| { |
| rangeStartIndex = element.sourceIndex; |
| rangeOffset = elementOffset; |
| } |
| } |
| else |
| { // shift does not apply to this element because of its type |
| if (rangeStartIndex != ManipulationConstants.NO_INDEX) |
| { |
| // if there is an open range, we have to close and remember it |
| shiftOperations.add(new ShiftOperation(rangeStartIndex, lastElementIndex, rangeOffset)); |
| // and reset the state |
| rangeStartIndex = ManipulationConstants.NO_INDEX; |
| rangeOffset = 0; |
| } |
| } |
| lastElementIndex = element.sourceIndex; |
| } |
| |
| // after the iteration, we have to make sure that we remember the last open range, if it is there |
| if (rangeStartIndex != ManipulationConstants.NO_INDEX) |
| { |
| shiftOperations.add(new ShiftOperation(rangeStartIndex, lastElementIndex, rangeOffset)); |
| } |
| |
| /* |
| * now process the operations. Move down operations can be performed directly, move up operations need to be |
| * performed later in the reverse direction |
| */ |
| ListIterator<ShiftOperation> operationIt = shiftOperations.listIterator(); |
| |
| IDBPreparedStatement shiftDownStmt = null; |
| int operationCounter = 0; |
| |
| try |
| { |
| while (operationIt.hasNext()) |
| { |
| ShiftOperation operation = operationIt.next(); |
| if (operation.offset < 0) |
| { |
| if (shiftDownStmt == null) |
| { |
| shiftDownStmt = accessor.getDBConnection().prepareStatement(sqlShiftDownIndex, ReuseProbability.HIGH); |
| idHandler.setCDOID(shiftDownStmt, 2, id); |
| } |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - shift down {0} ", operation); //$NON-NLS-1$ |
| } |
| |
| shiftDownStmt.setInt(1, operation.offset); |
| shiftDownStmt.setInt(3, operation.startIndex); |
| shiftDownStmt.setInt(4, operation.endIndex); |
| shiftDownStmt.addBatch(); |
| operationCounter++; |
| |
| operationIt.remove(); |
| } |
| } |
| |
| if (operationCounter > 0) |
| { |
| DBUtil.executeBatch(shiftDownStmt, operationCounter, false); |
| } |
| } |
| finally |
| { |
| close(shiftDownStmt); |
| } |
| |
| IDBPreparedStatement shiftUpStmt = null; |
| operationCounter = 0; |
| |
| try |
| { |
| |
| while (operationIt.hasPrevious()) |
| { |
| ShiftOperation operation = operationIt.previous(); |
| if (shiftUpStmt == null) |
| { |
| shiftUpStmt = accessor.getDBConnection().prepareStatement(sqlShiftUpIndex, ReuseProbability.HIGH); |
| idHandler.setCDOID(shiftUpStmt, 2, id); |
| } |
| |
| if (TRACER.isEnabled()) |
| { |
| TRACER.format(" - shift up {0} ", operation); //$NON-NLS-1$ |
| } |
| |
| shiftUpStmt.setInt(1, operation.offset); |
| shiftUpStmt.setInt(3, operation.startIndex); |
| shiftUpStmt.setInt(4, operation.endIndex); |
| shiftUpStmt.addBatch(); |
| operationCounter++; |
| } |
| |
| if (operationCounter > 0) |
| { |
| DBUtil.executeBatch(shiftUpStmt, operationCounter, false); |
| } |
| } |
| finally |
| { |
| close(shiftUpStmt); |
| } |
| } |
| |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static interface ManipulationConstants |
| { |
| public static final int NO_INDEX = Integer.MIN_VALUE; |
| |
| public static final int DELETE = 1 << 4; |
| |
| public static final int INSERT = 1 << 3; |
| |
| public static final int MOVE = 1 << 2; |
| |
| public static final int SET_VALUE = 1 << 1; |
| |
| public static final Object NIL = new Object(); |
| |
| public static final int NONE = 0; |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static final class ManipulationElement implements ManipulationConstants |
| { |
| public int type; |
| |
| public int sourceIndex; |
| |
| public int tempIndex; |
| |
| public int destinationIndex; |
| |
| public Object value; |
| |
| public ManipulationElement(int t, int srcIdx, int dstIdx, Object val) |
| { |
| sourceIndex = srcIdx; |
| tempIndex = NO_INDEX; |
| destinationIndex = dstIdx; |
| value = val; |
| type = t; |
| } |
| |
| /** |
| * Create a ManipulationElement which represents an element which already is in the list. |
| */ |
| public static ManipulationElement createOriginalElement(int index) |
| { |
| return new ManipulationElement(NONE, index, index, NIL); |
| } |
| |
| /** |
| * Create a ManipulationElement which represents an element which is inserted in the list. |
| */ |
| public static ManipulationElement createInsertedElement(int index, Object value) |
| { |
| return new ManipulationElement(ManipulationConstants.INSERT, NO_INDEX, index, value); |
| } |
| |
| public boolean is(int t) |
| { |
| return (type & t) > 0; |
| } |
| |
| public void addType(int t) |
| { |
| type |= t; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return MessageFormat.format( |
| "Manipulation[type={0}, sourceIndex={1}, tempIndex={2}, destinationIndex={3}, value={4}]", formatType(type), |
| formatIndex(sourceIndex), formatIndex(tempIndex), formatIndex(destinationIndex), formatValue(value)); |
| } |
| |
| private static String formatType(int type) |
| { |
| switch (type) |
| { |
| case NONE: |
| return "none"; |
| |
| case DELETE: |
| return "DELETE"; |
| |
| case INSERT: |
| return "INSERT"; |
| |
| case MOVE: |
| return "MOVE"; |
| |
| case SET_VALUE: |
| return "SET_VALUE"; |
| } |
| |
| return "<invalid>"; |
| } |
| |
| private static String formatIndex(int index) |
| { |
| if (index == NO_INDEX) |
| { |
| return "NONE"; |
| } |
| |
| return Integer.toString(index); |
| } |
| |
| private static String formatValue(Object val) |
| { |
| if (val == NIL) |
| { |
| return "NIL"; |
| } |
| |
| return String.valueOf(val); |
| } |
| } |
| |
| /** |
| * @author Eike Stepper |
| */ |
| private static class ShiftOperation |
| { |
| final int startIndex; |
| |
| final int endIndex; |
| |
| final int offset; |
| |
| ShiftOperation(int startIndex, int endIndex, int offset) |
| { |
| this.startIndex = startIndex; |
| this.endIndex = endIndex; |
| this.offset = offset; |
| } |
| |
| @Override |
| public String toString() |
| { |
| return "range [" + startIndex + ".." + endIndex + "] offset " + offset; |
| } |
| } |
| } |