/*
 * Copyright 2009 LinkedIn, Inc.
 * 
 * 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 voldemort.utils.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import voldemort.utils.RemoteOperationException;

/**
 * CommandLineRemoteOperation represents a command-line based remote operation.
 * That is, it invokes the remote system by means of invoking a command line
 * executable. Often (but not always) this is an SSH-based approach, using the
 * ssh client on the host operating system to communicate with the remote system
 * to invoke a command line script/program.
 * 
 * <p/>
 * 
 * What CommandLineRemoteOperation solves for its subclasses is converting
 * per-host String commands to invokable UnixCommand objects that are then
 * executed in parallel against the remote hosts. The results of those command
 * line invocations are then placed (in arbitrary order) into a list that is
 * returned to the caller.
 * 
 * 
 * @param <T> Return type for the operation, specific to the subclass
 *        implementation
 */

abstract class CommandLineRemoteOperation {

    protected final Log logger = LogFactory.getLog(getClass());

    /**
     * Executes the given commands in parallel on the remote hosts and
     * aggregates the results for the caller.
     * 
     * @param hostNameCommandLineMap Map with a key is the external host name
     *        and the value is the command line to execute remotely on that host
     * 
     * @return List of result types as dictated by the subclass
     * 
     * @throws RemoteOperationException Thrown on error invoking the command on
     *         one or more clients.
     */

    protected void execute(Map<String, String> hostNameCommandLineMap)
            throws RemoteOperationException {
        CommandLineParser commandLineParser = new CommandLineParser();

        ExecutorService threadPool = Executors.newFixedThreadPool(hostNameCommandLineMap.size());
        List<Future<?>> futures = new ArrayList<Future<?>>();

        for(Map.Entry<String, String> entry: hostNameCommandLineMap.entrySet()) {
            String hostName = entry.getKey();
            String commandLine = entry.getValue();

            if(logger.isDebugEnabled())
                logger.debug("Command to execute: " + commandLine);

            List<String> commandArgs = commandLineParser.parse(commandLine);
            UnixCommand command = new UnixCommand(hostName, commandArgs);
            Callable<?> callable = getCallable(command);
            Future<?> future = threadPool.submit(callable);
            futures.add(future);
        }

        // Build up a list of all the results and/or capture the errors as they
        // occur.
        try {
            StringBuilder errors = new StringBuilder();

            for(Future<?> future: futures) {
                Throwable t = null;

                try {
                    future.get();
                } catch(ExecutionException ex) {
                    t = ex.getCause();
                } catch(Exception e) {
                    t = e;
                }

                if(t != null) {
                    if(logger.isWarnEnabled())
                        logger.warn(t, t);

                    if(errors.length() > 0)
                        errors.append("; ");

                    errors.append(t.getMessage());
                }
            }

            if(errors.length() > 0)
                throw new RemoteOperationException(errors.toString());
        } finally {
            threadPool.shutdown();

            try {
                threadPool.awaitTermination(60, TimeUnit.SECONDS);
            } catch(InterruptedException e) {
                if(logger.isWarnEnabled())
                    logger.warn(e, e);
            }
        }
    }

    /**
     * By default we'll add a logging listener to output the output from the
     * remote invocation and return the result of the ExitCodeCallable.
     * 
     * <p/>
     * 
     * This method can be overridden by subclasses to provide a more useful
     * interaction with the output from the remote host.
     * 
     * @param command UnixCommand to execute
     * 
     * @return Callable that is used in the thread pool in the execute method
     */

    protected Callable<?> getCallable(UnixCommand command) {
        CommandOutputListener commandOutputListener = new LoggingCommandOutputListener(null,
                                                                                       logger,
                                                                                       true);
        return new ExitCodeCallable(command, commandOutputListener);
    }

}
