/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.uima;

import java.text.MessageFormat;
import java.util.Locale;
import java.util.ResourceBundle;

/**
 * The <code>InternationalizedException</code> class adds internationalization
 * support to the standard functionality provided by
 * <code>java.lang.Exception</code>.
 * <p>
 * To support internationalization, the thrower of an exception must not specify
 * a hardcoded message string. Instead, the thrower specifies a key that
 * identifies the message. That key is then looked up in a locale-specific
 * {@link java.util.ResourceBundle ResourceBundle} to find the actual message
 * associated with this exception.
 * <p>
 * This class also supports arguments to messages. The full message will be
 * constructed using the {@link java.text.MessageFormat MessageFormat} class.
 * For more information on internationalization, see the <a
 * href="http://java.sun.com/j2se/1.4/docs/guide/intl/index.html"> Java
 * Internationalization Guide</a>.
 * <p>
 * This version of this class works with JDK versions prior to 1.4, since it
 * does not assume support for exception chaining. The file
 * <code>InternationalizedException.java_1_4</code> is a version that uses the
 * exception chaining support built-in to JDK1.4.
 * 
 * 
 */
public class InternationalizedException extends Exception {
   private static final long serialVersionUID = 2306587442280738385L;

   /**
    * The base name of the resource bundle in which the message for this
    * exception is located.
    */
   private String mResourceBundleName;

   /**
    * An identifier that maps to the message for this exception.
    */
   private String mMessageKey;

   /**
    * The arguments to this exception's message, if any. This allows an
    * <code>InternationalizedException</code> to have a compound message, made
    * up of multiple parts that are concatenated in a language-neutral way.
    */
   private Object[] mArguments;

   /**
    * The exception that caused this exception to occur.
    */
   private Throwable mCause;

   /**
    * Creates a new <code>InternationalizedException</code> with a null
    * message.
    */
   public InternationalizedException() {
      this(null, null, null, null);
   }

   /**
    * Creates a new <code>InternationalizedException</code> with the specified
    * cause and a null message.
    * 
    * @param aCause
    *           the original exception that caused this exception to be thrown,
    *           if any
    */
   public InternationalizedException(Throwable aCause) {
      this(null, null, null, aCause);
   }

   /**
    * Creates a new <code>InternationalizedException</code> with the specified
    * message.
    * 
    * @param aResourceBundleName
    *           the base name of the resource bundle in which the message for
    *           this exception is located.
    * @param aMessageKey
    *           an identifier that maps to the message for this exception. The
    *           message may contain placeholders for arguments as defined by the
    *           {@link java.text.MessageFormat MessageFormat} class.
    * @param aArguments
    *           The arguments to the message. <code>null</code> may be used if
    *           the message has no arguments.
    */
   public InternationalizedException(String aResourceBundleName,
         String aMessageKey, Object[] aArguments) {
      this(aResourceBundleName, aMessageKey, aArguments, null);
   }

   /**
    * Creates a new <code>InternationalizedException</code> with the specified
    * message and cause.
    * 
    * @param aResourceBundleName
    *           the base name of the resource bundle in which the message for
    *           this exception is located.
    * @param aMessageKey
    *           an identifier that maps to the message for this exception. The
    *           message may contain placeholders for arguments as defined by the
    *           {@link java.text.MessageFormat MessageFormat} class.
    * @param aArguments
    *           The arguments to the message. <code>null</code> may be used if
    *           the message has no arguments.
    * @param aCause
    *           the original exception that caused this exception to be thrown,
    *           if any
    */
   public InternationalizedException(String aResourceBundleName,
         String aMessageKey, Object[] aArguments, Throwable aCause) {
      super();
      mCause = aCause;
      mResourceBundleName = aResourceBundleName;
      mMessageKey = aMessageKey;
      mArguments = aArguments;
      // if null message and mCause is Internationalized exception, "promote"
      // message
      if (mResourceBundleName == null && mMessageKey == null) {
         if (mCause instanceof InternationalizedException) {
            mResourceBundleName = ((InternationalizedException) mCause)
                  .getResourceBundleName();
            mMessageKey = ((InternationalizedException) mCause).getMessageKey();
            mArguments = ((InternationalizedException) mCause).getArguments();
         } else if (mCause instanceof InternationalizedRuntimeException) {
            mResourceBundleName = ((InternationalizedRuntimeException) mCause)
                  .getResourceBundleName();
            mMessageKey = ((InternationalizedRuntimeException) mCause)
                  .getMessageKey();
            mArguments = ((InternationalizedRuntimeException) mCause)
                  .getArguments();
         }
      }
   }

   /**
    * Gets the base name of the resource bundle in which the message for this
    * exception is located.
    * 
    * @return the resource bundle base name. May return <code>null</code> if
    *         this exception has no message.
    */
   public String getResourceBundleName() {
      return mResourceBundleName;
   }

   /**
    * Gets the identifier for this exception's message. This identifier can be
    * looked up in this exception's
    * {@link java.util.ResourceBundle ResourceBundle} to get the locale-specific
    * message for this exception.
    * 
    * @return the resource identifier for this exception's message. May return
    *         <code>null</code> if this exception has no message.
    */
   public String getMessageKey() {
      return mMessageKey;
   }

   /**
    * Gets the arguments to this exception's message. Arguments allow a
    * <code>InternationalizedException</code> to have a compound message, made
    * up of multiple parts that are concatenated in a language-neutral way.
    * 
    * @return the arguments to this exception's message.
    */
   public Object[] getArguments() {
      if (mArguments == null)
         return new Object[0];

      Object[] result = new Object[mArguments.length];
      System.arraycopy(mArguments, 0, result, 0, mArguments.length);
      return result;
   }

   /**
    * Gets the <i>English</i> detail message for this exception. For the
    * localized message use {@link #getLocalizedMessage()}.
    * 
    * @return the English detail message for this exception.
    */
   public String getMessage() {
      return getLocalizedMessage(Locale.ENGLISH);
   }

   /**
    * Gets the localized detail message for this exception. This uses the
    * default Locale for this JVM. A Locale may be specified using
    * {@link #getLocalizedMessage(Locale)}.
    * 
    * @return this exception's detail message, localized for the default Locale.
    */
   public String getLocalizedMessage() {
      return getLocalizedMessage(Locale.getDefault());
   }

   /**
    * Gets the localized detail message for this exception using the specified
    * <code>Locale</code>.
    * 
    * @param aLocale
    *           the locale to use for localizing the message
    * 
    * @return this exception's detail message, localized for the specified
    *         <code>Locale</code>.
    */
   public String getLocalizedMessage(Locale aLocale) {
      // check for null message
      if (getMessageKey() == null)
         return null;

      try {
         // locate the resource bundle for this exception's messages
         // turn over the classloader of the current object explicitly, so that the
         // message resolving also works for derived exception classes
         ResourceBundle bundle = ResourceBundle.getBundle(
               getResourceBundleName(), aLocale, this.getClass()
                     .getClassLoader());
         // retrieve the message from the resource bundle
         String message = bundle.getString(getMessageKey());
         // if arguments exist, use MessageFormat to include them
         if (getArguments().length > 0) {
            MessageFormat fmt = new MessageFormat(message);
            fmt.setLocale(aLocale);
            return fmt.format(getArguments());
         } else
            return message;
      } catch (Exception e) {
         return "EXCEPTION MESSAGE LOCALIZATION FAILED: " + e.toString();
      }
   }

   /**
    * Gets the cause of this Exception.
    * 
    * @return the Throwable that caused this Exception to occur, if any. Returns
    *         <code>null</code> if there is no such cause.
    */
   public Throwable getCause() {
      return mCause;
   }

   /**
    * Checks if this exception, or any of its root causes, has a particular UIMA
    * message key. This allows checking for particular error condition
    * 
    * @param messageKey
    *           to search for in the exception chain
    * @return true if this exception or any of its root causes has a particular UIMA message key.
    */
   public boolean hasMessageKey(String messageKey) {
      if (messageKey.equals(this.getMessageKey())) {
         return true;
      }
      Throwable cause = getCause();
      if (cause != null && cause instanceof InternationalizedException) {
         return ((InternationalizedException) cause).hasMessageKey(messageKey);
      }
      return false;
   }

   public synchronized Throwable initCause(Throwable cause) {
      mCause = cause;
      return this;
   }

}
