/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2007 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: IdentityServicesHandler.java,v 1.7 2008/12/15 19:50:21 arviranga Exp $
 *
 */

/*
 * Portions Copyrighted 2011-2015 ForgeRock AS.
 * Portions Copyrighted 2012 Open Source Solution Technology Corporation
 */

package com.sun.identity.idsvcs.rest;

import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

import com.iplanet.sso.SSOException;
import com.iplanet.sso.SSOToken;
import com.iplanet.sso.SSOTokenManager;
import com.sun.identity.authentication.client.AuthClientUtils;
import com.sun.identity.idsvcs.Attribute;
import com.sun.identity.idsvcs.GeneralFailure;
import com.sun.identity.idsvcs.IdentityDetails;
import com.sun.identity.idsvcs.IdentityServicesFactory;
import com.sun.identity.idsvcs.IdentityServicesImpl;
import com.sun.identity.idsvcs.ListWrapper;
import com.sun.identity.idsvcs.ObjectNotFound;
import com.sun.identity.idsvcs.Token;
import com.sun.identity.idsvcs.UserDetails;
import com.sun.identity.shared.debug.Debug;

/**
 * Provides a marshall/unmarshall layer to the Security interface.
 */
public class IdentityServicesHandler extends HttpServlet {

    private static final long serialVersionUID = 2774677132209419157L;
    private static Debug debug = Debug.getInstance("amIdentityServices");

    // =======================================================================
    // Constants
    // =======================================================================
    private static final String PARAM_PROVIDER = "provider";
    private static final Class PROVIDER_DEFAULT = IdentityServicesImpl.class;

    // =======================================================================
    // Fields
    // =======================================================================
    private IdentityServicesFactory factory;
    private String lbCookieName;
    private String lbCookieValue;

    // =======================================================================
    // Initialize/Destroy
    // =======================================================================
    /**
     * Loads the init parameters for use in the HTTP methods.
     *
     * @see javax.servlet.GenericServlet#init()
     */
    public void init() throws ServletException {
        super.init();
        // determine if the provider is correct..
        try {
            // get the security provider from the params.
            String def = PROVIDER_DEFAULT.toString();
            String provider = getInitParameter(PARAM_PROVIDER, def);
            this.factory = IdentityServicesFactory.getInstance(provider);
        } catch (Exception e) {
            // wrap in a servlet exception as to not scare the natives..
            throw new ServletException(e);
        }
        

    }

    // =======================================================================
    // HTTP Methods
    // =======================================================================
    /**
     * Determines unmarshalls the request and executes the proper method based
     * on the request parameters.
     *
     * @see javax.servlet.http.HttpServlet#service(HttpServletRequest request,
     *      HttpServletResponse response)
     */
    protected void service(HttpServletRequest request,
        HttpServletResponse response) throws ServletException, IOException {    
        // check/init LB cookie names
        if (lbCookieName == null || lbCookieValue == null) {
            initLbCookieSettings();
        }
        //set headers before executing the method, so they are set even if exception is being thrown
        response.addHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
        response.addHeader("Pragma", "no-cache");
        IdentityServicesImpl security = this.factory.newInstance();
        SecurityMethod.execute(security, request, response);
        
        // check/set LB cookie
        setLbCookie(request, response);
    }

    // =======================================================================
    // Helper Methods
    // =======================================================================
    /**
     * Get a consistent behaviour between application servers..
     */
    String getInitParameter(String param, String def) {
        String ret = getInitParameter(param);
        if (isBlank(param)) {
            ret = def;
        }
        return ret;
    }

    private static boolean isBlank(String val)
    {
        return (val == null) ? true :
            ((val.trim().length() == 0) ? true : false);
    }
    
    private void initLbCookieSettings() {
        lbCookieName = AuthClientUtils.getlbCookieName();
        lbCookieValue = AuthClientUtils.getlbCookieValue();
    }
    
    private void setLbCookie(HttpServletRequest request, HttpServletResponse response) {
        if (lbCookieName == null || lbCookieValue == null) {
            return;
        }

        Cookie[] cookies = request.getCookies();

        if (cookies != null) {
            for (int c = 0; c < cookies.length; c++) {
                if (cookies[c].getName().equals(lbCookieName)) {
                    return;
                }
            }
        }
        
        try {
            AuthClientUtils.setlbCookie(request, response);
        } catch (Exception ex) {
            // unable to set lb cookie
        }
    }
    
    /**
     * Enum to get the request parameters and test w/ the SecurityMethods.
     */
    public static class SecurityParameter {

        public static final SecurityParameter URI =
            new SecurityParameter("URI");
        public static final SecurityParameter ACTION =
            new SecurityParameter("ACTION");
        public static final SecurityParameter USERNAME =
            new SecurityParameter("USERNAME");
        public static final SecurityParameter PASSWORD =
            new SecurityParameter("PASSWORD");
        public static final SecurityParameter SUBJECTID =
            new SecurityParameter("SUBJECTID", Token.class);
        public static final SecurityParameter IDENTITY =
            new SecurityParameter("IDENTITY", IdentityDetails.class);
        public static final SecurityParameter ATTRIBUTENAMES =
            new SecurityParameter("ATTRIBUTENAMES",
            (new String[1]).getClass());
        public static final SecurityParameter LOGNAME =
            new SecurityParameter("LOGNAME");
        public static final SecurityParameter MESSAGE =
            new SecurityParameter("MESSAGE");
        public static final SecurityParameter APPID =
            new SecurityParameter("APPID", Token.class);
        public static final SecurityParameter ADMIN =
            new SecurityParameter("ADMIN", Token.class);
        public static final SecurityParameter NAME =
            new SecurityParameter("NAME");
        public static final SecurityParameter FILTER =
            new SecurityParameter("FILTER");
        public static final SecurityParameter ATTRIBUTES =
            new SecurityParameter("ATTRIBUTES", Attribute[].class);
        public static final SecurityParameter REFRESH =
                new SecurityParameter("REFRESH", Boolean.class);
        // ===================================================================
        // Fields
        // ===================================================================
        final Class type;
        final String name;

        SecurityParameter(String name) {
            this.name = name;
            this.type = String.class;
        }

        SecurityParameter(String name, Class type) {
            this.name = name;
            this.type = type;
        }

        String name() {
            return name;
        }

        Object getValue(ServletRequest request) {
            Object ret = null;
            if (this.type == Token.class) {
                ret = getToken(request);
            } else if (this.type == List.class) {
                ret = getList(request);
            } else if (this.type == String[].class) {
                ret = getArray(request);
            } else if (this.type == Attribute[].class) {
                ret = getAttributeArray(request);
            } else if (this.type == IdentityDetails.class) {
                ret = getIdentityDetails(request);
            } else if (type == Boolean.class) {
                ret = getBoolean(request);
            } else {
                ret = getString(request);
            }
            return ret;
        }

        public Boolean getBoolean(ServletRequest request) {
            String name = name().toLowerCase();
            return Boolean.valueOf(request.getParameter(name));
        }

        public String getString(ServletRequest request) {
            String name = name().toLowerCase();
            String ret = request.getParameter(name);
            if (isBlank(ret)) {
                ret = null;
            }
            return ret;
        }

        public Token getToken(ServletRequest request) {
            Token ret = null;
            String n = name().toLowerCase();
            String id = request.getParameter(n);
            if (isBlank(id)) {
                try {
                    // Check the cookie value "iPlanetDirectoryPro"
                    SSOTokenManager mgr = SSOTokenManager.getInstance();
                    SSOToken token = mgr.createSSOToken(
                        (HttpServletRequest) request);
                    if (token != null) {
                        id = token.getTokenID().toString();
                    }
                } catch (SSOException ex) {
                    // Ignore the exception, and no valid token
                }
            }
            if (!isBlank(id)) {
                ret = new Token();
                ret.setId(id);
            }
            return ret;
        }

        public List getList(ServletRequest request) {
            List ret = null;
            String n = name().toLowerCase();
            String[] values = request.getParameterValues(n);
            if (values != null) {
                ret = new ArrayList();
                for (int i = 0; i < values.length; i++) {
                    String value = values[i];
                    if (!isBlank(value)) {
                        ret.add(value);
                    }
                }
            }
            return ret;
        }

        public String[] getArray(ServletRequest request)
        {
            String[] ret = null;
            List valuesList = getList(request);

            if ((valuesList != null) && (valuesList.size() > 0)) {
                ret = new String[valuesList.size()];

                valuesList.toArray(ret);
            }

            return ret;
        }

        public Attribute[] getAttributeArray(ServletRequest request)
        {
            Attribute[] ret = null;
            List attributeList = null;
            String n = name().toLowerCase();
            String[] attrNames = request.getParameterValues(n + "_names");

            if (attrNames != null) {
                for (int i = 0; i < attrNames.length; i++) {
                    String attrName = attrNames[i];

                    if (isBlank(attrName)) {
                        break;
                    }

                    String attrValues[] =
                        request.getParameterValues(n + "_values_" + attrName);

                    if ((attrValues != null) && (attrValues.length > 0)) {
                        List attrValueList = new ArrayList();

                        for (int j = 0; j < attrValues.length; j++) {
                            String attrValue = attrValues[j];

                            if (!isBlank(attrValue)) {
                                attrValueList.add(attrValue);
                            }
                        }

                        String[] attrValuesArray =
                            new String[attrValueList.size()];

                        attrValueList.toArray(attrValuesArray);

                        Attribute attribute = new Attribute();
                        attribute.setName(attrName);
                        attribute.setValues(attrValuesArray);

                        if (attributeList == null) {
                            attributeList = new ArrayList();
                        }

                        attributeList.add(attribute);
                    } else {
                        // Add empyt attribute
                        Attribute attribute = new Attribute();
                        attribute.setName(attrName);
                        if (attributeList == null) {
                            attributeList = new ArrayList();
                        }
                        attributeList.add(attribute);
                    }
                }
            }

            if ((attributeList != null) && (attributeList.size() > 0)) {
                ret = new Attribute[attributeList.size()];
                attributeList.toArray(ret);
            }

            return ret;
        }

        public IdentityDetails getIdentityDetails(ServletRequest request)
        {
            IdentityDetails rv = null;
            String n = name().toLowerCase();
            String identityName = request.getParameter(n + "_name");

            if (!isBlank(identityName)) {
                rv = new IdentityDetails();
                rv.setName(identityName);

                String objType = request.getParameter(n + "_type");
                if (!isBlank(objType)) {
                    rv.setType(objType);
                }

                String realm = request.getParameter(n + "_realm");
                if (!isBlank(realm)) {
                    rv.setRealm(realm);
                }

                String[] roles = request.getParameterValues(n + "_roles");
                if (roles != null) {
                    List rolesList = new ArrayList();

                    for (int i = 0; i < roles.length; i++) {
                        String role = roles[i];

                        if (!isBlank(role)) {
                            rolesList.add(role);
                        }
                    }

                    String[] rolesArray = new String[rolesList.size()];

                    rolesList.toArray(rolesArray);
                    rv.setRoleList(new ListWrapper(rolesArray));
                }

                String[] groups = request.getParameterValues(n + "_groups");
                if (groups != null) {
                    List groupsList = new ArrayList();

                    for (int i = 0; i < groups.length; i++) {
                        String group = groups[i];

                        if (!isBlank(group)) {
                            groupsList.add(group);
                        }
                    }

                    String[] groupsArray = new String[groupsList.size()];

                    groupsList.toArray(groupsArray);
                    rv.setGroupList(new ListWrapper(groupsArray));
                }

                String[] members = request.getParameterValues(n + "_members");
                if (members != null) {
                    List membersList = new ArrayList();

                    for (int i = 0; i < members.length; i++) {
                        String member = members[i];

                        if (!isBlank(member)) {
                            membersList.add(member);
                        }
                    }

                    String[] membersArray = new String[membersList.size()];

                    membersList.toArray(membersArray);
                    rv.setMemberList(new ListWrapper(membersArray));
                }

                List attrList = new ArrayList();
                String[] attrNames =
                        request.getParameterValues(n + "_attribute_names");

                if (attrNames != null) {
                    for (int i = 0; i < attrNames.length; i++) {
                        String attrName = attrNames[i];

                        if (isBlank(attrName)) {
                            break;
                        }

                        Attribute attribute = null;
                        String attrValues[] =
                            request.getParameterValues(n + "_attribute_values_" + attrName);

                        if (attrValues != null) {
                            List attrValueList = new ArrayList();

                            for (int j = 0; j < attrValues.length; j++) {
                                String attrValue = attrValues[j];

                                if (!isBlank(attrValue)) {
                                    attrValueList.add(attrValue);
                                }
                            }

                            String[] attrValuesArray =
                                new String[attrValueList.size()];

                            attrValueList.toArray(attrValuesArray);
                            attribute = new Attribute();
                            attribute.setName(attrName);
                            attribute.setValues(attrValuesArray);
                            attrList.add(attribute);
                        } else {
                            attribute = new Attribute();
                            attribute.setName(attrName);
                            attrList.add(attribute);
                        }

                        if (attrList.size() > 0) {
                            Attribute[] attrArray = new Attribute[attrList.size()];

                            attrList.toArray(attrArray);
                            rv.setAttributes(attrArray);
                        }
                    }
                }
            }

            return rv;
        }
    }

   /**
     * Defined by the interface 'com.sun.identity.idsvcs.Security'.
     */
    public static class SecurityMethod {

        public static final SecurityMethod TOKENCOOKIE = new SecurityMethod(
            "GETCOOKIENAMEFORTOKEN", String.class, (SecurityParameter[]) null);
        public static final SecurityMethod ALLCOOKIES = new SecurityMethod(
            "GETCOOKIENAMESTOFORWARD", String[].class, (SecurityParameter[]) null);
        public static final SecurityMethod ATTRIBUTES = new SecurityMethod(
            "ATTRIBUTES", UserDetails.class, SecurityParameter.ATTRIBUTENAMES,
            SecurityParameter.SUBJECTID, SecurityParameter.REFRESH);
        public static final SecurityMethod LOG = new SecurityMethod(
                "LOG", Void.class, new SecurityParameter[]
                {SecurityParameter.APPID, SecurityParameter.SUBJECTID,
                 SecurityParameter.LOGNAME, SecurityParameter.MESSAGE});
        public static final SecurityMethod READ = new SecurityMethod(
            "READ", IdentityDetails.class, new SecurityParameter[]
            {SecurityParameter.NAME, SecurityParameter.ATTRIBUTES,
             SecurityParameter.ADMIN});

        // ===================================================================
        // Constructors
        // ===================================================================
        private SecurityMethod(String name, Class clazz,
            SecurityParameter[] params) {
            
            final Method[] SECURITY_METHODS =
                IdentityServicesImpl.class.getMethods();
            // find the method
            Method imethod = null;
            String lname = name.toLowerCase();
            for (int i = 0; i < SECURITY_METHODS.length; i++) {
                Method m = SECURITY_METHODS[i];
                // found the method by name..
                String mname = m.getName().toLowerCase();
                if (mname.equals(lname)) {
                    // lets check based on parameters..
                    imethod = m;
                    break;
                }
            }
            // need to throw if we can't find it..
            if (imethod == null) {
                throw new IllegalArgumentException();
            }
            // set the internal fields
            this.type = clazz;
            this.method = imethod;
            this.parameters = params;
        }

        private SecurityMethod(String name, Class clazz,
            SecurityParameter param1) {
            this(name, clazz, new SecurityParameter[]{param1});
        }

        private SecurityMethod(String name, Class clazz,
            SecurityParameter param1, SecurityParameter param2) {
            this(name, clazz, new SecurityParameter[]{param1, param2});
        }

        private SecurityMethod(String name, Class clazz,
            SecurityParameter param1, SecurityParameter param2,
            SecurityParameter param3) {
            this(name, clazz, new SecurityParameter[]{param1, param2, param3});
        }

        // ===================================================================
        // Fields
        // ===================================================================
        final Class type;
        final Method method;
        final SecurityParameter[] parameters;

        public static void execute(IdentityServicesImpl security,
            HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
            
            // find the security method from the path..
            response.setCharacterEncoding("UTF-8");
            Writer wrt = response.getWriter();
            StringWriter sw = null;
            String path = request.getPathInfo();
            MarshallerFactory mar = getMarshaller(path);
            // Set the respone content type
            if (mar.getProtocol().equalsIgnoreCase("XML")) {
                response.setContentType("text/xml");
            } else if (mar.getProtocol().equals("JSON")) {
                response.setContentType("application/json");
            } else {
                response.setContentType("text/plain");
            }
            path = path.substring(path.lastIndexOf('/') + 1).toUpperCase();
            SecurityMethod method = null;
            if (path.equals("ATTRIBUTES")) {
                method = SecurityMethod.ATTRIBUTES;
            } else if (path.equals("LOG")) {
                method = SecurityMethod.LOG;
            } else if (path.equals("READ")) {
                method = SecurityMethod.READ;
            } else if (path.equals("GETCOOKIENAMEFORTOKEN")) {
                method = SecurityMethod.TOKENCOOKIE;
            } else if (path.equals("GETCOOKIENAMESTOFORWARD")) {
                method = SecurityMethod.ALLCOOKIES;
            }
            
            try {
                if (method == null) {
                    // Throw Unsupported Operation Exception
                    response.setStatus(501);
                    mar.newInstance(Throwable.class).marshall(wrt,
                        new UnsupportedOperationException(path));
                    return;
                }

                // execute the method w/ the parameters..
                Object value = method.invoke(security, request);
                // marshall the response..
                if (method.type != Void.class && value != null) {
                    mar.newInstance(method.type).marshall(wrt, value);
                } else {
                    response.setContentType("text/plain");
                    if (value == null) {
                        wrt.write("NULL");
                    }
                }
            } catch (ObjectNotFound ex) {
                // write out the proper ObjectNotFound exception.
                // set the response error code
                if(debug.messageEnabled()) {
                    debug.message("An objecjNotFound exception has been caught; details: ", ex);
                }
                try {
                    mar.newInstance(ObjectNotFound.class).marshall(wrt, ex);
                    response.setStatus(401);
                } catch (Exception e) {
                    // something really went wrong so just give up..
                    throw new ServletException(e);
                }
            } catch (GeneralFailure ex) {
                // write out the proper security based exception..
                if(debug.messageEnabled()) {
                    debug.message("A generalFailure exception has been caught; details: ", ex);
                }
                try {
                    mar.newInstance(GeneralFailure.class).marshall(wrt, ex);
                    response.setStatus(500);
                } catch (Exception e) {
                    // something really went wrong so just give up..
                    throw new ServletException(e);
                }
            } catch (Throwable e) {
                if(debug.messageEnabled()) {
                    debug.message("An unknown exception has been caught; details: ", e);
                }
                try {
                    // something really went wrong so just give up..
                    mar.newInstance(Throwable.class).marshall(wrt, e);
                    if (e instanceof UnsupportedOperationException) {
                        response.setStatus(501);
                    } else if (e instanceof InvocationTargetException) {
                        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                    } else {
                        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    }
                } catch (Exception ex) {
                    throw new ServletException(ex);
                }
            } finally {
                if (sw != null) {
                    sw.close();
                }
            }
        }

       /**
        * Gets the realm being used for this request by parsing the query parameters on the URI parameter (if present).
        * This matches the logic used in opensso/IdentityServicesImpl. Defaults to "/".
        */
       private static String getRealm(HttpServletRequest request) {
           String realm = "/";

           final String uri = SecurityParameter.URI.getString(request);
           if (uri != null) {
               for (String param : uri.split("&")) {
                   String[] parts = param.split("=");
                   if (parts.length == 2 && "realm".equals(parts[0])) {
                       realm = parts[1];
                       break;
                   }
               }
           }

           return realm;
       }

        /**
         * If both exist on the path then return JSON, XML, and then Properites
         * in that order.
         */
        private static MarshallerFactory getMarshaller(String path) {
            // default is properties format
            boolean xml = path.indexOf("xml/") != -1;
            boolean json = path.indexOf("json/") != -1;
            return (json) ? MarshallerFactory.JSON :
                (xml) ? MarshallerFactory.XML : MarshallerFactory.PROPS;
        }

        private Object invoke(IdentityServicesImpl security,
                              ServletRequest request)
            throws Throwable
        {
            // find the value for each parameter..
            Object[] params = null;
            if (parameters != null) {
                params = new Object[this.parameters.length];
                for (int i = 0; i < this.parameters.length; i++) {
                    SecurityParameter param = this.parameters[i];
                    params[i] = param.getValue(request);
                }
            }
            
            try {
                // invoke the actual security param..
                return method.invoke(security, params);
            } catch (IllegalArgumentException e) {
                throw new GeneralFailure(e.getMessage());
            } catch (IllegalAccessException e) {
                throw new GeneralFailure(e.getMessage());
            } catch (InvocationTargetException e) {
                if (debug.warningEnabled()) {
                    debug.warning("Exception during invocation", e);
                }
                throw e.getTargetException();
            }
        }
    }
}
