/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/util/org/apache/slide/util/os/Catalina.java,v 1.1 2004/03/25 16:18:12 juergen Exp $
 * $Revision: 1.1 $
 * $Date: 2004/03/25 16:18:12 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.slide.util.os;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import org.apache.commons.httpclient.util.Base64;
import org.apache.slide.util.Files;
import org.apache.slide.util.IO;
import org.apache.slide.util.JDom;
import org.apache.slide.util.Properties;
import org.apache.slide.util.Strings;
import org.apache.slide.util.XAssertionFailed;
import org.apache.slide.util.XException;
import org.apache.slide.util.XUri;
import org.apache.slide.util.cli.Abort;
import org.apache.slide.util.os.Executable;
import org.apache.slide.util.os.Platform;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.xpath.XPath;


/**
 ** @version   $Revision: 1.1 $
 **/
public class Catalina {
    public static final String MANAGER_USER = "tws";
    
    private static final String CATALINA_HOME = "catalina.home";
    private static final String CATALINA_OPTS = "catalina.opts";
    private static final String WEBAPP_CONTEXT = "webapp.context";
    
    public static Catalina create() throws Abort {
        return create(System.getProperty(CATALINA_HOME), System.getProperty(CATALINA_OPTS, ""), System.getProperty(WEBAPP_CONTEXT));
    }
    
    public static Catalina create(Properties props) throws Abort {
        return create(props.get(CATALINA_HOME), props.get(CATALINA_OPTS), props.get(WEBAPP_CONTEXT));
    }
    
    public static Catalina create(String home, String opts, String context) throws Abort {
        String[] ar;
        
        if (home == null) {
            throw new Abort(CATALINA_HOME + " system property not specified");
        }
        if (context == null) {
            throw new Abort(WEBAPP_CONTEXT + " system property not specified");
        }
        ar = Strings.split(opts, " ");
        Strings.trim(ar);
        return new Catalina(new File(home), ar, true, context);
    }
    
    
    public final File home;
    public final String[] opts;
    private final String context;
    private int httpPort;
    private int ajpPort;
    
    /** to access tomcat manager; null if not configured **/
    private final String passwd;
    
    private Catalina(File home, String[] opts, boolean manager, String context) throws Abort {
        this.home = home;
        this.opts = opts;
        this.context = context;
        this.httpPort = -1;
        this.ajpPort = -1;
        if (manager) {
            try {
                this.passwd = readPassword();
            } catch (XException e) {
                throw new Abort("error reading Tomcat manager configuration", e);
            }
        } else {
            this.passwd = null;
        }
    }
    
    //--
    
    public String getContext() {
        return context;
    }
    
    public String getUrl() throws XException {
        return XUri.HTTP_PREFIX + "localhost:" + getHttpPort() + "/" + getContext();
    }
    
    
    public File getConfDir() {
        return new File(home, "conf");
    }
    
    public int getAjpPort() throws XException {
        checkPort();
        return ajpPort;
    }
    
    public int getHttpPort() throws XException {
        checkPort();
        return httpPort;
    }
    
    private void checkPort() throws XException {
        Iterator attrs;
        Attribute attr;
        int port;
        
        if (ajpPort == -1 || httpPort == -1) {
            try {
                attrs = listPorts(readServerXml());
                while (attrs.hasNext()) {
                    attr = (Attribute) attrs.next();
                    port = attr.getIntValue();
                    if (isAjpPort(attr)) {
                        ajpPort = port;
                    } else if (isHttpPort(attr)) {
                        httpPort = port;
                    } else {
                        // ignore - stop port
                    }
                }
            } catch (JDOMException e) {
                throw new XException("error accessing port number", e);
            }
            if (ajpPort == -1) {
                throw new XException("missing ajp port in server.xml");
            }
            if (httpPort == -1) {
                throw new XException("missing http port in server.xml");
            }
        }
    }
    
    private static Iterator listPorts(Document doc) {
        try {
            return XPath.newInstance("/Server//@port").selectNodes(doc).iterator();
        } catch (JDOMException e) {
            throw new XAssertionFailed(e);
        }
    }
    
    public void setPort(int httpPort) throws IOException, XException, JDOMException {
        File file;
        Document doc;
        Iterator attrs;
        OutputStream dest;
        
        this.httpPort = -1;
        this.ajpPort = -1;
        
        doc = readServerXml();
        attrs = listPorts(doc);
        while (attrs.hasNext()) {
            setPort((Attribute) attrs.next(), httpPort);
        }
        file = serverXml();
        file.delete();
        dest = new FileOutputStream(file);
        JDom.outputter().output(doc, dest);
        dest.close();
    }
    
    
    private static void setPort(Attribute attr, int httpPort) throws JDOMException {
        int port;
        
        if (isHttpPort(attr)) {
            port = httpPort;
        } else if (isAjpPort(attr)) {
            port = httpPort + 9;
        } else if (isStopPort(attr)) {
            port = httpPort + 5;
        } else {
            throw new JDOMException("unkown port attribute: " + attr);
        }
        attr.setValue("" + port);
    }
    
    private static boolean isStopPort(Attribute attr) {
        return attr.getParent().getName().equals("Server");
    }
    private static boolean isHttpPort(Attribute attr) {
        return isJk(attr.getParent()) && !isAjp(attr.getParent());
    }
    private static boolean isAjpPort(Attribute attr) {
        return isJk(attr.getParent()) && isAjp(attr.getParent());
    }
    
    private static boolean isAjp(Element e) {
        String protocol = e.getAttributeValue("protocol");
        String clazz = e.getAttributeValue("protocolHandlerClassName");
        
        if (protocol == null && clazz == null) {
            return false;  // tc 5, http
        }
        if (protocol != null) {
            return true;  // tc 5, http
        }
        // tc 4
        return "org.apache.jk.server.JkCoyoteHandler".equals(clazz);
    }
    
    private static boolean isJk(Element e) {
        String className = e.getAttributeValue("className");
        return (e.getName().equals("Connector")
                    && (className == null || "org.apache.coyote.tomcat4.CoyoteConnector".equals(className)));   // tc5 has no class name
    }
    
    //--
    
    public int run(PrintStream out) throws Abort {
        return exec("start", out, true);
    }
    
    public void start(PrintStream out) throws Abort {
        int result;
        
        result = exec("start", out, false);
        if (result != 0) {
            throw new Abort("catalina start failed: return code=" + result);
        }
    }
    
    public void stop() throws Abort {
        ByteArrayOutputStream out;
        
        out = new ByteArrayOutputStream();
        execChecked("catalina stop", exec("stop", out, true), out);
    }

    private static void execChecked(String cmd, int result, ByteArrayOutputStream out)
        throws Abort
    {
        if (result != 0) {
            throw new Abort(cmd + " failed: return code=" + result + ", message: "
                                + new String(out.toByteArray()));
        }
    }
    
    public void service(String[] args) throws IOException, Abort {
        ByteArrayOutputStream out;
        Executable exec;
        String name;

        out = new ByteArrayOutputStream();
        name = Files.join(home, "bin", "tomcat.exe").getPath();
        exec = new Executable(name, args, null);
        execChecked("tomcat service", exec.run(Platform.USER_DIR, out, true), out);
    }
    
    private File serverXml() {
        return new File(getConfDir(), "server.xml");
    }
    
    private Document readServerXml() throws XException {
        return JDom.forFile(serverXml());
    }
    
    //-- tomcat manager access
    
    public boolean hasManagerAccess() {
        return passwd != null;
    }
    
    /**
     ** Checks the availability tomcat on the defined port
     **
     ** @return 'true' if ping is successful, else 'false'
     **/
    public boolean ping() throws XException {
        Socket socket;
        int i;
        
        socket = null;
        for (i = 0; true; i++) {
            socket = null;
            try {
                socket = new Socket("localhost", getHttpPort());
                socket.close();
                return true;
            } catch (IOException e) {
                if (socket != null) {
                    // close failed - don't try again
                } else {
                    // constructor failed - nothing to close
                }
                return false;
            }
        }
    }
    
    /**
     /** synchronized because this instance is shared amoung all sessions **/
    public String manager(String command) throws IOException, XException, JDOMException {
        final String TC_URL = XUri.HTTP_PREFIX + "localhost:" + getHttpPort() + "/manager";
        URLConnection conn;
        String input;
        String output;
        String str;
        
        if (!hasManagerAccess()) {
            throw new IOException("tomcat manager access not configured");
        }
        // Create a connection for this command
        conn = (new URL(TC_URL + command)).openConnection();
        HttpURLConnection hconn = (HttpURLConnection) conn;
        
        // Set up standard connection characteristics
        hconn.setAllowUserInteraction(false);
        hconn.setDoInput(true);
        hconn.setUseCaches(false);
        hconn.setDoOutput(false);
        hconn.setRequestMethod("GET");
        hconn.setRequestProperty("User-Agent", "Catalina-Ant-Task/1.0");
        
        // Set up an authorization header with our credentials
        input = MANAGER_USER + ":" + passwd;
        output = new String(Base64.encode(input.getBytes()));
        hconn.setRequestProperty("Authorization", "Basic " + output);
        
        // Establish the connection with the server
        hconn.connect();
        
        str = new IO().readString(hconn.getInputStream()); // TODO: encoding
        if (!str.startsWith("OK")) {
            throw new IOException(str);
        }
        return str;
    }
    
    //-- core functionality
    
    private int exec(String cmd, OutputStream out, boolean wait) throws Abort {
        Executable exec;
        
        exec = createExec(cmd, out, wait);
        try {
            return exec.run(home, out, wait);
        } catch (IOException e) {
            throw new Abort("tomcat failure", e);
        }
    }
    
    private Executable createExec(String cmd, OutputStream out, boolean wait) throws Abort {
        Properties env;
        String executable;
        String[] actuals;
        
        if (Platform.isWindows() && !wait) {
            executable = "cmd";
            actuals = new String[] {
                "/c", "start",
                    "Tamino WebDAV Server",
                    getJava()
            };
            actuals = Strings.append(actuals, getJavaArgs(cmd));
        } else {
            executable = getJava();
            actuals = getJavaArgs(cmd);
        }
        env = new Properties();
        try {
            addLibraryPath(env);
            return new Executable(executable, actuals, env);
        } catch (IOException e) {
            throw new Abort("tomcat library path failure", e);
        }
    }

    private String getJava() {
        String str;
        
        if (Platform.isWindows()) {
            str = "\\bin\\java.exe";
        } else {
            str = "/bin/java";
        }
        return Platform.JDK_HOME.getPath() + str;
    }
    
    private String[] getJavaArgs(String cmd) {
        String cp;
        File tools;
        
        // caution: don't use "java -jar bootstrap.jar" because javac from tools.jar
        // would be unavailable
        
        cp = Files.join(home, "bin", "bootstrap.jar").getPath();
        tools = Files.join(Platform.JDK_HOME, "lib", "tools.jar");
        if (tools.exists()) {
            cp = cp + Platform.CURRENT.pathSeparator + tools;
        }
        return Strings.append(opts, new String[] {
                    // note:
                    //  "-Djava.endorsed.dirs=" + Files.join(home.getPath(), "common", "endorsed"),
                    // is not needed because bootstrap.jar manually adds this directory to
                    // the root classloader
                    "-D" + CATALINA_HOME + "=" + home.getPath(),
                        "-D" + WEBAPP_CONTEXT + "=" + context,
                        "-cp", cp,
                        "org.apache.catalina.startup.Bootstrap",
                        cmd
                });
    }
    
    
    private void addLibraryPath(Properties env) throws IOException {
        String libPath;
        Platform platform;
        Properties externalEnv;
        String path;
        String pathVariable;
        
        libPath = Files.join(home, "native", "lib").getPath();
        if (Platform.isWindows()) {
            platform = Platform.CURRENT;
            pathVariable = platform.pathVariable;
            externalEnv = platform.getEnvironment();
            path = externalEnv.get(pathVariable);
            path = libPath + platform.pathSeparator + path;
            env.add(pathVariable, path);
        } else {
            // unix does not set libraries in PATH
            env.add("LD_LIBRARY_PATH", libPath);
        }
    }
    
    //--  TODO
    
    private String readPassword() throws Abort, XException {
        Document doc;
        Iterator iter;
        Element user;
        String result;
        
        doc = JDom.forFile(new File(getConfDir(), "tomcat-users.xml"));
        iter = doc.getRootElement().getChildren("user").iterator();
        while (iter.hasNext()) {
            user = (Element) iter.next();
            if (MANAGER_USER.equals(user.getAttributeValue("username"))) {
                result = user.getAttributeValue("password");
                if (result == null) {
                    throw new Abort("password not found");
                } else {
                    return result;
                }
            }
        }
        return null;
    }
}

