/*
 * Copyright (c) OSGi Alliance (2004, 2010). All Rights Reserved.
 * 
 * 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 info.dmtree;

import java.io.PrintStream;
import java.util.Vector;

/**
 * Checked exception received when a DMT operation fails. Beside the exception
 * message, a {@code DmtException} always contains an error code (one of
 * the constants specified in this class), and may optionally contain the URI of
 * the related node, and information about the cause of the exception.
 * <p>
 * Some of the error codes defined in this class have a corresponding error code
 * defined in OMA DM, in these cases the name and numerical value from OMA DM is
 * used. Error codes without counterparts in OMA DM were given numbers from a
 * different range, starting from 1.
 * <p>
 * The cause of the exception (if specified) can either be a single
 * {@code Throwable} instance, or a list of such instances if several
 * problems occurred during the execution of a method. An example for the latter
 * is the {@code close} method of {@code DmtSession} that tries to
 * close multiple plugins, and has to report the exceptions of all failures.
 * <p>
 * Each constructor has two variants, one accepts a {@code String} node
 * URI, the other accepts a {@code String[]} node path. The former is used
 * by the DmtAdmin implementation, the latter by the plugins, who receive the
 * node URI as an array of segment names. The constructors are otherwise
 * identical.
 * <p>
 * Getter methods are provided to retrieve the values of the additional
 * parameters, and the {@code printStackTrace(PrintWriter)} method is
 * extended to print the stack trace of all causing throwables as well.
 * 
 * @version $Id: 4c28c365160136f6d2da2229ab0ba127dea78527 $
 */
public class DmtException extends Exception {
    private static final long serialVersionUID = -63006267148118655L;

    // ----- Public constants -----//

    /**
     * The originator's authentication credentials specify a principal with
     * insufficient rights to complete the command.
     * <p>
     * This status code is used as response to device originated sessions if the
     * remote management server cannot authorize the device to perform the
     * requested operation.
     * <p>
     * This error code corresponds to the OMA DM response status code 401
     * &quot;Unauthorized&quot;.
     */
    public static final int UNAUTHORIZED = 401;

    /**
     * The requested target node was not found. No indication is given as to
     * whether this is a temporary or permanent condition, unless otherwise
     * noted.
     * <p>
     * This is only used when the requested node name is valid, otherwise the
     * more specific error codes {@link #URI_TOO_LONG} or {@link #INVALID_URI}
     * are used. This error code corresponds to the OMA DM response status code
     * 404 &quot;Not Found&quot;.
     */
    public static final int NODE_NOT_FOUND = 404;

    /**
     * The requested command is not allowed on the target node. This includes
     * the following situations:
     * <ul>
     * <li>an interior node operation is requested for a leaf node, or vice
     * versa (e.g. trying to retrieve the children of a leaf node)
     * <li>an attempt is made to create a node where the parent is a leaf node
     * <li>an attempt is made to rename or delete the root node of the tree
     * <li>an attempt is made to rename or delete the root node of the session
     * <li>a write operation (other than setting the ACL) is performed in a
     * non-atomic write session on a node provided by a plugin that is read-only
     * or does not support non-atomic writing
     * <li>a node is copied to its descendant
     * <li>the ACL of the root node is changed not to include Add rights for
     * all principals
     * </ul>
     * <p>
     * This error code corresponds to the OMA DM response status code 405
     * &quot;Command not allowed&quot;.
     */
    public static final int COMMAND_NOT_ALLOWED = 405;

    /**
     * The requested command failed because an optional feature required by the
     * command is not supported. For example, opening an atomic session might
     * return this error code if the DmtAdmin implementation does not support
     * transactions. Similarly, accessing the optional node properties (Title,
     * Timestamp, Version, Size) might not succeed if either the DmtAdmin
     * implementation or the underlying plugin does not support the property.
     * <p>
     * When getting or setting values for interior nodes (an optional
     * optimization feature), a plugin can use this error code to indicate that
     * the given interior node does not support values.
     * <p>
     * This error code corresponds to the OMA DM response status code 406
     * &quot;Optional feature not supported&quot;.
     */
    public static final int FEATURE_NOT_SUPPORTED = 406;
    
    /**
     * The requested command failed because the target URI or one of its
     * segments is too long for what the recipient is able or willing to
     * process, or the target URI contains too many segments. The length and
     * segment number limits are implementation dependent, their minimum values
     * can be found in the Non Functional Requirements section of the OSGi
     * specification.
     * <p>
     * The {@link Uri#mangle(String)} method provides support for ensuring that
     * a URI segment conforms to the length limits set by the implementation.
     * <p>
     * This error code corresponds to the OMA DM response status code 414
     * &quot;URI too long&quot;.
     * 
     * @see "OSGi Service Platform, Mobile Specification Release 4"
     */
    public static final int URI_TOO_LONG = 414;

    /**
     * The requested node creation operation failed because the target already
     * exists. This can occur if the node is created directly (with one of the
     * {@code create...} methods), or indirectly (during a
     * {@code copy} operation).
     * <p>
     * This error code corresponds to the OMA DM response status code 418
     * &quot;Already exists&quot;.
     */
    public static final int NODE_ALREADY_EXISTS = 418;

    /**
     * The requested command failed because the principal associated with the
     * session does not have adequate access control permissions (ACL) on the
     * target. This can only appear in case of remote sessions, i.e. if the
     * session is associated with an authenticated principal.
     * <p>
     * This error code corresponds to the OMA DM response status code 425
     * &quot;Permission denied&quot;.
     */
    public static final int PERMISSION_DENIED = 425;

    /**
     * The recipient encountered an error which prevented it from fulfilling the
     * request.
     * <p>
     * This error code is only used in situations not covered by any of the
     * other error codes that a method may use. Some methods specify more
     * specific error situations for this code, but it can generally be used for
     * any unexpected condition that causes the command to fail.
     * <p>
     * This error code corresponds to the OMA DM response status code 500
     * &quot;Command Failed&quot;.
     */
    public static final int COMMAND_FAILED = 500;

    /**
     * An error related to the recipient data store occurred while processing
     * the request. This error code may be thrown by any of the methods
     * accessing the tree, but whether it is really used depends on the
     * implementation, and the data store it uses.
     * <p>
     * This error code corresponds to the OMA DM response status code 510
     * &quot;Data store failure&quot;.
     */
    public static final int DATA_STORE_FAILURE = 510;

    /**
     * The rollback command was not completed successfully. The tree might be in
     * an inconsistent state after this error.
     * <p>
     * This error code corresponds to the OMA DM response status code 516
     * &quot;Atomic roll back failed&quot;.
     */
    public static final int ROLLBACK_FAILED = 516;
    

    /**
     * A device initiated remote operation failed. This is used when the
     * protocol adapter fails to send an alert for any reason.
     * <p>
     * Alert routing errors (that occur while looking for the proper protocol
     * adapter to use) are indicated by {@link #ALERT_NOT_ROUTED}, this code is
     * only for errors encountered while sending the routed alert. This error
     * code does not correspond to any OMA DM response status code. It should be
     * translated to the code 500 &quot;Command Failed&quot; when transferring
     * over OMA DM.
     */
    public static final int REMOTE_ERROR = 1;

    /**
     * Operation failed because of meta data restrictions. This covers any
     * attempted deviation from the parameters defined by the
     * {@code MetaNode} objects of the affected nodes, for example in the
     * following situations:
     * <ul>
     * <li>creating, deleting or renaming a permanent node, or modifying its
     * type or value
     * <li>creating an interior node where the meta-node defines it as a leaf,
     * or vice versa
     * <li>any operation on a node which does not have the required access type
     * (e.g. executing a node that lacks the {@code MetaNode.CMD_EXECUTE}
     * access type)
     * <li>any node creation or deletion that would violate the cardinality
     * constraints
     * <li>any leaf node value setting that would violate the allowed formats,
     * values, mime types, etc.
     * <li>any node creation that would violate the allowed node names
     * </ul>
     * <p>
     * This error code can also be used to indicate any other meta data
     * violation, even if it cannot be described by the {@code MetaNode}
     * class. For example, detecting a multi-node constraint violation while
     * committing an atomic session should result in this error.
     * <p>
     * This error code does not correspond to any OMA DM response status code.
     * It should be translated to the code 405 &quot;Command not allowed&quot;
     * when transferring over OMA DM.
     */
    public static final int METADATA_MISMATCH = 2;

    /**
     * The requested command failed because the target URI or node name is
     * {@code null} or syntactically invalid. This covers the following
     * cases:
     * <ul>
     * <li>the URI or node name ends with the '\' or '/' character
     * <li>the URI is an empty string (only invalid if the method does not 
     * accept relative URIs)
     * <li>the URI contains the segment &quot;{@code .}&quot; at a position
     * other than the beginning of the URI
     * <li>the node name is &quot;{@code ..}&quot; or the URI contains such
     * a segment
     * <li>the node name is an empty string or the URI contains an empty segment
     * <li>the node name contains an unescaped '/' character
     * </ul>
     * <p>
     * See the {@link Uri#mangle(String)} method for support on escaping invalid
     * characters in a URI.
     * <p>
     * This code is only used if the URI or node name does not match any of the
     * criteria for {@link #URI_TOO_LONG}. This error code does not correspond
     * to any OMA DM response status code. It should be translated to the code
     * 404 &quot;Not Found&quot; when transferring over OMA DM.
     */
    public static final int INVALID_URI = 3;

    /**
     * An error occurred related to concurrent access of nodes. This can happen
     * for example if a configuration node was deleted directly through the
     * Configuration Admin service, while the node was manipulated via the tree.
     * <p>
     * This error code does not correspond to any OMA DM response status code.
     * It should be translated to the code 500 &quot;Command Failed&quot; when
     * transferring over OMA DM.
     */
    public static final int CONCURRENT_ACCESS = 4;

    /**
     * An alert can not be sent from the device to the given principal. This can
     * happen if there is no Remote Alert Sender willing to forward the alert to
     * the given principal, or if no principal was given and the DmtAdmin did
     * not find an appropriate default destination.
     * <p>
     * This error code does not correspond to any OMA DM response status code.
     * It should be translated to the code 500 &quot;Command Failed&quot; when
     * transferring over OMA DM.
     */
    public static final int ALERT_NOT_ROUTED = 5;

    /**
     * A transaction-related error occurred in an atomic session. This error is
     * caused by one of the following situations:
     * <ul>
     * <li>an updating method within an atomic session can not be executed
     * because the underlying plugin is read-only or does not support atomic
     * writing</li>
     * <li>a commit operation at the end of an atomic session failed because
     * one of the underlying plugins failed to close</li>
     * </ul>
     * The latter case may leave the tree in an inconsistent state due to the
     * lack of a two-phase commit system, see {@link DmtSession#commit} for
     * details.
     * <p>
     * This error code does not correspond to any OMA DM response status code.
     * It should be translated to the code 500 &quot;Command Failed&quot; when
     * transferring over OMA DM.
     */
    public static final int TRANSACTION_ERROR = 6;

    /**
     * Creation of a session timed out because of another ongoing session. The
     * length of time while the DmtAdmin waits for the blocking session(s) to
     * finish is implementation dependant.
     * <p>
     * This error code does not correspond to any OMA DM response status code.
     * OMA has several status codes related to timeout, but these are meant to
     * be used when a request times out, not if a session can not be
     * established. This error code should be translated to the code 500
     * &quot;Command Failed&quot; when transferring over OMA DM.
     */
    public static final int SESSION_CREATION_TIMEOUT = 7;

    // ----- Content fields -----//

    /**
     * The URI of the node on which the failed DMT operation was issued, or
     * {@code null} if the operation was not associated with a node.
     */
    private final String uri;

    /**
     * The error code of the failure, one of the constants defined in this
     * class.
     */
    private final int code;

	/**
	 * The message associated with the exception, or {@code null} if there
	 * is no error message.
	 */
	private final String		message;

	/**
	 * The list of originating exceptions, or empty list or {@code null} if
	 * there are no originating exceptions.
	 */
    private final Throwable[] causes;

    /**
     * Determines whether the exception is fatal or not. This is basically a
     * two-state severity indicator, with the 'fatal' severity being the more
     * serious one.
     */
    private final boolean fatal;

    // ----- Constructors -----//

    /**
     * Create an instance of the exception. The {@code uri} and
     * {@code message} parameters are optional. No originating exception
     * is specified.
     * 
     * @param uri the node on which the failed DMT operation was issued, or
     *        {@code null} if the operation is not associated with a node
     * @param code the error code of the failure
     * @param message the message associated with the exception, or
     *        {@code null} if there is no error message
     */
    public DmtException(String uri, int code, String message) {
        this(uri, code, message, new Throwable[0], false);
    }

    /**
     * Create an instance of the exception, specifying the cause exception. The
     * {@code uri}, {@code message} and {@code cause}
     * parameters are optional.
     * 
     * @param uri the node on which the failed DMT operation was issued, or
     *        {@code null} if the operation is not associated with a node
     * @param code the error code of the failure
     * @param message the message associated with the exception, or
     *        {@code null} if there is no error message
     * @param cause the originating exception, or {@code null} if there
     *        is no originating exception
     */
    public DmtException(String uri, int code, String message, Throwable cause) {
        this(uri, code, message, (cause == null) ? new Throwable[0]
                : new Throwable[] { cause }, false);
    }

    /**
     * Create an instance of the exception, specifying the list of cause
     * exceptions and whether the exception is a fatal one. This constructor is
     * meant to be used by plugins wishing to indicate that a serious error
     * occurred which should invalidate the ongoing atomic session. The
     * {@code uri}, {@code message} and {@code causes}
     * parameters are optional.
     * <p>
     * If a fatal exception is thrown, no further business methods will be
     * called on the originator plugin. In case of atomic sessions, all other
     * open plugins will be rolled back automatically, except if the fatal
     * exception was thrown during commit.
     * 
     * @param uri the node on which the failed DMT operation was issued, or
     *        {@code null} if the operation is not associated with a node
     * @param code the error code of the failure
     * @param message the message associated with the exception, or
     *        {@code null} if there is no error message
     * @param causes the list of originating exceptions, or empty list or
     *        {@code null} if there are no originating exceptions
     * @param fatal whether the exception is fatal
     */
    public DmtException(String uri, int code, String message, Vector causes,
            boolean fatal) {
        this(uri, code, message, (causes == null) ? new Throwable[0]
                : (Throwable[]) causes.toArray(new Throwable[causes.size()]),
                fatal);
    }
    
    private DmtException(String uri, int code, String message, 
            Throwable[] causes, boolean fatal) {
    	super((Throwable) null);
        this.uri = uri;
        this.code = code;
        this.message = message;
        this.causes = causes;
        this.fatal = fatal;
    }

    /**
     * Create an instance of the exception, specifying the target node as an
     * array of path segments. This method behaves in exactly the same way as if
     * the path was given as a URI string.
     * 
     * @param path the path of the node on which the failed DMT operation was
     *        issued, or {@code null} if the operation is not associated
     *        with a node
     * @param code the error code of the failure
     * @param message the message associated with the exception, or
     *        {@code null} if there is no error message
     * @see #DmtException(String, int, String)
     */
    public DmtException(String[] path, int code, String message) {
        this(pathToUri(path), code, message);
    }

    /**
     * Create an instance of the exception, specifying the target node as an
     * array of path segments, and specifying the cause exception. This method
     * behaves in exactly the same way as if the path was given as a URI string.
     * 
     * @param path the path of the node on which the failed DMT operation was
     *        issued, or {@code null} if the operation is not associated
     *        with a node
     * @param code the error code of the failure
     * @param message the message associated with the exception, or
     *        {@code null} if there is no error message
     * @param cause the originating exception, or {@code null} if there
     *        is no originating exception
     * @see #DmtException(String, int, String, Throwable)
     */
    public DmtException(String[] path, int code, String message, Throwable cause) {
        this(pathToUri(path), code, message, cause);
    }

    /**
     * Create an instance of the exception, specifying the target node as an
     * array of path segments, the list of cause exceptions, and whether the
     * exception is a fatal one. This method behaves in exactly the same way as
     * if the path was given as a URI string.
     * 
     * @param path the path of the node on which the failed DMT operation was
     *        issued, or {@code null} if the operation is not associated
     *        with a node
     * @param code the error code of the failure
     * @param message the message associated with the exception, or
     *        {@code null} if there is no error message
     * @param causes the list of originating exceptions, or empty list or
     *        {@code null} if there are no originating exceptions
     * @param fatal whether the exception is fatal
     * @see #DmtException(String, int, String, Vector, boolean)
     */
    public DmtException(String[] path, int code, String message, Vector causes,
            boolean fatal) {
        this(pathToUri(path), code, message, causes, fatal);
    }

    // ----- Public methods -----//

    /**
     * Get the node on which the failed DMT operation was issued. Some
     * operations like {@code DmtSession.close()} don't require an URI,
     * in this case this method returns {@code null}.
     * 
     * @return the URI of the node, or {@code null}
     */
    public String getURI() {
        return uri;
    }

    /**
     * Get the error code associated with this exception. Most of the error
     * codes within this exception correspond to OMA DM error codes.
     * 
     * @return the error code
     */
    public int getCode() {
        return code;
    }

    /**
     * Get the message associated with this exception. The returned string also
     * contains the associated URI (if any) and the exception code. The
     * resulting message has the following format (parts in square brackets are
     * only included if the field inside them is not {@code null}):
     * 
     * <pre>
     *  &lt;exception_code&gt;[: '&lt;uri&gt;'][: &lt;error_message&gt;]
     * </pre>
     * 
     * @return the error message in the format described above
     */
    public String getMessage() {
        StringBuffer sb = new StringBuffer(getCodeText(code));
        if (uri != null)
            sb.append(": '").append(uri).append('\'');
        if (message != null)
			sb.append(": ").append(message);

        return sb.toString();
    }

    /**
     * Get the cause of this exception. Returns non-{@code null}, if
     * this exception is caused by one or more other exceptions (like a
     * {@code NullPointerException} in a DmtPlugin). If there are more
     * than one cause exceptions, the first one is returned.
     * 
     * @return the cause of this exception, or {@code null} if no cause
     *         was given
     */
    public Throwable getCause() {
        return causes.length == 0 ? null : causes[0];
    }

    /**
     * Get all causes of this exception. Returns the causing exceptions in an
     * array. If no cause was specified, an empty array is returned.
     * 
     * @return the list of causes of this exception
     */
    public Throwable[] getCauses() {
        return (Throwable[]) causes.clone();
    }

    /**
     * Check whether this exception is marked as fatal in the session. Fatal
     * exceptions trigger an automatic rollback of atomic sessions.
     * 
     * @return whether the exception is marked as fatal
     */
    public boolean isFatal() {
        return fatal;
    }

    /**
     * Prints the exception and its backtrace to the specified print stream. Any
     * causes that were specified for this exception are also printed, together
     * with their backtraces.
     * 
     * @param s {@code PrintStream} to use for output
     */
    public void printStackTrace(PrintStream s) {
        super.printStackTrace(s);
        for (int i = 0; i<causes.length; i++) {
            s.print("Caused by" + (i > 0 ? " (" + (i+1) + ")" : "") + ": ");
            causes[i].printStackTrace(s);
        }
    }

    // ----- Utility methods -----//

    /**
     * Converts the given path, given as an array of path segments, to a single
     * URI string.
     * 
     * @param path the path to convert
     * @return the URI string representing the same node as the given path
     */
    static String pathToUri(String[] path) {
        if (path == null)
            return null;

        return Uri.toUri(path);
    }

    /**
     * Returns the name of the given error code.
     * 
     * @param code the error code
     * @return a string containing the error code name
     */
    private static String getCodeText(int code) {
        // todo sync codes
        switch (code) {
        case NODE_NOT_FOUND:
            return "NODE_NOT_FOUND";
        case COMMAND_NOT_ALLOWED:
            return "COMMAND_NOT_ALLOWED";
        case FEATURE_NOT_SUPPORTED:
            return "FEATURE_NOT_SUPPORTED";
        case URI_TOO_LONG:
            return "URI_TOO_LONG";
        case NODE_ALREADY_EXISTS:
            return "NODE_ALREADY_EXISTS";
        case PERMISSION_DENIED:
            return "PERMISSION_DENIED";
        case COMMAND_FAILED:
            return "COMMAND_FAILED";
        case DATA_STORE_FAILURE:
            return "DATA_STORE_FAILURE";
        case ROLLBACK_FAILED:
            return "ROLLBACK_FAILED";

        case REMOTE_ERROR:
            return "REMOTE_ERROR";
        case METADATA_MISMATCH:
            return "METADATA_MISMATCH";
        case INVALID_URI:
            return "INVALID_URI";
        case CONCURRENT_ACCESS:
            return "CONCURRENT_ACCESS";
        case ALERT_NOT_ROUTED:
            return "ALERT_NOT_ROUTED";
        case TRANSACTION_ERROR:
            return "TRANSACTION_ERROR";
        case SESSION_CREATION_TIMEOUT:
            return "SESSION_CREATION_TIMEOUT";
        default:
            return "<unknown code>";
        }
    }
}
