/*
 * Copyright (c) 1998, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v. 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0,
 * or the Eclipse Distribution License v. 1.0 which is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
 */

// Contributors:
//     Oracle - initial API and implementation from Oracle TopLink
//     08/29/2016 Jody Grassel
//       - 500441: Eclipselink core has System.getProperty() calls that are not potentially executed under doPriv()
package org.eclipse.persistence.exceptions;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;

import org.eclipse.persistence.exceptions.i18n.ExceptionMessageGenerator;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.sessions.AbstractSession;

/**
 * <p>
 * <b>Purpose</b>: Any exception raised by EclipseLink should be a subclass of this exception class.
 */
public abstract class EclipseLinkException extends RuntimeException {
    protected transient AbstractSession session;
    protected Throwable internalException;
    protected static Boolean shouldPrintInternalException = null;
    protected String indentationString;
    protected int errorCode;
    protected static final String CR = PrivilegedAccessHelper.getSystemProperty("line.separator");
    //Bug#3559280  Added to avoid logging an exception twice
    protected boolean hasBeenLogged;

    /**
     * INTERNAL:
     * Return a new exception.
     */
    public EclipseLinkException() {
        this("");
    }

    /**
     * INTERNAL:
     * EclipseLink exception should only be thrown by EclipseLink.
     */
    public EclipseLinkException(String theMessage) {
        super(theMessage);
        this.indentationString = "";
        hasBeenLogged = false;
    }

    /**
     * INTERNAL:
     * EclipseLink exception should only be thrown by EclipseLink.
     */
    public EclipseLinkException(String message, Throwable internalException) {
        this(message);
        setInternalException(internalException);
    }

    /**
     * INTERNAL:
     * Convenience method - return a platform-specific line-feed.
     */
    protected static String cr() {
        return org.eclipse.persistence.internal.helper.Helper.cr();
    }

    /**
     * PUBLIC:
     * Return the exception error code.
     */
    public int getErrorCode() {
        return errorCode;
    }

    /**
     * INTERNAL:
     * Used to print things nicely in the testing tool.
     */
    public String getIndentationString() {
        return indentationString;
    }

    /**
     * PUBLIC:
     * Return the internal native exception.
     * EclipseLink frequently catches Java exceptions and wraps them in its own exception
     * classes to provide more information.
     * The internal exception can still be accessed if required.
     */
    public Throwable getInternalException() {
        return internalException;
    }

    /**
     * PUBLIC:
     * Return the exception error message.
     * EclipseLink error messages are multi-line so that detail descriptions of the exception are given.
     */
    public String getMessage() {
        StringWriter writer = new StringWriter(100);

        // Avoid printing internal exception error message twice.
        if ((getInternalException() == null) || ((super.getMessage() != null) && !super.getMessage().equals(getInternalException().toString()))) {
            writer.write(cr());
            writer.write(getIndentationString());
            writer.write(ExceptionMessageGenerator.getHeader("DescriptionHeader"));
            writer.write(super.getMessage());
        }

        if (getInternalException() != null) {
            writer.write(cr());
            writer.write(getIndentationString());
            writer.write(ExceptionMessageGenerator.getHeader("InternalExceptionHeader"));
            writer.write(getInternalException().toString());

            if ((getInternalException() instanceof java.lang.reflect.InvocationTargetException) && ((((java.lang.reflect.InvocationTargetException) getInternalException()).getTargetException()) != null)) {
                writer.write(cr());
                writer.write(getIndentationString());
                writer.write(ExceptionMessageGenerator.getHeader("TargetInvocationExceptionHeader"));
                writer.write(((java.lang.reflect.InvocationTargetException) getInternalException()).getTargetException().toString());
            }
        }

        return writer.toString();
    }

    /**
     * PUBLIC:
     * Return the session.
     */
    public AbstractSession getSession() {
        return session;
    }

    /**
     * INTERNAL:
     * Return if this exception has been logged to avoid being logged more than once.
     */
    public boolean hasBeenLogged() {
        return hasBeenLogged;
    }

    /**
     * PUBLIC:
     * Print both the normal and internal stack traces.
     */
    public void printStackTrace() {
        printStackTrace(System.err);
    }

    /**
     * PUBLIC:
     * Print both the normal and internal stack traces.
     */
    public void printStackTrace(PrintStream outStream) {
        printStackTrace(new PrintWriter(outStream));
    }

    /**
     * PUBLIC:
     * Print both the normal and internal stack traces.
     */
    public void printStackTrace(PrintWriter writer) {
        writer.write(ExceptionMessageGenerator.getHeader("LocalExceptionStackHeader"));
        writer.write(cr());
        super.printStackTrace(writer);

        if ((getInternalException() != null) && shouldPrintInternalException()) {
            writer.write(ExceptionMessageGenerator.getHeader("InternalExceptionStackHeader"));
            writer.write(cr());
            getInternalException().printStackTrace(writer);

            if ((getInternalException() instanceof java.lang.reflect.InvocationTargetException) && ((((java.lang.reflect.InvocationTargetException) getInternalException()).getTargetException()) != null)) {
                writer.write(ExceptionMessageGenerator.getHeader("TargetInvocationExceptionStackHeader"));
                writer.write(cr());
                ((java.lang.reflect.InvocationTargetException) getInternalException()).getTargetException().printStackTrace(writer);
            }
        }
        writer.flush();
    }

    /**
     * INTERNAL:
     */
    public void setErrorCode(int errorCode) {
        this.errorCode = errorCode;
    }

    /**
     * INTERNAL:
     * Set this flag to avoid logging an exception more than once.
     */
    public void setHasBeenLogged(boolean logged) {
        this.hasBeenLogged = logged;
    }

    /**
     * INTERNAL:
     * Used to print things nicely in the testing tool.
     */
    public void setIndentationString(String indentationString) {
        this.indentationString = indentationString;
    }

    /**
     * INTERNAL:
     * Used to specify the internal exception.
     */
    public void setInternalException(Throwable exception) {
        internalException = exception;
        if (getCause() == null) {
            initCause(exception);
        }
    }

    /**
     *  INTERNAL:
     */
    public void setSession(AbstractSession session) {
        this.session = session;
    }

    /**
     * PUBLIC:
     * Allows overriding of EclipseLink's exception chaining detection.
     * @param printException - If printException is true, the EclipseLink-stored
     * Internal exception will be included in a stack trace or in the exception message of a EclipseLinkException.
     * If printException is false, the EclipseLink-stored Internal Exception will not be included
     * in the stack trace or the exception message of EclipseLinkExceptions
     */
    public static void setShouldPrintInternalException(boolean printException) {
        shouldPrintInternalException = Boolean.valueOf(printException);
    }

    /**
     * INTERNAL
     * Check to see if the EclipseLink-stored internal exception should be printed in this
     * a EclipseLinkException's stack trace.  This method will check the static ShouldPrintInternalException
     * variable and if it is not set, estimate based on the JDK version used.
     */
    public static boolean shouldPrintInternalException() {
        if (shouldPrintInternalException == null) {
            shouldPrintInternalException = Boolean.FALSE;
        }
        return shouldPrintInternalException.booleanValue();
    }

    /**
     * INTERNAL:
     */
    public String toString() {
        return getIndentationString() + ExceptionMessageGenerator.getHeader("ExceptionHeader") + getErrorCode() + "] (" + org.eclipse.persistence.sessions.DatabaseLogin.getVersion() + "): " + getClass().getName() + getMessage();
    }

    /**
     * INTERNAL:
     */
    public String getUnformattedMessage() {
        return super.getMessage();
    }
}
