/*
 * Copyright (c) OSGi Alliance (2004, 2016). 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 org.osgi.service.dmt;

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.
 * 
 * @author $Id: 771a15819801eae69baa162cda16f17156b179e0 $
 */
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>
	 * <li>an attempt is made to create a node where the parent is a leaf node</li>
	 * <li>an attempt is made to rename or delete the root node of the tree</li>
	 * <li>an attempt is made to rename or delete the root node of the session</li>
	 * <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>
	 * <li>a node is copied to its descendant</li>
	 * <li>the ACL of the root node is changed not to include Add rights for all
	 * principals</li>
	 * </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 operation failed because a specific limit was exceeded,
	 * e.g. if a requested resource exceeds a size limit.
	 * <p>
	 * This error code corresponds to the OMA DM response status code 413
	 * &quot;Request entity too large&quot;.
	 * 
	 * @since 2.0
	 */
	public static final int		LIMIT_EXCEEDED				= 413;

	/**
	 * The requested command failed because the target URI is too long for what
	 * the recipient is able or willing to process.
	 * <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</li>
	 * <li>creating an interior node where the meta-node defines it as a leaf,
	 * or vice versa</li>
	 * <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>
	 * <li>any node creation or deletion that would violate the cardinality
	 * constraints</li>
	 * <li>any leaf node value setting that would violate the allowed formats,
	 * values, mime types, etc.</li>
	 * <li>any node creation that would violate the allowed node names</li>
	 * </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>
	 * <li>the URI is an empty string (only invalid if the method does not
	 * accept relative URIs)</li>
	 * <li>the URI contains the segment &quot;{@code .}&quot; at a position
	 * other than the beginning of the URI</li>
	 * <li>the node name is &quot;{@code ..}&quot; or the URI contains such a
	 * segment</li>
	 * <li>the node name contains an unescaped '/' character</li>
	 * </ul>
	 * <p>
	 * See the {@link Uri#encode(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 dependent.
	 * <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<? extends Throwable> causes, boolean fatal) {
		this(uri, code, message,
				(causes == null) ? new Throwable[0]
						: (Throwable[]) causes.toArray(new Throwable[0]),
				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<? extends Throwable> 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
	 */
	@Override
	public String getMessage() {
		StringBuilder sb = new StringBuilder(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
	 */
	@Override
	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 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 stacktrace to the specified print stream.
	 * Any causes that were specified for this exception are also printed,
	 * together with their stacktraces.
	 * 
	 * @param s {@code PrintStream} to use for output
	 */
	@Override
	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>";
		}
	}
}
