/* * Copyright (c) 2009-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: * 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 */ 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(DELETE) 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(); 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 (ManipulationElement element : manipulations) { /* * 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 Object NIL = new Object(); public static final int NONE = 0; public static final int SET_VALUE = 1 << 1; public static final int MOVE = 1 << 2; public static final int INSERT = 1 << 3; public static final int DELETE = 1 << 4; } /** * @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; } } }