/*
 * WrapperUtils.java
 *
 * Copyright (C) 2006 - 2007 Martin Slota
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program 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 General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/*
 * History:
 * v0.1 (2007-01-08): initial version
 * v0.2 (2007-01-10):
 * - getCommandName, getShortCommandName tested, implemented, documented
 * - 2 transfer methods tested, implemented, documented
 * - 2 exec methods tested, implemented, documented
 * v0.2.1 (2007-01-12):
 * - changed from static methods to a singleton
 * v0.2.2 (2007-01-14):
 * - exec methods moved to AbstractWrapper
 * - documentation and tests updated
 * v0.2.3 (2007-01-20):
 * - dumpToString added, tested, documented
 * v0.2.4 (2007-01-28):
 * - checked exceptions eliminated (see
 *   http://www.mindview.net/Etc/Discussions/CheckedExceptions)
 * - documentation updated
 * v0.2.5 (2007-02-14):
 * - static methods from AbstractWrapper moved here
 * v1.0.0 (2007-05-05):
 * - simplified to allow for parallel pipelining
 */

package lp.wrap;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.util.StringTokenizer;
import lp.util.ExceptionAdapter;

/**
 * This class provides some static methods that can be useful when handling
 * external commands that receive data on their standard input. It provides:
 * <ul>
 * <li>convenience methods for extracting a command name from the command string
 * ({@link #getCommandName(String)} and {@link #getShortCommandName(String)}),
 * </li>
 * <li>methods for transfering bytes from an {@link InputStream} to an
 * {@link OutputStream} ({@link #transfer(InputStream, OutputStream)}) or to a
 * {@link StringBuilder} ({@link #transfer(InputStream, StringBuilder)})</li>
 * </ul>
 *
 * @author Martin Slota
 * @version 1.0.0
 */
public class WrapperUtils {
	/**
	 * A byte buffer for the {@link #transfer(InputStream, OutputStream)}
	 * method.
	 */
	private final byte[] byteBuffer;
	
	/**
	 * A character buffer for the {@link #transfer(InputStream, StringBuilder)}
	 * method.
	 */
	private final char[] charBuffer;
	
	/**
	 * Reference to the singleton instance of this class.
	 */
	private static WrapperUtils instance = null;
	
	/**
	 * Returns the singleton instance.
	 */
	public static synchronized WrapperUtils getInstance() {
		if (instance == null)
			instance = new WrapperUtils();
		return instance;
	}
	
	/**
	 * A private constructor to ensure this is really a singleton.
	 */
	private WrapperUtils() {
		byteBuffer = new byte[8192];
		charBuffer = new char[8192];
	}
	
	/**
	 * Returns the command's name given the command string, i.e. the part of the
	 * command string that precedes the first whitespace character.
	 *
	 * For example, if the separator character is '/' and the {@code command}
	 * is "/usr/bin/mkdir tmp", then the result will be "/usr/bin/mkdir".
	 *
	 * If {@code command} is {@code null}, then {@code null} is returned.
	 *
	 * @param command the command string
	 * @return the command's name
	 */
	public String getCommandName(String command) {
		if (command == null)
			return null;
		return new StringTokenizer(command).nextToken();
	}
	
	/**
	 * Returns the command's name without the preceding path given the command
	 * string, i.e. the part of the command's name (as defined in
	 * {@link #getCommandName(String)}) that follows the last
	 * {@link File#separatorChar} character.
	 *
	 * For example, if the separator character is '/' and the {@code command}
	 * is "/usr/bin/mkdir tmp", then the result will be "mkdir".
	 *
	 * If {@code command} is {@code null}, then {@code null} is returned.
	 *
	 * @param command the command string
	 * @return the command's name
	 * @throws IllegalArgumentException if {@code command} is {@code null}.
	 * @see #getCommandName(String)
	 */
	public String getShortCommandName(String command) {
		if (command == null)
			return null;
		String name = getCommandName(command);
		int lastSlash = name.lastIndexOf(File.separatorChar);
		if (lastSlash == -1)
			return name;
		else
			return name.substring(lastSlash + 1);
	}
	
	/**
	 * Transfers the contents of {@code from} to {@code to}. Invokes
	 * {@code from.close()} after the transfer. This method is thread-safe.
	 *
	 * @param from source of the transfer
	 * @param to destination of the transfer
	 * @throws NullPointerException if {@code from} contains some bytes and
	 * {@code to} is {@code null} (in other cases the null values don't matter)
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter})
	 * if an I/O error occurs during the transfer
	 * @link #byteBuffer
	 */
	public void transfer(InputStream from, OutputStream to) {
		if (from == null)
			return;
		try {
			synchronized (byteBuffer) {
				int numRead = from.read(byteBuffer);
				while (numRead != -1) {
					to.write(byteBuffer, 0, numRead);
					numRead = from.read(byteBuffer);
				}
			}
			from.close();
		} catch (IOException e) {
			throw new ExceptionAdapter(e);
		}
	}
	
	/**
	 * Transfers the contents of {@code from} to {@code to} using the default
	 * character encoding. This method is thread-safe. If {@code from} is
	 * {@code null}, nothing is appended.
	 *
	 * @param from string convert to bytes and append to {@code to}
	 * @param to destination of the transfer
	 * @throws NullPointerException if {@code from} contains some bytes and
	 * {@code to} is {@code null} (otherwise the null values are gracefully
	 * ignored)
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O error occurs during the transfer
	 * @link #charBuffer
	 */
	public void transfer(String from, OutputStream to) {
		if (from == null)
			return;
		transfer(new ByteArrayInputStream(from.getBytes()), to);
	}
	
	/**
	 * Transfers the contents of {@code from} to {@code to} using the default
	 * character encoding. Invokes {@code from.close()} after the transfer. This
	 * method is thread-safe.
	 *
	 * @param from source of the transfer
	 * @param to destination of the transfer
	 * @throws NullPointerException if {@code from} contains some bytes and
	 * {@code to} is {@code null} (otherwise the null values are gracefully
	 * ignored)
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O error occurs during the transfer
	 * @link #charBuffer
	 */
	public StringBuilder transfer(InputStream from, StringBuilder to) {
		if (from == null)
			return to;
		Reader reader = new InputStreamReader(from);
		try {
			synchronized (charBuffer) {
				int numRead = reader.read(charBuffer);
				while (numRead != -1) {
					to.append(charBuffer, 0, numRead);
					numRead = reader.read(charBuffer);
				}
			}
			from.close();
		} catch (IOException e) {
			throw new ExceptionAdapter(e);
		}
		return to;
	}
	
	/**
	 * Returns the contents of an {@code InputStream} as a string. Uses the
	 * default encoding to convert bytes to characters. If {@code from} is
	 * {@code null}, an empty string is returned. This method is thread-safe.
	 *
	 * @param from the source input stream
	 * @return the string with contents of {@code from}
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O error occurs during the conversion
	 */
	public String dumpToString(InputStream from) {
		return transfer(from, new StringBuilder()).toString();
	}
	
	/**
	 * Executes the command {@code command} through {@link Runtime#exec(String)}
	 * and puts the contents of {@code input} on the process's standard input.
	 * The created {@link Process} instance is returned.
	 *
	 * @param command the command to be executed
	 * @return the created {@code Process} object
	 * @throws NullPointerException if {@code command} is {@code null}
	 * @throws IllegalArgumentException if {@code command} is an empty string
	 * @throws WrapperException if an {@link IOException} occurs while creating
	 * the process
	 */
	public Process exec(String command) throws WrapperException {
		try {
			return Runtime.getRuntime().exec(command);
		} catch (IOException e) {
			throw new WrapperException(
					getShortCommandName(command) + " could not be executed " +
					"(path `" + getCommandName(command) +
					"' is probably invalid)", e);
		}
	}
}