/*
 * Copyright 1999-2010 University of Chicago
 *
 * Licensed 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.globus.gsi.gridmap;

import java.util.Map;
import java.util.Vector;
import java.util.HashMap;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.io.IOException;
import java.io.File;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.Serializable;

import org.globus.util.I18n;

import org.globus.util.QuotedStringTokenizer;
import org.globus.util.ConfigUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class GridMap implements Serializable {

    private static I18n i18n =
        I18n.getI18n("org.globus.gsi.gridmap.errors",
                     GridMap.class.getClassLoader());

    private static Log logger =
	LogFactory.getLog(GridMap.class.getName());

    private static final String COMMENT_CHARS = "#";
    // keywords that need to be replaced
    private static final char[] EMAIL_KEYWORD_1 = { 'e', '=' };
    private static final char[] EMAIL_KEYWORD_2 = { 'e', 'm', 'a', 'i', 'l',
                                                    '=' };
    private static final char[] UID_KEYWORD = { 'u', 'i', 'd', '=' };
    // Length of key words that need to be replaced
    private static final int EMAIL_KEYWORD_1_L = 2;
    private static final int EMAIL_KEYWORD_2_L = 6;
    private static final int UID_KEYWORD_L = 4;
    // Keywords to be replaced with.
    private static final String EMAIL_KEYWORD = "emailaddress=";
    private static final String USERID_KEYWORD = "userid=";

    protected Map map;

    // the file the grim map was loaded from
    private File file;
    // last time the file was modified
    private long lastModified;
    // log or throw exception on bad entries
    private boolean ignoreErrors = false;

    /**
     * Sets whether errors in the gridmap file
     * should be ignored. Errors are not ignored by default.
     *
     * @param ignoreErrors if true, errors in the gridmap file
     *        will be ignored (warnings will be logged). If false,
     *        an exception will be raised on errors.
     */
    public void setIgnoreErrors(boolean ignoreErrors) {
        this.ignoreErrors = ignoreErrors;
    }

    /**
     * Returns whether errors in the gridmap file are
     * ignored.
     *
     * @return true if errors in the gridmap file are ignored.
     *         False, otherwise.
     */
    public boolean getIgnoreErrors() {
        return this.ignoreErrors;
    }

    /**
     * Returns the absolute path anme of file used to load gridmap
     * either using the {@link #load(File) load} or {@link
     * #load(String) load} functions. If no file was used, returns
     * null.
     *
     * @return absolute file path, if gridmap was loaded from
     *         file. Null otherwise.
     */
    public String getFileName() {

        if (this.file == null) {
            return null;
        }

        return this.file.getAbsolutePath();
    }

    /**
     * Loads grid map definition from a given file.
     *
     * @param file the grid map file
     * @exception IOException in case of I/O error or
     *         when there are parsing errors in the file (only when errors
     *         are not ignored).
     * @return <tt>true</tt> if the file was parsed and loaded successfully.
     *         <tt>False</tt> otherwise.
     */
    public boolean load(String file)
        throws IOException {
        return load(new File(file));
    }

    /**
     * Loads grid map definition from a given file.
     *
     * @param file the grid map file
     * @exception IOException in case of I/O error or
     *         when there are parsing errors in the file (only when errors
     *         are not ignored).
     * @return <tt>true</tt> if the file was parsed and loaded successfully.
     *         <tt>False</tt> otherwise.
     */
    public boolean load(File file)
        throws IOException {
        InputStream in = null;
        try {
            in = new FileInputStream(file);
            this.file = file;
            this.lastModified = file.lastModified();
            return load(in);
        } finally {
            if (in != null) {
                try { in.close(); } catch(Exception e) {}
            }
        }
    }

    /**
     * Reloads the gridmap from a file only if the gridmap
     * was initially loaded using the {@link #load(File)
     * load} or {@link #load(String) load} functions.
     * The file will only be reloaded if it has changed
     * since the last time.
     *
     * @exception IOException in case of I/O error or
     *         when there are parsing errors in the file (only when errors
     *         are not ignored).
     * @return <tt>true</tt> if the file was parsed and refreshed successfully.
     *         <tt>False</tt> otherwise.
     */
    public boolean refresh()
        throws IOException {
        if (this.file != null &&
            this.file.lastModified() != this.lastModified) {
            return load(this.file);
        } else {
            return true;
        }
    }

    /**
     * Loads grid map file definition from a given input stream. The input
     * stream is not closed in case of an error.
     *
     * @param input the input stream that contains the gridmap
     *        definitions.
     * @exception IOException in case of I/O error or
     *         when there are parsing errors in the input (only when errors
     *         are not ignored).
     * @return <tt>true</tt> if the input was parsed successfully.
     *         <tt>False</tt> otherwise.
     */
    public boolean load(InputStream input)
        throws IOException {
        boolean success = true;

        BufferedReader reader =
            new BufferedReader(new InputStreamReader(input));

        Map localMap = new HashMap();
        GridMapEntry entry;
        QuotedStringTokenizer tokenizer;
        StringTokenizer idTokenizer;
        String line;
        while( (line = reader.readLine()) != null) {
            line = line.trim();
            if ( (line.length() == 0) ||
                 ( COMMENT_CHARS.indexOf(line.charAt(0)) != -1) ) {
                continue;
            }

            tokenizer = new QuotedStringTokenizer(line);

            String globusID = null;

            if (tokenizer.hasMoreTokens()) {
                globusID = tokenizer.nextToken();
            } else {
                if (this.ignoreErrors) {
                    success = false;
                    logger.warn("Globus ID missing: " + line);
                    continue;
                } else {
                    throw new IOException(i18n.getMessage("globusIdErr",
                                                          line));
                }
            }

            String userIDs = null;

            if (tokenizer.hasMoreTokens()) {
                userIDs = tokenizer.nextToken();
            } else {
                if (this.ignoreErrors) {
                    success = false;
                    logger.warn("User ID mapping missing: " + line);
                    continue;
                } else {
                    throw new IOException(i18n.getMessage("userIdErr", line));
                }
            }

            idTokenizer = new StringTokenizer(userIDs, ",");
            String [] ids = new String [ idTokenizer.countTokens() ];
            int i = 0;
            while(idTokenizer.hasMoreTokens()) {
                ids[i++] = idTokenizer.nextToken();
            }

            String normalizedDN = normalizeDN(globusID);
            entry = (GridMapEntry)localMap.get(normalizedDN);
            if (entry == null) {
                entry = new GridMapEntry();
                entry.setGlobusID(globusID);
                entry.setUserIDs(ids);
                localMap.put(normalizedDN, entry);
            } else {
                entry.addUserIDs(ids);
            }
        }

        this.map = localMap;

        return success;
    }

    /**
     * Returns first local user name mapped to the specified
     * globusID.
     *
     * @param globusID globusID
     * @return local user name for the specified globusID.
     *         Null if the globusID is not mapped
     *         to a local user name.
     */
    public String getUserID(String globusID) {
        String [] ids = getUserIDs(globusID);
        if (ids != null && ids.length > 0) {
            return ids[0];
        } else {
            return null;
        }
    }

    /**
     * Returns local user names mapped to the specified
     * globusID.
     *
     * @param globusID globusID
     * @return array of local user names for the specified globusID.
     *         Null if the globusID is not mapped
     *         to any local user name.
     */
    public String[] getUserIDs(String globusID) {
        if (globusID == null) {
            throw new IllegalArgumentException(i18n
                                               .getMessage("globusIdNull"));
        }

        if (this.map == null) {
            return null;
        }

        GridMapEntry entry = (GridMapEntry)this.map.get(normalizeDN(globusID));
        return (entry == null) ? null : entry.getUserIDs();
    }

    /**
     * Checks if a given globus ID is associated with given
     * local user account.
     *
     * @param globusID globus ID
     * @param userID userID
     * @return true if globus ID is associated with given local
     *         user account, false, otherwise.
     */
    public boolean checkUser(String globusID, String userID) {
        if (globusID == null) {
            throw new IllegalArgumentException(i18n.getMessage("glousIdNull"));
        }
        if (userID == null) {
            throw new IllegalArgumentException(i18n.getMessage("userIdNull"));
        }

        if (this.map == null) {
            return false;
        }

        GridMapEntry entry = (GridMapEntry)this.map.get(normalizeDN(globusID));
        return (entry == null) ? false : entry.containsUserID(userID);
    }

    /**
     * Returns globus ID associated with the
     * specified local user name.
     *
     * @param userID local user name
     * @return associated globus ID, null
     *         if there is not any.
     */
    public String getGlobusID(String userID) {
        if (userID == null) {
            throw new IllegalArgumentException(i18n.getMessage("userIdNull"));
        }

        if (this.map == null) {
            return null;
        }

        Iterator iter = this.map.entrySet().iterator();
        Map.Entry mapEntry;
        GridMapEntry entry;
        while(iter.hasNext()) {
            mapEntry = (Map.Entry)iter.next();
            entry = (GridMapEntry)mapEntry.getValue();
            if (entry.containsUserID(userID)) {
                return entry.getGlobusID();
            }
        }
        return null;
    }

    /**
     * Returns all globus IDs associated with the
     * specified local user name.
     *
     * @param userID local user name
     * @return associated globus ID, null
     *         if there is not any.
     */
    public String[] getAllGlobusID(String userID) {
        if (userID == null) {
            throw new IllegalArgumentException(i18n.getMessage("userIdNull"));
        }

        if (this.map == null) {
            return null;
        }

        Vector v = new Vector();

        Iterator iter = this.map.entrySet().iterator();
        Map.Entry mapEntry;
        GridMapEntry entry;
        while(iter.hasNext()) {
            mapEntry = (Map.Entry)iter.next();
            entry = (GridMapEntry)mapEntry.getValue();
            if (entry.containsUserID(userID)) {
                v.add(entry.getGlobusID());
           }
        }

        // create array of strings and add values back in
        if(v.size() == 0) {
            return null;
        }

        String idS[] = new String[v.size()];
        for(int ctr = 0; ctr < v.size(); ctr++) {
            idS[ctr] = (String) v.elementAt(ctr);
        }

        return idS;
    }

    public void map(String globusID, String userID) {
        if (globusID == null) {
            throw new IllegalArgumentException(i18n
                                               .getMessage("globusIdNull"));
        }
        if (userID == null) {
            throw new IllegalArgumentException(i18n.getMessage("userIdNull"));
        }

        if (this.map == null) {
            this.map = new HashMap();
        }

        String normalizedDN = normalizeDN(globusID);

        GridMapEntry entry = (GridMapEntry)this.map.get(normalizedDN);
        if (entry == null) {
            entry = new GridMapEntry();
            entry.setGlobusID(globusID);
            entry.setUserIDs(new String [] {userID});
            this.map.put(normalizedDN, entry);
        } else {
            entry.addUserID(userID);
        }
    }

    static class GridMapEntry implements Serializable {
        String globusID;
        String[] userIDs;

        public String getFirstUserID() {
            return userIDs[0];
        }

        public String[] getUserIDs() {
            return userIDs;
        }

        public String getGlobusID() {
            return globusID;
        }

        public void setGlobusID(String globusID) {
            this.globusID = globusID;
        }

        public void setUserIDs(String [] userIDs) {
            this.userIDs = userIDs;
        }

        public boolean containsUserID(String userID) {
            if (userID == null) {
                return false;
            }
            for (int i=0;i<userIDs.length;i++) {
                if (userIDs[i].equalsIgnoreCase(userID)) {
                    return true;
                }
            }
            return false;
        }

        public void addUserID(String userID) {
            if (containsUserID(userID)) return;
            String [] ids = new String[ userIDs.length + 1 ];
            System.arraycopy(userIDs, 0, ids, 0, userIDs.length);
            ids[userIDs.length] = userID;
            userIDs = ids;
        }

        public void addUserIDs(String [] userIDs) {
            for (int i=0;i<userIDs.length;i++) {
                addUserID(userIDs[i]);
            }
        }

    }

    private static boolean keyWordPresent(char[] args, int startIndex,
                                          char[] keyword, int length) {

        if (startIndex + length > args.length) {
            return false;
        }

        int j=startIndex;
        for (int i=0; i<length; i++) {
            if (args[j] != keyword[i]) {
                return false;
            }
            j++;
        }
        return true;
    }

    public static String normalizeDN(String globusID) {

        if (globusID == null) {
            return null;
        }

        globusID = globusID.toLowerCase();
        char[] globusIdChars = globusID.toCharArray();

        StringBuffer normalizedDN = new StringBuffer();

        int i=0;

        while (i<globusIdChars.length) {

            if (globusIdChars[i] == '/') {

                normalizedDN.append("/");

                if (keyWordPresent(globusIdChars, i+1, EMAIL_KEYWORD_1,
                                   EMAIL_KEYWORD_1_L)) {
                    normalizedDN.append(EMAIL_KEYWORD);
                    i = i + EMAIL_KEYWORD_1_L;
                } else if (keyWordPresent(globusIdChars, i+1, EMAIL_KEYWORD_2,
                                          EMAIL_KEYWORD_2_L)) {
                    normalizedDN.append(EMAIL_KEYWORD);
                    i = i + EMAIL_KEYWORD_2_L;
                } else if (keyWordPresent(globusIdChars, i+1, UID_KEYWORD,
                                          UID_KEYWORD_L)) {
                    normalizedDN.append(USERID_KEYWORD);
                    i = i + UID_KEYWORD_L;
                }
                i++;
            } else {
                normalizedDN.append(globusIdChars[i]);
                i++;
            }
        }

        return normalizedDN.toString();
    }

    /*
     * Determine the location of the gridmap file based on the standard
     * Globus logic:
     *   1. If the GRIDMAP system property is set, return it.
     *   2. If the user is not root, return $HOME/.gridmap
     *   3. Otherwise, return /etc/grid-security/grid-mapfile
     */
    public static String getDefaultGridMapLocation() {
        String location = System.getProperty("GRIDMAP");
        if (location == null || location.length() == 0) {
            String uid = null;
            try {
                uid = ConfigUtil.getUID();
            } catch (IOException e) {
                // ignore it
            }
            if (uid != null && uid.equals("0")) {
                location = "/etc/grid-security/grid-mapfile";
            } else {
                location = System.getProperty("user.home") + "/.gridmap";
            }
        }
        return location;
    }
}
