/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.openjpa.kernel;

import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.Modifier;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.TimeZone;
import java.util.UUID;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.openjpa.conf.OpenJPAConfiguration;
import org.apache.openjpa.enhance.DynamicPersistenceCapable;
import org.apache.openjpa.enhance.FieldManager;
import org.apache.openjpa.enhance.ManagedInstanceProvider;
import org.apache.openjpa.enhance.PCRegistry;
import org.apache.openjpa.enhance.PersistenceCapable;
import org.apache.openjpa.enhance.RedefinitionHelper;
import org.apache.openjpa.enhance.StateManager;
import org.apache.openjpa.event.LifecycleEvent;
import org.apache.openjpa.event.LifecycleEventManager;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.meta.AccessCode;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.meta.FetchGroup;
import org.apache.openjpa.meta.FieldMetaData;
import org.apache.openjpa.meta.JavaTypes;
import org.apache.openjpa.meta.UpdateStrategies;
import org.apache.openjpa.meta.ValueMetaData;
import org.apache.openjpa.meta.ValueStrategies;
import org.apache.openjpa.util.ApplicationIds;
import org.apache.openjpa.util.Exceptions;
import org.apache.openjpa.util.ImplHelper;
import org.apache.openjpa.util.InternalException;
import org.apache.openjpa.util.InvalidStateException;
import org.apache.openjpa.util.ObjectNotFoundException;
import org.apache.openjpa.util.OpenJPAId;
import org.apache.openjpa.util.ProxyManager;
import org.apache.openjpa.util.RuntimeExceptionTranslator;
import org.apache.openjpa.util.UserException;

/**
 * Implementation of the {@link OpenJPAStateManager} interface for use
 * with this runtime. Each state manager manages the state of a single
 * persistence capable instance. The state manager is also responsible for
 * all communications about the instance to the {@link StoreManager}.
 *  The state manager uses the State pattern in both its interaction with
 * the governed instance and its interaction with the broker.
 * In its interactions with the persistence capable instance, it uses the
 * {@link FieldManager} interface. Similarly, when interacting with the
 * broker, it uses the {@link PCState} singleton that represents
 * the current lifecycle state of the instance.
 *
 * @author Abe White
 */
public class StateManagerImpl implements OpenJPAStateManager, Serializable {
    
    private static final long serialVersionUID = 1L;
    public static final int LOAD_FGS = 0;
    public static final int LOAD_ALL = 1;
    public static final int LOAD_SERIALIZE = 2;

    private static final int FLAG_SAVE = 2 << 0;
    private static final int FLAG_DEREF = 2 << 1;
    private static final int FLAG_LOADED = 2 << 2;
    private static final int FLAG_READ_LOCKED = 2 << 3;
    private static final int FLAG_WRITE_LOCKED = 2 << 4;
    private static final int FLAG_OID_ASSIGNED = 2 << 5;
    private static final int FLAG_LOADING = 2 << 6;
    private static final int FLAG_PRE_DELETING = 2 << 7;
    private static final int FLAG_FLUSHED = 2 << 8;
    private static final int FLAG_PRE_FLUSHED = 2 << 9;
    private static final int FLAG_FLUSHED_DIRTY = 2 << 10;
    private static final int FLAG_IMPL_CACHE = 2 << 11;
    private static final int FLAG_INVERSES = 2 << 12;
    private static final int FLAG_NO_UNPROXY = 2 << 13;
    private static final int FLAG_VERSION_CHECK = 2 << 14;
    private static final int FLAG_VERSION_UPDATE = 2 << 15;
    private static final int FLAG_DETACHING = 2 << 16;
    private static final int FLAG_EMBED_DEREF = 2 << 17;

    private static final Localizer _loc = Localizer.forPackage
        (StateManagerImpl.class);

    // information about the instance
    private transient PersistenceCapable _pc = null;
    protected transient ClassMetaData _meta = null;
    protected BitSet _loaded = null;

    // Care needs to be taken when accessing these fields as they will can be null if no fields are
    // dirty, or have been flushed.
    private BitSet _dirty = null;
    private BitSet _flush = null;

    private BitSet _delayed = null;
    private int _flags = 0;

    // id is the state manager identity; oid is the persistent identity.  oid
    // may be null for embedded and transient-transactional objects or new
    // instances that haven't been assigned an oid.  id is reassigned to oid
    // on successful oid assignment (or flush completion if assignment is
    // during flush)
    private Object _id = null;
    private Object _oid = null;

    // the managing persistence manager and lifecycle state
    private transient BrokerImpl _broker; // this is serialized specially
    protected PCState _state = PCState.TRANSIENT;

    // the current and last loaded version indicators, and the lock object
    protected Object _version = null;
    protected Object _loadVersion = null;
    private Object _lock = null;
    private int _readLockLevel = -1;
    private int _writeLockLevel = -1;

    // delegates when providing/replacing instance data
    private SingleFieldManager _single = null;
    private SaveFieldManager _saved = null;
    private FieldManager _fm = null;

    // impldata; field impldata and intermediate data share the same array
    private Object _impl = null;
    protected Object[] _fieldImpl = null;

    // information about the owner of this instance, if it is embedded
    private StateManagerImpl _owner = null;
    // for embeddable object from query result
    private Object _ownerId = null;
    private int _ownerIndex = -1;
    private List<FieldMetaData> _mappedByIdFields = null;

    private transient ReentrantLock _instanceLock = null;

    private int _datePrecision = -1;

    /**
     * <p>set to <code>false</code> to prevent the postLoad method from
     * sending lifecycle callback events.</p>
     * <p>Callbacks are enabled by default</>
     */
    private boolean postLoadCallback = true;

    /**
     * Constructor; supply id, type metadata, and owning persistence manager.
     */
    protected StateManagerImpl(Object id, ClassMetaData meta, BrokerImpl broker) {
        _id = id;
        _meta = meta;
        _broker = broker;
        _single = new SingleFieldManager(this, broker);
        if (broker.getMultithreaded())
        	_instanceLock = new ReentrantLock();

        if (_meta.getIdentityType() == ClassMetaData.ID_UNKNOWN && !_meta.isEmbeddable())
            throw new UserException(_loc.get("meta-unknownid", _meta));
    }

    /**
     * Create a new StateManager instance based on the StateManager provided. A
     * new PersistenceCapable instance will be created and associated with the
     * new StateManager. All fields will be copied into the ne PC instance as
     * well as the dirty, loaded, and flushed bitsets.
     *
     * @param sm A statemanager instance which will effectively be cloned.
     */
    public StateManagerImpl(StateManagerImpl sm) {
        this(sm, sm.getPCState());
    }

    /**
     * Create a new StateManager instance, optionally overriding the state
     * (FLUSHED, DELETED, etc) of the underlying PersistenceCapable instance).
     *
     * @param sm
     *            A statemanager instance which will effectively be cloned.
     * @param newState
     *            The new state of the underlying persistence capable object.
     */
    public StateManagerImpl(StateManagerImpl sm, PCState newState) {
        this(sm.getId(), sm.getMetaData(), sm.getBroker());

        PersistenceCapable origPC = sm.getPersistenceCapable();
        _pc = origPC.pcNewInstance(sm, false);

        int[] fields = new int[sm.getMetaData().getFields().length];
        for (int i = 0; i < fields.length; i++) {
            fields[i] = i;
        }
        _pc.pcCopyFields(origPC, fields);
        _pc.pcReplaceStateManager(this);
        _state = newState;

        // clone the field bitsets.
        _dirty=(BitSet)sm.getDirty().clone();
        _loaded = (BitSet)sm.getLoaded().clone();
        _flush = (BitSet) sm.getFlushed().clone();
        _version = sm.getVersion();

        _oid = sm.getObjectId();
        _id = sm.getId();

        // more data may need to be copied.
    }

    /**
     * Set the owning state and field if this is an embedded instance.
     */
    void setOwner(StateManagerImpl owner, ValueMetaData ownerMeta) {
        _owner = owner;
        _ownerIndex = ownerMeta.getFieldMetaData().getIndex();
    }

    /**
     * Whether this state manager is in the middle of a load.
     */
    boolean isLoading() {
        return (_flags & FLAG_LOADING) > 0;
    }

    /**
     * Whether this state manager is in the middle of a load initiated
     * by outside code; for any internal methods that cause loading, the
     * loading flag is set automatically.
     */
    void setLoading(boolean loading) {
        if (loading)
            _flags |= FLAG_LOADING;
        else
            _flags &= ~FLAG_LOADING;
    }

    /**
     * Set or reset the lifecycle state of the managed instance. If the
     * transactional state of the instance changes, it will be enlisted/
     * delisted from the current transaction as necessary. The given
     * state will be initialized after being set. If the given state
     * is the same as the current state, this method will have no effect.
     */
    private void setPCState(PCState state) {
        if (_state == state)
            return;

        PCState prev = _state;
        lock();
        try {
            // notify the store manager that we're changing states; can veto
            _broker.getStoreManager().beforeStateChange(this, _state, state);

            // replace state
            boolean wasDeleted = _state.isDeleted();
            boolean wasDirty = _state.isDirty();
            boolean wasPending = _state.isPendingTransactional();
            _state = state;

            // enlist/delist from transaction
            if (_state.isTransactional()) {
                _broker.addToTransaction(this);
                if (_state.isDeleted() != wasDeleted)
                    _broker.setDirty(this, !wasDirty || isFlushed());
                else if (_state.isDirty() && !wasDirty)
                    _broker.setDirty(this, true);
            } else if (!wasPending && _state.isPendingTransactional())
                _broker.addToPendingTransaction(this);
            else if (wasPending && !_state.isPendingTransactional())
                _broker.removeFromPendingTransaction(this);
            else
                _broker.removeFromTransaction(this);

            _state.initialize(this, prev);
            if (_state.isDeleted() && !wasDeleted)
                fireLifecycleEvent(LifecycleEvent.AFTER_DELETE);
        } finally {
            unlock();
        }
    }

    //////////////////////////////////////
    // OpenJPAStateManager implementation
    //////////////////////////////////////

    @Override
    public void initialize(Class cls, PCState state) {
        // check to see if our current object id instance is the
        // correct id type for the specified class; this is for cases
        // when we have an application id hierarchy and we had set the
        // metadata to a superclass id -- the subclass' id may be a
        // different class, so we need to reset it
        if (_meta.getDescribedType() != cls) {
            ClassMetaData sub = _meta.getRepository().getMetaData
                (cls, _broker.getClassLoader(), true);
            if (_oid != null) {
                if (_meta.getIdentityType() == ClassMetaData.ID_DATASTORE)
                    _oid = _broker.getStoreManager().copyDataStoreId(_oid,
                        sub);
                else if (_meta.isOpenJPAIdentity())
                    _oid = ApplicationIds.copy(_oid, sub);
                else if (sub.getObjectIdType() != _meta.getObjectIdType()) {
                    Object[] pkFields = ApplicationIds.toPKValues(_oid, _meta);
                    _oid = ApplicationIds.fromPKValues(pkFields, sub);
                }
            }
            _meta = sub;
        }

        PersistenceCapable inst = PCRegistry.newInstance(cls, this, _oid, true);
        if (inst == null) {
            // the instance was null: check to see if the instance is
            // abstract (as can sometimes be the case when the
            // class discriminator strategy is not configured correctly)
            if (Modifier.isAbstract(cls.getModifiers()))
                throw new UserException(_loc.get("instantiate-abstract",
                    cls.getName(), _oid));
            throw new InternalException();
        }

        initialize(inst, state);
    }

    /**
     * Initialize with the given instance and state.
     */
    protected void initialize(PersistenceCapable pc, PCState state) {
        if (pc == null)
            throw new UserException(_loc.get("init-null-pc", _meta));
        if (pc.pcGetStateManager() != null && pc.pcGetStateManager() != this)
            throw new UserException(_loc.get("init-sm-pc",
                Exceptions.toString(pc))).setFailedObject(pc);
        pc.pcReplaceStateManager(this);

        FieldMetaData[] fmds = _meta.getFields();
        _loaded = new BitSet(fmds.length);

        // mark primary key and non-persistent fields as loaded
        for(int i : _meta.getPkAndNonPersistentManagedFmdIndexes()){
            _loaded.set(i);
        }

        _mappedByIdFields = _meta.getMappyedByIdFields();

        // record whether there are any managed inverse fields
        if (_broker.getInverseManager() != null && _meta.hasInverseManagedFields())
            _flags |= FLAG_INVERSES;

        pc.pcSetDetachedState(null);
        _pc = pc;

        if (_oid instanceof OpenJPAId)
            ((OpenJPAId) _oid).setManagedInstanceType(_meta.getDescribedType());

        // initialize our state and add ourselves to the broker's cache
        setPCState(state);
        if ( _oid == null ||
            _broker.getStateManagerImplById(_oid, false) == null) {
        	_broker.setStateManager(_id, this, BrokerImpl.STATUS_INIT);
        }
        if (state == PCState.PNEW)
            fireLifecycleEvent(LifecycleEvent.AFTER_PERSIST);

        // if this is a non-tracking PC, add a hard ref to the appropriate data
        // sets and give it an opportunity to make a state snapshot.
        if (!isIntercepting()) {
            saveFields(true);
            if (!isNew())
                RedefinitionHelper.assignLazyLoadProxies(this);
        }
    }

    /**
     * Whether or not data access in this instance is intercepted. This differs
     * from {@link ClassMetaData#isIntercepting()} in that it checks for
     * property access + subclassing in addition to the redefinition /
     * enhancement checks.
     *
     * @since 1.0.0
     */
    public boolean isIntercepting() {
        if (getMetaData().isIntercepting())
            return true;
        // TODO:JRB Intercepting
        if (AccessCode.isProperty(getMetaData().getAccessType())
            && _pc instanceof DynamicPersistenceCapable)
            return true;
        return false;
    }

    /**
     * Fire the given lifecycle event to all listeners.
     */
    private boolean fireLifecycleEvent(int type) {
        if (type == LifecycleEvent.AFTER_PERSIST
                && _broker.getConfiguration().getCallbackOptionsInstance().getPostPersistCallbackImmediate()) {
            fetchObjectId();
        }
        return _broker.fireLifecycleEvent(getManagedInstance(), null, _meta, type);
    }

    @Override
    public void load(FetchConfiguration fetch) {
        load(fetch, LOAD_FGS, null, null, false);
    }

    /**
     * Load the state of this instance based on the given fetch configuration
     * and load mode. Return true if any data was loaded, false otherwise.
     */
    protected boolean load(FetchConfiguration fetch, int loadMode,
        BitSet exclude, Object sdata, boolean forWrite) {
        if (!forWrite
            && (!isPersistent() || (isNew() && !isFlushed()) || isDeleted()))
            return false;

        // if any fields being loaded, do state transitions for read
        BitSet fields = getUnloadedInternal(fetch, loadMode, exclude);
        boolean active = _broker.isActive();
        if (!forWrite && fields != null)
            beforeRead(-1);

        // call load even if no fields are being loaded, because it takes
        // care of checking if the DFG is loaded, making sure version info
        // is loaded, etc
        int lockLevel = calculateLockLevel(active, forWrite, fetch);
        boolean ret = loadFields(fields, fetch, lockLevel, sdata);
        obtainLocks(active, forWrite, lockLevel, fetch, sdata);
        return ret;
    }

    @Override
    public Object getManagedInstance() {
        if (_pc instanceof ManagedInstanceProvider)
            return ((ManagedInstanceProvider) _pc).getManagedInstance();
        else
            return _pc;
    }

    @Override
    public PersistenceCapable getPersistenceCapable() {
        return _pc;
    }

    @Override
    public ClassMetaData getMetaData() {
        return _meta;
    }

    @Override
    public OpenJPAStateManager getOwner() {
        return _owner;
    }

    @Override
    public int getOwnerIndex() {
        return _ownerIndex;
    }

    public void setOwner(Object oid) {
        _ownerId = oid;
    }

    @Override
    public boolean isEmbedded() {
        // _owner may not be set if embed object is from query result
        return _owner != null || _state instanceof ENonTransState;
    }

    @Override
    public boolean isFlushed() {
        return (_flags & FLAG_FLUSHED) > 0;
    }

    @Override
    public boolean isFlushedDirty() {
        return (_flags & FLAG_FLUSHED_DIRTY) > 0;
    }

    @Override
    public BitSet getLoaded() {
        return _loaded;
    }

    @Override
    public BitSet getUnloaded(FetchConfiguration fetch) {
        // collect fields to load from data store based on fetch configuration
        BitSet fields = getUnloadedInternal(fetch, LOAD_FGS, null);
        return (fields == null) ? new BitSet(0) : fields;
    }

    /**
     * Internal version of {@link OpenJPAStateManager#getUnloaded} that avoids
     * creating an empty bit set by returning null when there are no unloaded
     * fields.
     */
    private BitSet getUnloadedInternal(FetchConfiguration fetch, int mode,
        BitSet exclude) {
        if (exclude == StoreContext.EXCLUDE_ALL)
            return null;

        BitSet fields = null;
        FieldMetaData[] fmds = _meta.getFields();
        boolean load;
        for (int i = 0; i < fmds.length; i++) {
            if (_loaded.get(i) || (exclude != null && exclude.get(i)))
                continue;

            switch (mode) {
                case LOAD_SERIALIZE:
                    load = !fmds[i].isTransient();
                    break;
                case LOAD_FGS:
                    load = fetch == null || fetch.requiresFetch(fmds[i])
                        != FetchConfiguration.FETCH_NONE;
                    break;
                default: // LOAD_ALL
                    load = true;
            }

            if (load) {
                if (fields == null)
                    fields = new BitSet(fmds.length);
                fields.set(i);
            }
        }
        return fields;
    }

    @Override
    public StoreContext getContext() {
        return _broker;
    }

    /**
     * Managing broker.
     */
    BrokerImpl getBroker() {
        return _broker;
    }

    @Override
    public Object getId() {
        return _id;
    }

    @Override
    public Object getObjectId() {
        StateManagerImpl sm = this;
        while (sm.getOwner() != null)
            sm = (StateManagerImpl) sm.getOwner();
        if (sm.isEmbedded() && sm.getOwner() == null)
            return sm._ownerId;
        return sm._oid;
    }

    @Override
    public void setObjectId(Object oid) {
        _oid = oid;
        if (_pc != null && oid instanceof OpenJPAId)
            ((OpenJPAId) oid).setManagedInstanceType(_meta.getDescribedType());
    }

    public StateManagerImpl getObjectIdOwner() {
        StateManagerImpl sm = this;
        while (sm.getOwner() != null)
            sm = (StateManagerImpl) sm.getOwner();
        return sm;
    }
    @Override
    public boolean assignObjectId(boolean flush) {
        lock();
        try {
            return assignObjectId(flush, false);
        } finally {
            unlock();
        }
    }

    /**
     * Ask store manager to assign our oid, optionally flushing and
     * optionally recaching on the new oid.
     */
    boolean assignObjectId(boolean flush, boolean preFlushing) {
        if (_oid != null || isEmbedded() || !isPersistent())
            return true;

        if (_broker.getStoreManager().assignObjectId(this, preFlushing)) {
            if (!preFlushing)
                assertObjectIdAssigned(true);
        } else if (flush)
            _broker.flush();
        else
            return false;
        return true;
    }

    /**
     * Make sure we were assigned an oid, and perform actions to make it
     * permanent.
     *
     * @param recache whether to recache ourself on the new oid
     */
    private void assertObjectIdAssigned(boolean recache) {
        if (!isNew() || isDeleted() || isProvisional()
            || (_flags & FLAG_OID_ASSIGNED) != 0)
            return;
        if (_oid == null) {
            if (_meta.getIdentityType() == ClassMetaData.ID_DATASTORE)
                throw new InternalException(Exceptions.toString
                    (getManagedInstance()));
            _oid = ApplicationIds.create(_pc, _meta);
        }

        Object orig = _id;
        _id = _oid;
        if (recache) {
            try {
                _broker.setStateManager(orig, this,
                    BrokerImpl.STATUS_OID_ASSIGN);
            } catch (RuntimeException re) {
                _id = orig;
                _oid = null;
                throw re;
            }
        }
        _flags |= FLAG_OID_ASSIGNED;
    }

    /**
     * Assign the proper generated value to the given field based on its
     * value-strategy.
     */
    private boolean assignField(int field, boolean preFlushing) {
        OpenJPAStateManager sm = this;
        while (sm != null && sm.isEmbedded())
            sm = sm.getOwner();
        if (sm == null)
            return false;
        if (!sm.isNew() || sm.isFlushed() || sm.isDeleted())
            return false;

        // special-case oid fields, which require us to look inside the oid
        // object
        FieldMetaData fmd = _meta.getField(field);
        if (fmd.getDeclaredTypeCode() == JavaTypes.OID) {
            // try to shortcut if possible
            if (_oid != null || isEmbedded() || !isPersistent())
                return true;

            // check embedded fields of oid for value strategy + default value
            FieldMetaData[] pks = fmd.getEmbeddedMetaData().getFields();
            OpenJPAStateManager oidsm = null;
            boolean assign = false;
            for (int i = 0; !assign && i < pks.length; i++) {
                if (pks[i].getValueStrategy() == ValueStrategies.NONE)
                    continue;
                if (oidsm == null)
                    oidsm = new ObjectIdStateManager(fetchObjectField(field),
                        this, fmd);
                assign = oidsm.isDefaultValue(i);
            }
            return assign && assignObjectId(!preFlushing, preFlushing);
        }

        // Just return if there's no value generation strategy
        if (fmd.getValueStrategy() == ValueStrategies.NONE)
            return false;

        // Throw exception if field already has a value assigned.
        // @GeneratedValue overrides POJO initial values and setter methods
        if (!fmd.isValueGenerated() && !isDefaultValue(field))
            throw new InvalidStateException(_loc.get(
                "existing-value-override-excep", fmd.getFullName(false)));

        // for primary key fields, assign the object id and recache so that
        // to the user, so it looks like the oid always matches the pk fields
        if (fmd.isPrimaryKey() && !isEmbedded())
            return assignObjectId(!preFlushing, preFlushing);

        // for other fields just assign the field or flush if needed
        if (_broker.getStoreManager().assignField(this, field, preFlushing)) {
            fmd.setValueGenerated(true);
            return true;
        }
        if (!preFlushing)
            _broker.flush();
        return !preFlushing;
    }

    @Override
    public Object getLock() {
        return _lock;
    }

    @Override
    public void setLock(Object lock) {
        _lock = lock;
    }

    @Override
    public Object getVersion() {
        return _version;
    }

    @Override
    public void setVersion(Object version) {
        _loadVersion = version;
        assignVersionField(version);
    }

    Object getLoadVersion() {
        return _loadVersion;
    }

    @Override
    public void setNextVersion(Object version) {
        assignVersionField(version);
    }

    public static Timestamp roundTimestamp(Timestamp val, int datePrecision) {
        // ensure that we do not insert dates at a greater precision than
        // that at which they will be returned by a SELECT
        int rounded = (int) Math.round(val.getNanos() / (double) datePrecision);
        long time = val.getTime();
        int nanos = rounded * datePrecision;
        if (nanos > 999999999) {
            // rollover to next second
            time = time + 1000;
            nanos = 0;
        }

        val = new Timestamp(time);
        val.setNanos(nanos);
        return val;
    }

    private void assignVersionField(Object version) {

        if (version instanceof Timestamp) {
            if (_datePrecision == -1) {
                try {
                    OpenJPAConfiguration conf = _broker.getConfiguration();
                    Class confCls = Class.forName("org.apache.openjpa.jdbc.conf.JDBCConfigurationImpl");
                    if (confCls.isAssignableFrom(conf.getClass())) {
                        Object o = conf.getClass().getMethod("getDBDictionaryInstance").invoke(conf, (Object[]) null);
                        _datePrecision = o.getClass().getField("datePrecision").getInt(o);
                    } else {
                        _datePrecision = 1000;
                    }
                } catch (Throwable e) {
                    _datePrecision = 1000;
                }
            }

            version = roundTimestamp((Timestamp) version, _datePrecision);
        }
        _version = version;
        FieldMetaData vfield = _meta.getVersionField();
        if (vfield != null)
            store(vfield.getIndex(), JavaTypes.convert(version, vfield.getTypeCode()));
    }

    @Override
    public PCState getPCState() {
        return _state;
    }

    @Override
    public synchronized Object getImplData() {
        return _impl;
    }

    @Override
    public synchronized Object setImplData(Object data, boolean cacheable) {
        Object old = _impl;
        _impl = data;
        if (cacheable && data != null)
            _flags |= FLAG_IMPL_CACHE;
        else
            _flags &= ~FLAG_IMPL_CACHE;
        return old;
    }

    @Override
    public boolean isImplDataCacheable() {
        return (_flags & FLAG_IMPL_CACHE) != 0;
    }

    @Override
    public Object getImplData(int field) {
        return getExtraFieldData(field, true);
    }

    @Override
    public Object setImplData(int field, Object data) {
        return setExtraFieldData(field, data, true);
    }

    @Override
    public synchronized boolean isImplDataCacheable(int field) {
        if (_fieldImpl == null || !_loaded.get(field))
            return false;
        if (_meta.getField(field).usesImplData() != null)
            return false;
        int idx = _meta.getExtraFieldDataIndex(field);
        return idx != -1 && _fieldImpl[idx] != null;
    }

    @Override
    public Object getIntermediate(int field) {
        return getExtraFieldData(field, false);
    }

    @Override
    public void setIntermediate(int field, Object data) {
        setExtraFieldData(field, data, false);
    }

    /**
     * Return the data from the proper index of the extra field data array.
     */
    protected synchronized Object getExtraFieldData(int field, boolean isLoaded) {
        // only return the field data if the field is in the right loaded
        // state; otherwise we might return intermediate for impl data or
        // vice versa
        if (_fieldImpl == null || _loaded.get(field) != isLoaded)
            return null;
        int idx = _meta.getExtraFieldDataIndex(field);
        return (idx == -1) ? null : _fieldImpl[idx];
    }

    /**
     * Set the data from to proper index of the extra field data array.
     */
    private synchronized Object setExtraFieldData(int field, Object data,
        boolean loaded) {
        int idx = _meta.getExtraFieldDataIndex(field);
        if (idx == -1)
            throw new InternalException(String.valueOf(_meta.getField(field)));

        Object old = (_fieldImpl == null) ? null : _fieldImpl[idx];
        if (data != null) {
            // cannot set if field in wrong loaded state
            if (_loaded.get(field) != loaded)
                throw new InternalException(String.valueOf(_meta.getField
                    (field)));

            // set data
            if (_fieldImpl == null)
                _fieldImpl = new Object[_meta.getExtraFieldDataLength()];
            _fieldImpl[idx] = data;
        } else if (_fieldImpl != null && _loaded.get(field) == loaded)
            _fieldImpl[idx] = null;
        return old;
    }

    @Override
    public Object fetch(int field) {
        Object val = fetchField(field, false);
        return _meta.getField(field).getExternalValue(val, _broker);
    }

    @Override
    public Object fetchField(int field, boolean transitions) {
        FieldMetaData fmd = _meta.getField(field);
        if (fmd == null)
            throw new UserException(_loc.get("no-field",
                String.valueOf(field), getManagedInstance().getClass())).
                setFailedObject(getManagedInstance());

        // do normal state transitions
        if (!fmd.isPrimaryKey() && transitions)
            accessingField(field);

        switch (fmd.getDeclaredTypeCode()) {
            case JavaTypes.STRING:
                return fetchStringField(field);
            case JavaTypes.OBJECT:
                return fetchObjectField(field);
            case JavaTypes.BOOLEAN:
                return (fetchBooleanField(field)) ? Boolean.TRUE
                    : Boolean.FALSE;
            case JavaTypes.BYTE:
                return fetchByteField(field);
            case JavaTypes.CHAR:
                return fetchCharField(field);
            case JavaTypes.DOUBLE:
                return fetchDoubleField(field);
            case JavaTypes.FLOAT:
                return fetchFloatField(field);
            case JavaTypes.INT:
                return fetchIntField(field);
            case JavaTypes.LONG:
                return fetchLongField(field);
            case JavaTypes.SHORT:
                return fetchShortField(field);
            default:
                return fetchObjectField(field);
        }
    }

    @Override
    public void store(int field, Object val) {
