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

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.TreeSet;

import javax.security.auth.login.LoginException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.util.Shell;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.WritableUtils;

/** An implementation of UserGroupInformation in the Unix system */
public class UnixUserGroupInformation extends UserGroupInformation {
  public static final String DEFAULT_USERNAME = "DrWho";
  public static final String DEFAULT_GROUP = "Tardis";

  final static public String UGI_PROPERTY_NAME = "hadoop.job.ugi";
  final static private HashMap<String, UnixUserGroupInformation> user2UGIMap =
    new HashMap<String, UnixUserGroupInformation>();

  /** Create an immutable {@link UnixUserGroupInformation} object. */
  public static UnixUserGroupInformation createImmutable(String[] ugi) {
    return new UnixUserGroupInformation(ugi) {
      public void readFields(DataInput in) throws IOException {
        throw new UnsupportedOperationException();
      }
    };
  }

  private String userName;
  private String[] groupNames;

  /** Default constructor
   */
  public UnixUserGroupInformation() {
  }

  /** Constructor with parameters user name and its group names.
   * The first entry in the groups list is the default  group.
   * 
   * @param userName a user's name
   * @param groupNames groups list, first of which is the default group
   * @exception IllegalArgumentException if any argument is null
   */
  public UnixUserGroupInformation(String userName, String[] groupNames) {
    setUserGroupNames(userName, groupNames);
  }

  /** Constructor with parameter user/group names
   * 
   * @param ugi an array containing user/group names, the first
   *                     element of which is the user name, the second of
   *                     which is the default group name.
   * @exception IllegalArgumentException if the array size is less than 2 
   *                                     or any element is null.
   */
  public UnixUserGroupInformation(String[] ugi) {
    if (ugi==null || ugi.length < 2) {
      throw new IllegalArgumentException( "Parameter does contain at least "+
          "one user name and one group name");
    }
    String[] groupNames = new String[ugi.length-1];
    System.arraycopy(ugi, 1, groupNames, 0, groupNames.length);
    setUserGroupNames(ugi[0], groupNames);
  }
  
  /* Set this object's user name and group names
   * 
   * @param userName a user's name
   * @param groupNames groups list, the first of which is the default group
   * @exception IllegalArgumentException if any argument is null
   */
  private void setUserGroupNames(String userName, String[] groupNames) {
    if (userName==null || userName.length()==0 ||
        groupNames== null || groupNames.length==0) {
      throw new IllegalArgumentException(
          "Parameters should not be null or an empty string/array");
    }
    for (int i=0; i<groupNames.length; i++) {
      if(groupNames[i] == null || groupNames[i].length() == 0) {
        throw new IllegalArgumentException("A null group name at index " + i);
      }
    }
    this.userName = userName;
    this.groupNames = groupNames;
  }

  /** Return an array of group names
   */
  public String[] getGroupNames() {
    return groupNames;
  }

  /** Return the user's name
   */
  public String getUserName() {
    return userName;
  }

  /* The following two methods implements Writable interface */
  final private static String UGI_TECHNOLOGY = "STRING_UGI"; 
  /** Deserialize this object
   * First check if this is a UGI in the string format.
   * If no, throw an IOException; otherwise
   * set this object's fields by reading them from the given data input
   *  
   *  @param in input stream
   *  @exception IOException is thrown if encounter any error when reading
   */
  public void readFields(DataInput in) throws IOException {
    // read UGI type first
    String ugiType = Text.readString(in);
    if (!UGI_TECHNOLOGY.equals(ugiType)) {
      throw new IOException("Expect UGI prefix: " + UGI_TECHNOLOGY +
          ", but receive a prefix: " + ugiType);
    }
    
    // read this object
    userName = Text.readString(in);
    int numOfGroups = WritableUtils.readVInt(in);
    groupNames = new String[numOfGroups];
    for (int i = 0; i < numOfGroups; i++) {
      groupNames[i] = Text.readString(in);
    }
  }

  /** Serialize this object
   * First write a string marking that this is a UGI in the string format,
   * then write this object's serialized form to the given data output
   * 
   * @param out output stream
   * @exception IOException if encounter any error during writing
   */
  public void write(DataOutput out) throws IOException {
    // write a prefix indicating the type of UGI being written
    Text.writeString(out, UGI_TECHNOLOGY);
    // write this object
    Text.writeString(out, userName);
    WritableUtils.writeVInt(out, groupNames.length);
    for (String groupName : groupNames) {
      Text.writeString(out, groupName);
    }
  }

  /* The following two methods deal with transferring UGI through conf. 
   * In this pass of implementation we store UGI as a string in conf. 
   * Later we may change it to be a more general approach that stores 
   * it as a byte array */
  /** Store the given <code>ugi</code> as a comma separated string in
   * <code>conf</code> as a property <code>attr</code>
   * 
   * The String starts with the user name followed by the default group names,
   * and other group names.
   * 
   * @param conf configuration
   * @param attr property name
   * @param ugi a UnixUserGroupInformation
   */
  public static void saveToConf(Configuration conf, String attr, 
      UnixUserGroupInformation ugi ) {
    conf.set(attr, ugi.toString());
  }
  
  /** Read a UGI from the given <code>conf</code>
   * 
   * The object is expected to store with the property name <code>attr</code>
   * as a comma separated string that starts
   * with the user name followed by group names.
   * If the property name is not defined, return null.
   * It's assumed that there is only one UGI per user. If this user already
   * has a UGI in the ugi map, return the ugi in the map.
   * Otherwise, construct a UGI from the configuration, store it in the
   * ugi map and return it.
   * 
   * @param conf configuration
   * @param attr property name
   * @return a UnixUGI
   * @throws LoginException if the stored string is ill-formatted.
   */
  public static UnixUserGroupInformation readFromConf(
      Configuration conf, String attr) throws LoginException {
    String[] ugi = conf.getStrings(attr);
    if(ugi == null) {
      return null;
    }
    UnixUserGroupInformation currentUGI = null;
    if (ugi.length>0 ){
      currentUGI = user2UGIMap.get(ugi[0]);
    }
    if (currentUGI == null) {
      try {
        currentUGI = new UnixUserGroupInformation(ugi);
        user2UGIMap.put(currentUGI.getUserName(), currentUGI);
      } catch (IllegalArgumentException e) {
        throw new LoginException("Login failed: "+e.getMessage());
      }
    }
    
    return currentUGI;
  }
  
  /**
   * Get current user's name and the names of all its groups from Unix.
   * It's assumed that there is only one UGI per user. If this user already
   * has a UGI in the ugi map, return the ugi in the map.
   * Otherwise get the current user's information from Unix, store it
   * in the map, and return it.
   *
   * If the current user's UNIX username or groups are configured in such a way
   * to throw an Exception, for example if the user uses LDAP, then this method
   * will use a the {@link #DEFAULT_USERNAME} and {@link #DEFAULT_GROUP}
   * constants.
   */
  public static UnixUserGroupInformation login() throws LoginException {
    try {
      String userName;

      // if an exception occurs, then uses the
      // default user
      try {
        userName =  getUnixUserName();
      } catch (Exception e) {
        LOG.warn("Couldn't get unix username, using " + DEFAULT_USERNAME, e);
        userName = DEFAULT_USERNAME;
      }

      // check if this user already has a UGI object in the ugi map
      UnixUserGroupInformation ugi = user2UGIMap.get(userName);
      if (ugi != null) {
        return ugi;
      }

      /* get groups list from UNIX. 
       * It's assumed that the first group is the default group.
       */
      String[]  groupNames;

      // if an exception occurs, then uses the
      // default group
      try {
        groupNames = getUnixGroups();
      } catch (Exception e) {
        LOG.warn("Couldn't get unix groups, using " + DEFAULT_GROUP, e);
        groupNames = new String[1];
        groupNames[0] = DEFAULT_GROUP;
      }

      // construct a Unix UGI
      ugi = new UnixUserGroupInformation(userName, groupNames);
      user2UGIMap.put(ugi.getUserName(), ugi);
      return ugi;
    } catch (Exception e) {
      throw new LoginException("Login failed: "+e.getMessage());
    }
  }

  /** Equivalent to login(conf, false). */
  public static UnixUserGroupInformation login(Configuration conf)
    throws LoginException {
    return login(conf, false);
  }
  
  /** Get a user's name & its group names from the given configuration; 
   * If it is not defined in the configuration, get the current user's
   * information from Unix.
   * If the user has a UGI in the ugi map, return the one in
   * the UGI map.
   * 
   *  @param conf either a job configuration or client's configuration
   *  @param save saving it to conf?
   *  @return UnixUserGroupInformation a user/group information
   *  @exception LoginException if not able to get the user/group information
   */
  public static UnixUserGroupInformation login(Configuration conf, boolean save
      ) throws LoginException {
    UnixUserGroupInformation ugi = readFromConf(conf, UGI_PROPERTY_NAME);
    if (ugi == null) {
      ugi = login();
      LOG.debug("Unix Login: " + ugi);
      if (save) {
        saveToConf(conf, UGI_PROPERTY_NAME, ugi);
      }
    }
    return ugi;
  }
  
  /* Return a string representation of a string array.
   * Two strings are separated by a blank.
   */
  private static String toString(String[] strArray) {
    if (strArray==null || strArray.length==0) {
      return "";
    }
    StringBuilder buf = new StringBuilder(strArray[0]);
    for (int i=1; i<strArray.length; i++) {
      buf.append(' ');
      buf.append(strArray[i]);
    }
    return buf.toString();
  }
  
  /** Get current user's name from Unix by running the command whoami.
   * 
   * @return current user's name
   * @throws IOException if encounter any error while running the command
   */
  static String getUnixUserName() throws IOException {
    String[] result = executeShellCommand(
        new String[]{Shell.USER_NAME_COMMAND});
    if (result.length!=1) {
      throw new IOException("Expect one token as the result of " + 
          Shell.USER_NAME_COMMAND + ": " + toString(result));
    }
    return result[0];
  }

  /** Get the current user's group list from Unix by running the command groups
   * 
   * @return the groups list that the current user belongs to
   * @throws IOException if encounter any error when running the command
   */
  private static String[] getUnixGroups() throws IOException {
    return executeShellCommand(Shell.getGROUPS_COMMAND());
  }
  
  /* Execute a command and return the result as an array of Strings */
  private static String[] executeShellCommand(String[] command)
  throws IOException {
    String groups = Shell.execCommand(command);
    StringTokenizer tokenizer = new StringTokenizer(groups);
    int numOfTokens = tokenizer.countTokens();
    String[] tokens = new String[numOfTokens];
    for (int i=0; tokenizer.hasMoreTokens(); i++) {
      tokens[i] = tokenizer.nextToken();
    }

    return tokens;
  }

  /** Decide if two UGIs are the same
   *
   * @param other other object
   * @return true if they are the same; false otherwise.
   */
  public boolean equals(Object other) {
    if (this == other) {
      return true;
    }
    
    if (!(other instanceof UnixUserGroupInformation)) {
      return false;
    }
    
    UnixUserGroupInformation otherUGI = (UnixUserGroupInformation)other;
    
    // check userName
    if (userName == null) {
      if (otherUGI.getUserName() != null) {
        return false;
      }
    } else {
      if (!userName.equals(otherUGI.getUserName())) {
        return false;
      }
    }
    
    // checkGroupNames
    if (groupNames == otherUGI.groupNames) {
      return true;
    }
    if (groupNames.length != otherUGI.groupNames.length) {
      return false;
    }
    // check default group name
    if (groupNames.length>0 && !groupNames[0].equals(otherUGI.groupNames[0])) {
      return false;
    }
    // check all group names, ignoring the order
    return new TreeSet<String>(Arrays.asList(groupNames)).equals(
           new TreeSet<String>(Arrays.asList(otherUGI.groupNames)));
  }

  /** Returns a hash code for this UGI. 
   * The hash code for a UGI is the hash code of its user name string.
   * 
   * @return  a hash code value for this UGI.
   */
  public int hashCode() {
    return getUserName().hashCode();
  }
  
  /** Convert this object to a string
   * 
   * @return a comma separated string containing the user name and group names
   */
  public String toString() {
    StringBuilder buf = new StringBuilder();
    buf.append(userName);
    for (String groupName : groupNames) {
      buf.append(',');
      buf.append(groupName);
    }
    return buf.toString();
  }

  @Override
  public String getName() {
    return toString();
  }
}
