/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2006 Sun Microsystems Inc. All Rights Reserved
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * https://opensso.dev.java.net/public/CDDLv1.0.html or
 * opensso/legal/CDDLv1.0.txt
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at opensso/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * $Id: AccountUtils.java,v 1.2 2008/06/25 05:47:45 qcheng Exp $
 *
 */

package com.sun.identity.saml2.common;

import java.security.AccessController;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Properties;
import java.io.FileInputStream; 
import java.io.IOException;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

import com.sun.identity.plugin.datastore.DataStoreProviderException;
import com.sun.identity.saml2.meta.SAML2MetaException;
import com.sun.identity.saml2.meta.SAML2MetaManager;
import com.sun.identity.shared.configuration.SystemPropertiesManager;

/**
 * This class <code>AccountUtils</code> is a utility class for
 * setting and retrieving the <code>SAML2<code> account federation information.
 */

public class AccountUtils {

    private static final String DELIM = "|";
    private static final String NAMEID_INFO_ATTRIBUTE = 
            "com.sun.identity.saml2.nameidinfo.attribute";
    private static final String NAMEID_INFO_KEY_ATTRIBUTE = 
            "com.sun.identity.saml2.nameidinfokey.attribute";
    static SAML2MetaManager metaManager = null;
    
    static DataSource datasource; //for JNDI style connections
    static String nameIdSQL;
    static String datasourceName; //for JNDI style connections 
    static Properties properties;
    static String nameIdInfoColumn = "sun_fm_saml2_nameid_info";
    static String removeNameIdSQL;
   
    static {
        try {
            metaManager= new SAML2MetaManager();
        } catch (SAML2MetaException se) {
            SAML2Utils.debug.error("Unable to obtain Meta Manager.", se);
        }
        System.out.println("Starting to initial the JDBC datasource.....");
        //set the datasource class field 
        FileInputStream inputStream = null;       
        try {
        	  String configPath = System.getProperty("configPath");
        	  System.out.println("configPath: " + configPath);
        	  properties = new Properties(); 
        	 
        	  String jdbcFile = configPath+"/jdbc"+".properties";
        	  System.out.println("jdbcFile: " + jdbcFile);
        	 	inputStream = new FileInputStream(jdbcFile);
        	 	properties.load(inputStream);
        	  datasourceName = "java:comp/env/" + properties.getProperty("datasourceName");
        	  System.out.println("datasourceName: " + datasourceName);
        	  nameIdSQL = properties.getProperty("nameIdSQL");
        	  System.out.println("nameIdSQL: " + nameIdSQL);
        	  nameIdInfoColumn = properties.getProperty("nameIdInfoColumn");
        	  removeNameIdSQL = properties.getProperty("removeNameIdSQL");
            Context ctx = new InitialContext();
            //java:comp/env requires a resource-ref in web.xml
            datasource = (DataSource) ctx.lookup(datasourceName);   
                     
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
        	  try {
							inputStream.close();
						} catch (IOException e){
							e.printStackTrace();
						}
        }
    }
    
     private static Connection getConnection() throws SQLException {
        Connection conn = null;
       
        conn = datasource.getConnection();
      
        return conn;
    }
    
    //I could move these methods below to a helper class later if desired,
    //since they are not db-specific or table-specific or mysql-specific...
    //just general jdbc. Would make them static if in another helper class
    //but it would require the debug class
    
    //For Now they are PRIVATE so I can make sure debug has already been
    //set before calling and I can leave them as static methods
    
    //should I catch all Exceptions instead of just SQL ????? I think so
    private static void closeConnection(Connection dbConnection) {
        try {
            if (dbConnection != null && !dbConnection.isClosed()) {
                dbConnection.close();
            }
        } catch (SQLException e) {
           e.printStackTrace();
        }
    }
    
    private static String getUserName (String userId){
    		
    	  StringTokenizer st = new StringTokenizer(userId, ",");
    	  String idInfo = st.nextToken();
    	  System.out.println("id username: " + idInfo);
    	  String userName = idInfo.substring(3,idInfo.length());
    	  System.out.println("userName: " + userName);
    	  return userName;
    	  
    	  
    }
   
    private static String getNameIdbyUserId(String userName){
    	 
    		Connection con = null;
        PreparedStatement stmt = null;
        ResultSet result = null;
       
        System.out.println("nameIdSQL: " + nameIdSQL);
   	   String nameIdInfo = null;
        try {
        
        	 con = getConnection();
           stmt = con.prepareStatement(nameIdSQL);  
           stmt.setString(1, userName.toLowerCase());   
           result = stmt.executeQuery();
          
           while( result.next() ) {
           		nameIdInfo = result.getString(nameIdInfoColumn);
           }  
           	
        							
        } catch (Exception ex1) {
           ex1.printStackTrace();
        } finally {
            closeStatement(stmt);
            closeConnection(con);
        }
        
        System.out.println("nameIdInfo from database: " + nameIdInfo);
        
        return nameIdInfo;
    }
  
  
   private static void removeNameIdbyUserId(String userName){
    	 
    		Connection con = null;
        PreparedStatement stmt = null;
       
       
        System.out.println("removeNameIdSQL: " + removeNameIdSQL);
        try {
        
        	 con = getConnection();
           stmt = con.prepareStatement(removeNameIdSQL);  
           stmt.setString(1, userName.toLowerCase());   
           int total = stmt.executeUpdate();
          
           System.out.println("update status: " + total);
           	
        							
        } catch (Exception ex1) {
           ex1.printStackTrace();
        } finally {
            closeStatement(stmt);
            closeConnection(con);
        }
        
       
    }
    

     //should I catch all Exceptions instead of just SQL ????? I think so
    private static void closeStatement(PreparedStatement stmt) {
        try {
            if (stmt != null) {
                stmt.close();
            }
        } catch (SQLException se) {
           se.printStackTrace();
        }
    }

    /**
     * Returns the account federation information of a user for the given 
     * identity provider and a service provider. 
     * @param userID user id for which account federation needs to be returned.
     * @param hostEntityID <code>EntityID</code> of the hosted entity.
     * @param remoteEntityID <code>EntityID</code> of the remote entity.
     * @return the account federation info object.
     *         null if the account federation does not exist.
     * @exception SAML2Exception if account federation retrieval is failed.
     */ 
    public static NameIDInfo getAccountFederation(
           String userID, 
           String hostEntityID,
           String remoteEntityID) throws SAML2Exception {

        SAML2Utils.debug.message("AccountUtils.getAccountFederation:");

        if(userID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullUserID"));
        } 
				System.out.println("userID: " + userID);
				 String userName = getUserName(userID);
				 System.out.println("userName: " + userName);
        if(hostEntityID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullHostEntityID"));
        }
				System.out.println("hostEntityID: " + hostEntityID);
        if(remoteEntityID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullRemoteEntityID"));
        }
				System.out.println("remoteEntityID: " + hostEntityID);
        try {
            Set set = SAML2Utils.getDataStoreProvider().getAttribute(
                  userID, getNameIDInfoAttribute());
						String info = null;
            if(set == null || set.isEmpty()) {
               
               //return null;
               // we do have the user_id here, we should able to do the select to get the nameId
               System.out.println("Go to my code fix for termination");
               info = getNameIdbyUserId(userName);
               if (info == null){
	               		if(SAML2Utils.debug.messageEnabled()) {
	                  SAML2Utils.debug.message("AccountUtils.getAccount" +
	                  "Federation : user does not have any account federations.");
	              	 }
	              	 return null;
              	}
               
            } else {
          
	            String filter = hostEntityID + DELIM + remoteEntityID + DELIM;
	            if(SAML2Utils.debug.messageEnabled()) {
	               SAML2Utils.debug.message("AccountUtils.getAccountFederation: "+
	               " filter = " + filter + " userID = " + userID);
	            }
	           
	
	            for(Iterator iter = set.iterator(); iter.hasNext();) {
	                String value = (String)iter.next();
	                if(value.startsWith(filter)) {
	                   info = value;
	                   break;
	                }
	            }
	 
	            if(info == null) { 
	               if(SAML2Utils.debug.messageEnabled()) {
	                  SAML2Utils.debug.message("AccountUtils.getAccount" +
	                  "Federation : user does not have account federation " +
	                  " corresponding to =" + filter);
	               }
	               return null;
	            }
	          	
	          }

            return NameIDInfo.parse(info);

        } catch (DataStoreProviderException dse) {

           SAML2Utils.debug.error("AccountUtils.readAccountFederation" +
           "Info: DataStoreProviderException", dse);
           throw new SAML2Exception(dse.getMessage());
        }
        
    }

    /**
     * Sets the account federation information to the datastore for a user.
     * @param info <code>NameIDInfo</code> object to be set.
     * @param userID user identifier for which the account federation to be set.
     * @exception SAML2Exception if any failure.
     */
    public static void setAccountFederation(
           NameIDInfo info, String userID) throws SAML2Exception {

        SAML2Utils.debug.message("AccountUtils.setAccountFederation:");

        if(info == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullNameIDInfo"));
        }

        if(userID == null) {
           throw new SAML2Exception(SAML2Utils.bundle.getString(
                 "nullUserID"));
        }

        NameIDInfoKey infoKey = new NameIDInfoKey(info.getNameIDValue(),
              info.getHostEntityID(), info.getRemoteEntityID());

        if(SAML2Utils.debug.messageEnabled()) {
           SAML2Utils.debug.message("AccountUtils.setAccountFederation: "+
           "info to be set:"+ info.toValueString() + ","  +
            "infoKey to be set:" + infoKey.toValueString());
        }

        String filter = info.getHostEntityID() + DELIM +
                info.getRemoteEntityID() + DELIM;

        try {
            String nameIDInfoAttr = getNameIDInfoAttribute();
            String nameIDInfoKeyAttr = getNameIDInfoKeyAttribute();
            Set set = new HashSet();
            set.add(nameIDInfoAttr);
            set.add(nameIDInfoKeyAttr);

            Map map = new HashMap();
            Map existMap = SAML2Utils.getDataStoreProvider().
                  getAttributes(userID, set);

            if(existMap == null || existMap.isEmpty()) {
               Set set1 = new HashSet();
               set1.add(infoKey.toValueString());
               map.put(nameIDInfoKeyAttr, set1);

               Set set2= new HashSet();
               set2.add(info.toValueString());
               map.put(nameIDInfoAttr, set2);
            } else {

               Set set1 = (Set)existMap.get(nameIDInfoAttr);
               if(set1 != null) {
                  for(Iterator iter1 = set1.iterator(); iter1.hasNext();) {
                      String value = (String)iter1.next(); 
                      if(value.startsWith(filter)) {
                         iter1.remove();
                      }
                  }
               } else {
                  set1 = new HashSet();
               }

               set1.add(info.toValueString());
               map.put(nameIDInfoAttr, set1);

               Set set2 = (Set)existMap.get(nameIDInfoKeyAttr);
               if(set2 != null) {
                  for(Iterator iter2 = set2.iterator(); iter2.hasNext();) {
                      String value = (String)iter2.next(); 
                      if(value.startsWith(filter)) {
                         iter2.remove();
                      }
                  }
               } else {
                  set2 = new HashSet();
               }

               set2.add(infoKey.toValueString());
               map.put(nameIDInfoKeyAttr, set2);
            }

            if(SAML2Utils.debug.messageEnabled()) {
               SAML2Utils.debug.message("AccountUtils.setAccountFederation: "+
               " set fedinfo " + map + " userID = " + userID);
            }

            SAML2Utils.getDataStoreProvider().setAttributes(userID, map);

        } catch (DataStoreProviderException dse) {
            SAML2Utils.debug.error("SAML2Utils.setAccountFederation: " +
            "DataStoreProviderException", dse);
            throw new SAML2Exception(dse.getMessage());
        }
    }

    /**
     * Removes the account federation of a user.
     * @param info <code>NameIDInfo</code> object. 
     * @param userID user identifie for which the account federation needs to
     *               be removed.
     * @return true if the account federation is removed successfully.
     * @exception SAML2Exception if any failure.
     */
    public static boolean removeAccountFederation(
        NameIDInfo info, String userID) throws SAML2Exception {

         SAML2Utils.debug.message("AccountUtils.removeAccountFederation:");
         if(info == null) {
            throw new SAML2Exception(SAML2Utils.bundle.getString(
                  "nullNameIDInfo"));
         }

         if(userID == null) {
            throw new SAML2Exception(SAML2Utils.bundle.getString(
                  "nullUserID"));
         }

         try {
             Set existingFed =  SAML2Utils.getDataStoreProvider().
                   getAttribute(userID, getNameIDInfoAttribute()); 
             Set existingInfoKey = SAML2Utils.getDataStoreProvider().
                   getAttribute(userID, getNameIDInfoKeyAttribute());

             if(existingFed == null || existingFed.isEmpty()) {
                if(SAML2Utils.debug.messageEnabled()) {
                   SAML2Utils.debug.message("AccountUtils.removeAccount" +
                   "Federation: The cache is screwing up again");
                }
                // The cache is screw up again, do it manually
                System.out.println("Manually remove the database");
                String userName = getUserName(userID);
                System.out.println("userName: " + userName);
                removeNameIdbyUserId(userName);
                
	              return true;
             } else {

 
	             String infoValue = info.toValueString();
	             String infoKeyValue = info.getNameIDInfoKey().toValueString();
	
	             if(SAML2Utils.debug.messageEnabled()) {
	                SAML2Utils.debug.message("AccountUtils.removeAccount" +
	                "Federation: info to be removed:"+ infoValue + "user="+ 
	                 userID + "infoKeyValue = " + infoKeyValue);
	             }
	             
	             if(existingFed.contains(infoValue)) {
	
	                existingFed.remove(infoValue);
	                if(existingInfoKey != null &&
	                       existingInfoKey.contains(infoKeyValue)) {
	                   existingInfoKey.remove(infoKeyValue);
	                }
	
	                Map map = new HashMap();
	                map.put(getNameIDInfoAttribute(), existingFed);
	                map.put(getNameIDInfoKeyAttribute(), existingInfoKey);
	                SAML2Utils.getDataStoreProvider().setAttributes(userID, map);
	                return true;
	             }
	
	             if(SAML2Utils.debug.messageEnabled()) {
	                SAML2Utils.debug.message("AccountUtils.removeAccount" +
	                "Federation: account federation info not found.");
	             }
	             return false;
	           }

         } catch (DataStoreProviderException dse) {
             SAML2Utils.debug.error("SAML2Utils.removeAccountFederation: " +
             "DataStoreProviderException", dse);
             throw new SAML2Exception(dse.getMessage());
         }
    }
    /**
     * Returns the SAML2 Name Identifier Info attribute name.
     * @return the SAML2 Name Identifier Info attribute name.
     */
    public static String getNameIDInfoAttribute() {
        return SystemPropertiesManager.get(NAMEID_INFO_ATTRIBUTE, 
           SAML2Constants.NAMEID_INFO);
    }

    /**
     * Returns the SAML2 Name Identifier InfoKey attribute name.
     * @return the SAML2 Name Identifier InfoKey attribute name.
     */
    public static String getNameIDInfoKeyAttribute() {
        return SystemPropertiesManager.get(NAMEID_INFO_KEY_ATTRIBUTE,
           SAML2Constants.NAMEID_INFO_KEY);
    }
}

