/*
 * 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.uima.adapter.jms.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InvalidClassException;
import java.net.Socket;

import org.apache.uima.UIMAFramework;
import org.apache.uima.UimaContext;
import org.apache.uima.UimaContextAdmin;
import org.apache.uima.aae.UimaAsVersion;
import org.apache.uima.aae.controller.AnalysisEngineController;
import org.apache.uima.aae.jmx.monitor.BasicUimaJmxMonitorListener;
import org.apache.uima.aae.jmx.monitor.JmxMonitor;
import org.apache.uima.aae.jmx.monitor.JmxMonitorListener;
import org.apache.uima.adapter.jms.JmsConstants;
import org.apache.uima.adapter.jms.activemq.SpringContainerDeployer;
import org.apache.uima.resource.ResourceInitializationException;
import org.apache.uima.util.Level;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.support.FileSystemXmlApplicationContext;

public class UIMA_Service implements ApplicationListener {
  private static final Class CLASS_NAME = UIMA_Service.class;

  protected boolean serviceInitializationCompleted;

  protected boolean serviceInitializationException;

  protected Object serviceMonitor = new Object();

  private JmxMonitor monitor = null;

  private Thread monitorThread = null;

  /**
   * Parse command args, run dd2spring on the deployment descriptors to generate Spring context
   * files.
   * 
   * @param args
   *          - command line arguments
   * @return - an array of Spring context files generated from provided deployment descriptors
   * @throws Exception
   */
  public String[] initialize(String[] args) throws Exception {
    UIMAFramework.getLogger(CLASS_NAME).log(Level.INFO,
            "UIMA-AS version " + UimaAsVersion.getFullVersionString());

    String[] springConfigFileArray = {};
    String[] deploymentDescriptors = {};
    int nbrOfArgs = args.length;

    deploymentDescriptors = getMultipleArg("-d", args);
    if (deploymentDescriptors.length == 0) {
      // allow multiple args for one key
      deploymentDescriptors = getMultipleArg2("-dd", args);
    }
    String saxonURL = getArg("-saxonURL", args);
    String xslTransform = getArg("-xslt", args);
    String uimaAsDebug = getArg("-uimaEeDebug", args);

    if (nbrOfArgs < 1
            || (args[0].startsWith("-") && (deploymentDescriptors.length == 0
                    || saxonURL.equals("") || xslTransform.equals("")))) {
      printUsageMessage();
      return null;
    }
    String brokerURL = getArg("-brokerURL", args);
    // Check if broker URL is specified on the command line. If it is not, use the default
    // localhost:61616. In either case, set the System property defaultBrokerURL. It will be used
    // by Spring Framework to substitute a place holder in Spring xml.
    if (brokerURL != "") {
      System.setProperty("defaultBrokerURL", brokerURL);
      System.out.println(">>> Setting defaultBrokerURL to:" + brokerURL);
    } else if ( System.getProperty("defaultBrokerURL") == null) {  // perhaps already set using -D
      System.setProperty("defaultBrokerURL", "tcp://localhost:61616");
    }

    if (System.getProperty(JmsConstants.SessionTimeoutOverride) != null) {
      System.out.println(">>> Setting Inactivity Timeout To: "
              + System.getProperty(JmsConstants.SessionTimeoutOverride));
    }
    if (deploymentDescriptors.length == 0) {
      // array of context files passed in
      springConfigFileArray = args;
    } else {
      // create a String array of spring context files
      springConfigFileArray = new String[deploymentDescriptors.length];

      Dd2spring aDd2Spring = new Dd2spring();
      for (int dd = 0; dd < deploymentDescriptors.length; dd++) {
        String deploymentDescriptor = deploymentDescriptors[dd];

        File springConfigFile = aDd2Spring.convertDd2Spring(deploymentDescriptor, xslTransform,
                saxonURL, uimaAsDebug);

        // if any are bad, fail
        if (null == springConfigFile) {
          return null;
        }
        springConfigFileArray[dd] = springConfigFile.getAbsolutePath();

        // get the descriptor to register with the engine controller
        String deployDescriptor = "";
        File afile = null;
        FileInputStream fis = null;
        try {
          afile = new File(deploymentDescriptor);
          fis = new FileInputStream(afile);
          byte[] bytes = new byte[(int) afile.length()];
          fis.read(bytes);
          deployDescriptor = new String(bytes);
          // Log Deployment Descriptor
          UIMAFramework.getLogger(CLASS_NAME).logrb(Level.FINEST, CLASS_NAME.getName(), "main",
                  JmsConstants.JMS_LOG_RESOURCE_BUNDLE, "UIMAJMS_deploy_desc__FINEST",
                  new Object[] { deployDescriptor });
        } catch (IOException e) {
          if (UIMAFramework.getLogger(CLASS_NAME).isLoggable(Level.WARNING)) {
            UIMAFramework.getLogger(CLASS_NAME).logrb(Level.WARNING, CLASS_NAME.getName(),
                    "initialize", JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
                    "UIMAJMS_exception__WARNING", e);
          }
        } finally {
          if (fis != null) {
            try {
              fis.close();
            } catch (IOException e) {
            }
          }
        }
      }
    }
    return springConfigFileArray;

  }

  /**
   * Deploy Spring context files in a Spring Container.
   * 
   * @param springContextFiles
   *          - array of Spring context files
   * 
   * @throws Exception
   */
  public SpringContainerDeployer deploy(String[] springContextFiles) throws Exception {
    SpringContainerDeployer springDeployer = new SpringContainerDeployer();
    // now try to deploy the array of spring context files
    springDeployer.deploy(springContextFiles);
    // Poll the deployer for the initialization status. Wait for either successful
    // initialization or failure.
    while (!springDeployer.isInitialized() ) { 
      if ( springDeployer.initializationFailed()) {
        throw new ResourceInitializationException();
      }
      synchronized (springDeployer) {
        springDeployer.wait(100);
      }
    }
    // Check if the deployer failed
    // Register this class to receive Spring container notifications. Specifically, looking
    // for an even signaling the container termination. This is done so that we can stop
    // the monitor thread
    FileSystemXmlApplicationContext context = springDeployer.getSpringContext();
    context.addApplicationListener(this);
    springDeployer.startListeners();
    
    return springDeployer;
  }

  /**
   * Creates an instance of a {@link JmxMonitor}, initializes it with the JMX Server URI and
   * checkpoint frequency, and finally starts the monitor.
   * 
   * @param samplingFrequency
   *          - how often the JmxMonitor should checkpoint to fetch service metrics
   * 
   * @throws Exception
   *           - error on monitor initialization or startup
   */
  public void startMonitor(long samplingFrequency) throws Exception {
    monitor = new JmxMonitor();

    // Check if the monitor should run in the verbose mode. In this mode
    // the monitor dumps JMX Server URI, and a list of UIMA-AS services
    // found in that server. The default is to not show this info.
    if (System.getProperty("verbose") != null) {
      monitor.setVerbose();
    }

    // Use the URI provided in the first arg to connect to the JMX server.
    // Also define sampling frequency. The monitor will collect the metrics
    // at this interval.
    String jmxServerPort = null;
    // get the port of the JMX Server. This property can be set on the command line via -d
    // OR it is set automatically by the code that creates an internal JMX Server. The latter
    // is created in the {@link BaseAnalysisEngineController} constructor.
    if ((jmxServerPort = System.getProperty("com.sun.management.jmxremote.port")) != null) {
      // parameter is set, compose the URI
      String jmxServerURI = "service:jmx:rmi:///jndi/rmi://localhost:" + jmxServerPort + "/jmxrmi";
      // Connect to the JMX Server, configure checkpoint frequency, create MBean proxies for
      // UIMA-AS MBeans and service input queues
      monitor.initialize(jmxServerURI, samplingFrequency);
      // Create formatter listener
      JmxMonitorListener listener = null;
      String formatterListenerClass = null;
      // Check if a custom monitor formatter listener class is provided. The user provides this
      // formatter by adding a -Duima.jmx.monitor.formatter=<class> parameter which specifies a class
      // that implements {@link JmxMonitorListener} interface
      if ((formatterListenerClass = System.getProperty(JmxMonitor.FormatterListener)) != null) {
        Object object = null;
        try {
          // Instantiate the formatter listener class
          Class formatterClass = Class.forName(formatterListenerClass);
          object = formatterClass.newInstance();
        } catch (ClassNotFoundException e) {
          System.out
                  .println("Class Not Found:"
                          + formatterListenerClass
                          + ". Provide a Formatter Class Which Implements:org.apache.uima.aae.jmx.monitor.JmxMonitorListener");
          throw e;
        }
        if (object instanceof JmxMonitorListener) {
          listener = (JmxMonitorListener) object;
        } else {
          throw new InvalidClassException(
                  "Invalid Monitor Formatter Class:"
                          + formatterListenerClass
                          + ".The Monitor Requires a Formatter Which Implements:org.apache.uima.aae.jmx.monitor.JmxMonitorListener");
        }
      } else {
        // The default formatter listener which logs to the UIMA log
        listener = new BasicUimaJmxMonitorListener(monitor.getMaxServiceNameLength());
      }
      // Plug in the monitor listener
      monitor.addJmxMonitorListener(listener);
      // Create and start the monitor thread
      monitorThread = new Thread(monitor);

      // Start the monitor thread. It will run until the Spring container stops. When this happens
      // the UIMA_Service receives notication via a {@code onApplicationEvent()} callback. There
      // the monitor is stopped allowing the service to terminate.
      monitorThread.start();
      System.out.println(">>> Started JMX Monitor.\n\t>>> MBean Server Port:" + jmxServerPort
              + "\n\t>>> Monitor Sampling Interval:" + samplingFrequency
              + "\n\t>>> Monitor Formatter Class:" + listener.getClass().getName());
    }

  }

  /**
   * scan args for a particular arg, return the following token or the empty string if not found
   * 
   * @param id
   *          the arg to search for
   * @param args
   *          the array of strings
   * @return the following token, or a 0 length string if not found
   */
  private static String getArg(String id, String[] args) {
    for (int i = 0; i < args.length; i++) {
      if (id.equals(args[i]))
        return (i + 1 < args.length) ? args[i + 1] : "";
    }
    return "";
  }

  /**
   * scan args for a particular arg, return the following token(s) or the empty string if not found
   * 
   * @param id
   *          the arg to search for
   * @param args
   *          the array of strings
   * @return the following token, or a 0 length string array if not found
   */
  private static String[] getMultipleArg(String id, String[] args) {
    String[] retr = {};
    for (int i = 0; i < args.length; i++) {
      if (id.equals(args[i])) {
        String[] temp = new String[retr.length + 1];
        for (int s = 0; s < retr.length; s++) {
          temp[s] = retr[s];
        }
        retr = temp;
        retr[retr.length - 1] = (i + 1 < args.length) ? args[i + 1] : null;
      }
    }
    return retr;
  }

  /**
   * scan args for a particular arg, return the following token(s) or the empty string if not found
   * 
   * @param id
   *          the arg to search for
   * @param args
   *          the array of strings
   * @return the following token, or a 0 length string array if not found
   */
  private static String[] getMultipleArg2(String id, String[] args) {
    String[] retr = {};
    for (int i = 0; i < args.length; i++) {
      if (id.equals(args[i])) {
        int j = 0;
        while ((i + 1 + j < args.length) && !args[i + 1 + j].startsWith("-")) {
          String[] temp = new String[retr.length + 1];
          for (int s = 0; s < retr.length; s++) {
            temp[s] = retr[s];
          }
          retr = temp;
          retr[retr.length - 1] = args[i + 1 + j++];
        }
        return retr;
      }
    }
    return retr;
  }

  protected void finalize() {
    System.err.println(this + " finalized");
  }

  private static void printUsageMessage() {
    System.out
            .println(" Arguments to the program are as follows : \n"
                    + "-d path-to-UIMA-Deployment-Descriptor [-d path-to-UIMA-Deployment-Descriptor ...] \n"
                    + "-saxon path-to-saxon.jar \n"
                    + "-xslt path-to-dd2spring-xslt\n"
                    + "   or\n"
                    + "path to Spring XML Configuration File which is the output of running dd2spring\n"
                    + "-defaultBrokerURL the default broker URL to use for the service and all its delegates");
  }

  public void onApplicationEvent(ApplicationEvent event) {
    if (event instanceof ContextClosedEvent && monitor != null && monitor.isRunning()) {
      System.out.println("Stopping Monitor");
      // Stop the monitor. The service has stopped
      monitor.doStop();
    }
  }

  /**
   * The main routine for starting the deployment of a UIMA-AS instance. The args are either: 1 or
   * more "paths" to Spring XML descriptors representing the information needed or some number of
   * parameters, preceeded by a "-" sign. If the first arg doesn't start with a "-" it is presumed
   * to be the first format.
   * 
   * For the 2nd style, the arguments are: -saxonURL a-URL-to-the-saxon-jar usually starting with
   * "file:", -xslt path-to-the-dd2spring.xsl file, -d path-to-UIMA-deployment-descriptor [-d
   * path-to-another-dd ...] these arguments may be in any order)
   * 
   * For the 3rd style, like #2 but with multiple dd-files following a single -dd Useful for calling
   * from scripts.
   * 
   * @param args
   */
  public static void main(String[] args) {
    try {
      UIMA_Service service = new UIMA_Service();
      // parse command args and run dd2spring to generate spring context
      // files from deployment descriptors
      String contextFiles[] = service.initialize(args);
      // If no context files generated there is nothing to do
      if (contextFiles == null) {
        return;
      }
      // Deploy components defined in Spring context files. This method blocks until
      // the container is fully initialized and all UIMA-AS components are succefully
      // deployed.
      SpringContainerDeployer serviceDeployer = service.deploy(contextFiles);

      if (serviceDeployer == null) {
        System.out.println(">>> Failed to Deploy UIMA Service. Check Logs for Details");
        System.exit(1);
      }
      // Add a shutdown hook to catch kill signal and to force quiesce and stop
      ServiceShutdownHook shutdownHook = new ServiceShutdownHook(serviceDeployer);
      Runtime.getRuntime().addShutdownHook(shutdownHook);
      // Check if we should start an optional JMX-based monitor that will provide service metrics
      // The monitor is enabled by existence of -Duima.jmx.monitor.interval=<number> parameter. By
      // default
      // the monitor is not enabled.
      String monitorCheckpointFrequency;
      if ((monitorCheckpointFrequency = System.getProperty(JmxMonitor.SamplingInterval)) != null) {
        // Found monitor checkpoint frequency parameter, configure and start the monitor.
        // If the monitor fails to initialize the service is not effected.
        service.startMonitor(Long.parseLong(monitorCheckpointFrequency));
      }
      AnalysisEngineController topLevelControllor = serviceDeployer.getTopLevelController();
      String prompt = "Press 'q'+'Enter' to quiesce and stop the service or 's'+'Enter' to stop it now.\nNote: selected option is not echoed on the console.";
      if (topLevelControllor != null) {
        System.out.println(prompt);
        // Loop forever or until the service is stopped
        while (!topLevelControllor.isStopped()) {
          if (System.in.available() > 0) {
            int c = System.in.read();
            if (c == 's') {
              serviceDeployer.undeploy(SpringContainerDeployer.STOP_NOW);
            } else if (c == 'q') {
              serviceDeployer.undeploy(SpringContainerDeployer.QUIESCE_AND_STOP);
            } else if (Character.isLetter(c) || Character.isDigit(c)) {
              System.out.println(prompt);
            }
          }
          // This is a polling loop. Sleep for 1 sec
          try {
        	if (!topLevelControllor.isStopped()) 
              Thread.sleep(1000);
          } catch (InterruptedException ex) {
          }
        } // while
      }
    } catch (Exception e) {
      if (UIMAFramework.getLogger(CLASS_NAME).isLoggable(Level.WARNING)) {
        UIMAFramework.getLogger(CLASS_NAME).logrb(Level.WARNING, CLASS_NAME.getName(),
                "main", JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
                "UIMAJMS_exception__WARNING", e);
      }
    }
  }
  static class ServiceShutdownHook extends Thread {

    public SpringContainerDeployer serviceDeployer;

    public ServiceShutdownHook(SpringContainerDeployer serviceDeployer) {
      this.serviceDeployer = serviceDeployer;
    }

    public void run() {
      try {
      	AnalysisEngineController topLevelController = serviceDeployer.getTopLevelController();
      	if (topLevelController != null && !topLevelController.isStopped() ) {
      	  UIMAFramework.getLogger(CLASS_NAME).logrb(Level.WARNING, CLASS_NAME.getName(),
                "run", JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
                "UIMAJMS_caught_signal__INFO", new Object[] { topLevelController.getComponentName() });
    	    serviceDeployer.undeploy(SpringContainerDeployer.QUIESCE_AND_STOP);
    	  }
      } catch( Exception e) {
        if (UIMAFramework.getLogger(CLASS_NAME).isLoggable(Level.WARNING)) {
          UIMAFramework.getLogger(CLASS_NAME).logrb(Level.WARNING, CLASS_NAME.getName(),
                  "run", JmsConstants.JMS_LOG_RESOURCE_BUNDLE,
                  "UIMAJMS_exception__WARNING", e);
        }
      }
    }
  } 
}
