/* * Copyright 2007-2010 Sun Microsystems, Inc. * * This file is part of Project Darkstar Server. * * Project Darkstar Server is free software: you can redistribute it * and/or modify it under the terms of the GNU General Public License * version 2 as published by the Free Software Foundation and * distributed hereunder to you. * * Project Darkstar Server is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * -- */ package com.sun.sgs.impl.service.data; import com.sun.sgs.app.ManagedObject; import com.sun.sgs.app.ManagedReference; import com.sun.sgs.app.ObjectIOException; import com.sun.sgs.app.ObjectNotFoundException; import com.sun.sgs.app.TransactionNotActiveException; import com.sun.sgs.impl.sharedutil.LoggerWrapper; import com.sun.sgs.impl.sharedutil.Objects; import java.io.ObjectStreamException; import java.io.Serializable; import java.math.BigInteger; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; /** * Provides an implementation of ManagedReference. Instances of this class are * associated with a single transaction. Within a transaction, instances are * canonicalized: only a single instance appears for a given object ID or * object. */ final class ManagedReferenceImpl<T> implements ManagedReference<T>, Serializable { /** The version of the serialized form. */ private static final long serialVersionUID = 1; /** The logger for this class. */ private static final LoggerWrapper logger = DataServiceImpl.logger; /** * The logger for messages about managed objects that are modified but for * which markForUpdate was not called. */ private static final LoggerWrapper debugDetectLogger = new LoggerWrapper( Logger.getLogger( DataServiceImpl.class.getName() + ".detect.modifications")); /** * The possible states of a reference. * * Here's a table relating state values to the values of the object and * unmodifiedBytes fields: * * State object unmodifiedBytes * NEW non-null null * EMPTY null null * NOT_MODIFIED non-null null * MAYBE_MODIFIED non-null non-null * MODIFIED non-null null * FLUSHED null null * REMOVED_EMPTY null null * REMOVED_FETCHED non-null null */ private static enum State { /** A object created in this transaction. */ NEW, /** * A reference to an existing object that has not been dereferenced * yet. */ EMPTY, /** * An object that has been read and will be marked explicitly when * modified. */ NOT_MODIFIED, /** * An object that has been read and will be checked for modification at * commit. */ MAYBE_MODIFIED, /** An object that has been explicitly marked modified. */ MODIFIED, /** * An object whose contents have been flushed to the database during * transaction preparation. */ FLUSHED, /** An object that has been removed without being dereferenced */ REMOVED_EMPTY, /** An object that has been removed after being dereferenced. */ REMOVED_FETCHED } /** * Information related to the transaction in which this reference was * created. This field is logically final, but is not declared final so * that it can be set during deserialization. */ private transient Context context; /** The value returned by getId, or null. */ private transient BigInteger id; /** * The object ID. * * @serial */ final long oid; /** * The associated object or null. Note that managed references cannot * refer to null, so this field will only be null if the object has not * been fetched yet. */ private transient ManagedObject object; /** * The serialized form of the object before it was modified, or null. Note * that this value could be different from the bytes used to deserialize * the object if the serialized form of the object is not stable. * Unfortunately, the built-in collection types have non-stable serialized * forms. */ private transient byte[] unmodifiedBytes; /** The current state. */ private transient State state; /* -- Getting instances -- */ /** * Returns the reference associated with a context and object, or null if * the reference is not found. Throws ObjectNotFoundException if the * object has been removed. */ static <T> ManagedReferenceImpl<T> findReference( Context context, T object) { assert object != null : "Object is null"; assert object instanceof ManagedObject : "Object is not a managed object"; ManagedReferenceImpl<T> ref = Objects.uncheckedCast( context.refs.find((ManagedObject) object)); if (ref != null && ref.isRemoved()) { throw new ObjectNotFoundException("Object has been removed"); } return ref; } /** * Returns the reference associated with a context and object, or null if * the reference is not found or the object has been removed. */ static <T> ManagedReferenceImpl<T> safeFindReference( Context context, T object) { if (object instanceof ManagedObject) { return Objects.uncheckedCast( context.refs.find((ManagedObject) object)); } else { return null; } } /** * Returns the reference associated with a context and object, creating a * NEW reference if none is found. */ static <T> ManagedReferenceImpl<T> getReference( Context context, T object) { assert object != null : "Object is null"; assert object instanceof ManagedObject : "Object is not a managed object"; ManagedReferenceImpl<T> ref = Objects.uncheckedCast( context.refs.find((ManagedObject) object)); if (ref == null) { ref = new ManagedReferenceImpl<T>(context, object); context.refs.add(ref); } else if (ref.isRemoved()) { throw new ObjectNotFoundException("Object has been removed"); } if (logger.isLoggable(Level.FINEST)) { logger.log(Level.FINEST, "getReference tid:{0,number,#}, type:{1}" + " returns oid:{2,number,#}", context.getTxnId(), DataServiceImpl.typeName(object), ref.getId()); } return ref; } /** * Returns the reference associated with a context and object ID, creating * an EMPTY reference if none is found. */ static ManagedReferenceImpl<?> getReference( Context context, long oid) { ManagedReferenceImpl<?> ref = context.refs.find(oid); if (ref == null) { ref = new ManagedReferenceImpl<ManagedObject>(context, oid); context.refs.add(ref); } else if (ref.isRemoved()) { throw new ObjectNotFoundException("Object has been removed"); } if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "getReference tid:{0,number,#}, oid:{1,number,#} returns", context.getTxnId(), oid); } return ref; } /** Creates a NEW reference to an object. */ private ManagedReferenceImpl(Context context, T object) { this.context = context; oid = context.store.createObject(context.txn); this.object = (ManagedObject) object; state = State.NEW; validate(); } /** Creates an EMPTY reference to an object ID. */ private ManagedReferenceImpl(Context context, long oid) { this.context = context; this.oid = oid; state = State.EMPTY; validate(); } /* -- Methods for DataService -- */ @SuppressWarnings("fallthrough") void removeObject() { switch (state) { case EMPTY: context.store.removeObject(context.txn, oid); state = State.REMOVED_EMPTY; break; case MAYBE_MODIFIED: /* Call store before modifying fields, in case the call fails */ context.store.removeObject(context.txn, oid); unmodifiedBytes = null; state = State.REMOVED_FETCHED; break; case NOT_MODIFIED: case MODIFIED: context.store.removeObject(context.txn, oid); /* Fall through */ case NEW: state = State.REMOVED_FETCHED; break; case FLUSHED: throw new TransactionNotActiveException( "Attempt to remove a managed object when its transaction is" + " not active"); case REMOVED_EMPTY: case REMOVED_FETCHED: throw new ObjectNotFoundException("The object is not found"); default: throw new AssertionError(); } } @SuppressWarnings("fallthrough") void markForUpdate() { switch (state) { case EMPTY: /* * Presumably this object is being marked for update because it * will be modified, so fetch the object now. */ object = deserialize( context.store.getObject( context.txn, oid, !context.optimisticWriteLocks())); context.refs.registerObject(this); context.store.setObjectDescription(context.txn, oid, object); state = State.MODIFIED; break; case MAYBE_MODIFIED: if (!context.optimisticWriteLocks()) { context.store.markForUpdate(context.txn, oid); } unmodifiedBytes = null; state = State.MODIFIED; break; case NOT_MODIFIED: if (!context.optimisticWriteLocks()) { context.store.markForUpdate(context.txn, oid); } state = State.MODIFIED; break; case MODIFIED: case NEW: break; case FLUSHED: throw new TransactionNotActiveException( "Attempt to mark a managed object for update when its" + " transaction is not active"); case REMOVED_EMPTY: case REMOVED_FETCHED: throw new ObjectNotFoundException("The object is not found"); default: throw new AssertionError(); } } /* -- Implement ManagedReference -- */ /** {@inheritDoc} */ public T get() { return get(true); } /** * Like get, but with optional checking of the context. Checking the * context should only be suppressed if the reference was just obtained * from the context. */ @SuppressWarnings("fallthrough") T get(boolean checkContext) { RuntimeException exception = null; try { if (checkContext) { DataServiceImpl.checkContext(context); } switch (state) { case EMPTY: ManagedObject tempObject = deserialize( context.store.getObject(context.txn, oid, false)); if (context.detectModifications) { unmodifiedBytes = SerialUtil.serialize( tempObject, context.classSerial); state = State.MAYBE_MODIFIED; } else { state = State.NOT_MODIFIED; } /* Do after creating unmodified bytes, in case that fails */ object = tempObject; context.refs.registerObject(this); context.store.setObjectDescription(context.txn, oid, object); break; case NEW: case NOT_MODIFIED: case MAYBE_MODIFIED: case MODIFIED: break; case FLUSHED: exception = new TransactionNotActiveException( "Attempt to get the object associated with a managed" + " reference when its transaction is not active"); break; case REMOVED_EMPTY: case REMOVED_FETCHED: throw new ObjectNotFoundException("The object is not found"); default: throw new AssertionError(); } } catch (TransactionNotActiveException e) { exception = new TransactionNotActiveException( "Attempt to get the object associated with a managed" + " reference when its transaction is not active: " + e.getMessage(), e); } catch (RuntimeException e) { exception = e; } if (exception == null) { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "get tid:{0,number,#}, oid:{1,number,#} returns" + " type:{2}", context.getTxnId(), oid, DataServiceImpl.typeName(object)); } @SuppressWarnings("unchecked") T result = (T) object; return result; } else { DataServiceImpl.getExceptionLogger(exception).logThrow( Level.FINEST, exception, "get tid:{0,number,#}, oid:{1,number,#} throws", context.getTxnId(), oid); throw exception; } } /** {@inheritDoc} */ public T getForUpdate() { return getForUpdate(true); } /** * Like getForUpdate, but with optional checking of the context. Checking * the context should only be suppressed if the reference was just obtained * from the context. */ @SuppressWarnings("fallthrough") T getForUpdate(boolean checkContext) { RuntimeException exception = null; try { if (checkContext) { DataServiceImpl.checkContext(context); } switch (state) { case EMPTY: object = deserialize( context.store.getObject( context.txn, oid, !context.optimisticWriteLocks())); context.refs.registerObject(this); context.store.setObjectDescription(context.txn, oid, object); state = State.MODIFIED; break; case MAYBE_MODIFIED: if (!context.optimisticWriteLocks()) { context.store.markForUpdate(context.txn, oid); } unmodifiedBytes = null; state = State.MODIFIED; break; case NOT_MODIFIED: if (!context.optimisticWriteLocks()) { context.store.markForUpdate(context.txn, oid); } state = State.MODIFIED; break; case FLUSHED: exception = new TransactionNotActiveException( "Attempt to get the object associated with a managed" + " reference when its transaction is not active"); break; case NEW: case MODIFIED: break; case REMOVED_EMPTY: case REMOVED_FETCHED: throw new ObjectNotFoundException("The object is not found"); default: throw new AssertionError(); } } catch (TransactionNotActiveException e) { exception = new TransactionNotActiveException( "Attempt to get the object associated with a managed" + " reference when its transaction is not active: " + e.getMessage(), e); } catch (RuntimeException e) { exception = e; } if (exception == null) { if (logger.isLoggable(Level.FINEST)) { logger.log( Level.FINEST, "getForUpdate tid:{0,number,#}, oid:{1,number,#}" + " returns type:{2}", context.getTxnId(), oid, DataServiceImpl.typeName(object)); } @SuppressWarnings("unchecked") T result = (T) object; return result; } else { DataServiceImpl.getExceptionLogger(exception).logThrow( Level.FINEST, exception, "getForUpdate tid:{0,number,#}, type:{1}, oid:{2,number,#}" + " throws", context.getTxnId(), DataServiceImpl.typeName(object), oid); throw exception; } } /** {@inheritDoc} */ public BigInteger getId() { if (id == null) { id = BigInteger.valueOf(oid); } return id; } /** {@inheritDoc} */ public boolean equals(Object object) { if (object == this) { return true; } else if (object instanceof ManagedReferenceImpl) { /* * This implementation depends on the fact that references are * associated with the current data service, which is true since * the data service is obtained from the current context rather * than being represented explicitly within the reference. If it * were possible to compare references associated with different * data services, that would produce false equality for references * to objects in the different data services that happened to have * the same object IDs. -tjb@sun.com (11/09/2007) */ return oid == (((ManagedReferenceImpl) object).oid); } else { return false; } } /** {@inheritDoc} */ public int hashCode() { /* * Follow the suggestions in Effective Java to XOR the upper and lower * 32 bits of a long field, and add a non-zero constant. */ return (int) (oid ^ (oid >>> 32)) + 6883; } /* -- Implement Serializable -- */ /** Replaces this instance with a canonical instance. */ private Object readResolve() throws ObjectStreamException { context = DataServiceImpl.getContextNoJoin(); state = State.EMPTY; validate(); ManagedReferenceImpl ref = context.refs.find(oid); if (ref == null) { context.refs.add(this); return this; } else { return ref; } } /* -- Object methods -- */ public String toString() { return "ManagedReferenceImpl[oid:" + oid + ", state:" + state + "]"; } /* -- Other methods -- */ /** * Checks the consistency of the managed references table, throwing an * exception if a problem is found. */ static void checkAllState(Context context) { logger.log(Level.FINE, "Checking state"); try { context.refs.checkAllState(); } catch (AssertionError e) { logger.logThrow(Level.SEVERE, e, "State check failed"); throw e; } } /** * Checks the fields of this object to make sure they have valid values, * throwing an assertion error if a problem is found. */ @SuppressWarnings("fallthrough") void checkState() { switch (state) { case NEW: if (object == null) { throw new AssertionError("NEW with no object"); } else if (unmodifiedBytes != null) { throw new AssertionError("NEW with unmodifiedBytes"); } break; case EMPTY: case FLUSHED: case REMOVED_EMPTY: if (object != null) { throw new AssertionError(state + " with object"); } else if (unmodifiedBytes != null) { throw new AssertionError(state + " with unmodifiedBytes"); } break; case NOT_MODIFIED: case MODIFIED: case REMOVED_FETCHED: if (object == null) { throw new AssertionError(state + " with no object"); } else if (unmodifiedBytes != null) { throw new AssertionError(state + " with unmodifiedBytes"); } break; case MAYBE_MODIFIED: if (object == null) { throw new AssertionError( "MAYBE_MODIFIED with no object"); } else if (unmodifiedBytes == null) { throw new AssertionError( "MAYBE_MODIFIED with no unmodifiedBytes"); } break; default: throw new AssertionError(); } } /** Saves all object modifications to the data store. */ static void flushAll(Context context) { FlushInfo info = context.refs.flushModifiedObjects(); if (info != null) { context.store.setObjects( context.txn, info.getOids(), info.getDataArray()); } } /** * Returns the next object ID, or -1 if there are no more objects. Does * not return IDs for removed objects. Specifying -1 requests the first * ID. */ static long nextObjectId(Context context, long oid) { /* * Find the next newly created object, but only return it if there are * no existing objects with a lower ID. */ long nextNew = context.refs.nextNewObjectId(oid); long last = oid; while (true) { long nextOld = context.store.nextObjectId(context.txn, last); if (nextOld == -1 || (nextNew != -1 && nextOld > nextNew)) { return nextNew; } ManagedReferenceImpl ref = context.refs.find(nextOld); if (ref == null || !ref.isRemoved()) { return nextOld; } last = nextOld; } } /** * Returns any modifications that need to be stored to the data store, or * null if there are none, and changes the state to FLUSHED. */ @SuppressWarnings("fallthrough") byte[] flush() { byte[] result = null; switch (state) { case EMPTY: case REMOVED_EMPTY: break; case NEW: case MODIFIED: result = SerialUtil.serialize(object, context.classSerial); context.refs.unregisterObject(object); break; case MAYBE_MODIFIED: byte[] modified = SerialUtil.serialize(object, context.classSerial); if (!Arrays.equals(modified, unmodifiedBytes)) { result = modified; if (debugDetectLogger.isLoggable(Level.FINEST)) { debugDetectLogger.log( Level.FINEST, "Modified object was not marked for update: {0}", Objects.fastToString(object)); } } /* Fall through */ case NOT_MODIFIED: case REMOVED_FETCHED: context.refs.unregisterObject(object); break; case FLUSHED: throw new IllegalStateException("Object already flushed"); default: throw new AssertionError(); } object = null; unmodifiedBytes = null; state = State.FLUSHED; return result; } /** * Checks if the object has been marked removed. This method will return * false if the object was not removed in this transaction. */ boolean isRemoved() { return state == State.REMOVED_EMPTY || state == State.REMOVED_FETCHED; } /** * Checks if the object has been created in the current transaction and not * removed. */ boolean isNew() { return state == State.NEW; } /** Returns the object currently associated with this reference or null. */ ManagedObject getObject() { return object; } /** Validates the values of the context and oid fields. */ private void validate() { if (context == null) { throw new NullPointerException("The context must not be null"); } if (oid < 0) { throw new IllegalArgumentException("The oid must not be negative"); } } /** * Returns the managed object associated with serialized data. Checks that * the return value is not null. */ private ManagedObject deserialize(byte[] data) { Object obj = SerialUtil.deserialize(data, context.classSerial); if (obj == null) { throw new ObjectIOException( "Managed object must not deserialize to null", false); } else if (!(obj instanceof ManagedObject)) { throw new ObjectIOException( "Deserialized object must implement ManagedObject", false); } return (ManagedObject) obj; } }