ExceptionThrower.java

package com.github.dakusui.symfonion.exceptions;

import com.github.dakusui.json.JsonUtils;
import com.github.dakusui.symfonion.utils.midi.MidiDeviceRecord;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;

import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiUnavailableException;
import java.io.Closeable;
import java.io.File;
import java.util.HashMap;
import java.util.function.Predicate;
import java.util.regex.Matcher;

import static com.github.dakusui.symfonion.cli.CliUtils.composeErrMsg;
import static com.github.dakusui.symfonion.exceptions.ExceptionThrower.ContextKey.*;
import static com.github.dakusui.valid8j.ValidationFluents.all;
import static com.github.dakusui.valid8j.ValidationFluents.that;
import static com.github.dakusui.valid8j_pcond.fluent.Statement.objectValue;
import static java.lang.String.format;

public class ExceptionThrower {
  public enum ContextKey {
    MIDI_DEVICE_INFO(MidiDevice.Info.class),
    MIDI_DEVICE_INFO_IO(String.class),
    JSON_ELEMENT_ROOT(JsonObject.class),
    SOURCE_FILE(File.class);
    private final Class<?> type;

    ContextKey(Class<?> type) {
      this.type = type;
    }
  }

  public record ContextEntry(ContextKey key, Object value) {
  }

  public static class Context implements Closeable {
    private final Context parent;
    private final HashMap<ExceptionThrower.ContextKey, Object> values = HashMap.newHashMap(100);

    private Context(Context parent) {
      this.parent = parent;
    }

    public Context() {
      this.parent = null;
    }

    public Context set(ExceptionThrower.ContextKey key, Object value) {
      assert all(
          objectValue(key).then().isNotNull().$(),
          objectValue(value).then().isNotNull().isInstanceOf(key.type).$());
      this.values.put(key, value);
      return this;
    }

    public Context parent() {
      return this.parent;
    }

    /**
     *
     */
    @SuppressWarnings({"unchecked"})
    <T> T get(ContextKey key) {
      assert that(objectValue(key).then().isNotNull().$());
      // In production, we do not want to produce a NullPointerException, even if the key is null.
      // Just return null in such a situation.
      if (key == null)
        return null;
      if (!this.values.containsKey(key)) {
        assert that(objectValue(this).invoke("parent").then().isNotNull());
        // In production, we do not want to produce a NullPointerException, even if a value associated with the key doesn't exist.
        // Just return null, in such a situation.
        if (this.parent() == null)
          return null;
        return this.parent().get(key);
      }
      return (T) this.values.get(key);
    }


    @Override
    public void close() {
      ExceptionThrower.context.set(this.parent);
    }

    Context createChild() {
      return new Context(this);
    }
  }

  private static final ThreadLocal<Context> context = new ThreadLocal<>();

  public static ContextEntry contextEntry(ContextKey key, Object value) {
    assert all(
        objectValue(key).then().isNotNull().$(),
        objectValue(value).then().isNotNull().isInstanceOf(classOfValueFor(key)).$());
    return new ContextEntry(key, value);
  }


  public static ContextEntry $(ContextKey key, Object value) {
    return contextEntry(key, value);
  }

  public static Context context(ContextEntry... entries) {
    Context ret = currentContext().createChild();
    context.set(ret);
    for (ContextEntry each : entries)
      ret.set(each.key(), each.value());
    return ret;
  }

  public static SymfonionException compilationException(String msg, Throwable e) throws SymfonionException {
    throw new SymfonionException(msg, e, contextValueOf(SOURCE_FILE));
  }

  public static SymfonionException fileNotFoundException(File file, Throwable e) throws SymfonionException {
    throw new SymfonionException(format("%s: File not found (%s)", file, e.getMessage()), file);
  }

  public static SymfonionException loadFileException(Throwable e) throws SymfonionException {
    throw new SymfonionException(format("%s: %s", contextValueOf(SOURCE_FILE), e.getMessage()), contextValueOf(SOURCE_FILE));
  }

  public static SymfonionException loadResourceException(String resourceName, Throwable e) throws SymfonionException {
    throw new SymfonionException(format("%s: Failed to read resource (%s)", resourceName, e.getMessage()), contextValueOf(SOURCE_FILE));
  }

  public static SymfonionReferenceException noteMapNotFoundException(JsonElement problemCausingJsonNode, String missingReference) throws SymfonionException {
    throw new SymfonionReferenceException(missingReference, "notemap", problemCausingJsonNode, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE), contextValueOf(JSON_ELEMENT_ROOT));
  }

  public static SymfonionReferenceException noteNotDefinedException(JsonElement problemCausingJsonNode, String missingReference, String notemapName) throws SymfonionException {
    throw new SymfonionReferenceException(missingReference, format("note in %s", notemapName), problemCausingJsonNode, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE), contextValueOf(JSON_ELEMENT_ROOT));
  }

  public static SymfonionException syntaxErrorInNotePattern(String s, int i, Matcher m) {
    return new SymfonionException("Error:" + s.substring(0, i) + "[" + s.substring(i, m.start()) + "]" + s.substring(m.start()), contextValueOf(SOURCE_FILE));
  }

  public static SymfonionReferenceException grooveNotDefinedException(JsonElement problemCausingJsonNode, String missingReference) throws SymfonionException {
    throw new SymfonionReferenceException(missingReference, "groove", problemCausingJsonNode, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE), JsonUtils.asJsonElement(contextValueOf(JSON_ELEMENT_ROOT), "$grooves"));
  }

  public static SymfonionReferenceException partNotFound(JsonElement problemCausingJsonNode, String missingReference) throws SymfonionException {
    throw new SymfonionReferenceException(missingReference, "part", problemCausingJsonNode, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE), JsonUtils.asJsonElement(contextValueOf(JSON_ELEMENT_ROOT), "$parts"));
  }

  public static SymfonionReferenceException patternNotFound(JsonElement problemCausingJsonNode, String missingReference) throws SymfonionException {
    throw new SymfonionReferenceException(missingReference, "pattern", problemCausingJsonNode, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE), JsonUtils.asJsonElement(contextValueOf(JSON_ELEMENT_ROOT), "$patterns"));
  }

  public static SymfonionTypeMismatchException typeMismatchException(JsonElement actualJSON, String... expectedTypes) throws SymfonionSyntaxException {
    throw new SymfonionTypeMismatchException(expectedTypes, actualJSON, actualJSON, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE));
  }

  public static SymfonionIllegalFormatException illegalFormatException(JsonElement actualJSON, String acceptableExample) throws SymfonionIllegalFormatException {
    throw new SymfonionIllegalFormatException(actualJSON, acceptableExample, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE));
  }

  public static SymfonionIllegalFormatException syntaxErrorWhenExpandingDotsIn(JsonArray errorContainingJsonArray) {
    return new SymfonionIllegalFormatException(
        errorContainingJsonArray,
        "In this array, a string can contain only dots. E.g. '[1, \"...\",3]'. This will be expanded and interpolation of integer values will happen.",
        contextValueOf(JSON_ELEMENT_ROOT),
        contextValueOf(SOURCE_FILE) );
  }

  public static SymfonionIllegalFormatException typeMismatchWhenExpandingDotsIn(JsonArray errorContainingJsonArray) {
    return new SymfonionIllegalFormatException(
        errorContainingJsonArray,
        "This array, only integers, nulls, and strings containing only dots (...) are allowed.",
        contextValueOf(JSON_ELEMENT_ROOT),
        contextValueOf(SOURCE_FILE) );
  }

  public static SymfonionMissingElementException requiredElementMissingException(JsonElement problemCausingJsonNode, Object relativePathFromProblemCausingJsonNode) throws SymfonionMissingElementException {
    throw new SymfonionMissingElementException(problemCausingJsonNode, relativePathFromProblemCausingJsonNode, contextValueOf(JSON_ELEMENT_ROOT), contextValueOf(SOURCE_FILE));
  }

  public static SymfonionMissingElementException requiredElementMissingException(JsonElement actualJSON, JsonObject root, Object relPath) throws SymfonionMissingElementException {
    throw new SymfonionMissingElementException(actualJSON, relPath, root, contextValueOf(SOURCE_FILE));
  }

  public static SymfonionException deviceException(String msg, Throwable e) throws SymfonionException {
    throw new SymfonionException(msg, e, contextValueOf(SOURCE_FILE));
  }

  public static RuntimeException runtimeException(String msg, Throwable e) {
    throw new RuntimeException(msg, e);
  }

  public static FractionFormatException throwFractionFormatException(String fraction) throws FractionFormatException {
    throw new FractionFormatException(fraction);
  }

  public static SymfonionInterruptedException interrupted(InterruptedException e) {
    Thread.currentThread().interrupt();
    throw new SymfonionInterruptedException(e.getMessage(), e);
  }

  public static CliException failedToRetrieveTransmitterFromMidiIn(MidiUnavailableException e, MidiDevice.Info inMidiDeviceInfo) {
    throw new CliException(format("(-) Failed to get transmitter from MIDI-in device (%s)", inMidiDeviceInfo.getName()), e);
  }

  public static CliException failedToOpenMidiDevice(MidiUnavailableException ee) {
    throw new CliException(format("(-) Failed to open MIDI-%s device (%s)",
        ExceptionThrower.<MidiDevice.Info>contextValueOf(MIDI_DEVICE_INFO),
        ExceptionThrower.<String>contextValueOf(MIDI_DEVICE_INFO_IO).toLowerCase()), ee);
  }

  public static CliException failedToAccessMidiDevice(String deviceType, MidiUnavailableException e, MidiDevice.Info[] matchedInfos) {
    throw new CliException(composeErrMsg(format("Failed to access MIDI-%s device:'%s'.", deviceType, matchedInfos[0].getName()), "O"), e);
  }

  public static RuntimeException multipleMidiDevices(MidiDeviceRecord e1, MidiDeviceRecord e2, Predicate<MidiDeviceRecord> cond) {
    throw new CliException(format("Multiple midi devices (at least: '%s', '%s') are found for: '%s'", e1, e2, cond));
  }

  public static RuntimeException noSuchMidiDeviceWasFound(Predicate<MidiDeviceRecord> cond) {
    throw new CliException(format("No such MIDI device was found for: '%s'", cond));
  }

  public static RuntimeException failedToGetTransmitter() {
    throw new RuntimeException();
  }

  public static RuntimeException failedToSetSequence() {
    throw new RuntimeException();
  }

  private static <T> T contextValueOf(ContextKey contextKey) {
    return currentContext().get(contextKey);
  }

  private static Context currentContext() {
    if (context.get() == null)
      context.set(new Context());
    return context.get();
  }

  private static Class<?> classOfValueFor(ContextKey key) {
    class GivenKeyIsNull {
    }
    return key != null ? key.type : GivenKeyIsNull.class;
  }
}