/*
 * Copyright (c) 1997, 2012, 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.server;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.ServerSocket;
import java.rmi.MarshalledObject;
import java.rmi.NoSuchObjectException;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.activation.Activatable;
import java.rmi.activation.ActivationDesc;
import java.rmi.activation.ActivationException;
import java.rmi.activation.ActivationGroup;
import java.rmi.activation.ActivationGroupID;
import java.rmi.activation.ActivationID;
import java.rmi.activation.UnknownObjectException;
import java.rmi.server.RMIClassLoader;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.RMISocketFactory;
import java.rmi.server.UnicastRemoteObject;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import sun.rmi.registry.RegistryImpl;

/**
 * The default activation group implementation.
 *
 * @author      Ann Wollrath
 * @since       1.2
 * @see         java.rmi.activation.ActivationGroup
 */
public class ActivationGroupImpl extends ActivationGroup {

    // use serialVersionUID from JDK 1.2.2 for interoperability
    private static final long serialVersionUID = 5758693559430427303L;

    /** maps persistent IDs to activated remote objects */
    private final Hashtable<ActivationID,ActiveEntry> active =
        new Hashtable<>();
    private boolean groupInactive = false;
    private final ActivationGroupID groupID;
    private final List<ActivationID> lockedIDs = new ArrayList<>();

    /**
     * Creates a default activation group implementation.
     *
     * @param id the group's identifier
     * @param data ignored
     */
    public ActivationGroupImpl(ActivationGroupID id, MarshalledObject<?> data)
        throws RemoteException
    {
        super(id);
        groupID = id;

        /*
         * Unexport activation group impl and attempt to export it on
         * an unshared anonymous port.  See 4692286.
         */
        unexportObject(this, true);
        RMIServerSocketFactory ssf = new ServerSocketFactoryImpl();
        UnicastRemoteObject.exportObject(this, 0, null, ssf);

        if (System.getSecurityManager() == null) {
            try {
                // Provide a default security manager.
                System.setSecurityManager(new SecurityManager());

            } catch (Exception e) {
                throw new RemoteException("unable to set security manager", e);
            }
        }
    }

    /**
     * Trivial server socket factory used to export the activation group
     * impl on an unshared port.
     */
    private static class ServerSocketFactoryImpl
        implements RMIServerSocketFactory
    {
        public ServerSocket createServerSocket(int port) throws IOException
        {
            RMISocketFactory sf = RMISocketFactory.getSocketFactory();
            if (sf == null) {
                sf = RMISocketFactory.getDefaultSocketFactory();
            }
            return sf.createServerSocket(port);
        }
    }

    /*
     * Obtains a lock on the ActivationID id before returning. Allows only one
     * thread at a time to hold a lock on a particular id.  If the lock for id
     * is in use, all requests for an equivalent (in the Object.equals sense)
     * id will wait for the id to be notified and use the supplied id as the
     * next lock. The caller of "acquireLock" must execute the "releaseLock"
     * method" to release the lock and "notifyAll" waiters for the id lock
     * obtained from this method.  The typical usage pattern is as follows:
     *
     * try {
     *    acquireLock(id);
     *    // do stuff pertaining to id...
     * } finally {
     *    releaseLock(id);
     *    checkInactiveGroup();
     * }
     */
    private void acquireLock(ActivationID id) {

        ActivationID waitForID;

        for (;;) {

            synchronized (lockedIDs) {
                int index = lockedIDs.indexOf(id);
                if (index < 0) {
                    lockedIDs.add(id);
                    return;
                } else {
                    waitForID = lockedIDs.get(index);
                }
            }

            synchronized (waitForID) {
                synchronized (lockedIDs) {
                    int index = lockedIDs.indexOf(waitForID);
                    if (index < 0) continue;
                    ActivationID actualID = lockedIDs.get(index);
                    if (actualID != waitForID)
                        /*
                         * don't wait on an id that won't be notified.
                         */
                        continue;
                }

                try {
                    waitForID.wait();
                } catch (InterruptedException ignore) {
                }
            }
        }

    }

    /*
     * Releases the id lock obtained via the "acquireLock" method and then
     * notifies all threads waiting on the lock.
     */
    private void releaseLock(ActivationID id) {
        synchronized (lockedIDs) {
            id = lockedIDs.remove(lockedIDs.indexOf(id));
        }

        synchronized (id) {
            id.notifyAll();
        }
    }

    /**
     * Creates a new instance of an activatable remote object. The
     * <code>Activator</code> calls this method to create an activatable
     * object in this group. This method should be idempotent; a call to
     * activate an already active object should return the previously
     * activated object.
     *
     * Note: this method assumes that the Activator will only invoke
     * newInstance for the same object in a serial fashion (i.e.,
     * the activator will not allow the group to see concurrent requests
     * to activate the same object.
     *
     * @param id the object's activation identifier
     * @param desc the object's activation descriptor
     * @return a marshalled object containing the activated object's stub
     */
    public MarshalledObject<? extends Remote>
                                      newInstance(final ActivationID id,
                                                  final ActivationDesc desc)
        throws ActivationException, RemoteException
    {
        RegistryImpl.checkAccess("ActivationInstantiator.newInstance");

        if (!groupID.equals(desc.getGroupID()))
            throw new ActivationException("newInstance in wrong group");

        try {
            acquireLock(id);
            synchronized (this) {
                if (groupInactive == true)
                    throw new InactiveGroupException("group is inactive");
            }

            ActiveEntry entry = active.get(id);
            if (entry != null)
                return entry.mobj;

            String className = desc.getClassName();

            final Class<? extends Remote> cl =
                RMIClassLoader.loadClass(desc.getLocation(), className)
                .asSubclass(Remote.class);
            Remote impl = null;

            final Thread t = Thread.currentThread();
            final ClassLoader savedCcl = t.getContextClassLoader();
            ClassLoader objcl = cl.getClassLoader();
            final ClassLoader ccl = covers(objcl, savedCcl) ? objcl : savedCcl;

            /*
             * Fix for 4164971: allow non-public activatable class
             * and/or constructor, create the activatable object in a
             * privileged block
             */
            try {
                /*
                 * The code below is in a doPrivileged block to
                 * protect against user code which code might have set
                 * a global socket factory (in which case application
                 * code would be on the stack).
                 */
                impl = AccessController.doPrivileged(
                      new PrivilegedExceptionAction<Remote>() {
                      public Remote run() throws InstantiationException,
                          NoSuchMethodException, IllegalAccessException,
                          InvocationTargetException
                      {
                          Constructor<? extends Remote> constructor =
                              cl.getDeclaredConstructor(
                                  ActivationID.class, MarshalledObject.class);
                          constructor.setAccessible(true);
                          try {
                              /*
                               * Fix for 4289544: make sure to set the
                               * context class loader to be the class
                               * loader of the impl class before
                               * constructing that class.
                               */
                              t.setContextClassLoader(ccl);
                              return constructor.newInstance(id,
                                                             desc.getData());
                          } finally {
                              t.setContextClassLoader(savedCcl);
                          }
                      }
                  });
            } catch (PrivilegedActionException pae) {
                Throwable e = pae.getException();

                // narrow the exception's type and rethrow it
                if (e instanceof InstantiationException) {
                    throw (InstantiationException) e;
                } else if (e instanceof NoSuchMethodException) {
                    throw (NoSuchMethodException) e;
                } else if (e instanceof IllegalAccessException) {
                    throw (IllegalAccessException) e;
                } else if (e instanceof InvocationTargetException) {
                    throw (InvocationTargetException) e;
                } else if (e instanceof RuntimeException) {
                    throw (RuntimeException) e;
                } else if (e instanceof Error) {
                    throw (Error) e;
                }
            }

            entry = new ActiveEntry(impl);
            active.put(id, entry);
            return entry.mobj;

        } catch (NoSuchMethodException | NoSuchMethodError e) {
            /* user forgot to provide activatable constructor?
             * or code recompiled and user forgot to provide
             *  activatable constructor?
             */
            throw new ActivationException
                ("Activatable object must provide an activation"+
                 " constructor", e );

        } catch (InvocationTargetException e) {
            throw new ActivationException("exception in object constructor",
                                          e.getTargetException());

        } catch (Exception e) {
            throw new ActivationException("unable to activate object", e);
        } finally {
            releaseLock(id);
            checkInactiveGroup();
        }
    }


   /**
    * The group's <code>inactiveObject</code> method is called
    * indirectly via a call to the <code>Activatable.inactive</code>
    * method. A remote object implementation must call
    * <code>Activatable</code>'s <code>inactive</code> method when
    * that object deactivates (the object deems that it is no longer
    * active). If the object does not call
    * <code>Activatable.inactive</code> when it deactivates, the
    * object will never be garbage collected since the group keeps
    * strong references to the objects it creates. <p>
    *
    * The group's <code>inactiveObject</code> method
    * unexports the remote object from the RMI runtime so that the
    * object can no longer receive incoming RMI calls. This call will
    * only succeed if the object has no pending/executing calls. If
    * the object does have pending/executing RMI calls, then false
    * will be returned.
    *
    * If the object has no pending/executing calls, the object is
    * removed from the RMI runtime and the group informs its
    * <code>ActivationMonitor</code> (via the monitor's
    * <code>inactiveObject</code> method) that the remote object is
    * not currently active so that the remote object will be
    * re-activated by the activator upon a subsequent activation
    * request.
    *
    * @param id the object's activation identifier
    * @returns true if the operation succeeds (the operation will
    * succeed if the object in currently known to be active and is
    * either already unexported or is currently exported and has no
    * pending/executing calls); false is returned if the object has
    * pending/executing calls in which case it cannot be deactivated
    * @exception UnknownObjectException if object is unknown (may already
    * be inactive)
    * @exception RemoteException if call informing monitor fails
    */
    public boolean inactiveObject(ActivationID id)
        throws ActivationException, UnknownObjectException, RemoteException
    {

        try {
            acquireLock(id);
            synchronized (this) {
                if (groupInactive == true)
                    throw new ActivationException("group is inactive");
            }

            ActiveEntry entry = active.get(id);
            if (entry == null) {
                // REMIND: should this be silent?
                throw new UnknownObjectException("object not active");
            }

            try {
                if (Activatable.unexportObject(entry.impl, false) == false)
                    return false;
            } catch (NoSuchObjectException allowUnexportedObjects) {
            }

            try {
                super.inactiveObject(id);
            } catch (UnknownObjectException allowUnregisteredObjects) {
            }

            active.remove(id);

        } finally {
            releaseLock(id);
            checkInactiveGroup();
        }

        return true;
    }

    /*
     * Determines if the group has become inactive and
     * marks it as such.
     */
    private void checkInactiveGroup() {
        boolean groupMarkedInactive = false;
        synchronized (this) {
            if (active.size() == 0 && lockedIDs.size() == 0 &&
                groupInactive == false)
            {
                groupInactive = true;
                groupMarkedInactive = true;
            }
        }

        if (groupMarkedInactive) {
            try {
                super.inactiveGroup();
            } catch (Exception ignoreDeactivateFailure) {
            }

            try {
                UnicastRemoteObject.unexportObject(this, true);
            } catch (NoSuchObjectException allowUnexportedGroup) {
            }
        }
    }

    /**
     * The group's <code>activeObject</code> method is called when an
     * object is exported (either by <code>Activatable</code> object
     * construction or an explicit call to
     * <code>Activatable.exportObject</code>. The group must inform its
     * <code>ActivationMonitor</code> that the object is active (via
     * the monitor's <code>activeObject</code> method) if the group
     * hasn't already done so.
     *
     * @param id the object's identifier
     * @param obj the remote object implementation
     * @exception UnknownObjectException if object is not registered
     * @exception RemoteException if call informing monitor fails
     */
    public void activeObject(ActivationID id, Remote impl)
        throws ActivationException, UnknownObjectException, RemoteException
    {

        try {
            acquireLock(id);
            synchronized (this) {
                if (groupInactive == true)
                    throw new ActivationException("group is inactive");
            }
            if (!active.contains(id)) {
                ActiveEntry entry = new ActiveEntry(impl);
                active.put(id, entry);
                // created new entry, so inform monitor of active object
                try {
                    super.activeObject(id, entry.mobj);
                } catch (RemoteException e) {
                    // daemon can still find it by calling newInstance
                }
            }
        } finally {
            releaseLock(id);
            checkInactiveGroup();
        }
    }

    /**
     * Entry in table for active object.
     */
    private static class ActiveEntry {
        Remote impl;
        MarshalledObject<Remote> mobj;

        ActiveEntry(Remote impl) throws ActivationException {
            this.impl =  impl;
            try {
                this.mobj = new MarshalledObject<Remote>(impl);
            } catch (IOException e) {
                throw new
                    ActivationException("failed to marshal remote object", e);
            }
        }
    }

    /**
     * Returns true if the first argument is either equal to, or is a
     * descendant of, the second argument.  Null is treated as the root of
     * the tree.
     */
    private static boolean covers(ClassLoader sub, ClassLoader sup) {
        if (sup == null) {
            return true;
        } else if (sub == null) {
            return false;
        }
        do {
            if (sub == sup) {
                return true;
            }
            sub = sub.getParent();
        } while (sub != null);
        return false;
    }
}
