/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * Contact: jonas-team@objectweb.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.objectweb.jonas.server;

import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.rmi.server.RMIClassLoader;
import java.rmi.server.RMIClassLoaderSpi;
import java.util.StringTokenizer;

import org.objectweb.carol.util.configuration.CarolDefaultValues;

/**
 * Class <code>RemoteClassLoaderSpi</code> is the CAROL JRMP CLass Loader SPI
 * for serialization performances.
 * @author Guillaume Riviere (Guillaume.Riviere@inrialpes.fr)
 * @author S. Ali Tokmen
 */
public class RemoteClassLoaderSpi extends RMIClassLoaderSpi {

    /**
     * Carol was already initialized
     */
    private static boolean carolIsInitialized = false;

    /**
     * Local call optimization is set
     */
    private static boolean carolIsOptimized = false;

    /**
     * Current provider
     */
    private final RMIClassLoaderSpi defaultProvider = RMIClassLoader.getDefaultProviderInstance();

    /**
     * Loads a class from a codebase URL path, optionally using the supplied
     * loader.
     * @param codebase the list of URLs (separated by spaces) to load the class
     *        from, or <code>null</code>
     * @param name the name of the class to load
     * @param defaultLoader additional contextual class loader to use, or
     *        <code>null</code>
     * @return the <code>Class</code> object representing the loaded class
     * @throws MalformedURLException if <code>codebase</code> is non-<code>null</code>
     *         and contains an invalid URL, or if <code>codebase</code> is
     *         <code>null</code> and the system property
     *         <code>java.rmi.server.codebase</code> contains an invalid URL
     * @throws ClassNotFoundException if a definition for the class could not be
     *         found at the specified location
     */
    public Class loadClass(String codebase, String name, ClassLoader defaultLoader) throws MalformedURLException,
            ClassNotFoundException {
        return defaultProvider.loadClass(normalizeCodebase(codebase), name, defaultLoader);
    }

    /**
     * Loads a dynamic proxy class (see {@link java.lang.reflect.Proxy} that
     * implements a set of interfaces with the given names from a codebase URL
     * path, optionally using the supplied loader.
     * @param codebase the list of URLs (space-separated) to load classes from,
     *        or <code>null</code>
     * @param interfaces the names of the interfaces for the proxy class to
     *        implement
     * @return a dynamic proxy class that implements the named interfaces
     * @param defaultLoader additional contextual class loader to use, or
     *        <code>null</code>
     * @throws MalformedURLException if <code>codebase</code> is non-<code>null</code>
     *         and contains an invalid URL, or if <code>codebase</code> is
     *         <code>null</code> and the system property
     *         <code>java.rmi.server.codebase</code> contains an invalid URL
     * @throws ClassNotFoundException if a definition for one of the named
     *         interfaces could not be found at the specified location, or if
     *         creation of the dynamic proxy class failed (such as if
     *         {@link java.lang.reflect.Proxy#getProxyClass(ClassLoader,Class[])}
     *         would throw an <code>IllegalArgumentException</code> for the
     *         given interface list)
     */
    public Class loadProxyClass(String codebase, String[] interfaces, ClassLoader defaultLoader)
            throws MalformedURLException, ClassNotFoundException {
        return defaultProvider.loadProxyClass(normalizeCodebase(codebase), interfaces, defaultLoader);
    }

    /**
     * Returns a class loader that loads classes from the given codebase URL
     * path.
     * @param codebase the list of URLs (space-separated) from which the
     *        returned class loader will load classes from, or <code>null</code>
     * @return a class loader that loads classes from the given codebase URL
     *         path
     * @throws MalformedURLException if <code>codebase</code> is non-<code>null</code>
     *         and contains an invalid URL, or if <code>codebase</code> is
     *         <code>null</code> and the system property
     *         <code>java.rmi.server.codebase</code> contains an invalid URL
     */
    public ClassLoader getClassLoader(String codebase) throws MalformedURLException {
        return defaultProvider.getClassLoader(normalizeCodebase(codebase));
    }

    /**
     * Returns the annotation string (representing a location for the class
     * definition) that RMI will use to annotate the class descriptor when
     * marshalling objects of the given class.<br>
     * By default, remove rmi annotations of JClassLoader class. Between two
     * JOnAS, commons classes are the same, don't need t have a bigger
     * annotation. When local call is set, always disable annotation.
     * @param cl the class to obtain the annotation for
     * @return a string to be used to annotate the given class when it gets
     *         marshalled, or <code>null</code>
     */
    public String getClassAnnotation(Class cl) {
        ClassLoader loader = cl.getClassLoader();

        // Init values
        if (!carolIsInitialized) {
            String sValue = System.getProperty(CarolDefaultValues.LOCALREG_JRMP_PROPERTY, "init");
            if (!sValue.equals("init")) {
                carolIsOptimized = new Boolean(sValue).booleanValue();
                carolIsInitialized = true;
            }
        }

        if (loader instanceof JClassLoader) {
            return null;
        } else if ((loader instanceof URLClassLoader) && (carolIsOptimized)) {
            return null;
        } else {
            return defaultProvider.getClassAnnotation(cl);
        }
    }

    /**
     * The standard separator between codebases is the space character and
     * many operating systems use paths with spaces. One classical example
     * would be <b>C:\Program Files\JOnAS\</b> in Windows.
     *
     * This function does the necessary transformation in order the codebase to
     * be separated correctly and the <b>file:/C:/Program Files/JOnAS/</b> path
     * not to be interpreted as two separate codebases (Namely,
     * <b>file:/C:/Program</b> and <b>/JOnAS/</b>). It also successfully
     * normalizes multiple paths in one codebase (for example,
     * <b>file:/C:/Program Files/foo.jar file:/C:/Program Files/bar.jar</b>).
     *
     * @param input  Codebase to normalize (may be null).
     *
     * @return Normalized codebase.
     *
     * @throws MalformedURLException  If URL really is malformed.
     */
    static String normalizeCodebase(final String input) throws MalformedURLException {
        // null is a perfectly valid codebase
        if (input == null) {
            return null;
        }

        StringBuffer codebase = new StringBuffer();
        StringBuffer current = new StringBuffer();
        StringTokenizer stok = new StringTokenizer(input, " \t\n\r\f", false);

        while (stok.hasMoreTokens()) {
            String item = stok.nextToken();

            // Ignore empty tokens
            if (item.length() < 1) {
                continue;
            }

            if (item.indexOf(':') != -1) {
                // This is the beginning of a new URL,
                // therefore the end of an the previous URL
                onGotURL(current, codebase);
            } else {
                // This is the inner part of a URL that contains a space
                current.append(' ');
            }

            current.append(item);
        }

        // Make sure we don't miss the last URL
        onGotURL(current, codebase);

        if (codebase.length() > 0) {
            return codebase.toString();
        } else {
            return null;
        }
    }

    /**
     * Callback when the {@link RemoteClassLoaderSpi#normalizeCodebase(String)}
     * method has recognized a URL. The URL (that may be empty) will be escaped
     * using {@link java.net.URL} and added to the codebase.
     *
     * @param current   Recognized URL.
     * @param codebase  Base to add the reformatted URL to.
     *
     * @throws MalformedURLException  If URL current's content is invalid.
     */
    static void onGotURL(final StringBuffer current, final StringBuffer codebase) throws MalformedURLException {
        // Ignore empty recognitions
        if (current.length() < 1) {
            return;
        }

        URL url;
        String currentToString = current.toString();
        try {
            // Check if the URL is valid, therefore already escaped correctly
            url = new URI(new URL(currentToString).toString()).toURL();
        } catch (URISyntaxException e) {
            try {
                // Given URL wasn't escaped correctly, escape spaces
                url = new URI(new URL(currentToString.replaceAll("\\ ", "%20")).toString()).toURL();
            } catch (URISyntaxException ie) {
                // URL is really not valid
                url = null;
            }
        }

        if (url != null) {
            if (codebase.length() > 0) {
                codebase.append(' ');
            }
            codebase.append(url.toString());
            current.setLength(0);
        }
    }
}
