/*
 * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package sun.rmi.transport;

import java.rmi.Remote;
import java.rmi.NoSuchObjectException;
import java.rmi.dgc.VMID;
import java.rmi.server.ObjID;
import java.rmi.server.Unreferenced;
import java.util.*;
import sun.rmi.runtime.Log;
import sun.rmi.runtime.RuntimeUtil;
import sun.rmi.server.Dispatcher;

/**
 * A target contains information pertaining to a remote object that
 * resides in this address space.  Targets are located via the
 * ObjectTable.
 */
public final class Target {
    /** object id for target */
    private final ObjID id;
    /** flag indicating whether target is subject to collection */
    private final boolean permanent;
    /** weak reference to remote object implementation */
    private final WeakRef weakImpl;
    /** dispatcher for remote object */
    private volatile Dispatcher disp;
    /** stub for remote object */
    private final Remote stub;
    /** set of clients that hold references to this target */
    private final Vector<VMID> refSet = new Vector<>();
    /** table that maps client endpoints to sequence numbers */
    private final Hashtable<VMID, SequenceEntry> sequenceTable =
        new Hashtable<>(5);
    /** context class loader in which target was created */
    private final ClassLoader ccl;
    /** number of pending/executing calls */
    private int callCount = 0;
    /** true if this target has been removed from the object table */
    private boolean removed = false;
    /**
     * the transport through which this target was exported and
     * through which remote calls will be allowed
     */
    private volatile Transport exportedTransport = null;

    /** number to identify next callback thread created here */
    private static int nextThreadNum = 0;

    /**
     * Construct a Target for a remote object "impl" with
     * a specific object id.
     *
     * If "permanent" is true, then the impl is pinned permanently
     * (the impl will not be collected via distributed and/or local
     * GC).  If "on" is false, than the impl is subject to
     * collection. Permanent objects do not keep a server from
     * exiting.
     */
    public Target(Remote impl, Dispatcher disp, Remote stub, ObjID id,
                  boolean permanent)
    {
        this.weakImpl = new WeakRef(impl, ObjectTable.reapQueue);
        this.disp = disp;
        this.stub = stub;
        this.id = id;

        /*
         * Fix for 4149366: so that downloaded parameter types unmarshalled
         * for this impl will be compatible with types known only to the
         * impl class's class loader (when it's not identical to the
         * exporting thread's context class loader), mark the impl's class
         * loader as the loader to use as the context class loader in the
         * server's dispatch thread while a call to this impl is being
         * processed (unless this exporting thread's context class loader is
         * a child of the impl's class loader, such as when a registry is
         * exported by an application, in which case this thread's context
         * class loader is preferred).
         */
        ClassLoader threadContextLoader =
            Thread.currentThread().getContextClassLoader();
        ClassLoader serverLoader = impl.getClass().getClassLoader();
        if (checkLoaderAncestry(threadContextLoader, serverLoader)) {
            this.ccl = threadContextLoader;
        } else {
            this.ccl = serverLoader;
        }

        this.permanent = permanent;
        if (permanent) {
            pinImpl();
        }
    }

    /**
     * Return true if the first class loader is a child of (or identical
     * to) the second class loader.  Either loader may be "null", which is
     * considered to be the parent of any non-null class loader.
     *
     * (utility method added for the 1.2beta4 fix for 4149366)
     */
    private static boolean checkLoaderAncestry(ClassLoader child,
                                               ClassLoader ancestor)
    {
        if (ancestor == null) {
            return true;
        } else if (child == null) {
            return false;
        } else {
            for (ClassLoader parent = child;
                 parent != null;
                 parent = parent.getParent())
            {
                if (parent == ancestor) {
                    return true;
                }
            }
            return false;
        }
    }

    /** Get the stub (proxy) object for this target
     */
    public Remote getStub() {
        return stub;
    }

    /**
     * Returns the object endpoint for the target.
     */
    ObjectEndpoint getObjectEndpoint() {
        return new ObjectEndpoint(id, exportedTransport);
    }

    /**
     * Get the weak reference for the Impl of this target.
     */
    WeakRef getWeakImpl() {
        return weakImpl;
    }

    /**
     * Returns the dispatcher for this remote object target.
     */
    Dispatcher getDispatcher() {
        return disp;
    }

    ClassLoader getContextClassLoader() {
        return ccl;
    }

    /**
     * Get the impl for this target.
     * Note: this may return null if the impl has been garbage collected.
     * (currently, there is no need to make this method public)
     */
    Remote getImpl() {
        return (Remote)weakImpl.get();
    }

    /**
     * Returns true if the target is permanent.
     */
    boolean isPermanent() {
        return permanent;
    }

    /**
     * Pin impl in target. Pin the WeakRef object so it holds a strong
     * reference to the object to it will not be garbage collected locally.
     * This way there is a single object responsible for the weak ref
     * mechanism.
     */
    synchronized void pinImpl() {
        weakImpl.pin();
    }

    /**
     * Unpin impl in target.  Weaken the reference to impl so that it
     * can be garbage collected locally. But only if there the refSet
     * is empty.  All of the weak/strong handling is in WeakRef
     */
    synchronized void unpinImpl() {
        /* only unpin if:
         * a) impl is not permanent, and
         * b) impl is not already unpinned, and
         * c) there are no external references (outside this
         *    address space) for the impl
         */
        if (!permanent && refSet.isEmpty()) {
            weakImpl.unpin();
        }
    }

    /**
     * Enable the transport through which remote calls to this target
     * are allowed to be set if it has not already been set.
     */
    void setExportedTransport(Transport exportedTransport) {
        if (this.exportedTransport == null) {
            this.exportedTransport = exportedTransport;
        }
    }

    /**
     * Add an endpoint to the remembered set.  Also adds a notifier
     * to call back if the address space associated with the endpoint
     * dies.
     */
    synchronized void referenced(long sequenceNum, VMID vmid) {
        // check sequence number for vmid
        SequenceEntry entry = sequenceTable.get(vmid);
        if (entry == null) {
            sequenceTable.put(vmid, new SequenceEntry(sequenceNum));
        } else if (entry.sequenceNum < sequenceNum) {
            entry.update(sequenceNum);
        } else  {
            // late dirty call; ignore.
            return;
        }

        if (!refSet.contains(vmid)) {
            /*
             * A Target must be pinned while its refSet is not empty.  It may
             * have become unpinned if external LiveRefs only existed in
             * serialized form for some period of time, or if a client failed
             * to renew its lease due to a transient network failure.  So,
             * make sure that it is pinned here; this fixes bugid 4069644.
             */
            pinImpl();
            if (getImpl() == null)      // too late if impl was collected
                return;

            if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
                DGCImpl.dgcLog.log(Log.VERBOSE, "add to dirty set: " + vmid);
            }

            refSet.addElement(vmid);

            DGCImpl.getDGCImpl().registerTarget(vmid, this);
        }
    }

    /**
     * Remove endpoint from remembered set.  If set becomes empty,
     * remove server from Transport's object table.
     */
    synchronized void unreferenced(long sequenceNum, VMID vmid, boolean strong)
    {
        // check sequence number for vmid
        SequenceEntry entry = sequenceTable.get(vmid);
        if (entry == null || entry.sequenceNum > sequenceNum) {
            // late clean call; ignore
            return;
        } else if (strong) {
            // strong clean call; retain sequenceNum
            entry.retain(sequenceNum);
        } else if (entry.keep == false) {
            // get rid of sequence number
            sequenceTable.remove(vmid);
        }

        if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
            DGCImpl.dgcLog.log(Log.VERBOSE, "remove from dirty set: " + vmid);
        }

        refSetRemove(vmid);
    }

    /**
     * Remove endpoint from the reference set.
     */
    private synchronized void refSetRemove(VMID vmid) {
        // remove notification request
        DGCImpl.getDGCImpl().unregisterTarget(vmid, this);

        if (refSet.removeElement(vmid) && refSet.isEmpty()) {
            // reference set is empty, so server can be garbage collected.
            // remove object from table.
            if (DGCImpl.dgcLog.isLoggable(Log.VERBOSE)) {
                DGCImpl.dgcLog.log(Log.VERBOSE,
                    "reference set is empty: target = " + this);
            }

            /*
             * If the remote object implements the Unreferenced interface,
             * invoke its unreferenced callback in a separate thread.
             */
            Remote obj = getImpl();
            if (obj instanceof Unreferenced unrefObj) {
                RuntimeUtil.newUserThread(() -> {
                    Thread.currentThread().setContextClassLoader(ccl);
                    unrefObj.unreferenced();
                }, "Unreferenced-" + nextThreadNum++, false).start();
                // REMIND: access to nextThreadNum not synchronized; you care?
            }

            unpinImpl();
        }
    }

    /**
     * Mark this target as not accepting new calls if any of the
     * following conditions exist: a) the force parameter is true,
     * b) the target's call count is zero, or c) the object is already
     * not accepting calls. Returns true if target is marked as not
     * accepting new calls; returns false otherwise.
     */
    synchronized boolean unexport(boolean force) {

        if ((force == true) || (callCount == 0) || (disp == null)) {
            disp = null;
            /*
             * Fix for 4331349: unpin object so that it may be gc'd.
             * Also, unregister all vmids referencing this target
             * so target can be gc'd.
             */
            unpinImpl();
            DGCImpl dgc = DGCImpl.getDGCImpl();
            Enumeration<VMID> enum_ = refSet.elements();
            while (enum_.hasMoreElements()) {
                VMID vmid = enum_.nextElement();
                dgc.unregisterTarget(vmid, this);
            }
            return true;
        } else {
            return false;
        }
    }

    /**
     * Mark this target as having been removed from the object table.
     */
    synchronized void markRemoved() {
        if (!(!removed)) { throw new AssertionError(); }

        removed = true;
        if (!permanent && callCount == 0) {
            ObjectTable.decrementKeepAliveCount();
        }

        if (exportedTransport != null) {
            exportedTransport.targetUnexported();
        }
    }

    /**
     * Increment call count.
     */
    synchronized void incrementCallCount() throws NoSuchObjectException {

        if (disp != null) {
            callCount ++;
        } else {
            throw new NoSuchObjectException("object not accepting new calls");
        }
    }

    /**
     * Decrement call count.
     */
    synchronized void decrementCallCount() {

        if (--callCount < 0) {
            throw new Error("internal error: call count less than zero");
        }

        /*
         * The "keep-alive count" is the number of non-permanent remote
         * objects that are either in the object table or still have calls
         * in progress.  Therefore, this state change may affect the
         * keep-alive count: if this target is for a non-permanent remote
         * object that has been removed from the object table and now has a
         * call count of zero, it needs to be decremented.
         */
        if (!permanent && removed && callCount == 0) {
            ObjectTable.decrementKeepAliveCount();
        }
    }

    /**
     * Returns true if remembered set is empty; otherwise returns
     * false
     */
    boolean isEmpty() {
        return refSet.isEmpty();
    }

    /**
     * This method is called if the address space associated with the
     * vmid dies.  In that case, the vmid should be removed
     * from the reference set.
     */
    public synchronized void vmidDead(VMID vmid) {
        if (DGCImpl.dgcLog.isLoggable(Log.BRIEF)) {
            DGCImpl.dgcLog.log(Log.BRIEF, "removing endpoint " +
                            vmid + " from reference set");
        }

        sequenceTable.remove(vmid);
        refSetRemove(vmid);
    }
}

class SequenceEntry {
    long sequenceNum;
    boolean keep;

    SequenceEntry(long sequenceNum) {
        this.sequenceNum = sequenceNum;
        keep = false;
    }

    void retain(long sequenceNum) {
        this.sequenceNum = sequenceNum;
        keep = true;
    }

    void update(long sequenceNum) {
        this.sequenceNum = sequenceNum;
    }
}
