/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
package org.eclipse.persistence.internal.sessions.remote;

import java.rmi.server.ObjID;
import java.io.*;
import org.eclipse.persistence.sessions.remote.*;
import org.eclipse.persistence.queries.*;
import org.eclipse.persistence.descriptors.ClassDescriptor;
import org.eclipse.persistence.internal.indirection.*;
import org.eclipse.persistence.internal.sessions.AbstractRecord;
import org.eclipse.persistence.indirection.*;
import org.eclipse.persistence.mappings.*;

/**
 * Remote value holders can be invoked locally and remotely.
 * In both situations the associated indirect object is invoked.
 */
public class RemoteValueHolder extends DatabaseValueHolder implements Externalizable {
    // This is a unique id for remote value holder.
    protected ObjID id;

    // Primary key row for target object.
    protected Object targetObjectPrimaryKeys;
    protected transient DatabaseMapping mapping;
    protected transient ObjectLevelReadQuery query;

    // The server side original value holder.
    protected transient ValueHolderInterface wrappedServerValueHolder;

    // point back to the object holding the remote value holder;
    // for the moment, used only by TransparentIndirection
    protected transient Object serverIndirectionObject;

    public RemoteValueHolder() {
        // This assigns unique id to the remote value holder when it is created.
        this.id = new ObjID();
    }

    public RemoteValueHolder(ObjID id) {
        this.id = id;
    }

    /**
     * If the reference object is mapped thru one to one mapping and the object derives its primary key value
     * from this relationship then remote value holder has a primary key row for.
     */
    protected boolean canDoCacheCheck() {
        if ((getMapping() == null) || (getQuery() == null)) {
            return false;
        }

        return (getTargetObjectPrimaryKeys() != null) && getMapping().isOneToOneMapping() && (!getQuery().shouldRefreshIdentityMapResult()) && (!getQuery().shouldRefreshRemoteIdentityMapResult()) && (getQuery().shouldMaintainCache());
    }

    /**
     * Only the id is checked for equality check.
     */
    public boolean equals(Object object) {
        if (!(object instanceof RemoteValueHolder)) {
            return false;
        }

        return getID().equals(((RemoteValueHolder)object).getID());
    }

    /**
     * This method is used to remove the RemoteValueHolder from the dispatcher on Garbage
     * collection from the client
     */
    public void finalize() {
        if ((this.session != null) && (this.session instanceof RemoteSession) && ((RemoteSession)this.session).shouldEnableDistributedIndirectionGarbageCollection()) {
            // This must be on the client
            RemoveServerSideRemoteValueHolderCommand command = new RemoveServerSideRemoteValueHolderCommand(getID());
            ((DistributedSession)getSession()).getRemoteConnection().processCommand(command);
            //remove this value holder from the session dispatcher as it will no longer be needed
        }
    }

    /**
     * Return the unique id.
     */
    public ObjID getID() {
        return id;
    }

    /**
     * Return the associated mapping.
     */
    public DatabaseMapping getMapping() {
        return mapping;
    }

    /**
     * Get object from the cache if there is one.
     */
    protected Object getObjectFromCache() {
        Object cachedObject = null;

        if (getMapping().isOneToOneMapping()) {
            ClassDescriptor descriptor = ((ObjectReferenceMapping)getMapping()).getReferenceDescriptor();
            cachedObject = getSession().getIdentityMapAccessorInstance().getFromIdentityMap(getTargetObjectPrimaryKeys(), descriptor.getJavaClass(), descriptor);
        }

        return cachedObject;
    }

    /**
     * Return the associated query.
     */
    public ObjectLevelReadQuery getQuery() {
        return query;
    }

    /**
     * Return the object on the server that holds on
     * to the remote value holder.
     * Currently used only by TransparentIndirection so we
     * can get back to the original IndirectContainer.
     */
    public Object getServerIndirectionObject() {
        return serverIndirectionObject;
    }

    /**
     * Get target object primary key row.
     */
    protected Object getTargetObjectPrimaryKeys() {
        return targetObjectPrimaryKeys;
    }

    /**
     * Return the original value holder.
     * This is null on the client side because it is tagged transient.
     * This is how we know whether the remote value holder is
     * being invoked on the client or on the server.
     */
    public ValueHolderInterface getWrappedServerValueHolder() {
        return wrappedServerValueHolder;
    }

    /**
     * Return the hashcode for id, because it is unqiue.
     */
    public int hashCode() {
        return getID().hashCode();
    }

    /**
     * Return the object.
     */
    public synchronized Object instantiate() {
        Object valueOfServerValueHolder = null;

        if (getWrappedServerValueHolder() != null) {// server invocation
            valueOfServerValueHolder = getWrappedServerValueHolder().getValue();
        } else {// client invocation
            // check whether object exists on the client
            if (canDoCacheCheck()) {
                valueOfServerValueHolder = getObjectFromCache();
            }

            // does not exist on the client - so invoke the value holder on the server
            if (valueOfServerValueHolder == null) {
                valueOfServerValueHolder = ((DistributedSession)getSession()).instantiateRemoteValueHolderOnServer(this);
            }
        }
        return valueOfServerValueHolder;
    }

    /**
     * INTERNAL:
     * Answers if this valueholder is easy to instantiate.
     * @return true if getValue() won't trigger a database read.
     */
    public boolean isEasilyInstantiated() {
        // Nothing is easily instantiated when on the client side.
        return this.isInstantiated || ((this.wrappedServerValueHolder != null)
                && (!(this.wrappedServerValueHolder instanceof DatabaseValueHolder) || ((DatabaseValueHolder)this.wrappedServerValueHolder).isEasilyInstantiated()));
    }

    /**
     * INTERNAL:
     * Answers if this valueholder is a pessimistic locking one.  Such valueholders
     * are special in that they can be triggered multiple times by different
     * UnitsOfWork.  Each time a lock query will be issued.  Hence even if
     * instantiated it may have to be instantiated again, and once instantatiated
     * all fields can not be reset.
     * Note: This method is not thread-safe.  It must be used in a synchronizaed manner
     */
    public boolean isPessimisticLockingValueHolder() {
        // This abstract method needs to be implemented but is not meaningfull for
        // this subclass.
        if (getWrappedServerValueHolder() != null) {
            return ((getWrappedServerValueHolder() instanceof DatabaseValueHolder) && ((DatabaseValueHolder)getWrappedServerValueHolder()).isPessimisticLockingValueHolder());
        } else {
            // Pessimistic locking may not be supported on remote sessions, but if
            // it is make every attempt to do the right thing.
            return ((getQuery() != null) && getQuery().isLockQuery(getSession()));
        }
    }

    /**
     * Triggers UnitOfWork valueholders directly without triggering the wrapped
     * valueholder (this).
     * <p>
     * When in transaction and/or for pessimistic locking the UnitOfWorkValueHolder
     * needs to be triggered directly without triggering the wrapped valueholder.
     * However only the wrapped valueholder knows how to trigger the indirection,
     * i.e. it may be a batchValueHolder, and it stores all the info like the row
     * and the query.
     * Note: This method is not thread-safe.  It must be used in a synchronizaed manner
     */
    public Object instantiateForUnitOfWorkValueHolder(UnitOfWorkValueHolder unitOfWorkValueHolder) {
        if ((getWrappedServerValueHolder() != null) && (getWrappedServerValueHolder() instanceof DatabaseValueHolder)) {
            DatabaseValueHolder wrapped = (DatabaseValueHolder)getWrappedServerValueHolder();
            return wrapped.instantiateForUnitOfWorkValueHolder(unitOfWorkValueHolder);
        }

        // The scenario of triggering a valueholder in transaction when
        // the RemoteUnitOfWork is on the client side may be impossible.
        return unitOfWorkValueHolder.buildCloneFor(getValue());
    }

    /**
     *  Override the default serialization for a remote valueholder so as not to serialize the value
     *  Note: Changed for bug 3145211.  We used to use the java.io.Serializable interface, but need to convert
     *  to Externalizable interface to avoid sending extra data through the superclass's serialization
     */
    public void readExternal(ObjectInput in) throws IOException, java.lang.ClassNotFoundException {
        this.id = (ObjID)in.readObject();
        this.targetObjectPrimaryKeys = in.readObject();
        this.row = (AbstractRecord)in.readObject();
        this.isInstantiated = in.readBoolean();
    }

    /**
     * Set the unique id.
     */
    protected void setID(ObjID anID) {
        this.id = anID;
    }

    /**
     * Set mapping
     */
    public void setMapping(DatabaseMapping mapping) {
        this.mapping = mapping;
    }

    /**
     * Set the query.
     */
    public void setQuery(ObjectLevelReadQuery query) {
        this.query = query;
    }

    /**
     * Set the object on the server that holds on
     * to the remote value holder.
     * Currently used only by TransparentIndirection so we
     * can get back to the original IndirectContainer.
     */
    public void setServerIndirectionObject(Object serverIndirectionObject) {
        this.serverIndirectionObject = serverIndirectionObject;
    }

    /**
     * Set target object primary keys.
     */
    public void setTargetObjectPrimaryKeys(Object primaryKeys) {
        this.targetObjectPrimaryKeys = primaryKeys;
    }

    /**
     * Set the object.
     */
    public void setValue(Object theValue) {
        super.setValue(theValue);
        if (getWrappedServerValueHolder() != null) {
            // This is a local setting of remote value holder
            // and will only happen with basic indirection
            getWrappedServerValueHolder().setValue(theValue);
        }
    }

    /**
     * Set the original value holder.
     */
    public void setWrappedServerValueHolder(ValueHolderInterface wrappedServerValueHolder) {
        this.wrappedServerValueHolder = wrappedServerValueHolder;
    }

    /**
     *  Override the default serialization for a remote valueholder so as not to serialize the value
     *  Note: Changed for bug 3145211.  We used to use the java.io.Serializable interface, but need to convert
     *  to Externalizable interface to avoid sending extra data through the superclass's serialization
     */
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(id);
        out.writeObject(targetObjectPrimaryKeys);
        out.writeObject(row);
        out.writeBoolean(false);
    }
}
