/**
 * 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.hadoop.ant;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;

import java.util.LinkedList;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FsShell;

import org.apache.tools.ant.AntClassLoader;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.hadoop.util.ToolRunner;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.hdfs.HdfsConfiguration;

/**
 * {@link org.apache.hadoop.fs.FsShell FsShell} wrapper for ant Task.
 */
@InterfaceAudience.Private
public class DfsTask extends Task {

  /**
   * Default sink for {@link java.lang.System.out System.out}
   * and {@link java.lang.System.err System.err}.
   */
  private static final OutputStream nullOut = new OutputStream() {
      public void write(int b)    { /* ignore */ }
      public String toString()    { return ""; }
  };
  private static final FsShell shell = new FsShell();

  protected AntClassLoader confloader;
  protected OutputStream out = nullOut;
  protected OutputStream err = nullOut;

  // set by ant
  protected String cmd;
  protected final LinkedList<String> argv = new LinkedList<String>();
  protected String outprop;
  protected String errprop;
  protected boolean failonerror = true;

  // saved ant context
  private PrintStream antOut;
  private PrintStream antErr;

  /**
   * Sets the command to run in {@link org.apache.hadoop.fs.FsShell FsShell}.
   * @param cmd A valid command to FsShell, sans &quot;-&quot;.
   */
  public void setCmd(String cmd) {
    this.cmd = "-" + cmd.trim();
  }

  /**
   * Sets the argument list from a String of comma-separated values.
   * @param args A String of comma-separated arguments to FsShell.
   */
  public void setArgs(String args) {
    for (String s : args.trim().split("\\s*,\\s*"))
      argv.add(s);
  }

  /**
   * Sets the property into which System.out will be written.
   * @param outprop The name of the property into which System.out is written.
   * If the property is defined before this task is executed, it will not be updated.
   */
  public void setOut(String outprop) {
    this.outprop = outprop;
    out = new ByteArrayOutputStream();
    if (outprop.equals(errprop))
      err = out;
  }

  /**
   * Sets the property into which System.err will be written. If this property
   * has the same name as the property for System.out, the two will be interlaced.
   * @param errprop The name of the property into which System.err is written.
   * If the property is defined before this task is executed, it will not be updated.
   */
  public void setErr(String errprop) {
    this.errprop = errprop;
    err = (errprop.equals(outprop)) ? err = out : new ByteArrayOutputStream();
  }

  /**
   * Sets the path for the parent-last ClassLoader, intended to be used for
   * {@link org.apache.hadoop.conf.Configuration Configuration}.
   * @param confpath The path to search for resources, classes, etc. before
   * parent ClassLoaders.
   */
  public void setConf(String confpath) {
    confloader = new AntClassLoader(getClass().getClassLoader(), false);
    confloader.setProject(getProject());
    if (null != confpath)
      confloader.addPathElement(confpath);
  }

  /**
   * Sets a property controlling whether or not a
   * {@link org.apache.tools.ant.BuildException BuildException} will be thrown
   * if the command returns a value less than zero or throws an exception.
   * @param failonerror If true, throw a BuildException on error.
   */
  public void setFailonerror(boolean failonerror) {
    this.failonerror = failonerror;
  }

  /**
   * Save the current values of System.out, System.err and configure output
   * streams for FsShell.
   */
  protected void pushContext() {
    antOut = System.out;
    antErr = System.err;
    System.setOut(new PrintStream(out));
    System.setErr(out == err ? System.out : new PrintStream(err));
  }

  /**
   * Create the appropriate output properties with their respective output,
   * restore System.out, System.err and release any resources from created
   * ClassLoaders to aid garbage collection.
   */
  protected void popContext() {
    // write output to property, if applicable
    if (outprop != null && !System.out.checkError())
      getProject().setNewProperty(outprop, out.toString());
    if (out != err && errprop != null && !System.err.checkError())
      getProject().setNewProperty(errprop, err.toString());

    System.setErr(antErr);
    System.setOut(antOut);
    confloader.cleanup();
    confloader.setParent(null);
  }

  // in case DfsTask is overridden
  protected int postCmd(int exit_code) {
    if ("-test".equals(cmd) && exit_code != 0)
      outprop = null;
    return exit_code;
  }

  /**
   * Invoke {@link org.apache.hadoop.fs.FsShell#doMain FsShell.doMain} after a
   * few cursory checks of the configuration.
   */
  public void execute() throws BuildException {
    if (null == cmd)
      throw new BuildException("Missing command (cmd) argument");
    argv.add(0, cmd);

    if (null == confloader) {
      setConf(getProject().getProperty("hadoop.conf.dir"));
    }

    int exit_code = 0;
    try {
      pushContext();

      Configuration conf = new HdfsConfiguration();
      conf.setClassLoader(confloader);
      exit_code = ToolRunner.run(conf, shell,
          argv.toArray(new String[argv.size()]));
      exit_code = postCmd(exit_code);

      if (0 > exit_code) {
        StringBuilder msg = new StringBuilder();
        for (String s : argv)
          msg.append(s + " ");
        msg.append("failed: " + exit_code);
        throw new Exception(msg.toString());
      }
    } catch (Exception e) {
      if (failonerror)
          throw new BuildException(e);
    } finally {
      popContext();
    }
  }
}
