/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.jk.server;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.ObjectName;

import org.apache.jk.core.JkHandler;
import org.apache.jk.core.WorkerEnv;
import org.apache.tomcat.util.IntrospectionUtils;
import org.apache.tomcat.util.modeler.Registry;

/** Main class used to startup and configure jk. It manages the conf/jk2.properties file
 *  and is the target of JMX proxy.
 *
 *  It implements a policy of save-on-change - whenever a property is changed at
 *  runtime the jk2.properties file will be overriden. 
 *
 *  You can edit the config file when tomcat is stoped ( or if you don't use JMX or
 *  other admin tools ).
 *
 *  The format of jk2.properties:
 *  <dl>
 *   <dt>TYPE[.LOCALNAME].PROPERTY_NAME=VALUE
 *   <dd>Set a property on the associated component. TYPE will be used to
 *   find the class name and instantiate the component. LOCALNAME allows
 *   multiple instances. In JMX mode, TYPE and LOCALNAME will form the
 *   JMX name ( eventually combined with a 'jk2' component )
 *
 *   <dt>NAME=VALUE
 *   <dd>Define global properties to be used in ${} substitutions
 *
 *   <dt>class.COMPONENT_TYPE=JAVA_CLASS_NAME
 *   <dd>Adds a new 'type' of component. We predefine all known types.
 * </dl>
 *
 * Instances are created the first time a component name is found. In addition,
 * 'handler.list' property will override the list of 'default' components that are
 * loaded automatically.
 *
 *  Note that the properties file is just one (simplistic) way to configure jk. We hope
 *  to see configs based on registry, LDAP, db, etc. ( XML is not necesarily better )
 * 
 * @author Costin Manolache
 */
public class JkMain implements MBeanRegistration
{
    WorkerEnv wEnv;
    String propFile;
    Properties props=new Properties();

    Properties modules=new Properties();
    boolean modified=false;
    boolean started=false;
    boolean saveProperties=false;

    public JkMain()
    {
        JkMain.jkMain=this;
        modules.put("channelSocket", "org.apache.jk.common.ChannelSocket");
        modules.put("channelNioSocket", "org.apache.jk.common.ChannelNioSocket");
        modules.put("channelUnix", "org.apache.jk.common.ChannelUn");
        modules.put("channelJni", "org.apache.jk.common.ChannelJni");
        modules.put("apr", "org.apache.jk.apr.AprImpl");
        modules.put("mx", "org.apache.jk.common.JkMX");
        modules.put("modeler", "org.apache.jk.common.JkModeler");
        modules.put("shm", "org.apache.jk.common.Shm");
        modules.put("request","org.apache.jk.common.HandlerRequest");
        modules.put("container","org.apache.jk.common.HandlerRequest");
        modules.put("modjk","org.apache.jk.common.ModJkMX");

    }

    public static JkMain getJkMain() {
        return jkMain;
    }

    private static String DEFAULT_HTTPS="com.sun.net.ssl.internal.www.protocol";
    private void initHTTPSUrls() {
        try {
            // 11657: if only ajp is used, https: redirects need to work ( at least for 1.3+)
            String value = System.getProperty("java.protocol.handler.pkgs");
            if (value == null) {
                value = DEFAULT_HTTPS;
            } else if (value.indexOf(DEFAULT_HTTPS) >= 0  ) {
                return; // already set
            } else {
                value += "|" + DEFAULT_HTTPS;
            }
            System.setProperty("java.protocol.handler.pkgs", value);
        } catch(Exception ex ) {
            log.info("Error adding SSL Protocol Handler",ex);
        }
    }

    // -------------------- Setting --------------------
    
    /** Load a .properties file into and set the values
     *  into jk2 configuration.
     */
    public void setPropertiesFile( String p  ) {
        propFile=p;
        if( started ) {
            loadPropertiesFile();
        }
    }

    public String getPropertiesFile() {
        return propFile;
    }

    public void setSaveProperties( boolean b ) {
        saveProperties=b;
    }

    /** Set a name/value as a jk2 property
     */
    public void setProperty( String n, String v ) {
        if( "jkHome".equals( n ) ) {
            setJkHome( v );
        } 
        if( "propertiesFile".equals( n ) ) {
            setPropertiesFile( v );
        }
        props.put( n, v );
        if( started ) {
            // Replacements need special processing only when started==true,
            // because preProcessProperties() handles them during startup.
            String alias = (String) replacements.get(n);
            if (alias != null) {
                props.put( alias, v );
                if (log.isDebugEnabled()) {
                    log.debug("Substituting " + n + " " + alias + " " + v);
                }
            }
            processProperty( n, v );
            if (alias != null) {
                processProperty( alias, v );
            }
            saveProperties();
        }
    }
    /**
     * Retrieve a property.
     */
    public Object getProperty(String name) {
        String alias = (String)replacements.get(name);
        Object result = null;
        if(alias != null) {
            result = props.get(alias);
        }
        if(result == null) {
            result = props.get(name);
        }
        return result;
    }
    /**
     * Set the <code>channelClassName</code> that will used to connect to
     * httpd.
     */
    public void setChannelClassName(String name) {
        props.put( "handler.channel.className",name);
    }

    public String getChannelClassName() {
        return (String)props.get( "handler.channel.className");
    }

    /**
     * Set the <code>workerClassName</code> that will handle the request.
     * ( sort of 'pivot' in axis :-)
     */
    public void setWorkerClassName(String name) {
        props.put( "handler.container.className",name);
    }

    public String getWorkerClassName() {
        return (String)props.get( "handler.container.className");
    }

    /** Set the base dir of jk2. ( including WEB-INF if in a webapp ).
     *  We'll try to guess it from classpath if none is set ( for
     *  example on command line ), but if in a servlet environment
     *  you need to use Context.getRealPath or a system property or
     *  set it expliciltey.
     */
    public void setJkHome( String s ) {
        getWorkerEnv().setJkHome(s);
    }

    public String getJkHome() {
        return getWorkerEnv().getJkHome();
    }

    String out;
    String err;
    File propsF;
    
    public void setOut( String s ) {
        this.out=s;
    }

    public String getOut() {
        return this.out;
    }

    public void setErr( String s ) {
        this.err=s;
    }
    
    public String getErr() {
        return this.err;
    }
    
    // -------------------- Initialization --------------------
    
    public void init() throws IOException
    {
        long t1=System.currentTimeMillis();
        if(null != out) {
            PrintStream outS=new PrintStream(new FileOutputStream(out));
            System.setOut(outS);
        }
        if(null != err) {
            PrintStream errS=new PrintStream(new FileOutputStream(err));
            System.setErr(errS);
        }

        String home=getWorkerEnv().getJkHome();
        if( home==null ) {
            // XXX use IntrospectionUtil to find myself
            this.guessHome();
        }
        home=getWorkerEnv().getJkHome();
        if( home==null ) {
            log.info( "Can't find home, jk2.properties not loaded");
        }
        if(log.isDebugEnabled())
            log.debug("Starting Jk2, base dir= " + home  );
        loadPropertiesFile();

        String initHTTPS = (String)props.get("class.initHTTPS");
        if("true".equalsIgnoreCase(initHTTPS)) {
            initHTTPSUrls();
        }

        long t2=System.currentTimeMillis();
        initTime=t2-t1;
    }
    
    static String defaultHandlers[]= { "request",
                                       "container",
                                       "channelSocket"};
    /*
     static String defaultHandlers[]= { "apr",
                                       "shm",
                                       "request",
                                       "container",
                                       "channelSocket",
                                       "channelJni",
                                       "channelUnix"};
    */
    
    public void stop() 
    {
        // Clean up the handlers
        MBeanServer s = Registry.getRegistry(null,null).getMBeanServer();
        for( int i=0; i<wEnv.getHandlerCount(); i++ ) {
            JkHandler handler = wEnv.getHandler(i);
            if(handler != null) {
                String handlerName = handler.getName();
                try {
                    handler.destroy();
                } catch( IOException ex) {
                    log.error("Error stopping " + handlerName, ex);
                }
                if(domain != null) {
                    try {
                        ObjectName handlerOname = new ObjectName(
                                this.domain + ":" + "type=JkHandler,name=" +
                                handlerName);
                        if (s.isRegistered(handlerOname)) {
                            s.unregisterMBean(handlerOname);
                        }
                    } catch (Exception e) {
                        log.error( "Error unregistering " + handlerName, e );
                    }

                }
            }
        }

        started=false;
        
        // De-register JMX for Env
        if (domain != null) {
            try {
                ObjectName wEnvName =
                    new ObjectName(domain + ":type=JkWorkerEnv");
                if (s.isRegistered(wEnvName)) {
                    s.unregisterMBean(wEnvName);
                }
            } catch (Exception e) {
                log.error( "Error unregistering JkWorkerEnv", e );
            }
        }

        // De-register JMX for JkMain
        if(oname != null) {
            if (s.isRegistered(oname)) {
                try {
                    Registry.getRegistry(null, null)
                        .unregisterComponent(oname);
                } catch (Exception e) {
                    log.error( "Error unregistering jkmain " + e );
                }
            }
        }
    }
    
    public void start() throws IOException
    {
        long t1=System.currentTimeMillis();
        // We must have at least 3 handlers:
        // channel is the 'transport'
        // request is the request processor or 'global' chain
        // container is the 'provider'
        // Additional handlers may exist and be used internally
        // or be chained to create one of the standard handlers 

        String handlers[]=defaultHandlers;
        // backward compat
        String workers=props.getProperty( "handler.list", null );
        if( workers!=null ) {
            handlers= split( workers, ",");
        }

        // Load additional component declarations
        processModules();
        
        for( int i=0; i<handlers.length; i++ ) {
            String name= handlers[i];
            JkHandler w=getWorkerEnv().getHandler( name );
            if( w==null ) {
                newHandler( name, "", name );
            }
        }

        // Process properties - and add aditional handlers.
        processProperties();

        for( int i=0; i<wEnv.getHandlerCount(); i++ ) {
            if( wEnv.getHandler(i) != null ) {
                try {
                    wEnv.getHandler(i).init();
                } catch( IOException ex) {
                    if( "apr".equals(wEnv.getHandler(i).getName() )) {
                        log.info( "APR not loaded, disabling jni components: " + ex.toString());
                    } else {
                        log.error( "error initializing " + wEnv.getHandler(i).getName(), ex );
                    }
                }
            }
        }

        started=true;
        long t2=System.currentTimeMillis();
        startTime=t2-t1;

        this.saveProperties();
        log.info("Jk running ID=" + wEnv.getLocalId() + " time=" + initTime + "/" + startTime +
                 "  config=" + propFile);
    }

    // -------------------- Usefull methods --------------------
    
    public WorkerEnv getWorkerEnv() {
        if( wEnv==null ) { 
            wEnv=new WorkerEnv();
        }
        return wEnv;
    }

    public void setWorkerEnv(WorkerEnv wEnv) {
        this.wEnv = wEnv;
    }

    /* A bit of magic to support workers.properties without giving
       up the clean get/set
    */
    public void setBeanProperty( Object target, String name, String val ) {
        if( val!=null )
            val=IntrospectionUtils.replaceProperties( val, props, null );
        if( log.isDebugEnabled())
            log.debug( "setProperty " + target + " " + name + "=" + val );
        
        IntrospectionUtils.setProperty( target, name, val );
    }

    /* 
     * Set a handler property
     */
    public void setPropertyString( String handlerN, String name, String val ) {
        if( log.isDebugEnabled() )
            log.debug( "setProperty " + handlerN + " " + name + "=" + val );
        Object target=getWorkerEnv().getHandler( handlerN );

        setBeanProperty( target, name, val );
        if( started ) {
            saveProperties();
        }

    }

    /** The time it took to initialize jk ( ms)
     */
    public long getInitTime() {
        return initTime;
    }

    /** The time it took to start jk ( ms )
     */
    public long getStartTime() {
        return startTime;
    }
    
    // -------------------- Main --------------------

    long initTime;
    long startTime;
    static JkMain jkMain=null;

    public static void main(String args[]) {
        try {
            if( args.length == 1 &&
                ( "-?".equals(args[0]) || "-h".equals( args[0])) ) {
                System.out.println("Usage: ");
                System.out.println("  JkMain [args]");
                System.out.println();
                System.out.println("  Each bean setter corresponds to an arg ( like -debug 10 )");
                System.out.println("  System properties:");
                System.out.println("    jk2.home    Base dir of jk2");
                return;
            }

            jkMain=new JkMain();

            IntrospectionUtils.processArgs( jkMain, args, new String[] {},
                                            null, new Hashtable());

            jkMain.init();
            jkMain.start();
        } catch( Exception ex ) {
            log.warn("Error running",ex);
        }
    }

    // -------------------- Private methods --------------------


    private boolean checkPropertiesFile() {
        if(propFile == null) {
            return false;
        }
        propsF = new File(propFile);
        if(!propsF.isAbsolute()) {
            String home = getWorkerEnv().getJkHome();
            if( home == null ) {
                return false;
            }
            propsF = new File(home, propFile);
        }
        return propsF.exists();
    }
            
    private void loadPropertiesFile() {
        if(!checkPropertiesFile()) {
            return;
        }

        try {
            props.load( new FileInputStream(propsF) );
        } catch(IOException ex ){
            log.warn("Unable to load properties from "+propsF,ex);
        }
    }

    public  void saveProperties() {
        if( !saveProperties) return;
        
        if(propsF == null) {
            log.warn("No properties file specified. Unable to save");
            return;
        }
        // Temp - to check if it works
        File outFile= new File(propsF.getParentFile(), propsF.getName()+".save");
        log.debug("Saving properties " + outFile );
        try {
            props.store( new FileOutputStream(outFile), "AUTOMATICALLY GENERATED" );
        } catch(IOException ex ){
            log.warn("Unable to save to "+outFile,ex);
        }
    }

    // translate top-level keys ( from coyote or generic ) into component keys
    static Hashtable replacements=new Hashtable();
    static {
        replacements.put("port","channelSocket.port");
        replacements.put("maxPort", "channelSocket.maxPort");
        replacements.put("maxThreads", "channelSocket.maxThreads");   
        replacements.put("minSpareThreads", "channelSocket.minSpareThreads");   
        replacements.put("maxSpareThreads", "channelSocket.maxSpareThreads");   
        replacements.put("backlog", "channelSocket.backlog");   
        replacements.put("tcpNoDelay", "channelSocket.tcpNoDelay");
        replacements.put("soTimeout", "channelSocket.soTimeout");
        replacements.put("timeout", "channelSocket.timeout");
        replacements.put("address", "channelSocket.address");            
        replacements.put("bufferSize", "channelSocket.bufferSize");
        replacements.put("tomcatAuthentication", "request.tomcatAuthentication");
        replacements.put("packetSize", "channelSocket.packetSize");
        replacements.put("maxHeaderCount", "request.maxHeaderCount");
    }

    private void preProcessProperties() {
        Enumeration keys=props.keys();
        Vector v=new Vector();
        
        while( keys.hasMoreElements() ) {
            String key=(String)keys.nextElement();          
            Object newName=replacements.get(key);
            if( newName !=null ) {
                v.addElement(key);
            }
        }
        keys=v.elements();
        while( keys.hasMoreElements() ) {
            String key=(String)keys.nextElement();
            Object propValue=props.getProperty( key );
            String replacement=(String)replacements.get(key);
            props.put(replacement, propValue);
            if( log.isDebugEnabled()) 
                log.debug("Substituting " + key + " " + replacement + " " + 
                    propValue);
        }
    }
    
    private void processProperties() {
        preProcessProperties();
        Enumeration keys=props.keys();

        while( keys.hasMoreElements() ) {
            String name=(String)keys.nextElement();
            String propValue=props.getProperty( name );

            processProperty( name, propValue );
        }
    }

    private void processProperty(String name, String propValue) {
        String type=name;
        String fullName=name;
        String localName="";
        String propName="";
        // ignore
        if( name.startsWith("key.")) return;

        int dot=name.indexOf(".");
        int lastDot=name.lastIndexOf(".");
        if( dot > 0 ) {
            type=name.substring(0, dot );
            if( dot != lastDot ) {
                localName=name.substring( dot + 1, lastDot );
                fullName=type + "." + localName;
            } else {
                fullName=type;
            }
            propName=name.substring( lastDot+1);
        } else {
            return;
        }
        
        if( log.isDebugEnabled() )
            log.debug( "Processing " + type + ":" + localName + ":" + fullName + " " + propName );
        if( "class".equals( type ) || "handler".equals( type ) ) {
            return;
        }
        
        JkHandler comp=getWorkerEnv().getHandler( fullName );
        if( comp==null ) {
            comp=newHandler( type, localName, fullName );
        }
        if( comp==null )
            return;
        
        if( log.isDebugEnabled() ) 
            log.debug("Setting " + propName + " on " + fullName + " " + comp);
        this.setBeanProperty( comp, propName, propValue );
    }

    private JkHandler newHandler( String type, String localName, String fullName )
    {
        JkHandler handler;
        String classN=modules.getProperty(type);
        if( classN == null ) {
            log.error("No class name for " + fullName + " " + type );
            return null;
        }
        try {
            Class channelclass = Class.forName(classN);
            handler=(JkHandler)channelclass.newInstance();
        } catch (Throwable ex) {
            handler=null;
            log.error( "Can't create " + fullName, ex );
            return null;
        }
        if( this.domain != null ) {
            try {
                ObjectName handlerOname = new ObjectName
                    (this.domain + ":" + "type=JkHandler,name=" + fullName);
                Registry.getRegistry(null, null).registerComponent(handler, handlerOname, classN);
            } catch (Exception e) {
                log.error( "Error registering " + fullName, e );
            }

        }
        wEnv.addHandler( fullName, handler );
        return handler;
    }

    private void processModules() {
        Enumeration keys=props.keys();
        int plen=6;
        
        while( keys.hasMoreElements() ) {
            String k=(String)keys.nextElement();
            if( ! k.startsWith( "class." ) )
                continue;

            String name= k.substring( plen );
            String propValue=props.getProperty( k );

            if( log.isDebugEnabled()) log.debug("Register " + name + " " + propValue );
            modules.put( name, propValue );
        }
    }

    private String[] split(String s, String delim ) {
         Vector v=new Vector();
        StringTokenizer st=new StringTokenizer(s, delim );
        while( st.hasMoreTokens() ) {
            v.addElement( st.nextToken());
        }
        String res[]=new String[ v.size() ];
        for( int i=0; i<res.length; i++ ) {
            res[i]=(String)v.elementAt(i);
        }
        return res;
    }

    // guessing home
    private static String CNAME="org/apache/jk/server/JkMain.class";

    private void guessHome() {
        String home= wEnv.getJkHome();
        if( home != null )
            return;
        home=IntrospectionUtils.guessInstall( "jk2.home","jk2.home",
                                              "tomcat-jk2.jar", CNAME );
        if( home != null ) {
            log.info("Guessed home " + home );
            wEnv.setJkHome( home );
        }
    }

    static org.apache.juli.logging.Log log=
        org.apache.juli.logging.LogFactory.getLog( JkMain.class );

    protected String domain;
    protected ObjectName oname;
    protected MBeanServer mserver;

    public ObjectName getObjectName() {
        return oname;
    }

    public String getDomain() {
        return domain;
    }

    public ObjectName preRegister(MBeanServer server,
                                  ObjectName name) throws Exception {
        oname=name;
        mserver=server;
        domain=name.getDomain();
        return name;
    }

    public void postRegister(Boolean registrationDone) {
    }

    public void preDeregister() throws Exception {
    }

    public void postDeregister() {
    }

    public void pause() throws Exception {
        // wEnv sometime null at shutdown - bug45591
        if (wEnv != null) {
            for( int i=0; i<wEnv.getHandlerCount(); i++ ) {
                if( wEnv.getHandler(i) != null ) {
                    wEnv.getHandler(i).pause();
                }
            }
        }
    }

    public void resume() throws Exception {
        for( int i=0; i<wEnv.getHandlerCount(); i++ ) {
            if( wEnv.getHandler(i) != null ) {
                wEnv.getHandler(i).resume();
            }
        }
    }


}
