1   /*
2    * SPDX-FileCopyrightText: none
3    * SPDX-License-Identifier: CC0-1.0
4    */
5   
6   package gov.nist.secauto.metaschema.core.metapath;
7   
8   import java.util.ArrayDeque;
9   import java.util.Collections;
10  import java.util.Deque;
11  
12  import edu.umd.cs.findbugs.annotations.NonNull;
13  import edu.umd.cs.findbugs.annotations.Nullable;
14  
15  /**
16   * {@code MetapathException} is the superclass of all exceptions that can be
17   * thrown during the compilation and evaluation of a Metapath.
18   */
19  public class MetapathException
20      extends RuntimeException {
21    /**
22     * the serial version UID.
23     */
24    private static final long serialVersionUID = 1L;
25    /**
26     * The error prefix which identifies what kind of error it is.
27     */
28    @NonNull
29    private final IErrorCode errorCode;
30  
31    /**
32     * The evaluation stack recording the expressions being evaluated when the
33     * exception occurred.
34     */
35    @Nullable
36    private Deque<IExpression> evaluationStack = null;
37  
38    /**
39     * Constructs a new Metapath exception with the provided {@code code} and
40     * {@code message} and no cause.
41     *
42     * @param errorCode
43     *          the error code that identifies the type of error
44     * @param message
45     *          the exception message
46     */
47    protected MetapathException(
48        @NonNull IErrorCode errorCode,
49        @Nullable String message) {
50      super(message);
51      this.errorCode = errorCode;
52    }
53  
54    /**
55     * Constructs a new Metapath exception with a {@code null} message and the
56     * provided {@code code} and {@code cause}.
57     *
58     * @param errorCode
59     *          the error code that identifies the type of error
60     * @param cause
61     *          the exception cause
62     */
63    protected MetapathException(
64        @NonNull IErrorCode errorCode,
65        @Nullable Throwable cause) {
66      super(cause);
67      this.errorCode = errorCode;
68    }
69  
70    /**
71     * Constructs a new Metapath exception with the provided {@code code},
72     * {@code message} and {@code cause}.
73     *
74     * @param errorCode
75     *          the error code that identifies the type of error
76     * @param message
77     *          the exception message
78     * @param cause
79     *          the exception cause
80     */
81    protected MetapathException(
82        @NonNull IErrorCode errorCode,
83        @Nullable String message,
84        @Nullable Throwable cause) {
85      super(message, cause);
86      this.errorCode = errorCode;
87    }
88  
89    /**
90     * Registers the evaluation context from the provided dynamic context.
91     * <p>
92     * A snapshot of the execution stack is captured from the dynamic context if not
93     * already set. This ensures the recorded state reflects the stack at the time
94     * of registration, avoiding confusion from post-throw mutations.
95     *
96     * @param dynamicContext
97     *          the dynamic context containing the execution stack
98     * @return this exception instance for chaining
99     */
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 }