/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2008, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.varia.process;

import java.util.Properties;
import java.util.Iterator;

import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.BufferedReader;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import org.jboss.system.ServiceMBeanSupport;

import org.jboss.util.NullArgumentException;

/**
 * A service to manage a child process.
 *
 * @jmx:mbean extends="org.jboss.system.ServiceMBean"
 *
 * @version <tt>$Revision: 89151 $</tt>
 * @author  <a href="mailto:jason@planet57.com">Jason Dillon</a>
 */
public class ChildProcessService
   extends ServiceMBeanSupport
   implements ChildProcessServiceMBean
{
   /** The command line of the process to execute. */
   protected String commandLine;

   /** The environment for the process. */
   protected Properties env;

   /** The working directory of the process. */
   protected File workingDir;

   /** The child, we are so proud. */
   protected Process childProcess;

   /**
    * The name of the logger adapter for the child process' streams.
    */
   protected String loggerAdapterName = this.getClass().getName();
   
   /**
    * The input adapter, which takes the process' STDOUT and
    * turns them into logger calls.
    */
   protected ReaderLoggerAdapter inputAdapter;

   /**
    * The input adapter, which takes the process' STDERR and
    * turns them into logger calls.
    */
   protected ReaderLoggerAdapter errorAdapter;
   
   /**
    * @jmx:managed-attribute
    */
   public void setCommandLine(final String commandLine)
   {
      if (commandLine == null)
         throw new NullArgumentException("commandLine");
      
      this.commandLine = commandLine;
   }

   /**
    * @jmx:managed-attribute
    */
   public String getCommandLine()
   {
      return commandLine;
   }

   /**
    * @jmx:managed-attribute
    */
   public void setEnvironment(final Properties env)
   {
      this.env = env;
   }

   /**
    * @jmx:managed-attribute
    */
   public Properties getEnvironment()
   {
      return env;
   }

   /**
    * @jmx:managed-attribute
    */
   public void setWorkingDirectory(final File dir)
   {
      // only check if not null
      if (dir != null) {
         if (dir.exists()) {
            if (!dir.isDirectory()) {
               throw new IllegalArgumentException
                  ("Directory argument does not point to a directory: " + dir);
            }
         }
      }

      this.workingDir = dir;
   }

   /**
    * @jmx:managed-attribute
    */
   public File getWorkingDirectory()
   {
      return workingDir;
   }

   /**
    * @jmx:managed-attribute
    */
   public Integer getExitValue()
   {
      if (childProcess != null) {
         return new Integer(childProcess.exitValue());
      }

      return null;
   }

   /**
    * @jmx:managed-attribute
    */
   public void setLoggerAdapterName(final String name)
   {
      this.loggerAdapterName = name;
   }

   /**
    * @jmx:managed-attribute
    */
   public String getLoggerAdapterName()
   {
      return loggerAdapterName;
   }
   
   protected String[] makeEnvArray(final Properties props)
   {
      if (props == null)
         return new String[0];
      
      String[] envArray = new String[props.keySet().size()];

      Iterator iter = props.keySet().iterator();
      int i = 0;
      while (iter.hasNext()) {
         String name = (String)iter.next();
         envArray[i++] = name + "=" + props.getProperty(name);
      }

      return envArray;
   }

   ///////////////////////////////////////////////////////////////////////////
   //                   Reader/InputStream Logger Adapter                   //
   ///////////////////////////////////////////////////////////////////////////

   protected static class ReaderLoggerAdapter
      implements Runnable
   {
      protected BufferedReader reader;
      protected boolean shutdown;
      protected Logger log;
      protected Level level;
      
      public ReaderLoggerAdapter(final Reader reader, final Logger log, final Level level)
      {
         if (reader instanceof BufferedReader) {
            this.reader = (BufferedReader)reader;
         }
         else {
            this.reader = new BufferedReader(reader);
         }

         this.log = log;
         this.level = level;
      }

      public ReaderLoggerAdapter(final InputStream input, final Logger log, final Level level)
      {
         this(new InputStreamReader(input), log, level);
      }

      public void shutdown()
      {
         shutdown = true;
      }

      public void run()
      {
         boolean intr = false;
         try
         {
            while (!shutdown) {
               try {
                  String data = reader.readLine();
                  if (data == null) {
                     try {
                        Thread.sleep(1000);
                     }
                     catch (InterruptedException ignore)
                     {
                        intr = true;
                     }
                  }
                  else {
                     log.log(level, data);
                  }
               }
               catch (IOException e) {
                  log.error("Failed to read data from reader", e);
               }
            }
         }
         finally
         {
            if (intr) Thread.currentThread().interrupt();
         }
      }
   }
   
   
   ///////////////////////////////////////////////////////////////////////////
   //                    ServiceMBeanSupport Overrides                      //
   ///////////////////////////////////////////////////////////////////////////

   protected void startService() throws Exception
   {
      Runtime rt = Runtime.getRuntime();

      childProcess = rt.exec(commandLine, makeEnvArray(env), workingDir);
      log.info("Spawned child process: " + commandLine);

      // hook up the processes output streams to logging
      Logger logger = Logger.getLogger(loggerAdapterName);
      
      InputStream input = childProcess.getInputStream();
      inputAdapter = new ReaderLoggerAdapter(input, logger, Level.INFO);
      new Thread(inputAdapter).start();
      
      InputStream error = childProcess.getErrorStream();
      errorAdapter = new ReaderLoggerAdapter(error, logger, Level.ERROR);
      new Thread(errorAdapter).start();
   }

   protected void stopService() throws Exception
   {
      childProcess.destroy();

      log.debug("Child process destroyed; waiting for process to exit");
      childProcess.waitFor();

      log.info("Child exited with code: " + getExitValue());

      inputAdapter.shutdown();
      errorAdapter.shutdown();
      
      childProcess = null;
      inputAdapter = null;
      errorAdapter = null;
   }
}
