All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.hadoop.fs.shell.Command Maven / Gradle / Ivy

There is a newer version: 3.4.1
Show newest version
/**
 * 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.fs.shell;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.PathNotFoundException;
import org.apache.hadoop.fs.RemoteIterator;
import org.apache.hadoop.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An abstract class for the execution of a file system command
 */
@InterfaceAudience.Private
@InterfaceStability.Evolving

abstract public class Command extends Configured {
  /** field name indicating the default name of the command */
  public static final String COMMAND_NAME_FIELD = "NAME";
  /** field name indicating the command's usage switches and arguments format */
  public static final String COMMAND_USAGE_FIELD = "USAGE";
  /** field name indicating the command's long description */
  public static final String COMMAND_DESCRIPTION_FIELD = "DESCRIPTION";
    
  protected String[] args;
  protected String name;
  protected int exitCode = 0;
  protected int numErrors = 0;
  protected boolean recursive = false;
  private int depth = 0;
  protected ArrayList exceptions = new ArrayList();

  private static final Logger LOG = LoggerFactory.getLogger(Command.class);

  /** allows stdout to be captured if necessary */
  public PrintStream out = System.out;
  /** allows stderr to be captured if necessary */
  public PrintStream err = System.err;
  /** allows the command factory to be used if necessary */
  private CommandFactory commandFactory = null;

  /** Constructor */
  protected Command() {
    out = System.out;
    err = System.err;
  }
  
  /** Constructor */
  protected Command(Configuration conf) {
    super(conf);
  }
  
  /** @return the command's name excluding the leading character - */
  abstract public String getCommandName();
  
  protected void setRecursive(boolean flag) {
    recursive = flag;
  }
  
  protected boolean isRecursive() {
    return recursive;
  }

  protected int getDepth() {
    return depth;
  }
  
  /** 
   * Execute the command on the input path
   * 
   * @param path the input path
   * @throws IOException if any error occurs
   */
  abstract protected void run(Path path) throws IOException;

  /**
   * Execute the command on the input path data. Commands can override to make
   * use of the resolved filesystem.
   * @param pathData The input path with resolved filesystem
   * @throws IOException
   */
  protected void run(PathData pathData) throws IOException {
    run(pathData.path);
  }

  /** 
   * For each source path, execute the command
   * 
   * @return 0 if it runs successfully; -1 if it fails
   */
  public int runAll() {
    int exitCode = 0;
    for (String src : args) {
      try {
        PathData[] srcs = PathData.expandAsGlob(src, getConf());
        for (PathData s : srcs) {
          run(s);
        }
      } catch (IOException e) {
        exitCode = -1;
        displayError(e);
      }
    }
    return exitCode;
  }

  /** sets the command factory for later use */
  public void setCommandFactory(CommandFactory factory) {
    this.commandFactory = factory;
  }
  /** retrieves the command factory */
  protected CommandFactory getCommandFactory() {
    return this.commandFactory;
  }

  /**
   * Invokes the command handler.  The default behavior is to process options,
   * expand arguments, and then process each argument.
   * 
   * run
   * |{@literal ->} {@link #processOptions(LinkedList)}
   * \{@literal ->} {@link #processRawArguments(LinkedList)}
   *      |{@literal ->} {@link #expandArguments(LinkedList)}
   *      |   \{@literal ->} {@link #expandArgument(String)}*
   *      \{@literal ->} {@link #processArguments(LinkedList)}
   *          |{@literal ->} {@link #processArgument(PathData)}*
   *          |   |{@literal ->} {@link #processPathArgument(PathData)}
   *          |   \{@literal ->} {@link #processPaths(PathData, PathData...)}
   *          |        \{@literal ->} {@link #processPath(PathData)}*
   *          \{@literal ->} {@link #processNonexistentPath(PathData)}
   * 
* Most commands will chose to implement just * {@link #processOptions(LinkedList)} and {@link #processPath(PathData)} * * @param argv the list of command line arguments * @return the exit code for the command * @throws IllegalArgumentException if called with invalid arguments */ public int run(String...argv) { LinkedList args = new LinkedList(Arrays.asList(argv)); try { if (isDeprecated()) { displayWarning( "DEPRECATED: Please use '"+ getReplacementCommand() + "' instead."); } processOptions(args); processRawArguments(args); } catch (CommandInterruptException e) { displayError("Interrupted"); return 130; } catch (IOException e) { displayError(e); } return (numErrors == 0) ? exitCode : exitCodeForError(); } /** * The exit code to be returned if any errors occur during execution. * This method is needed to account for the inconsistency in the exit * codes returned by various commands. * @return a non-zero exit code */ protected int exitCodeForError() { return 1; } /** * Must be implemented by commands to process the command line flags and * check the bounds of the remaining arguments. If an * IllegalArgumentException is thrown, the FsShell object will print the * short usage of the command. * @param args the command line arguments * @throws IOException */ protected void processOptions(LinkedList args) throws IOException {} /** * Allows commands that don't use paths to handle the raw arguments. * Default behavior is to expand the arguments via * {@link #expandArguments(LinkedList)} and pass the resulting list to * {@link #processArguments(LinkedList)} * @param args the list of argument strings * @throws IOException */ protected void processRawArguments(LinkedList args) throws IOException { processArguments(expandArguments(args)); } /** * Expands a list of arguments into {@link PathData} objects. The default * behavior is to call {@link #expandArgument(String)} on each element * which by default globs the argument. The loop catches IOExceptions, * increments the error count, and displays the exception. * @param args strings to expand into {@link PathData} objects * @return list of all {@link PathData} objects the arguments * @throws IOException if anything goes wrong... */ protected LinkedList expandArguments(LinkedList args) throws IOException { LinkedList expandedArgs = new LinkedList(); for (String arg : args) { try { expandedArgs.addAll(expandArgument(arg)); } catch (IOException e) { // other exceptions are probably nasty displayError(e); } } return expandedArgs; } /** * Expand the given argument into a list of {@link PathData} objects. * The default behavior is to expand globs. Commands may override to * perform other expansions on an argument. * @param arg string pattern to expand * @return list of {@link PathData} objects * @throws IOException if anything goes wrong... */ protected List expandArgument(String arg) throws IOException { PathData[] items = PathData.expandAsGlob(arg, getConf()); if (items.length == 0) { // it's a glob that failed to match throw new PathNotFoundException(arg); } return Arrays.asList(items); } /** * Processes the command's list of expanded arguments. * {@link #processArgument(PathData)} will be invoked with each item * in the list. The loop catches IOExceptions, increments the error * count, and displays the exception. * @param args a list of {@link PathData} to process * @throws IOException if anything goes wrong... */ protected void processArguments(LinkedList args) throws IOException { for (PathData arg : args) { try { processArgument(arg); } catch (IOException e) { displayError(e); } } } /** * Processes a {@link PathData} item, calling * {@link #processPathArgument(PathData)} or * {@link #processNonexistentPath(PathData)} on each item. * @param item {@link PathData} item to process * @throws IOException if anything goes wrong... */ protected void processArgument(PathData item) throws IOException { if (item.exists) { processPathArgument(item); } else { processNonexistentPath(item); } } /** * This is the last chance to modify an argument before going into the * (possibly) recursive {@link #processPaths(PathData, PathData...)} * {@literal ->} {@link #processPath(PathData)} loop. Ex. ls and du use * this to expand out directories. * @param item a {@link PathData} representing a path which exists * @throws IOException if anything goes wrong... */ protected void processPathArgument(PathData item) throws IOException { // null indicates that the call is not via recursion, ie. there is // no parent directory that was expanded depth = 0; processPaths(null, item); } /** * Provides a hook for handling paths that don't exist. By default it * will throw an exception. Primarily overriden by commands that create * paths such as mkdir or touch. * @param item the {@link PathData} that doesn't exist * @throws FileNotFoundException if arg is a path and it doesn't exist * @throws IOException if anything else goes wrong... */ protected void processNonexistentPath(PathData item) throws IOException { throw new PathNotFoundException(item.toString()); } /** * Iterates over the given expanded paths and invokes * {@link #processPath(PathData)} on each element. If "recursive" is true, * will do a post-visit DFS on directories. * @param parent if called via a recurse, will be the parent dir, else null * @param items a list of {@link PathData} objects to process * @throws IOException if anything goes wrong... */ protected void processPaths(PathData parent, PathData ... items) throws IOException { for (PathData item : items) { try { processPathInternal(item); } catch (IOException e) { displayError(e); } } } /** * Iterates over the given expanded paths and invokes * {@link #processPath(PathData)} on each element. If "recursive" is true, * will do a post-visit DFS on directories. * @param parent if called via a recurse, will be the parent dir, else null * @param itemsIterator a iterator of {@link PathData} objects to process * @throws IOException if anything goes wrong... */ protected void processPaths(PathData parent, RemoteIterator itemsIterator) throws IOException { int groupSize = getListingGroupSize(); if (groupSize == 0) { // No grouping of contents required. while (itemsIterator.hasNext()) { processPaths(parent, itemsIterator.next()); } } else { List items = new ArrayList(groupSize); while (itemsIterator.hasNext()) { items.add(itemsIterator.next()); if (!itemsIterator.hasNext() || items.size() == groupSize) { processPaths(parent, items.toArray(new PathData[items.size()])); items.clear(); } } } } private void processPathInternal(PathData item) throws IOException { processPath(item); if (recursive && isPathRecursable(item)) { recursePath(item); } postProcessPath(item); } /** * Whether the directory listing for a path should be sorted.? * @return true/false. */ protected boolean isSorted() { return false; } /** * While using iterator method for listing for a path, whether to group items * and process as array? If so what is the size of array? * @return size of the grouping array. */ protected int getListingGroupSize() { return 0; } /** * Determines whether a {@link PathData} item is recursable. Default * implementation is to recurse directories but can be overridden to recurse * through symbolic links. * * @param item * a {@link PathData} object * @return true if the item is recursable, false otherwise * @throws IOException * if anything goes wrong in the user-implementation */ protected boolean isPathRecursable(PathData item) throws IOException { return item.stat.isDirectory(); } /** * Hook for commands to implement an operation to be applied on each * path for the command. Note implementation of this method is optional * if earlier methods in the chain handle the operation. * @param item a {@link PathData} object * @throws RuntimeException if invoked but not implemented * @throws IOException if anything else goes wrong in the user-implementation */ protected void processPath(PathData item) throws IOException { throw new RuntimeException("processPath() is not implemented"); } /** * Hook for commands to implement an operation to be applied on each * path for the command after being processed successfully * @param item a {@link PathData} object * @throws IOException if anything goes wrong... */ protected void postProcessPath(PathData item) throws IOException { } /** * Gets the directory listing for a path and invokes * {@link #processPaths(PathData, PathData...)} * @param item {@link PathData} for directory to recurse into * @throws IOException if anything goes wrong... */ protected void recursePath(PathData item) throws IOException { try { depth++; if (isSorted()) { // use the non-iterative method for listing because explicit sorting is // required. Iterators not guaranteed to return sorted elements processPaths(item, item.getDirectoryContents()); } else { processPaths(item, item.getDirectoryContentsIterator()); } } finally { depth--; } } /** * Display an exception prefaced with the command name. Also increments * the error count for the command which will result in a non-zero exit * code. * @param e exception to display */ public void displayError(Exception e) { // build up a list of exceptions that occurred exceptions.add(e); // use runtime so it rips up through the stack and exits out if (e instanceof InterruptedIOException) { throw new CommandInterruptException(); } LOG.debug("{} failure", getName(), e); String errorMessage = e.getLocalizedMessage(); if (errorMessage == null) { // this is an unexpected condition, so dump the whole exception since // it's probably a nasty internal error where the backtrace would be // useful errorMessage = StringUtils.stringifyException(e); LOG.debug(errorMessage); } else { errorMessage = errorMessage.split("\n", 2)[0]; } displayError(errorMessage); } /** * Display an error string prefaced with the command name. Also increments * the error count for the command which will result in a non-zero exit * code. * @param message error message to display */ public void displayError(String message) { numErrors++; displayWarning(message); } /** * Display an warning string prefaced with the command name. * @param message warning message to display */ public void displayWarning(String message) { err.println(getName() + ": " + message); } /** * The name of the command. Will first try to use the assigned name * else fallback to the command's preferred name * @return name of the command */ public String getName() { return (name == null) ? getCommandField(COMMAND_NAME_FIELD) : name.startsWith("-") ? name.substring(1) : name; } /** * Define the name of the command. * @param name as invoked */ public void setName(String name) { this.name = name; } /** * The short usage suitable for the synopsis * @return "name options" */ public String getUsage() { String cmd = "-" + getName(); String usage = isDeprecated() ? "" : getCommandField(COMMAND_USAGE_FIELD); return usage.isEmpty() ? cmd : cmd + " " + usage; } /** * The long usage suitable for help output * @return text of the usage */ public String getDescription() { return isDeprecated() ? "(DEPRECATED) Same as '" + getReplacementCommand() + "'" : getCommandField(COMMAND_DESCRIPTION_FIELD); } /** * Is the command deprecated? * @return boolean */ public final boolean isDeprecated() { return (getReplacementCommand() != null); } /** * The replacement for a deprecated command * @return null if not deprecated, else alternative command */ public String getReplacementCommand() { return null; } /** * Get a public static class field * @param field the field to retrieve * @return String of the field */ private String getCommandField(String field) { String value; try { Field f = this.getClass().getDeclaredField(field); f.setAccessible(true); value = f.get(this).toString(); } catch (Exception e) { throw new RuntimeException( "failed to get " + this.getClass().getSimpleName()+"."+field, e); } return value; } @SuppressWarnings("serial") static class CommandInterruptException extends RuntimeException {} }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy