/**
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2005 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: SMSException.java,v 1.7 2009/01/28 05:35:03 ww203982 Exp $
 *
 * Portions Copyrighted 2011-2015 ForgeRock AS.
 */
package com.sun.identity.sm;

import static org.forgerock.opendj.ldap.ResultCode.*;

import com.iplanet.am.sdk.AMException;
import com.iplanet.services.ldap.LDAPServiceException;
import com.iplanet.services.util.XMLException;
import com.iplanet.sso.SSOException;
import com.iplanet.ums.IUMSConstants;
import com.sun.identity.authentication.internal.InvalidAuthContextException;
import com.sun.identity.shared.debug.Debug;
import com.sun.identity.shared.locale.AMResourceBundleCache;
import com.sun.identity.shared.locale.L10NMessage;
import com.sun.identity.shared.locale.Locale;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.MessageFormat;
import java.util.ResourceBundle;
import org.forgerock.opendj.ldap.LdapException;
import org.forgerock.opendj.ldap.ResultCode;

/**
 * The exception class whose instance is thrown if there is any error during the
 * operation of objects of the <code>com.sun.identity.sms</code> package. This
 * class maps the exception that occurred at a lower level to a high level
 * error. Using the exception status code <code>getExceptionCode()</code> the
 * errors are categorized as a <code>ABORT</code>, <code>RETRY</code>,
 * <code>CONFIG_PROBLEM</code> or <code>LDAP_OP_FAILED</code> (typically a
 * bug).
 *
 * @supported.all.api
 */
public class SMSException extends Exception implements L10NMessage {

    transient AMResourceBundleCache amCache = AMResourceBundleCache.getInstance();

    transient Debug debug = Debug.getInstance(IUMSConstants.UMS_DEBUG);

    private int exceptionStatus = STATUS_NONE;

    private Throwable rootCause;

    private String message;

    private String bundleName = IUMSConstants.UMS_BUNDLE_NAME;

    private String errorCode;

    private Object[] args;

    /**
     * Default constructor for <code> SMSException </code>
     */
    public SMSException() {
        super();
        exceptionStatus = STATUS_NONE;
    }

    /**
     * @param status
     *            The exception status code.
     * @param errorCode
     *            Key to resource bundle.
     */
    public SMSException(int status, String errorCode) {
        super();
        exceptionStatus = status;
        this.errorCode = errorCode;
        this.message = getL10NMessage(java.util.Locale.ENGLISH);
    }

    /**
     * @param status
     *            The Exception status code.
     * @param exMessage
     *            The message provided by the object which is throwing the
     *            exception
     * @param errorCode
     *            Key to resource bundle.
     */
    public SMSException(int status, String exMessage, String errorCode) {
        exceptionStatus = status;
        this.errorCode = errorCode;
        this.message = exMessage + ": " +
            getL10NMessage(java.util.Locale.ENGLISH);
    }

    /**
     * @param msg
     *            The message provided by the object which is throwing the
     *            exception
     */
    public SMSException(String msg) {
        exceptionStatus = STATUS_NONE;
        this.message = msg;
    }

    /**
     * @param msg
     *            The message provided by the object which is throwing the
     *            exception
     * @param errorCode
     *            Key to resource bundle.
     */
    public SMSException(String msg, String errorCode) {
        exceptionStatus = STATUS_NONE;
        this.errorCode = errorCode;
        this.message = msg + ": " + getL10NMessage(java.util.Locale.ENGLISH);
    }

    /**
     * Constructs an <code>SMSException</code>.
     * 
     * @param t
     *            The <code>Throwable</code> object provided by the object
     *            which is throwing the exception
     * @param errorCode
     *            Key to resource bundle.
     */
    public SMSException(Throwable t, String errorCode) {
        super(t);
        rootCause = t;
        this.errorCode = errorCode;
        this.message = getL10NMessage(java.util.Locale.ENGLISH);
        exceptionMapper();
    }

    /**
     * Constructs an <code>SMSException</code>.
     * 
     * @param message
     *            exception message.
     * @param t
     *            The <code>Throwable</code> object provided by the object
     *            which is throwing the exception.
     * @param errorCode
     *            Key to resource bundle.
     */
    public SMSException(String message, Throwable t, String errorCode) {
        super(message, t);
        rootCause = t;
        this.errorCode = errorCode;
        this.message = message + ": " +
            getL10NMessage(java.util.Locale.ENGLISH);
        exceptionMapper();
    }

    /**
     * Constructs an <code>SMSException</code>.
     * 
     * @param rbName
     *            Resource bundle Name to be used for getting localized error
     *            message.
     * @param message
     *            exception message.
     * @param t
     *            The <code>Throwable</code> object provided by the object
     *            which is throwing the exception.
     * @param errorCode
     *            Key to resource bundle.
     */
    public SMSException(String rbName, String message, Throwable t,
            String errorCode) {
        super(message, t);
        rootCause = t;
        this.errorCode = errorCode;
        this.bundleName = rbName;
        this.message = message + ": " +
            getL10NMessage(java.util.Locale.ENGLISH);
        if (rootCause != null && !(rootCause instanceof AMException)) {
            exceptionMapper();
        }
    }

    /**
     * This constructor is used to pass the localized error message At this
     * level, the locale of the caller is not known and it is not possible to
     * throw localized error message at this level. Instead this constructor
     * provides Resource Bundle name and error code for correctly locating the
     * error message. The default <code>getMessage()</code> will always return
     * English messages only. This is in consistent with current JRE.
     * 
     * @param rbName
     *            Resource bundle Name to be used for getting localized error
     *            message.
     * @param errorCode
     *            Key to resource bundle. You can use <code>ResourceBundle rb =
     *        ResourceBunde.getBundle(rbName,locale);
     *        String localizedStr = rb.getString(errorCode)</code>.
     * @param args
     *            arguments to message. If it is not present pass the as null.
     */
    public SMSException(String rbName, String errorCode, Object[] args) {
        exceptionStatus = STATUS_NONE;
        this.bundleName = rbName;
        this.errorCode = errorCode;
        this.args = args;
        this.message = getL10NMessage(java.util.Locale.ENGLISH);
    }

    /**
     * Returns a localized error message
     * 
     * @param locale
     *            Uses the locale object to create the appropriate localized
     *            error message
     * @return localized error message.
     * @see #SMSException(String, String, Object[])
     */
    public String getL10NMessage(java.util.Locale locale) {
        String result = errorCode;
        if (bundleName != null && locale != null) {
            ResourceBundle bundle = amCache.getResBundle(bundleName, locale);
            String mid = Locale.getString(bundle, errorCode, debug);
            if (args == null || args.length == 0) {
                result = mid;
            } else {
                result = MessageFormat.format(mid, args);
            }
        }
        return result;
    }

    /**
     * Returns <code>ResourceBundle</code> Name associated with this error
     * message.
     * 
     * @return <code>ResourceBundle</code> name associated with this error
     *         message.
     * @see #SMSException(String, String, Object[])
     */
    public String getResourceBundleName() {
        return bundleName;
    }

    /**
     * Returns error code associated with this error message.
     * 
     * @return Error code associated with this error message.
     * @see #SMSException(String, String, Object[])
     */
    public String getErrorCode() {
        return errorCode;
    }

    /**
     * Returns arguments for formatting this error message.
     * 
     * @return arguments for formatting this error message. You need to use
     *         <code>MessageFormat</code> class to format the message It can
     *         be null.
     * @see #SMSException(String, String, Object[])
     */
    public Object[] getMessageArgs() {
        return args;
    }

    /**
     * Returns the status code for this exception.
     * 
     * @return Integer representing the exception status code
     */
    public int getExceptionCode() {
        return exceptionStatus;
    }

    /**
     * The this package can set the exception code.
     * 
     * @param status
     *            The exception status code.
     */
    void setExceptionCode(int status) {
        exceptionStatus = status;
    }

    public String toString() {
        StringBuilder buf = new StringBuilder();
        if (exceptionStatus != -1) {
            buf.append("SMSException Exception Code:");
            buf.append(exceptionStatus);
            buf.append('\n');
        }
        String msg = message;
        if (msg != null && msg.length() > 0) {
            buf.append("Message:");
            buf.append(msg);
            buf.append("\n");
        }

        if (rootCause != null) {
            buf.append("--------------------------------------------------\n");
            buf.append("The lower level exception message\n");
            buf.append(rootCause.getMessage());
            buf.append('\n');
            buf.append("The lower level exception:\n");
            StringWriter sw = new StringWriter(100);
            rootCause.printStackTrace(new PrintWriter(sw));
            buf.append(sw.toString());
            buf.append('\n');
        }
        return buf.toString();
    }

    /**
     * Returns the error message of this exception.
     * 
     * @return String representing the error message
     */
    public String getMessage() {
        return message;
    }

    private String getString(String msgID) {
        errorCode = msgID;
        ResourceBundle bundle = null;
        if (bundleName != null) {
            bundle = amCache.getResBundle(bundleName, java.util.Locale.ENGLISH);
        }
        return (Locale.getString(bundle, msgID, debug));
    }

    private void exceptionMapper() {
        if (rootCause == null) {
            return;
        }
        if (rootCause instanceof LdapException) {
            message = mapLDAPException(((LdapException) rootCause).getResult().getResultCode());
        } else if (rootCause instanceof LDAPServiceException) {
            // do nothing
        } else if (rootCause instanceof XMLException) {
            exceptionStatus = STATUS_ABORT;
            message = getString(IUMSConstants.SMS_XML_PARSER_EXCEPTION);
        } else if (rootCause instanceof InvalidAuthContextException) {
            message = getString(IUMSConstants.SMS_AUTHENTICATION_ERROR);
            exceptionStatus = STATUS_ABORT;
        } else if (rootCause instanceof SSOException) {
            message = getString(IUMSConstants.SMS_AUTHENTICATION_ERROR);
            exceptionStatus = STATUS_ABORT;
        } else {
            message = getString(IUMSConstants.SMS_UNKNOWN_EXCEPTION_OCCURRED);
            exceptionStatus = STATUS_UNKNOWN_EXCEPTION;
        }
    }

    private String mapLDAPException(ResultCode resultCode) {

        String message = null;

        // ////////////////////////////////
        // Errors that need to be handled
        // ////////////////////////////////

        // Helpless errors
        // All errors are helpless situations
        // but some are more helpless than the others.
        // These errors are either problems in connection
        // or configuration. So, some can be retired and
        // some are already busted.
        if (resultCode.equals(CLIENT_SIDE_SERVER_DOWN) || resultCode.equals(OTHER)) {
            message = getString(IUMSConstants.SMS_SERVER_DOWN);
            exceptionStatus = STATUS_RETRY;
        } else if (resultCode.equals(CLIENT_SIDE_NOT_SUPPORTED)) {
            message = getString(IUMSConstants.SMS_LDAP_NOT_SUPPORTED);
            exceptionStatus = STATUS_ABORT;
        } else if (resultCode.equals(BUSY)) {
            message = getString(IUMSConstants.SMS_LDAP_SERVER_BUSY);
            exceptionStatus = STATUS_RETRY;
        } else if (resultCode.equals(INVALID_CREDENTIALS)) {
            message = getString("INVALID_CREDENTIALS");
            exceptionStatus = STATUS_CONFIG_PROBLEM;
        } else if (resultCode.equals(NO_SUCH_OBJECT)) {
            message = getString(IUMSConstants.SMS_NO_SUCH_OBJECT);
            exceptionStatus = STATUS_LDAP_OP_FAILED;
        } else if (resultCode.equals(INSUFFICIENT_ACCESS_RIGHTS)) {
            message = getString(IUMSConstants.SMS_INSUFFICIENT_ACCESS_RIGHTS);
            exceptionStatus = STATUS_NO_PERMISSION;
        } else if (resultCode.equals(ADMIN_LIMIT_EXCEEDED)) {
            message = getString(IUMSConstants.SMS_ADMIN_LIMIT_EXCEEDED);
            exceptionStatus = STATUS_ABORT;
        } else if (resultCode.equals(TIME_LIMIT_EXCEEDED)) {
            message = getString(IUMSConstants.SMS_TIME_LIMIT_EXCEEDED);
            exceptionStatus = STATUS_ABORT;
        } else if (resultCode.equals(REFERRAL)) {
            message = getString(IUMSConstants.SMS_LDAP_REFERRAL_EXCEPTION);
            exceptionStatus = STATUS_CONFIG_PROBLEM;
        } else if (resultCode.equals(OBJECTCLASS_VIOLATION) || resultCode.equals(NAMING_VIOLATION) ||
                resultCode.equals(CONSTRAINT_VIOLATION) || resultCode.equals(INVALID_DN_SYNTAX) ||
                resultCode.equals(ENTRY_ALREADY_EXISTS) || resultCode.equals(ATTRIBUTE_OR_VALUE_EXISTS) ||
                resultCode.equals(PROTOCOL_ERROR) || resultCode.equals(UNDEFINED_ATTRIBUTE_TYPE)) {
            SMSEntry.debug.error(rootCause.toString());
            message = getString(IUMSConstants.SMS_LDAP_OPERATION_FAILED);
            exceptionStatus = STATUS_LDAP_OP_FAILED;
        } else if (resultCode.equals(COMPARE_FALSE) || resultCode.equals(COMPARE_TRUE)) {
            exceptionStatus = STATUS_QUO_ANTE;
        } else {
            message = getString(IUMSConstants.SMS_UNEXPECTED_LDAP_EXCEPTION);
            exceptionStatus = STATUS_UNKNOWN_EXCEPTION;
        }
        return message;
    }

    // Error codes
    /** No status code is set */
    public static final int STATUS_NONE = -1;

    /** Retry connection to data store */
    public static final int STATUS_RETRY = 0;

    /** Repeated retry to data store failed */
    public static final int STATUS_REPEATEDLY_FAILED = 0;

    /** Status to abort operation */
    public static final int STATUS_ABORT = 1;

    /**
     * If root LDAP cause is <code>LDAP_PARTIAL_RESULTS </code> then this
     * status is set
     */
    public static final int STATUS_QUO_ANTE = 2;

    /**
     * If root LDAP cause is an LDAP exception with one of the following error
     * codes then this status is set.
     * <p>
     * 
     * <PRE>
     * 
     * NO_SUCH_OBJECT OBJECT_CLASS_VIOLATION NAMING_VIOLATION
     * CONSTRAINT_VIOLATION INVALID_DN_SYNTAX ENTRY_ALREADY_EXISTS
     * ATTRIBUTE_OR_VALUE_EXISTS PROTOCOL_ERROR UNDEFINED_ATTRIBUTE_TYPE
     * 
     * </PRE>
     */

    public static final int STATUS_LDAP_OP_FAILED = 3;

    /**
     * If the root LDAP exception is <code> INVALID_CREDENTIALS </code> or
     * <code> REFERRAL </code> then this status is set
     */
    public static final int STATUS_CONFIG_PROBLEM = 4;

    /** If root cause is other than any of those listed in other status codes */
    public static final int STATUS_UNKNOWN_EXCEPTION = 5;

    /** If the root LDAP cause is <code> INSUFFICIENT_ACCESS_RIGHTS </code> */
    public static final int STATUS_NO_PERMISSION = 8;

    /** the operation is not allowed. */
    public static final int STATUS_NOT_ALLOW = 9;

}
