package org.apache.tomcat.maven.runner;
/*
 * 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.
 */

import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Catalina;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.valves.AccessLogValve;
import org.apache.tomcat.util.http.fileupload.FileUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

/**
 * FIXME add junit for that but when https://issues.apache.org/bugzilla/show_bug.cgi?id=52028 fixed
 * Main class used to run the standalone wars in a Apache Tomcat instance.
 *
 * @author Olivier Lamy
 * @since 2.0
 */
public class Tomcat7Runner
{
    // true/false to use the server.xml located in the jar /conf/server.xml
    public static final String USE_SERVER_XML_KEY = "useServerXml";

    // contains war name wars=foo.war,bar.war
    public static final String WARS_KEY = "wars";

    public static final String ENABLE_NAMING_KEY = "enableNaming";

    public static final String ACCESS_LOG_VALVE_FORMAT_KEY = "accessLogValveFormat";

    /**
     * key of the property which contains http protocol : HTTP/1.1 or org.apache.coyote.http11.Http11NioProtocol
     */
    public static final String HTTP_PROTOCOL_KEY = "connectorhttpProtocol";


    public int httpPort;

    public int httpsPort;

    public int ajpPort;

    public String serverXmlPath;

    public Properties runtimeProperties;

    public boolean resetExtract;

    public boolean debug = false;

    public boolean clientAuth = false;

    public String keyAlias = null;

    public String httpProtocol;

    public String extractDirectory = ".extract";

    public File extractDirectoryFile;

    public String loggerName;

    Catalina container;

    Tomcat tomcat;

    String uriEncoding = "ISO-8859-1";

    /**
     * key = context of the webapp, value = war path on file system
     */
    Map<String, String> webappWarPerContext = new HashMap<String, String>();

    public Tomcat7Runner()
    {
        // no op
    }

    public void run()
        throws Exception
    {

        PasswordUtil.deobfuscateSystemProps();

        if ( loggerName != null && loggerName.length() > 0 )
        {
            installLogger( loggerName );
        }

        this.extractDirectoryFile = new File( this.extractDirectory );

        debugMessage( "use extractDirectory:" + extractDirectoryFile.getPath() );

        // do we have to extract content
        if ( !extractDirectoryFile.exists() || resetExtract )
        {
            extract();
        }
        else
        {
            String wars = runtimeProperties.getProperty( WARS_KEY );
            populateWebAppWarPerContext( wars );
        }

        // create tomcat various paths
        new File( extractDirectory, "conf" ).mkdirs();
        new File( extractDirectory, "logs" ).mkdirs();
        new File( extractDirectory, "webapps" ).mkdirs();
        new File( extractDirectory, "work" ).mkdirs();
        File tmpDir = new File( extractDirectory, "temp" );
        tmpDir.mkdirs();

        System.setProperty( "java.io.tmpdir", tmpDir.getAbsolutePath() );

        System.setProperty( "catalina.base", extractDirectoryFile.getAbsolutePath() );
        System.setProperty( "catalina.home", extractDirectoryFile.getAbsolutePath() );

        // start with a server.xml
        if ( serverXmlPath != null || useServerXml() )
        {
            container = new Catalina();
            container.setUseNaming( this.enableNaming() );
            if ( serverXmlPath != null && new File( serverXmlPath ).exists() )
            {
                container.setConfig( serverXmlPath );
            }
            else
            {
                container.setConfig( new File( extractDirectory, "conf/server.xml" ).getAbsolutePath() );
            }
            container.start();
        }
        else
        {
            tomcat = new Tomcat();

            if ( this.enableNaming() )
            {
                System.setProperty( "catalina.useNaming", "true" );
                tomcat.enableNaming();
            }

            tomcat.getHost().setAppBase( new File( extractDirectory, "webapps" ).getAbsolutePath() );

            String connectorHttpProtocol = runtimeProperties.getProperty( HTTP_PROTOCOL_KEY );

            if ( httpProtocol != null && httpProtocol.trim().length() > 0 )
            {
                connectorHttpProtocol = httpProtocol;
            }

            debugMessage( "use connectorHttpProtocol:" + connectorHttpProtocol );

            if ( httpPort > 0 )
            {
                Connector connector = new Connector( connectorHttpProtocol );
                connector.setPort( httpPort );

                if ( httpsPort > 0 )
                {
                    connector.setRedirectPort( httpsPort );
                }
                connector.setURIEncoding( uriEncoding );

                tomcat.getService().addConnector( connector );

                tomcat.setConnector( connector );
            }

            // add a default acces log valve
            AccessLogValve alv = new AccessLogValve();
            alv.setDirectory( new File( extractDirectory, "logs" ).getAbsolutePath() );
            alv.setPattern( runtimeProperties.getProperty( Tomcat7Runner.ACCESS_LOG_VALVE_FORMAT_KEY ) );
            tomcat.getHost().getPipeline().addValve( alv );

            // create https connector
            if ( httpsPort > 0 )
            {
                Connector httpsConnector = new Connector( connectorHttpProtocol );
                httpsConnector.setPort( httpsPort );
                httpsConnector.setSecure( true );
                httpsConnector.setProperty( "SSLEnabled", "true" );
                httpsConnector.setProperty( "sslProtocol", "TLS" );
                httpsConnector.setURIEncoding( uriEncoding );

                String keystoreFile = System.getProperty( "javax.net.ssl.keyStore" );
                String keystorePass = System.getProperty( "javax.net.ssl.keyStorePassword" );
                String keystoreType = System.getProperty( "javax.net.ssl.keyStoreType", "jks" );

                if ( keystoreFile != null )
                {
                    httpsConnector.setAttribute( "keystoreFile", keystoreFile );
                }
                if ( keystorePass != null )
                {
                    httpsConnector.setAttribute( "keystorePass", keystorePass );
                }
                httpsConnector.setAttribute( "keystoreType", keystoreType );

                String truststoreFile = System.getProperty( "javax.net.ssl.trustStore" );
                String truststorePass = System.getProperty( "javax.net.ssl.trustStorePassword" );
                String truststoreType = System.getProperty( "javax.net.ssl.trustStoreType", "jks" );
                if ( truststoreFile != null )
                {
                    httpsConnector.setAttribute( "truststoreFile", truststoreFile );
                }
                if ( truststorePass != null )
                {
                    httpsConnector.setAttribute( "truststorePass", truststorePass );
                }
                httpsConnector.setAttribute( "truststoreType", truststoreType );

                httpsConnector.setAttribute( "clientAuth", clientAuth );
                httpsConnector.setAttribute( "keyAlias", keyAlias );

                tomcat.getService().addConnector( httpsConnector );

                if ( httpPort <= 0 )
                {
                    tomcat.setConnector( httpsConnector );
                }
            }

            // create ajp connector
            if ( ajpPort > 0 )
            {
                Connector ajpConnector = new Connector( "org.apache.coyote.ajp.AjpProtocol" );
                ajpConnector.setPort( ajpPort );
                ajpConnector.setURIEncoding( uriEncoding );
                tomcat.getService().addConnector( ajpConnector );
            }

            // add webapps
            for ( Map.Entry<String, String> entry : this.webappWarPerContext.entrySet() )
            {
                String baseDir = null;
                if ( entry.getKey().equals( "/" ) )
                {
                    baseDir = new File( extractDirectory, "webapps/ROOT.war" ).getAbsolutePath();
                }
                else
                {
                    baseDir = new File( extractDirectory, "webapps/" + entry.getValue() ).getAbsolutePath();
                }
                Context context = tomcat.addWebapp( entry.getKey(), baseDir );
                URL contextFileUrl = getContextXml( baseDir );
                if ( contextFileUrl != null )
                {
                    context.setConfigFile( contextFileUrl );
                }
            }

            tomcat.start();
        }

        waitIndefinitely();

    }

    private URL getContextXml( String warPath )
        throws IOException
    {
        InputStream inputStream = null;
        try
        {
            String urlStr = "jar:file:" + warPath + "!/META-INF/context.xml";
            debugMessage( "search context.xml in url:'" + urlStr + "'" );
            URL url = new URL( urlStr );
            inputStream = url.openConnection().getInputStream();
            if ( inputStream != null )
            {
                return url;
            }
        }
        catch ( FileNotFoundException e )
        {
            return null;
        }
        finally
        {
            IOUtils.closeQuietly( inputStream );
        }
        return null;
    }

    private void waitIndefinitely()
    {
        Object lock = new Object();

        synchronized ( lock )
        {
            try
            {
                lock.wait();
            }
            catch ( InterruptedException exception )
            {
                throw new Error( "InterruptedException on wait Indefinitely lock:" + exception.getMessage(),
                                 exception );
            }
        }
    }

    public void stop()
        throws Exception
    {
        if ( container != null )
        {
            container.stop();
        }
        if ( tomcat != null )
        {
            tomcat.stop();
        }
    }

    protected void extract()
        throws Exception
    {

        if ( extractDirectoryFile.exists() )
        {
            FileUtils.deleteDirectory( extractDirectoryFile );
        }

        if ( !this.extractDirectoryFile.exists() )
        {
            boolean created = this.extractDirectoryFile.mkdirs();
            if ( !created )
            {
                throw new Exception( "FATAL: impossible to create directory:" + this.extractDirectoryFile.getPath() );
            }
        }

        // ensure webapp dir is here
        boolean created = new File( extractDirectory, "webapps" ).mkdirs();
        if ( !created )
        {
            throw new Exception(
                "FATAL: impossible to create directory:" + this.extractDirectoryFile.getPath() + "/webapps" );

        }

        String wars = runtimeProperties.getProperty( WARS_KEY );
        populateWebAppWarPerContext( wars );

        for ( Map.Entry<String, String> entry : webappWarPerContext.entrySet() )
        {
            debugMessage( "webappWarPerContext entry key/value: " + entry.getKey() + "/" + entry.getValue() );
            InputStream inputStream = null;
            try
            {
                inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( entry.getValue() );
                if ( !useServerXml() )
                {
                    if ( entry.getKey().equals( "/" ) )
                    {
                        File expandFile = new File( extractDirectory, "webapps/ROOT.war" );
                        debugMessage( "expand to file:" + expandFile.getPath() );
                        expand( inputStream, expandFile );
                    }
                    else
                    {
                        File expandFile = new File( extractDirectory, "webapps/" + entry.getValue() );
                        debugMessage( "expand to file:" + expandFile.getPath() );
                        expand( inputStream, expandFile );
                    }
                }
                else
                {
                    File expandFile = new File( extractDirectory, "webapps/" + entry.getValue() );
                    debugMessage( "expand to file:" + expandFile.getPath() );
                    expand( inputStream, new File( extractDirectory, "webapps/" + entry.getValue() ) );
                }
            }
            finally
            {
                if ( inputStream != null )
                {
                    inputStream.close();
                }
            }
        }

        // expand tomcat configuration files if there
        expandConfigurationFile( "catalina.properties", extractDirectoryFile );
        expandConfigurationFile( "logging.properties", extractDirectoryFile );
        expandConfigurationFile( "tomcat-users.xml", extractDirectoryFile );
        expandConfigurationFile( "catalina.policy", extractDirectoryFile );
        expandConfigurationFile( "context.xml", extractDirectoryFile );
        expandConfigurationFile( "server.xml", extractDirectoryFile );
        expandConfigurationFile( "web.xml", extractDirectoryFile );

    }

    private static void expandConfigurationFile( String fileName, File extractDirectory )
        throws Exception
    {
        InputStream inputStream = null;
        try
        {
            inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream( "conf/" + fileName );
            if ( inputStream != null )
            {
                File confDirectory = new File( extractDirectory, "conf" );
                if ( !confDirectory.exists() )
                {
                    confDirectory.mkdirs();
                }
                expand( inputStream, new File( confDirectory, fileName ) );
            }
        }
        finally
        {
            if ( inputStream != null )
            {
                inputStream.close();
            }
        }

    }

    /**
     * @param warsValue we can value in format: wars=foo.war|contextpath;bar.war  ( |contextpath is optionnal if empty use the war name)
     *                  so here we return war file name and populate webappWarPerContext
     */
    private void populateWebAppWarPerContext( String warsValue )
    {
        StringTokenizer st = new StringTokenizer( warsValue, ";" );
        while ( st.hasMoreTokens() )
        {
            String warValue = st.nextToken();
            debugMessage( "populateWebAppWarPerContext warValue:" + warValue );
            String warFileName = "";
            String contextValue = "";
            int separatorIndex = warValue.indexOf( "|" );
            if ( separatorIndex >= 0 )
            {
                warFileName = warValue.substring( 0, separatorIndex );
                contextValue = warValue.substring( separatorIndex + 1, warValue.length() );

            }
            else
            {
                warFileName = contextValue;
            }
            debugMessage( "populateWebAppWarPerContext contextValue/warFileName:" + contextValue + "/" + warFileName );
            this.webappWarPerContext.put( contextValue, warFileName );
        }
    }


    /**
     * Expand the specified input stream into the specified file.
     *
     * @param input InputStream to be copied
     * @param file  The file to be created
     * @throws java.io.IOException if an input/output error occurs
     */
    private static void expand( InputStream input, File file )
        throws IOException
    {
        BufferedOutputStream output = null;
        try
        {
            output = new BufferedOutputStream( new FileOutputStream( file ) );
            byte buffer[] = new byte[2048];
            while ( true )
            {
                int n = input.read( buffer );
                if ( n <= 0 )
                {
                    break;
                }
                output.write( buffer, 0, n );
            }
        }
        finally
        {
            if ( output != null )
            {
                try
                {
                    output.close();
                }
                catch ( IOException e )
                {
                    // Ignore
                }
            }
        }
    }

    public boolean useServerXml()
    {
        return Boolean.parseBoolean( runtimeProperties.getProperty( USE_SERVER_XML_KEY, Boolean.FALSE.toString() ) );
    }


    public void debugMessage( String message )
    {
        if ( debug )
        {
            System.out.println( message );
        }
    }


    public boolean enableNaming()
    {
        return Boolean.parseBoolean( runtimeProperties.getProperty( ENABLE_NAMING_KEY, Boolean.FALSE.toString() ) );
    }

    private void installLogger( String loggerName )
        throws SecurityException, NoSuchMethodException, IllegalArgumentException, IllegalAccessException,
        InvocationTargetException
    {
        if ( "slf4j".equals( loggerName ) )
        {

            try
            {
                // Check class is available
                final Class<?> clazz = Class.forName( "org.slf4j.bridge.SLF4JBridgeHandler" );

                // Remove all JUL handlers
                java.util.logging.LogManager.getLogManager().reset();

                // Install slf4j bridge handler
                final Method method = clazz.getMethod( "install", null );
                method.invoke( null );
            }
            catch ( ClassNotFoundException e )
            {
                System.out.println( "WARNING: issue configuring slf4j jul bridge, skip it" );
            }
        }
        else
        {
            System.out.println( "WARNING: loggerName " + loggerName + " not supported, skip it" );
        }
    }
}
