/**
 * 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;

import java.io.IOException;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.logging.Log;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability;
import org.apache.hadoop.fs.permission.ChmodParser;
import org.apache.hadoop.fs.permission.FsPermission;
import org.apache.hadoop.fs.shell.CommandFactory;
import org.apache.hadoop.fs.shell.CommandFormat;
import org.apache.hadoop.fs.shell.FsCommand;
import org.apache.hadoop.fs.shell.PathData;


/**
 * This class is the home for file permissions related commands.
 * Moved to this separate class since FsShell is getting too large.
 */
@InterfaceAudience.Private
@InterfaceStability.Unstable
public class FsShellPermissions extends FsCommand {

  static Log LOG = FsShell.LOG;
  
  /**
   * Register the permission related commands with the factory
   * @param factory the command factory
   */
  public static void registerCommands(CommandFactory factory) {
    factory.addClass(Chmod.class, "-chmod");
    factory.addClass(Chown.class, "-chown");
    factory.addClass(Chgrp.class, "-chgrp");
  }

  /**
   * The pattern is almost as flexible as mode allowed by chmod shell command.
   * The main restriction is that we recognize only rwxXt. To reduce errors we
   * also enforce octal mode specifications of either 3 digits without a sticky
   * bit setting or four digits with a sticky bit setting.
   */
  public static class Chmod extends FsShellPermissions {
    public static final String NAME = "chmod";
    public static final String USAGE = "[-R] <MODE[,MODE]... | OCTALMODE> PATH...";
    public static final String DESCRIPTION =
      "Changes permissions of a file.\n" +
      "\tThis works similar to shell's chmod with a few exceptions.\n\n" +
      "-R\tmodifies the files recursively. This is the only option\n" +
      "\tcurrently supported.\n\n" +
      "MODE\tMode is same as mode used for chmod shell command.\n" +
      "\tOnly letters recognized are 'rwxXt'. E.g. +t,a+r,g-w,+rwx,o=r\n\n" +
      "OCTALMODE Mode specifed in 3 or 4 digits. If 4 digits, the first may\n" +
      "be 1 or 0 to turn the sticky bit on or off, respectively.  Unlike " +
      "shell command, it is not possible to specify only part of the mode\n" +
      "\tE.g. 754 is same as u=rwx,g=rx,o=r\n\n" +
      "\tIf none of 'augo' is specified, 'a' is assumed and unlike\n" +
      "\tshell command, no umask is applied.";

    protected ChmodParser pp;

    @Override
    protected void processOptions(LinkedList<String> args) throws IOException {
      CommandFormat cf = new CommandFormat(2, Integer.MAX_VALUE, "R", null);
      cf.parse(args);
      setRecursive(cf.getOpt("R"));

      String modeStr = args.removeFirst();
      try {
        pp = new ChmodParser(modeStr);
      } catch (IllegalArgumentException iea) {
        // TODO: remove "chmod : " so it's not doubled up in output, but it's
        // here for backwards compatibility...
        throw new IllegalArgumentException(
            "chmod : mode '" + modeStr + "' does not match the expected pattern.");      
      }
    }
    
    @Override
    protected void processPath(PathData item) throws IOException {
      short newperms = pp.applyNewPermission(item.stat);
      if (item.stat.getPermission().toShort() != newperms) {
        try {
          item.fs.setPermission(item.path, new FsPermission(newperms));
        } catch (IOException e) {
          LOG.debug("Error changing permissions of " + item, e);
          throw new IOException(
              "changing permissions of '" + item + "': " + e.getMessage());
        }
      }
    }    
  }
  
  // used by chown/chgrp
  static private String allowedChars = "[-_./@a-zA-Z0-9]";  

  /**
   * Used to change owner and/or group of files 
   */
  public static class Chown extends FsShellPermissions {
    public static final String NAME = "chown";
    public static final String USAGE = "[-R] [OWNER][:[GROUP]] PATH...";
    public static final String DESCRIPTION =
      "Changes owner and group of a file.\n" +
      "\tThis is similar to shell's chown with a few exceptions.\n\n" +
      "\t-R\tmodifies the files recursively. This is the only option\n" +
      "\tcurrently supported.\n\n" +
      "\tIf only owner or group is specified then only owner or\n" +
      "\tgroup is modified.\n\n" +
      "\tThe owner and group names may only cosists of digits, alphabet,\n"+
      "\tand any of '-_.@/' i.e. [-_.@/a-zA-Z0-9]. The names are case\n" +
      "\tsensitive.\n\n" +
      "\tWARNING: Avoid using '.' to separate user name and group though\n" +
      "\tLinux allows it. If user names have dots in them and you are\n" +
      "\tusing local file system, you might see surprising results since\n" +
      "\tshell command 'chown' is used for local files.";

    ///allows only "allowedChars" above in names for owner and group
    static private final Pattern chownPattern = Pattern.compile(
        "^\\s*(" + allowedChars + "+)?([:](" + allowedChars + "*))?\\s*$");

    protected String owner = null;
    protected String group = null;

    @Override
    protected void processOptions(LinkedList<String> args) throws IOException {
      CommandFormat cf = new CommandFormat(2, Integer.MAX_VALUE, "R");
      cf.parse(args);
      setRecursive(cf.getOpt("R"));
      parseOwnerGroup(args.removeFirst());
    }
    
    /**
     * Parse the first argument into an owner and group
     * @param ownerStr string describing new ownership
     */
    protected void parseOwnerGroup(String ownerStr) {
      Matcher matcher = chownPattern.matcher(ownerStr);
      if (!matcher.matches()) {
        throw new IllegalArgumentException(
            "'" + ownerStr + "' does not match expected pattern for [owner][:group].");
      }
      owner = matcher.group(1);
      group = matcher.group(3);
      if (group != null && group.length() == 0) {
        group = null;
      }
      if (owner == null && group == null) {
        throw new IllegalArgumentException(
            "'" + ownerStr + "' does not specify owner or group.");
      }    
    }
    
    @Override
    protected void processPath(PathData item) throws IOException {
      //Should we do case insensitive match?
      String newOwner = (owner == null || owner.equals(item.stat.getOwner())) ?
                        null : owner;
      String newGroup = (group == null || group.equals(item.stat.getGroup())) ?
                        null : group;

      if (newOwner != null || newGroup != null) {
        try {
          item.fs.setOwner(item.path, newOwner, newGroup);
        } catch (IOException e) {
          LOG.debug("Error changing ownership of " + item, e);
          throw new IOException(
              "changing ownership of '" + item + "': " + e.getMessage());
        }
      }
    }
  }

  /**
   * Used to change group of files 
   */
  public static class Chgrp extends Chown {
    public static final String NAME = "chgrp";
    public static final String USAGE = "[-R] GROUP PATH...";
    public static final String DESCRIPTION =
      "This is equivalent to -chown ... :GROUP ...";

    static private final Pattern chgrpPattern = 
      Pattern.compile("^\\s*(" + allowedChars + "+)\\s*$");

    @Override
    protected void parseOwnerGroup(String groupStr) {
      Matcher matcher = chgrpPattern.matcher(groupStr);
      if (!matcher.matches()) {
        throw new IllegalArgumentException(
            "'" + groupStr + "' does not match expected pattern for group");
      }
      owner = null;
      group = matcher.group(1);
    }
  }
}
