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}