/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package javassist.tools.web;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

/**
 * A sample applet viewer.
 *
 * <p>This is a sort of applet viewer that can run any program even if
 * the main class is not a subclass of <code>Applet</code>.
 * This viewwer first calls <code>main()</code> in the main class.
 *
 * <p>To run, you should type:
 *
 * <pre>% java javassist.tools.web.Viewer <i>host port</i> Main arg1, ...</pre>
 *
 * <p>This command calls <code>Main.main()</code> with <code>arg1,...</code>
 * All classes including <code>Main</code> are fetched from
 * a server http://<i>host</i>:<i>port</i>.
 * Only the class file for <code>Viewer</code> must exist
 * on a local file system at the client side; even other
 * <code>javassist.*</code> classes are not needed at the client side.
 * <code>Viewer</code> uses only Java core API classes.
 *
 * <p>Note: since a <code>Viewer</code> object is a class loader,
 * a program loaded by this object can call a method in <code>Viewer</code>.
 * For example, you can write something like this:
 *
 * <pre>
 * Viewer v = (Viewer)this.getClass().getClassLoader();
 * String port = v.getPort();
 * </pre>
 *
 */
public class Viewer extends ClassLoader {
    private String server;
    private int port;

    /**
     * Starts a program.
     */
    public static void main(String[] args) throws Throwable {
        if (args.length >= 3) {
            Viewer cl = new Viewer(args[0], Integer.parseInt(args[1]));
            String[] args2 = new String[args.length - 3];
            System.arraycopy(args, 3, args2, 0, args.length - 3);
            cl.run(args[2], args2);
        }
        else
            System.err.println(
        "Usage: java javassist.tools.web.Viewer <host> <port> class [args ...]");
    }

    /**
     * Constructs a viewer.
     *
     * @param host              server name
     * @param p                 port number
     */
    public Viewer(String host, int p) {
        server = host;
        port = p;
    }

    /**
     * Returns the server name.
     */
    public String getServer() { return server; }

    /**
     * Returns the port number.
     */
    public int getPort() { return port; }

    /**
     * Invokes main() in the class specified by <code>classname</code>.
     *
     * @param classname         executed class
     * @param args              the arguments passed to <code>main()</code>.
     */
    public void run(String classname, String[] args)
        throws Throwable
    {
        Class<?> c = loadClass(classname);
        try {
            c.getDeclaredMethod("main", new Class[] { String[].class })
                .invoke(null, new Object[] { args });
        }
        catch (java.lang.reflect.InvocationTargetException e) {
            throw e.getTargetException();
        }
    }

    /**
     * Requests the class loader to load a class.
     */
    @Override
    protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        Class<?> c = findLoadedClass(name);
        if (c == null)
            c = findClass(name);

        if (c == null)
            throw new ClassNotFoundException(name);

        if (resolve)
            resolveClass(c);

        return c;
    }

    /**
     * Finds the specified class.  The implementation in this class
     * fetches the class from the http server.  If the class is
     * either <code>java.*</code>, <code>javax.*</code>, or
     * <code>Viewer</code>, then it is loaded by the parent class
     * loader.
     *
     * <p>This method can be overridden by a subclass of
     * <code>Viewer</code>.
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> c = null;
        if (name.startsWith("java.") || name.startsWith("javax.")
            || name.equals("javassist.tools.web.Viewer"))
            c = findSystemClass(name);

        if (c == null)
            try {
                byte[] b = fetchClass(name);
                if (b != null)
                    c = defineClass(name, b, 0, b.length);
            }
        catch (Exception e) {
        }

        return c;
    }

    /**
     * Fetches the class file of the specified class from the http
     * server.
     */
    protected byte[] fetchClass(String classname) throws Exception
    {
        byte[] b;
        URL url = new URL("http", server, port,
                          "/" + classname.replace('.', '/') + ".class");
        URLConnection con = url.openConnection();
        con.connect();
        int size = con.getContentLength();
        InputStream s = con.getInputStream();
        if (size <= 0)
            b = readStream(s);
        else {
            b = new byte[size];
            int len = 0;
            do {
                int n = s.read(b, len, size - len);
                if (n < 0) {
                    s.close();
                    throw new IOException("the stream was closed: "
                                          + classname);
                }
                len += n;
            } while (len < size);
        }

        s.close();
        return b;
    }

    private byte[] readStream(InputStream fin) throws IOException {
        byte[] buf = new byte[4096];
        int size = 0;
        int len = 0;
        do {
            size += len;
            if (buf.length - size <= 0) {
                byte[] newbuf = new byte[buf.length * 2];
                System.arraycopy(buf, 0, newbuf, 0, size);
                buf = newbuf;
            }

            len = fin.read(buf, size, buf.length - size);
        } while (len >= 0);

        byte[] result = new byte[size];
        System.arraycopy(buf, 0, result, 0, size);
        return result;
    }
}
