import java.util.logging.Logger;
import java.util.logging.Level;

/**
 * This RuntimeException wraps a checked exception which can neither
 * be handled properly, nor declared by the method that received it.
 *
 * This class should only be used as a temporary measure, with
 * the understanding that the method must eventually declare the
 * exception, or handle it completely.
 *
 * I recommend finding every catch block that contains a unsafe
 * catch of a generic Exception and inserting the static handle method:
 *
 * <pre>
 * try {
 *   //...
 * } catch (Exception e) {
 *   UndeclaredException.handle(e);
 *   //...
 * }
 * </pre>
 *
 * Set the system java property or environment variable
 * "exceptions.expose";
 * to set the policy on how these are handled.
 * The default behavior is only to log exceptions.
 */
public class UndeclaredException extends RuntimeException {

  /**
   * The user can control the exposure of badly handled exceptions
   * with this system property, accessed by System.getProperty(),
   * or the environment variable, accessed by System.getenv().
   * The environment variable overrides the java property.
   * "log" is the default option.
   *
   * Exceptions that are being caught in unsafe generic catches
   * will be handled according to the value.
   *
   * EXPOSE_EXCEPTION_PROPERTY = "exceptions.expose"
   * This is a valid identifier in csh.  In sh, replace . by _ or use "env".
   *
   * <ul>
   * <li>
   * "bugs": Unchecked exceptions from
   * avoidable programmer bugs, i.e. RuntimeExceptions, will
   * be rethrown.  Remaining exceptions will be logged.
   *
   * <li>
   * "all": All Exceptions will be rethrown as either the original
   * unchecked exception, or by wrapping a checked exception
   * as an UndeclaredException.  The handle method will never return.
   *
   * <li>
   * "none": The handle method does nothing at all.
   *
   * <li>
   * "log": The stack trace of all exceptions will be logged
   * with the Logger for this class.  Otherwise, behaves like "none".
   * This is the default option.
   * <ul/>
   */
  public static String EXPOSE_EXCEPTION_PROPERTY =
    "exceptions.expose";
  private static String EXPOSE_EXCEPTION_PROPERTY_UNDERSCORE =
    "exceptions_expose";

  /**
   * Logger to log all exceptions
   */
  private static Logger LOG =
    Logger.getLogger(UndeclaredException.class.getName());

  /**
   * Level to log all exceptions
   */
  private static Level LEVEL = Level.INFO;

  /**
   * Set logging level.
   *
   * @param level Level for logging.  Default is INFO.
   * @return Previous Level.
   */
  public static Level setLevel(Level level) {
    Level result = LEVEL;
    LEVEL = level;
    return result;
  }

  /**
   * Exceptions will be handled according to the system property
   * or environment variable
   * EXPOSE_EXCEPTION_PROPERTY = "exceptions.expose"
   * The environment variable overrides the java property.
   * This is a valid identifier in csh.  In sh, replace . by _ or use "env".
   *
   * If you discover a place where all exceptions are caught,
   * but improperly handled, then add this method first.
   *
   * @param e Exception that is being caught, thus hiding
   * RuntimeExceptions.  If null, then method does nothing.
   */
  public static void handle(Exception e) {
    if (e == null) return;
    if (!IGNORE_ALL) {
      handle(e, HANDLE_ALL);
    }
    if (LOG_EXCEPTION) {
      if (e instanceof RuntimeException) {
        LOG.log(LEVEL, "Poorly handled exception or HIDDEN BUG "+e, e);
      } else {
        LOG.log(LEVEL, "Poorly handled exception "+e, e);
      }
    }
  }

  /**
   * All Exceptions will be rethrown as either the original
   * unchecked exception, or by optionally wrapping a checked exception
   * as an UndeclaredException.
   *
   * If you discover a place where all exceptions are caught,
   * but improperly handled, then add this method first.
   *
   * This method should only be used as a temporary measure, with
   * the understanding that the method must eventually declare the
   * exception, or handle it completely.
   *
   * @param e Exception that is being caught, thus hiding
   * RuntimeExceptions.  If null, then method does nothing.
   * @param all If false, then only unchecked exceptions will be
   * thrown, and nothing will be done with checked Exceptions.
   * If true, then all exceptions will be rethrown, as
   * either the original unchecked exception, or by wrapping a
   * checked exception as an UndeclaredException.  If true, then
   * this method will never return (unless e is null).
   */
  public static void handle(Exception e, boolean all) {
    if (e == null) {
      return;
    }
    if (e instanceof RuntimeException) {
      throw (RuntimeException) e;
    }
    if (e instanceof InterruptedException) {
      Thread.currentThread().interrupt();
    }
    if (all) {
      String reason =
        "This exception should be declared by calling method " +
        e.getMessage();
      throw new UndeclaredException(reason, e);
    }
  }

  /** Create an UndeclaredException where a more specific
      exception should have been used, either checked or unchecked.
      Use to replace "new Exception" or "new RuntimeException".
      Use as a marker in the code that should be removed.
  */
  public UndeclaredException() {
    super ("No arguments provided");
    if (LOG_EXCEPTION) {LOG.log(LEVEL,"Need to use specific exception here",this);}
  }

  /** Create an UndeclaredException where a more specific
      exception should have been used, either checked or unchecked.
      Use to replace "new Exception" or "new RuntimeException".
      Use as a marker in the code that should be removed.
      @param reason Description of this Exception
  */
  public UndeclaredException(String reason) {
    super (reason);
    if (LOG_EXCEPTION) {LOG.log(LEVEL,"Need to use specific exception here",this);}
  }

  /**
   * Wrap a checked exception as an unchecked exception,
   * because something currently prevects the method from
   * declaring the exception.  Converts a design error into
   * a runtime bug, so that the exception is eventually handled
   * properly.
   * If this is an Error, then it will be rethrown immediately.
   * Otherwise calls UndeclaredException.handle(Exception cause).
   * @param reason Description of this Exception
   * @param cause The cause of this Exception.
   * If this is an Error, then it will be rethrown immediately.
   */
  public UndeclaredException(String reason, Throwable cause) {
    super (reason);
    if (cause instanceof Error) {throw (Error) cause;}
    assert cause instanceof Exception : cause.getClass();
    UndeclaredException.handle((Exception)cause, false);
    initCause(cause);
  }

  /**
   * Wrap a checked exception as an unchecked exception,
   * because something currently prevects the method from
   * declaring the exception.  Converts a design error into
   * a runtime bug, so that the exception is eventually handled
   * properly.
   * If this is an Error, then it will be rethrown immediately.
   * Otherwise calls UndeclaredException.handle(Exception cause).
   *
   * @param cause The cause of this Exception
   * If this is an Error, then it will be rethrown immediately.
   */
  public UndeclaredException(Throwable cause) {
    super (cause.toString());
    if (cause instanceof Error) {throw (Error) cause;}
    assert cause instanceof Exception : cause.getClass();
    UndeclaredException.handle((Exception)cause, false);
    initCause(cause);
  }

  /**
   * Reset behavior for handle method by rereading the system
   * the system property or environment variable
   * EXPOSE_EXCEPTION_PROPERTY = "exceptions.expose"
   * The environment variable overrides the java property.
   */
  public static void readEnvironment() {
    String exposeExceptions = null;
    for (String envvar: new String[] {EXPOSE_EXCEPTION_PROPERTY,
                                      EXPOSE_EXCEPTION_PROPERTY_UNDERSCORE}) {
      try {
        if (exposeExceptions == null) {
          exposeExceptions = System.getenv(envvar);
        }
      } catch (SecurityException se) {
        LOG.info("Do not have permission to read environment variable "+envvar);
      }
    }
    if (exposeExceptions == null) {
      exposeExceptions = System.getProperty(EXPOSE_EXCEPTION_PROPERTY);
    }
    if (exposeExceptions != null) {
      exposeExceptions = exposeExceptions.toLowerCase();
      if (exposeExceptions.equals("all")) {
        HANDLE_ALL = true;
        IGNORE_ALL = false;
        LOG_EXCEPTION = true;
      }
      else if (exposeExceptions.equals("bugs")) {
        HANDLE_ALL = false;
        IGNORE_ALL = false;
        LOG_EXCEPTION = true;
      }
      else if (exposeExceptions.equals("none")) {
        HANDLE_ALL = false;
        IGNORE_ALL = true;
        LOG_EXCEPTION = false;
      }
      else if (exposeExceptions.equals("log")) {
        HANDLE_ALL = false;
        IGNORE_ALL = true;
        LOG_EXCEPTION = true;
      }
      else {
        LOG.warning(
                    "Unrecognized value for " + EXPOSE_EXCEPTION_PROPERTY +
                    "=" + exposeExceptions);
      }
    }
    LOG.info("UndeclaredException used: "+
             EXPOSE_EXCEPTION_PROPERTY+"="+exposeExceptions);
  }

  // PRIVATE // PRIVATE // PRIVATE // PRIVATE

  // Static members
  /**
   * If true, then ignore all exceptions.
   */
  private static boolean IGNORE_ALL;

  /**
   * If true, then rethrow all exceptions as unchecked exceptions.
   */
  private static boolean HANDLE_ALL;

  /**
   * If true, then log all exceptions to this class's Logger.
   */
  private static boolean LOG_EXCEPTION;

  /**
   * Serializable
   */
  private static final long serialVersionUID = 1L;

  // static initializer
  static {
    HANDLE_ALL = false;
    IGNORE_ALL = true; // False preferred as default.
    LOG_EXCEPTION = true;
    readEnvironment(); // overrides defaults
  }
}


