QuimpException.java

package com.github.celldynamics.quimp;

import java.awt.Frame;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;

import javax.swing.JOptionPane;

import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.utils.QuimpToolsCollection;

import ch.qos.logback.classic.Logger;
import ij.IJ;

/**
 * Template of exceptions thrown by QuimP API.
 * 
 * <p>Allow for handling them and present to user depending on {@link MessageSinkTypes}.
 * 
 * @author p.baniukiewicz
 *
 */
public class QuimpException extends Exception {

  /**
   * If true sink type will not be changed in {@link #setMessageSinkType(MessageSinkTypes...)} and
   * {@link #setMessageSinkType(Set)}.
   */
  protected boolean persistent = false;
  /**
   * The LOGGER.
   */
  public Logger logger = (Logger) LoggerFactory.getLogger(this.getClass());

  /**
   * Define where the message should be displayed. Anu combination of values is supported by
   * {@link QuimpException#handleException(Frame, String)}.
   * <ol>
   * <li>CONSOLE - default, message goes to console.
   * <li>GUI - message should be shown in GUI
   * <li>IJERROR - use IJ error handling
   * <li>MESSAGE - print message in console using INFO level
   * <li>NONE - {@link QuimpException#handleException(Frame, String)} will return just formatted
   * string without any action.
   * </ol>
   * 
   * @author p.baniukiewicz
   *
   */
  public enum MessageSinkTypes {

    /**
     * Console sink (stderr).
     */
    CONSOLE,
    /**
     * Display window with message.
     */
    GUI,
    /**
     * Use IJ.error for log.
     */
    IJERROR,
    /**
     * Print message in console.
     * 
     * <p>Similar to {@link #CONSOLE} but message has INFO level.
     */
    MESSAGE,
    /**
     * None of above, just return formatted exception string.
     */
    NONE;
  }

  /**
   * Message sinks - where they will appear.
   * 
   * @see MessageSinkTypes
   */
  protected EnumSet<MessageSinkTypes> messageSinkType;

  /**
   * Set message sink. It can be combination of {@link MessageSinkTypes} values.
   * 
   * @param messageSinkType the messageSinkType to set.
   */
  public void setMessageSinkType(MessageSinkTypes... messageSinkType) {
    if (persistent) {
      return;
    }
    this.messageSinkType = EnumSet.copyOf(Arrays.asList(messageSinkType));
  }

  /**
   * Set message sink. It can be combination of {@link MessageSinkTypes} values.
   * 
   * @param messageSinkType the messageSinkType to set.
   */
  public void setMessageSinkType(Set<MessageSinkTypes> messageSinkType) {
    if (persistent) {
      return;
    }
    this.messageSinkType = EnumSet.copyOf(messageSinkType);
  }

  /**
   * Get message sink.
   * 
   * @return the type
   */
  public Set<MessageSinkTypes> getMessageSinkType() {
    return messageSinkType;
  }

  /**
   * serialVersionUID.
   */
  private static final long serialVersionUID = -7943488580659917234L;

  /**
   * Default constructor, set message sink to console.
   */
  public QuimpException() {
    this.messageSinkType = EnumSet.of(MessageSinkTypes.CONSOLE);
  }

  /**
   * Allow to set type of message, where it should be displayed.
   * 
   * @param types of message (list)
   * @see MessageSinkTypes
   */
  public QuimpException(MessageSinkTypes... types) {
    setMessageSinkType(types);
  }

  /**
   * Allow to set type of message, where it should be displayed.
   * 
   * @param type of message (one)
   * @see MessageSinkTypes
   */
  public QuimpException(MessageSinkTypes type) {
    this.messageSinkType = EnumSet.of(type);
  }

  /**
   * Default exception with message. Will be shown in console.
   * 
   * @param message message
   */
  public QuimpException(String message) {
    super(message);
    this.messageSinkType = EnumSet.of(MessageSinkTypes.CONSOLE);
  }

  /**
   * Exception with message.
   * 
   * @param message message
   * @param type where to show message
   */
  public QuimpException(String message, MessageSinkTypes type) {
    super(message);
    this.messageSinkType = EnumSet.of(type);
  }

  /**
   * Exception with message.
   * 
   * @param message message
   * @param type where to show message
   * @param persistent if true the sink will not be overwritten
   */
  public QuimpException(String message, MessageSinkTypes type, boolean persistent) {
    super(message);
    this.messageSinkType = EnumSet.of(type);
    this.persistent = persistent;
  }

  /**
   * Exception with message.
   * 
   * @param message message
   * @param types where to show message
   */
  public QuimpException(String message, MessageSinkTypes... types) {
    super(message);
    setMessageSinkType(types);
  }

  /**
   * QuimpException.
   * 
   * @param cause cause
   */
  public QuimpException(Throwable cause) {
    super(cause);
    this.messageSinkType = EnumSet.of(MessageSinkTypes.CONSOLE);
  }

  /**
   * QuimpException.
   * 
   * @param cause cause
   * @param type type (one)
   */
  public QuimpException(Throwable cause, MessageSinkTypes type) {
    super(cause);
    this.messageSinkType = EnumSet.of(type);
  }

  /**
   * QuimpException.
   * 
   * @param cause cause
   * @param types type (list)
   */
  public QuimpException(Throwable cause, MessageSinkTypes... types) {
    super(cause);
    setMessageSinkType(types);
  }

  /**
   * QuimpException.
   * 
   * @param message message
   * @param cause cause
   */
  public QuimpException(String message, Throwable cause) {
    super(message, cause);
    this.messageSinkType = EnumSet.of(MessageSinkTypes.CONSOLE);
  }

  /**
   * QuimpException.
   * 
   * @param message message
   * @param cause cause
   * @param enableSuppression enableSuppression
   * @param writableStackTrace writableStackTrace
   */
  public QuimpException(String message, Throwable cause, boolean enableSuppression,
          boolean writableStackTrace) {
    super(message, cause, enableSuppression, writableStackTrace);
    this.messageSinkType = EnumSet.of(MessageSinkTypes.CONSOLE);
  }

  /**
   * QuimpException.
   * 
   * @param message message
   * @param cause cause
   * @param type where to show message (one)
   */
  public QuimpException(String message, Throwable cause, MessageSinkTypes type) {
    super(message, cause);
    this.messageSinkType = EnumSet.of(type);
  }

  /**
   * QuimpException.
   * 
   * @param message message
   * @param cause cause
   * @param types where to show message (list)
   */
  public QuimpException(String message, Throwable cause, MessageSinkTypes... types) {
    super(message, cause);
    setMessageSinkType(types);
  }

  /**
   * Handle this exception displaying it and logging depending on {@link #messageSinkType}.
   * 
   * @param frame Swing frame to display message for user, can be null
   * @param appendMessage Message added to beginning of the exception message, can be ""
   * @return Exception message
   */
  public String handleException(Frame frame, String appendMessage) {
    logger.debug(getMessage(), this);
    String message = prepareMessage(this, appendMessage);

    if (getMessageSinkType().contains(MessageSinkTypes.CONSOLE)) {
      logger.error(message);
    }
    if (getMessageSinkType().contains(MessageSinkTypes.MESSAGE)) {
      logger.info(message);
    }
    if (getMessageSinkType().contains(MessageSinkTypes.GUI)) {
      showGuiWithMessage(frame, message);
    }
    if (getMessageSinkType().contains(MessageSinkTypes.IJERROR)) {
      List<String> ex = getExceptionMessageChain(this);
      if (ex.size() > 1) {
        IJ.log("Error messages stack:");
        for (int i = 0; i < ex.size(); i++) {
          if (ex.get(i) != null) {
            if (i == 0) {
              IJ.log(" " + ex.get(i));
            } else {
              IJ.log("  ->" + ex.get(i));
            }
          }
        }
      }
      IJ.handleException(this);
      IJ.error(QuimpToolsCollection.stringWrap(message, QuimP.LINE_WRAP));
    }
    return message;
  }

  /**
   * Show UI with message.
   * 
   * @param frame Swing frame to display message for user, can be null
   * @param message message produced by {@link #prepareMessage(Exception, String)}
   */
  public static void showGuiWithMessage(Frame frame, String message) {
    JOptionPane.showMessageDialog(frame, QuimpToolsCollection.stringWrap(message, QuimP.LINE_WRAP),
            "Error", JOptionPane.ERROR_MESSAGE);
  }

  /**
   * Prepare formatted message to show.
   * 
   * @param ex Exception to dig in.
   * @param appendMessage Message added to beginning of the exception message, can be ""
   * @return Exception message
   */
  public static String prepareMessage(Exception ex, String appendMessage) {
    String message = appendMessage;
    List<String> ch = getExceptionMessageChain(ex);
    int l = 0;
    for (String c : ch) {
      if (c != null) {
        if (l == 0) {
          message = message.concat(" (" + c);
        } else {
          message = message.concat(" -> " + c);
        }
        l++;
      }
    }
    if (l > 0) {
      message = message.concat(")");
    }
    return message;
  }

  /**
   * Get messages from exception stack.
   * 
   * <p>Taken from stackoverflow.com/questions/15987258/
   * 
   * @param throwable exception
   * @return list of messages from underlying exceptions
   */
  public static List<String> getExceptionMessageChain(Throwable throwable) {
    List<String> result = new ArrayList<String>();
    while (throwable != null) {
      result.add(throwable.getMessage());
      throwable = throwable.getCause();
    }
    return result;
  }

}