001/*
002 * SPDX-FileCopyrightText: none
003 * SPDX-License-Identifier: CC0-1.0
004 */
005
006package dev.metaschema.core.metapath;
007
008import java.util.ArrayDeque;
009import java.util.Collections;
010import java.util.Deque;
011
012import edu.umd.cs.findbugs.annotations.NonNull;
013import edu.umd.cs.findbugs.annotations.Nullable;
014
015/**
016 * {@code MetapathException} is the superclass of all exceptions that can be
017 * thrown during the compilation and evaluation of a Metapath.
018 */
019public class MetapathException
020    extends RuntimeException {
021  /**
022   * the serial version UID.
023   */
024  private static final long serialVersionUID = 1L;
025  /**
026   * The error prefix which identifies what kind of error it is.
027   */
028  @NonNull
029  private final IErrorCode errorCode;
030
031  /**
032   * The evaluation stack recording the expressions being evaluated when the
033   * exception occurred.
034   */
035  @Nullable
036  private Deque<IExpression> evaluationStack = null;
037
038  /**
039   * Constructs a new Metapath exception with the provided {@code code} and
040   * {@code message} and no cause.
041   *
042   * @param errorCode
043   *          the error code that identifies the type of error
044   * @param message
045   *          the exception message
046   */
047  protected MetapathException(
048      @NonNull IErrorCode errorCode,
049      @Nullable String message) {
050    super(message);
051    this.errorCode = errorCode;
052  }
053
054  /**
055   * Constructs a new Metapath exception with a {@code null} message and the
056   * provided {@code code} and {@code cause}.
057   *
058   * @param errorCode
059   *          the error code that identifies the type of error
060   * @param cause
061   *          the exception cause
062   */
063  protected MetapathException(
064      @NonNull IErrorCode errorCode,
065      @Nullable Throwable cause) {
066    super(cause);
067    this.errorCode = errorCode;
068  }
069
070  /**
071   * Constructs a new Metapath exception with the provided {@code code},
072   * {@code message} and {@code cause}.
073   *
074   * @param errorCode
075   *          the error code that identifies the type of error
076   * @param message
077   *          the exception message
078   * @param cause
079   *          the exception cause
080   */
081  protected MetapathException(
082      @NonNull IErrorCode errorCode,
083      @Nullable String message,
084      @Nullable Throwable cause) {
085    super(message, cause);
086    this.errorCode = errorCode;
087  }
088
089  /**
090   * Registers the evaluation context from the provided dynamic context.
091   * <p>
092   * A snapshot of the execution stack is captured from the dynamic context if not
093   * already set. This ensures the recorded state reflects the stack at the time
094   * of registration, avoiding confusion from post-throw mutations.
095   *
096   * @param dynamicContext
097   *          the dynamic context containing the execution stack
098   * @return this exception instance for chaining
099   */
100  public final MetapathException registerEvaluationContext(@NonNull DynamicContext dynamicContext) {
101    if (evaluationStack == null) {
102      // getExecutionStack() returns a defensive copy
103      evaluationStack = dynamicContext.getExecutionStack();
104    }
105    return this;
106  }
107
108  /**
109   * Registers the evaluation context from the provided evaluation stack.
110   * <p>
111   * A snapshot of the stack is captured if not already set.
112   *
113   * @param stack
114   *          the evaluation stack recording the expressions being evaluated
115   * @return this exception instance for chaining
116   */
117  public final MetapathException registerEvaluationContext(@NonNull Deque<? extends IExpression> stack) {
118    if (evaluationStack == null) {
119      evaluationStack = new ArrayDeque<>(stack);
120    }
121    return this;
122  }
123
124  /**
125   * Registers the evaluation context from the provided metapath expression.
126   * <p>
127   * The expression is recorded as the evaluation context if not already set.
128   *
129   * @param metapath
130   *          the metapath expression being evaluated
131   * @return this exception instance for chaining
132   */
133  public final MetapathException registerEvaluationContext(@NonNull IMetapathExpression metapath) {
134    if (evaluationStack == null) {
135      evaluationStack = new ArrayDeque<>(Collections.singleton(metapath));
136    }
137    return this;
138  }
139
140  /**
141   * Retrieves the evaluation stack recording the expressions being evaluated.
142   *
143   * @return the evaluation stack, or {@code null} if not set
144   */
145  @Nullable
146  protected Deque<IExpression> getEvaluationStack() {
147    return evaluationStack;
148  }
149
150  @Override
151  public final String getMessage() {
152    String message = getMessageText();
153    return String.format(
154        "%s%s",
155        getErrorCode().toString(),
156        message == null ? "" : ": " + message);
157  }
158
159  /**
160   * Get the message text without the error code prefix.
161   *
162   * @return the message text or {@code null}
163   */
164  @Nullable
165  public String getMessageText() {
166    String msg = super.getMessage();
167
168    Deque<IExpression> stack = getEvaluationStack();
169
170    if (stack != null && !stack.isEmpty()) {
171      IExpression head = stack.peekLast();
172      msg = String.format(
173          "An error occurred while evaluating the expression '%s'%s",
174          head.getPath(),
175          msg == null ? "" : ": " + msg);
176    }
177    return msg;
178  }
179
180  /**
181   * Get the error code, which indicates what type of error it is.
182   *
183   * @return the error code
184   */
185  @NonNull
186  public final IErrorCode getErrorCode() {
187    return errorCode;
188  }
189
190}