0001: /*
0002: *
0003: * JMoney - A Personal Finance Manager
0004: * Copyright (c) 2005 Nigel Westbury <westbury@users.sourceforge.net>
0005: *
0006: *
0007: * This program is free software; you can redistribute it and/or modify
0008: * it under the terms of the GNU General Public License as published by
0009: * the Free Software Foundation; either version 2 of the License, or
0010: * (at your option) any later version.
0011: *
0012: * This program is distributed in the hope that it will be useful,
0013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
0014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
0015: * GNU General Public License for more details.
0016: *
0017: * You should have received a copy of the GNU General Public License
0018: * along with this program; if not, write to the Free Software
0019: * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
0020: *
0021: */
0022:
0023: package net.sf.jmoney.isolation;
0024:
0025: import java.util.AbstractCollection;
0026: import java.util.Collection;
0027: import java.util.HashMap;
0028: import java.util.HashSet;
0029: import java.util.Iterator;
0030: import java.util.Map;
0031: import java.util.Set;
0032: import java.util.Vector;
0033:
0034: import net.sf.jmoney.JMoneyPlugin;
0035: import net.sf.jmoney.model2.AbstractDataOperation;
0036: import net.sf.jmoney.model2.Account;
0037: import net.sf.jmoney.model2.DataManager;
0038: import net.sf.jmoney.model2.Entry;
0039: import net.sf.jmoney.model2.EntryInfo;
0040: import net.sf.jmoney.model2.ExtendableObject;
0041: import net.sf.jmoney.model2.ExtendablePropertySet;
0042: import net.sf.jmoney.model2.ExtensionPropertySet;
0043: import net.sf.jmoney.model2.IListManager;
0044: import net.sf.jmoney.model2.IObjectKey;
0045: import net.sf.jmoney.model2.ISessionChangeFirer;
0046: import net.sf.jmoney.model2.IValues;
0047: import net.sf.jmoney.model2.ListKey;
0048: import net.sf.jmoney.model2.ListPropertyAccessor;
0049: import net.sf.jmoney.model2.ObjectCollection;
0050: import net.sf.jmoney.model2.PropertySet;
0051: import net.sf.jmoney.model2.ReferencePropertyAccessor;
0052: import net.sf.jmoney.model2.ScalarPropertyAccessor;
0053: import net.sf.jmoney.model2.Session;
0054: import net.sf.jmoney.model2.SessionChangeListener;
0055: import net.sf.jmoney.model2.SessionInfo;
0056: import net.sf.jmoney.model2.Transaction;
0057: import net.sf.jmoney.model2.TransactionInfo;
0058:
0059: import org.eclipse.core.commands.ExecutionException;
0060: import org.eclipse.core.commands.operations.IOperationHistory;
0061: import org.eclipse.core.commands.operations.IUndoContext;
0062: import org.eclipse.core.commands.operations.IUndoableOperation;
0063: import org.eclipse.core.runtime.Assert;
0064: import org.eclipse.core.runtime.IStatus;
0065: import org.eclipse.core.runtime.Status;
0066:
0067: /**
0068: * A transaction manager must be set before the datastore can be modified.
0069: * An exception will be throw if an attempt is made to modify the datastore
0070: * (setting a property, or creating or deleting an extendable object) when
0071: * no transaction manager is set in the session.
0072: * <P>
0073: * Changes to the datastore are stored in the transaction manager. They
0074: * are not applied to the underlying datastore until the transaction is
0075: * committed. Read accesses (property getters and queries) are passed on
0076: * to any transaction manager which will modify the results to reflect
0077: * changes stored in the transaction.
0078: *
0079: * @author Nigel Westbury
0080: */
0081: public class TransactionManager extends DataManager {
0082: private DataManager baseDataManager;
0083:
0084: // TODO: At some time, review this and ensure that we
0085: // really do need the session object here.
0086: private Session uncommittedSession;
0087:
0088: /**
0089: * Maps IObjectKey to Map, where IObjectKey is the key in the comitted
0090: * datastore and each Map maps PropertyAccessor to the value of that
0091: * property. Every object that has been modified by this transaction manager
0092: * (a scalar property in the object has been changed) will have an entry in
0093: * this map. The map keys are objects from the datastore and will contain
0094: * the property values that are currently committed in the datastore. The
0095: * map values are objects that contain details of the changes (changed
0096: * scalar property values, or an indication that the object has been
0097: * deleted).
0098: * <P>
0099: * If a value of a property is a reference to another object then an
0100: * UncommittedObjectKey is stored as the value. By doing this, the
0101: * referenced object does not need to be materialized unless necessary.
0102: * <P>
0103: * Deleted objects will also have an entry in this map. If an object
0104: * contains list properties and the object is deleted then all the objects
0105: * in the lists will also be added to this map with a ModifiedObject that
0106: * has a 'deleted' indication. This is necessary because this list is used
0107: * to determine if an object has been deleted, to ensure that we do not
0108: * attempt to modify a property value in an object that has in fact been
0109: * deleted.
0110: */
0111: // TODO: Now we have the allObjects map, this map can be removed. The code
0112: // should be significantly simplified now that we don't have to re-apply
0113: // the changes each time an object is requested.
0114: // (Of course, the complications now need to be added to the listener code
0115: // that needs to keep the objects in allObjects map up to date
0116: // and tell our listeners.
0117: Map<IObjectKey, ModifiedObject> modifiedObjects = new HashMap<IObjectKey, ModifiedObject>();
0118:
0119: /**
0120: * Every extendable object that has ever been created in this transaction and passed
0121: * to the user is in this map. This is required because all DataManager
0122: * objects must guarantee that there is only ever a single instance of an
0123: * object in existence.
0124: *
0125: * Objects that were created in this transaction are not in this map.
0126: * There is only one instance of such objects in any case, so only that one
0127: * instance would be returned.
0128: *
0129: * The object key is the key in the committed datastore. Only objects
0130: * that exist in the underlying datastore are in this map, and it is
0131: * just easier to use the committed key than to use an uncommitted key.
0132: */
0133: Map<IObjectKey, ExtendableObject> allObjects = new HashMap<IObjectKey, ExtendableObject>();
0134:
0135: /**
0136: * Every list that has been modified by this transaction manager
0137: * (objects added to the list or objects removed from the list)
0138: * will have an entry in this set. This enables the transaction
0139: * manager to easily find the added and deleted objects when committing
0140: * the changes.
0141: */
0142: Set<DeltaListManager> modifiedLists = new HashSet<DeltaListManager>();
0143:
0144: /**
0145: * true if a nested transaction is in the process of applying its
0146: * changes to our data
0147: */
0148: // TODO: use this flag to ensure that performRefresh is called correctly.
0149: private boolean insideTransaction = false;
0150:
0151: /**
0152: * Construct a transaction manager for use with the given session.
0153: * The transaction manager does not become the active transaction
0154: * manager for the session until it is specifically set as the
0155: * active transaction manager. By separating the construction of
0156: * the transaction manager from the activating of the transaction manager,
0157: * a transaction manager can be created and listeners can be set up to
0158: * listen to session changes within the transaction manager during an
0159: * initialization stage even though the transaction is not made active
0160: * until changes are made.
0161: *
0162: * @param session the session object from the committed datastore
0163: */
0164: public TransactionManager(DataManager baseDataManager) {
0165: this .baseDataManager = baseDataManager;
0166: this .uncommittedSession = getCopyInTransaction(baseDataManager
0167: .getSession());
0168:
0169: /*
0170: * Listen for changes to the base data. Note that
0171: * a weak reference is maintained to this listener
0172: * so we don't have to worry about removing the listener,
0173: * which is just as well because this object may be left
0174: * for the garbage collector without knowing when it is
0175: * no longer being used.
0176: */
0177: baseDataManager
0178: .addChangeListenerWeakly(new MySessionChangeListener());
0179: }
0180:
0181: /**
0182: * @return a session object representing an uncommitted
0183: * session object managed by this transaction manager
0184: */
0185: @Override
0186: public Session getSession() {
0187: return uncommittedSession;
0188: }
0189:
0190: /**
0191: * Indicate whether there are any uncommitted changes being held
0192: * by this transaction manager. This method is useful when the user
0193: * does something like selecting another transaction or closing a
0194: * dialog box and it is not clear whether the user wants to commit
0195: * or to cancel changes.
0196: *
0197: * @return true if property values have been changed or objects
0198: * have been created or deleted within the context of this
0199: * transaction manager since the last commit
0200: */
0201: public boolean hasChanges() {
0202: return !modifiedObjects.isEmpty() || !modifiedLists.isEmpty();
0203: }
0204:
0205: /**
0206: * Given an instance of an object in the datastore
0207: * (i.e. committed), obtain a copy of the object that
0208: * is in the version of the datastore managed by this
0209: * transaction manager.
0210: * <P>
0211: * Updates to property values in the returned object will
0212: * not be applied to the datastore until the changes held
0213: * by this transaction manager are committed to the datastore.
0214: *
0215: * @param an object that exists in the datastore (committed)
0216: * @return a copy of the given object, being an uncommitted version
0217: * of the object in this transaction, or null if the
0218: * given object has been deleted in this transaction
0219: */
0220: public <E extends ExtendableObject> E getCopyInTransaction(
0221: final E committedObject) {
0222: /*
0223: * As a convenience to the caller, this method accepts null objects. If
0224: * a reference is null in the committed version of the data then it
0225: * should of course be null in the uncommitted version of the data.
0226: */
0227: if (committedObject == null) {
0228: return null;
0229: }
0230:
0231: if (committedObject.getDataManager() != baseDataManager) {
0232: throw new RuntimeException(
0233: "Invalid call to getCopyInTransaction. The object passed must belong to the data manager that is the base data manager of this transaction manager.");
0234: }
0235:
0236: // First look in our map to see if this object has already been
0237: // modified within the context of this transaction manager.
0238: // If it has, return the modified version.
0239: // Also check to see if the object has been deleted within the
0240: // context of this transaction manager. If it has then we raise
0241: // an error.
0242:
0243: // Both these situations are not likely to happen
0244: // because usually one object is copied into the transaction
0245: // manager and all other objects are obtained by traversing
0246: // from that object. However, it is good to check.
0247:
0248: ExtendableObject objectInTransaction = allObjects
0249: .get(committedObject.getObjectKey());
0250: if (objectInTransaction != null) {
0251: // TODO: decide if and how we check for deleted objects.
0252: // if (objectInTransaction.isDeleted()) {
0253: // throw new RuntimeException("Attempt to get copy of object, but that object has been deleted in the transaction");
0254: // }
0255: return (E) committedObject.getClass().cast(
0256: objectInTransaction);
0257: }
0258:
0259: ExtendablePropertySet<? extends E> propertySet = PropertySet
0260: .getPropertySet((Class<? extends E>) committedObject
0261: .getClass());
0262:
0263: ListKey<? super E> committedListKey = committedObject
0264: .getParentListKey();
0265: ListKey<? super E> listKey;
0266: if (committedListKey == null) {
0267: listKey = null;
0268: } else {
0269: IObjectKey committedParentKey = committedListKey
0270: .getParentKey();
0271: UncommittedObjectKey parentKey = new UncommittedObjectKey(
0272: this , committedParentKey);
0273: listKey = new ListKey(parentKey, committedListKey
0274: .getListPropertyAccessor());
0275: }
0276:
0277: final UncommittedObjectKey key = new UncommittedObjectKey(this ,
0278: committedObject.getObjectKey());
0279:
0280: /*
0281: * If the object has been modified by this transaction, it will be in
0282: * the allObjects map (which contains all objects ever returned by this
0283: * transaction)
0284: */
0285:
0286: IValues values = new IValues() {
0287:
0288: public Collection<ExtensionPropertySet<?>> getNonDefaultExtensions() {
0289: // TODO: This is not correct. Extensions may contain properties
0290: // that are references to other extendable objects.
0291: // These need to be converted to versions in this transaction,
0292: // as is done in the getReferencedObjectKey below.
0293: return committedObject.getExtensions();
0294: }
0295:
0296: public IObjectKey getReferencedObjectKey(
0297: ReferencePropertyAccessor propertyAccessor) {
0298: IObjectKey committedObjectKey = propertyAccessor
0299: .invokeObjectKeyField(committedObject);
0300: return (committedObjectKey == null) ? null
0301: : new UncommittedObjectKey(
0302: TransactionManager.this ,
0303: committedObjectKey);
0304: }
0305:
0306: public <V> V getScalarValue(
0307: ScalarPropertyAccessor<V> propertyAccessor) {
0308: return committedObject
0309: .getPropertyValue(propertyAccessor);
0310: }
0311:
0312: public <E2 extends ExtendableObject> IListManager<E2> getListManager(
0313: IObjectKey listOwnerKey,
0314: ListPropertyAccessor<E2> listAccessor) {
0315: return new DeltaListManager<E2>(
0316: TransactionManager.this , committedObject, key,
0317: listAccessor);
0318: }
0319: };
0320:
0321: // We can now create the object.
0322: E copyInTransaction = (E) committedObject.getClass().cast(
0323: propertySet.constructImplementationObject(key, listKey,
0324: values));
0325:
0326: /*
0327: * Now we have created a version of this object that is valid in this datastore,
0328: * put it in the map so that we can guarantee that we always return the same
0329: * instance in future.
0330: */
0331: allObjects.put(committedObject.getObjectKey(),
0332: copyInTransaction);
0333:
0334: // We do not copy lists owned by the object at this time.
0335: // If a list is iterated, we must return copies in this transaction.
0336: // Originals may never be materialized.
0337:
0338: // If a list is iterated multiple times, a new set
0339: // is instantiated. This is consistent with list processing
0340: // when objects are stored in database - do not cache the
0341: // list because it must then be kept up to date and the
0342: // list may no longer be needed, plus lists are not in general
0343: // iterated more than once because the consumer should keep its
0344: // own data up to date using listeners.
0345:
0346: // This is ok because the API contract states
0347: // that if the user gets the same object
0348: // twice then the user must listen for changes and refresh
0349: // the other objects. This is something the user has to
0350: // do anyway because objects could be out of date due to
0351: // changes to the committed version.
0352:
0353: // Therefore, a list is really a delta from the committed list.
0354:
0355: // How does the delta get the committed list?
0356: // We could pass an iterator here, but that is a once
0357: // off. We must pass a dynamic collection (a collection
0358: // that is a view of the committed items in the list)
0359:
0360: return copyInTransaction;
0361: }
0362:
0363: /**
0364: * @param account
0365: * @return
0366: */
0367: @Override
0368: public boolean hasEntries(Account account) {
0369: return !new ModifiedAccountEntriesList(account).isEmpty();
0370: }
0371:
0372: /**
0373: * @param account
0374: * @return
0375: */
0376: @Override
0377: public Collection<Entry> getEntries(Account account) {
0378: return new ModifiedAccountEntriesList(account);
0379: }
0380:
0381: /**
0382: * Apply the changes that are stored in this transaction manager.
0383: * <P>
0384: * When changes are committed, they are seen in the datastore
0385: * and also in the version of the datastore as seen through other
0386: * transaction managers.
0387: * <P>
0388: * All datastore listeners and all listeners which are listening
0389: * for changes ......
0390: *
0391: */
0392: public void commit() {
0393: baseDataManager.startTransaction();
0394:
0395: // Add all the new objects, but set references to other
0396: // new objects to null because the other new object may
0397: // not have yet been added to the database and thus no
0398: // reference can be set to the other object.
0399: for (DeltaListManager<?> modifiedList : modifiedLists) {
0400: commitObjectsInList(modifiedList);
0401: }
0402:
0403: // Update all the updated objects
0404: for (Map.Entry<IObjectKey, ModifiedObject> mapEntry : modifiedObjects
0405: .entrySet()) {
0406: IObjectKey committedKey = mapEntry.getKey();
0407: ModifiedObject newValuesMap = mapEntry.getValue();
0408:
0409: if (!newValuesMap.isDeleted()) {
0410: Map<ScalarPropertyAccessor, Object> propertyMap = newValuesMap
0411: .getMap();
0412: /* Actually this does not work. We must go through the setters because we are 'outside' the datastore.
0413: * The values would not otherwise be set in the objects themselves.
0414:
0415: ExtendableObject committedObject = committedKey.getObject();
0416: PropertySet<?> actualPropertySet = PropertySet.getPropertySet(committedObject.getClass());
0417:
0418: int count = actualPropertySet.getScalarProperties3().size();
0419: Object [] newValues = new Object[count];
0420: Object [] oldValues = new Object[count];
0421:
0422: // TODO: It may be better if we save the data from the committed
0423: // object early on, before any changes. This really depends on
0424: // how we decide to cope with conflicts between the committed and
0425: // this data manager.
0426: int index = 0;
0427: for (ScalarPropertyAccessor<?> accessor: actualPropertySet.getScalarProperties3()) {
0428: Object value = committedObject.getPropertyValue(accessor);
0429: if (value instanceof ExtendableObject) {
0430: ExtendableObject referencedObject = (ExtendableObject)value;
0431: oldValues[index] = referencedObject.getObjectKey();
0432: } else {
0433: oldValues[index] = value;
0434: }
0435:
0436: if (propertyMap.containsKey(accessor)) {
0437: Object newValue = propertyMap.get(accessor);
0438: if (newValue instanceof UncommittedObjectKey) {
0439: UncommittedObjectKey referencedKey = (UncommittedObjectKey)newValue;
0440: newValues[index] = referencedKey.getCommittedObjectKey();
0441: } else {
0442: newValues[index] = value;
0443: }
0444: } else {
0445: // No change in the property value
0446: newValues[index] = oldValues[index];
0447: }
0448:
0449: index++;
0450: }
0451: */
0452:
0453: for (Map.Entry<ScalarPropertyAccessor, Object> mapEntry2 : propertyMap
0454: .entrySet()) {
0455: ScalarPropertyAccessor<?> accessor = mapEntry2
0456: .getKey();
0457: Object newValue = mapEntry2.getValue();
0458:
0459: // TODO: If we create a method in IObjectKey for updating properties
0460: // then we don't have to instantiate the committed object here.
0461: // The advantages are small, however, because the key is likely to
0462: // need to read the old properties from the database before setting
0463: // the new property values.
0464: ExtendableObject committedObject = committedKey
0465: .getObject();
0466:
0467: if (newValue instanceof UncommittedObjectKey) {
0468: UncommittedObjectKey referencedKey = (UncommittedObjectKey) newValue;
0469: // TODO: We should not have to instantiate an instance of the
0470: // referenced object in the committed datastore. However, there
0471: // is no method to set the key as the value.
0472: // We should add such a mechanism.
0473: newValue = referencedKey
0474: .getCommittedObjectKey().getObject();
0475: }
0476: setProperty(committedObject, accessor, newValue);
0477: }
0478: }
0479: }
0480:
0481: /*
0482: * Delete all object marked for deletion. This is a two-step process.
0483: * The first step involves iterating over all the objects marked for
0484: * deletion and seeing if they contain any references to other objects
0485: * that are also marked for deletion. If any such references are found,
0486: * the reference is set to null. The second step involves actually
0487: * deleting the objects. The reason why we must do this two-step process
0488: * is that there may be circular references between objects marked for
0489: * deletion, and the underlying database may raise a reference constaint
0490: * violation if an object is deleted while other objects contain
0491: * references to it.
0492: *
0493: * This code is not perfect and needs more work to make it perfect.
0494: * Firstly, there may be a problem if the underlying database does not
0495: * allow null values for a particular reference. In that case, we should
0496: * ignore the failure to set the value to null. We set what we can to
0497: * null and then go on to delete all the values. Secondly, we may have
0498: * to make multiple passes while attempting to delete all the objects
0499: * because if one contains a non-nullable reference to the other then it
0500: * must be deleted first. It should be theoretically possible to delete
0501: * the objects, because the database got into this state in the first
0502: * case, and we can remove the objects by reversing the steps taken to
0503: * get to this state.
0504: */
0505:
0506: /*
0507: * Step 1: Update all the objects marked for deletion, setting any
0508: * references to other objects also marked for deletion to be null
0509: * references.
0510: */
0511: for (Map.Entry<IObjectKey, ModifiedObject> mapEntry : modifiedObjects
0512: .entrySet()) {
0513: IObjectKey committedKey = mapEntry.getKey();
0514: ModifiedObject newValues = mapEntry.getValue();
0515:
0516: if (newValues.isDeleted()) {
0517: ExtendableObject deletedObject = committedKey
0518: .getObject();
0519:
0520: ExtendablePropertySet<?> propertySet = PropertySet
0521: .getPropertySet(deletedObject.getClass());
0522: for (ScalarPropertyAccessor<?> accessor : propertySet
0523: .getScalarProperties3()) {
0524: Object value = deletedObject
0525: .getPropertyValue(accessor);
0526: if (value instanceof ExtendableObject) {
0527: ExtendableObject referencedObject = (ExtendableObject) value;
0528: IObjectKey committedReferencedKey = referencedObject
0529: .getObjectKey();
0530: ModifiedObject referencedNewValues = modifiedObjects
0531: .get(committedReferencedKey);
0532: if (referencedNewValues != null
0533: && referencedNewValues.isDeleted()) {
0534: // This is a reference to an object that is marked for deletion.
0535: // Set the reference to null
0536: deletedObject.setPropertyValue(accessor,
0537: null);
0538: }
0539: }
0540: }
0541: }
0542: }
0543:
0544: /*
0545: * Step 2: Delete the deleted objects
0546: */
0547: for (DeltaListManager<?> modifiedList : modifiedLists) {
0548:
0549: ExtendableObject parent = modifiedList.committedParent;
0550:
0551: for (IObjectKey objectToDelete : modifiedList
0552: .getDeletedObjects()) {
0553: parent.getListPropertyValue(modifiedList.listAccessor)
0554: .remove(objectToDelete.getObject());
0555: }
0556: }
0557:
0558: baseDataManager.commitTransaction();
0559:
0560: // Clear out the changes in the object. These changes are the
0561: // delta between the datastore and the uncommitted view.
0562: // Now that the changes have been committed, these changes
0563: // must be cleared.
0564: for (DeltaListManager<?> modifiedList : modifiedLists) {
0565: modifiedList.addedObjects.clear();
0566: modifiedList.deletedObjects.clear();
0567: }
0568: modifiedLists.clear();
0569: modifiedObjects.clear();
0570: }
0571:
0572: /**
0573: * This method does the same as the commit() method but the changes
0574: * may be undone and redone. This support is available with no
0575: * coding needed by the caller other than to pass the label to be
0576: * used to describe the operation.
0577: *
0578: * @param label the label to be used to describe this operation
0579: * for undo/redo purposes
0580: */
0581: public void commit(String label) {
0582: IUndoContext undoContext = baseDataManager.getSession()
0583: .getUndoContext();
0584: IOperationHistory history = JMoneyPlugin.getDefault()
0585: .getWorkbench().getOperationSupport()
0586: .getOperationHistory();
0587:
0588: IUndoableOperation operation = new AbstractDataOperation(
0589: baseDataManager.getSession(), label) {
0590: @Override
0591: public IStatus execute() throws ExecutionException {
0592: commit();
0593: return Status.OK_STATUS;
0594: }
0595: };
0596:
0597: operation.addContext(undoContext);
0598: try {
0599: history.execute(operation, null, null);
0600: } catch (ExecutionException e) {
0601: // TODO Auto-generated catch block
0602: e.printStackTrace();
0603: }
0604: }
0605:
0606: private <V> void setProperty(ExtendableObject committedObject,
0607: ScalarPropertyAccessor<V> accessor, Object newValue) {
0608: committedObject.setPropertyValue(accessor, accessor
0609: .getClassOfValueObject().cast(newValue));
0610: }
0611:
0612: /**
0613: * Add a new uncommitted object to the committed datastore.
0614: *
0615: * All objects in any list properties in the object are also added, hence
0616: * this method is recursive.
0617: *
0618: * @param newObject the uncommitted version of the new object
0619: * @param parent the committed version of the parent into which this new
0620: * object is to be inserted
0621: * @param listAccessor the list into which this new object is to be
0622: * inserted
0623: * @return the committed version of the object
0624: */
0625: private <E extends ExtendableObject> E commitNewObject(
0626: final E newObject, ExtendableObject parent,
0627: ListPropertyAccessor<E> listAccessor,
0628: boolean isDescendentInsert) {
0629: ExtendablePropertySet<? extends E> actualPropertySet = listAccessor
0630: .getElementPropertySet().getActualPropertySet(
0631: (Class<? extends E>) newObject.getClass());
0632:
0633: /**
0634: * Holds references to new objects that have never been committed, so
0635: * that such references can be set later after all the new objects have
0636: * been committed.
0637: */
0638: final ModifiedObject deferredReferences = new ModifiedObject();
0639:
0640: IValues values = new IValues() {
0641:
0642: public Collection<ExtensionPropertySet<?>> getNonDefaultExtensions() {
0643: return newObject.getExtensions();
0644: }
0645:
0646: public IObjectKey getReferencedObjectKey(
0647: ReferencePropertyAccessor<? extends ExtendableObject> propertyAccessor) {
0648: ExtendableObject referencedObject = newObject
0649: .getPropertyValue(propertyAccessor);
0650: if (referencedObject == null) {
0651: return null;
0652: }
0653: UncommittedObjectKey uncommittedKey = (UncommittedObjectKey) referencedObject
0654: .getObjectKey();
0655: IObjectKey committedKey = uncommittedKey
0656: .getCommittedObjectKey();
0657: if (committedKey != null) {
0658: return committedKey;
0659: } else {
0660: /*
0661: * The property value is a reference to an object that has
0662: * not have yet been committed to the datastore. Such values
0663: * cannot be set in the datastore. We therefore set the
0664: * property to null for the time being but add it to our
0665: * list of properties to be set later.
0666: */
0667: deferredReferences.put(propertyAccessor,
0668: uncommittedKey);
0669: return null;
0670: }
0671: }
0672:
0673: public <V> V getScalarValue(
0674: ScalarPropertyAccessor<V> propertyAccessor) {
0675: return newObject.getPropertyValue(propertyAccessor);
0676: }
0677:
0678: public <E2 extends ExtendableObject> IListManager<E2> getListManager(
0679: IObjectKey listOwnerKey,
0680: ListPropertyAccessor<E2> listAccessor) {
0681: /*
0682: * Create an empty list manager. The implementation depends on
0683: * the data manager, thus we request a list manager from the
0684: * object key.
0685: */
0686: return listOwnerKey.constructListManager(listAccessor);
0687: }
0688: };
0689:
0690: // Create the object with the appropriate property values
0691: ObjectCollection<? super E> propertyValues = parent
0692: .getListPropertyValue(listAccessor);
0693: final E newCommittedObject = propertyValues.createNewElement(
0694: actualPropertySet, values, isDescendentInsert);
0695:
0696: // Update the uncommitted object key to indicate that there is now a committed
0697: // version of the object in the datastore
0698: ((UncommittedObjectKey) newObject.getObjectKey())
0699: .setCommittedObject(newCommittedObject);
0700:
0701: /*
0702: * Fire the event to indicate that a new object has been created. Note
0703: * that the objectCreated event, as opposed to the objectInserted event,
0704: * is fired as soon as any object is created and before it's children
0705: * are created. A later call to objectCreated will be made for each
0706: * descendant.
0707: */
0708: parent.getDataManager().fireEvent(new ISessionChangeFirer() {
0709: public void fire(SessionChangeListener listener) {
0710: listener.objectCreated(newCommittedObject);
0711: }
0712: });
0713:
0714: // Commit all the child objects in the list properties.
0715: for (ListPropertyAccessor<?> subListAccessor : actualPropertySet
0716: .getListProperties3()) {
0717: commitChildren(newObject, newCommittedObject,
0718: subListAccessor);
0719: }
0720:
0721: // If there are property changes that must be applied later, add the property
0722: // change map to the list
0723: if (!deferredReferences.isEmpty()) {
0724: modifiedObjects.put(newCommittedObject.getObjectKey(),
0725: deferredReferences);
0726: }
0727:
0728: return newCommittedObject;
0729: }
0730:
0731: private <E extends ExtendableObject> void commitChildren(
0732: ExtendableObject newObject,
0733: ExtendableObject newCommittedObject,
0734: ListPropertyAccessor<E> subListAccessor) {
0735: for (E childObject : newObject
0736: .getListPropertyValue(subListAccessor)) {
0737: commitNewObject(childObject, newCommittedObject,
0738: subListAccessor, true);
0739: }
0740: }
0741:
0742: private <E extends ExtendableObject> void commitObjectsInList(
0743: DeltaListManager<E> modifiedList) {
0744: ExtendableObject parent = modifiedList.committedParent;
0745:
0746: for (ExtendableObject newUntypedObject : modifiedList
0747: .getAddedObjects()) {
0748: E newObject = modifiedList.listAccessor
0749: .getElementPropertySet().getImplementationClass()
0750: .cast(newUntypedObject);
0751:
0752: final ExtendableObject newCommittedObject = commitNewObject(
0753: newObject, parent, modifiedList.listAccessor, false);
0754:
0755: /*
0756: * Now we can fire the notifications of newly inserted objects. This
0757: * is done now after all the descendants of the object have been
0758: * created.
0759: *
0760: * Note that if this object references other objects that are added
0761: * in this same transaction but that have not yet been added then
0762: * the reference will be null. A later objectChanged event will be
0763: * fired when the reference is later set to the correct value.
0764: */
0765: parent.getDataManager().fireEvent(
0766: new ISessionChangeFirer() {
0767: public void fire(SessionChangeListener listener) {
0768: listener.objectInserted(newCommittedObject);
0769: }
0770: });
0771: }
0772: }
0773:
0774: class DeletedObject {
0775: private ExtendableObject parent;
0776: private ListPropertyAccessor<?> owningListProperty;
0777:
0778: DeletedObject(ExtendableObject parent,
0779: ListPropertyAccessor owningListProperty) {
0780: this .parent = parent;
0781: this .owningListProperty = owningListProperty;
0782: }
0783:
0784: void deleteObject(ExtendableObject object) {
0785: parent.getListPropertyValue(owningListProperty).remove(
0786: object);
0787: }
0788: }
0789:
0790: public Object getAdapter(Class adapter) {
0791: // It is possible to implement query interfaces that execute
0792: // an optimized query against the committed datastore and then
0793: // adjusts the results with the uncommitted changes.
0794: // However, none are currently implemented.
0795: return null;
0796: }
0797:
0798: private class ModifiedAccountEntriesList extends
0799: AbstractCollection<Entry> {
0800:
0801: Account account;
0802:
0803: ModifiedAccountEntriesList(Account account) {
0804: this .account = account;
0805: }
0806:
0807: @Override
0808: public int size() {
0809: Vector<Entry> addedEntries = new Vector<Entry>();
0810: Vector<IObjectKey> removedEntries = new Vector<IObjectKey>();
0811: buildAddedAndRemovedEntryLists(addedEntries, removedEntries);
0812:
0813: IObjectKey committedAccountKey = ((UncommittedObjectKey) account
0814: .getObjectKey()).getCommittedObjectKey();
0815: if (committedAccountKey == null) {
0816: // This is a new account created in this transaction
0817: Assert.isTrue(removedEntries.isEmpty());
0818: return addedEntries.size();
0819: } else {
0820: Account committedAccount = (Account) committedAccountKey
0821: .getObject();
0822: Collection<Entry> committedCollection = committedAccount
0823: .getEntries();
0824: return committedCollection.size() + addedEntries.size()
0825: - removedEntries.size();
0826: }
0827: }
0828:
0829: @Override
0830: public Iterator<Entry> iterator() {
0831: // Build the list of differences between the committed
0832: // list and the list in this transaction.
0833:
0834: // This is done each time an iterator is requested.
0835:
0836: Vector<Entry> addedEntries = new Vector<Entry>();
0837: Vector<IObjectKey> removedEntries = new Vector<IObjectKey>();
0838: buildAddedAndRemovedEntryLists(addedEntries, removedEntries);
0839:
0840: IObjectKey committedAccountKey = ((UncommittedObjectKey) account
0841: .getObjectKey()).getCommittedObjectKey();
0842: if (committedAccountKey == null) {
0843: // This is a new account created in this transaction
0844: Assert.isTrue(removedEntries.isEmpty());
0845: return addedEntries.iterator();
0846: } else {
0847: Account committedAccount = (Account) committedAccountKey
0848: .getObject();
0849: Collection<Entry> committedCollection = committedAccount
0850: .getEntries();
0851: return new DeltaListIterator<Entry>(
0852: TransactionManager.this , committedCollection
0853: .iterator(), addedEntries,
0854: removedEntries);
0855: }
0856: }
0857:
0858: private void buildAddedAndRemovedEntryLists(
0859: Vector<Entry> addedEntries,
0860: Vector<IObjectKey> removedEntries) {
0861: // Process all the new objects added within this transaction
0862: for (DeltaListManager<?> modifiedList : modifiedLists) {
0863:
0864: // Find all entries added to existing transactions
0865: if (modifiedList.listAccessor == TransactionInfo
0866: .getEntriesAccessor()) {
0867: for (ExtendableObject newObject : modifiedList
0868: .getAddedObjects()) {
0869: Entry newEntry = (Entry) newObject;
0870: if (account.equals(newEntry.getAccount())) {
0871: addedEntries.add(newEntry);
0872: }
0873: }
0874: }
0875:
0876: // Find all entries in new transactions.
0877: if (modifiedList.listAccessor == SessionInfo
0878: .getTransactionsAccessor()) {
0879: for (ExtendableObject newObject : modifiedList
0880: .getAddedObjects()) {
0881: Transaction newTransaction = (Transaction) newObject;
0882: for (Entry newEntry : newTransaction
0883: .getEntryCollection()) {
0884: if (account.equals(newEntry.getAccount())) {
0885: addedEntries.add(newEntry);
0886: }
0887: }
0888: }
0889: }
0890: }
0891:
0892: /*
0893: * Process all the changed and deleted objects. (Deleted objects are
0894: * processed here and not from the deletedObjects list in modified
0895: * lists in the above code. This ensures that objects that are
0896: * deleted due to the deletion of the parent are also processed).
0897: */
0898: for (Map.Entry<IObjectKey, ModifiedObject> mapEntry : modifiedObjects
0899: .entrySet()) {
0900: IObjectKey committedKey = mapEntry.getKey();
0901: ModifiedObject newValues = mapEntry.getValue();
0902:
0903: ExtendableObject committedObject = committedKey
0904: .getObject();
0905:
0906: if (committedObject instanceof Entry) {
0907: Entry entry = (Entry) committedObject;
0908: if (!newValues.isDeleted()) {
0909: Map<ScalarPropertyAccessor, Object> propertyMap = newValues
0910: .getMap();
0911:
0912: // Object has changed property values.
0913: if (propertyMap.containsKey(EntryInfo
0914: .getAccountAccessor())) {
0915: boolean wasInIndex = account.equals(entry
0916: .getAccount());
0917: boolean nowInIndex = account
0918: .equals(((IObjectKey) propertyMap
0919: .get(EntryInfo
0920: .getAccountAccessor()))
0921: .getObject());
0922: if (wasInIndex) {
0923: if (!nowInIndex) {
0924: removedEntries.add(entry
0925: .getObjectKey());
0926: }
0927: } else {
0928: if (nowInIndex) {
0929: // Note that addedEntries must contain objects that
0930: // are being managed by the transaction manager
0931: // (not the committed versions).
0932: addedEntries
0933: .add(getCopyInTransaction(entry));
0934: }
0935: }
0936: }
0937: } else {
0938: // Object has been deleted.
0939: if (entry.getAccount().equals(account)) {
0940: removedEntries.add(entry.getObjectKey());
0941: }
0942: }
0943: }
0944: }
0945: }
0946: }
0947:
0948: /**
0949: * This method is called when a nested transaction manager is about to apply its
0950: * changes to our data.
0951: */
0952: @Override
0953: public void startTransaction() {
0954: /*
0955: * This method is called only when transactions are nested. The nested
0956: * transaction will call this method before it applies the changes to
0957: * this transaction.
0958: */
0959: insideTransaction = true;
0960: }
0961:
0962: /**
0963: * This method is called when a nested transaction manager has completed
0964: * making changes to our data.
0965: */
0966: @Override
0967: public void commitTransaction() {
0968: /*
0969: * This method is called only when transaction are nested.
0970: * The nested transaction will call this method after it has
0971: * applied the changes to this transaction.
0972: *
0973: * Changes are applied to this object's data as the changes are
0974: * made and there is nothing we need do to 'commit' the changes.
0975: * However, we do need to fire the performRefresh event method
0976: * at this time. This event notifies our listeners that a batch
0977: * of changes has completed and now is a good time to refresh
0978: * views.
0979: */
0980: fireEvent(new ISessionChangeFirer() {
0981: public void fire(SessionChangeListener listener) {
0982: listener.performRefresh();
0983: }
0984: });
0985:
0986: insideTransaction = false;
0987: }
0988:
0989: /**
0990: * This class contains the methods that merge changes
0991: * from the base data into the data for this object,
0992: * and also tell our listeners of such changes.
0993: */
0994: private class MySessionChangeListener implements
0995: SessionChangeListener {
0996:
0997: public void objectInserted(ExtendableObject newObject) {
0998: /*
0999: * The object may contain references to objects
1000: * that have been deleted in this view.
1001: *
1002: * In such a situation, the object deleted in this view
1003: * could be first 'undeleted'. If the undeleted object references
1004: * other objects that were deleted by this view then those
1005: * objects are in turn undeleted in a recursive manner.
1006: *
1007: * A simpler approach may be to ignore the new object until
1008: * the transaction is committed. At that time, the commit fails
1009: * with a conflict exception.
1010: */
1011: // TODO Implement this method
1012: }
1013:
1014: public void objectCreated(ExtendableObject newObject) {
1015: // TODO Auto-generated method stub
1016:
1017: }
1018:
1019: public void objectChanged(ExtendableObject changedObject,
1020: ScalarPropertyAccessor changedProperty,
1021: Object oldValue, Object newValue) {
1022: /*
1023: * The property may have been changed to reference an
1024: * object that has been deleted in this view.
1025: *
1026: * In such a situation, the object could be
1027: * first 'undeleted' in this view. If the undeleted object references
1028: * other objects that were deleted by this view then those
1029: * objects are in turn undeleted in a recursive manner.
1030: *
1031: * A simpler approach may be to ignore the new object until
1032: * the transaction is committed. At that time, the commit fails
1033: * with a conflict exception.
1034: *
1035: * We also need to consider what happens if this property change made
1036: * another property inapplicable, and that other property was changed
1037: * in this view, or if this property has been made inapplicable as
1038: * a result of a change in this view of another property.
1039: * All sorts of possibilities to consider.
1040: */
1041: // TODO Implement this method
1042: }
1043:
1044: public void objectRemoved(ExtendableObject deletedObject) {
1045: /*
1046: * If an object is deleted from the base data then we
1047: * know there are no references to the object from the
1048: * base data. However, it is possible that a reference
1049: * was created to this object in this version of the data.
1050: *
1051: * If that is the case then we could just not remove the object
1052: * from this view of the data. The object is removed from
1053: * this view of the data only if all references to it are
1054: * removed from this view. If references still remain when
1055: * this view is committed then a conflict error occurs.
1056: * The user should be told that the object no longer exists
1057: * and the user must remove references to it before attempting
1058: * again to commit.
1059: *
1060: * A simpler approach may be to ignore the deletion of the object until
1061: * the transaction is committed. At that time, the commit fails
1062: * with a conflict exception.
1063: */
1064: // TODO Implement this method
1065: }
1066:
1067: public void objectDestroyed(ExtendableObject deletedObject) {
1068: // TODO Auto-generated method stub
1069:
1070: }
1071:
1072: public void performRefresh() {
1073: // TODO Auto-generated method stub
1074:
1075: }
1076:
1077: public void sessionReplaced(Session oldSession,
1078: Session newSession) {
1079: // TODO This method is not applicable here.
1080: // Do we need a version of this listener that does
1081: // not have this method???
1082: }
1083:
1084: public void objectMoved(ExtendableObject movedObject,
1085: ExtendableObject originalParent,
1086: ExtendableObject newParent,
1087: ListPropertyAccessor originalParentListProperty,
1088: ListPropertyAccessor newParentListProperty) {
1089: /*
1090: * If an object was moved and the move conflicts with outstanding changes
1091: * held by this transaction then we have problems.
1092: * However, with the current JMoney feature set, this is unlikely
1093: * to happen, so just ignore. Views working off transactions will not
1094: * see the move. When the transaction commits, the changes should
1095: * not conflict with the move.
1096: */
1097: }
1098: }
1099: }
|