001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.jexl2;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.UndeclaredThrowableException;
021import org.apache.commons.jexl2.parser.JexlNode;
022import org.apache.commons.jexl2.parser.ParseException;
023import org.apache.commons.jexl2.parser.TokenMgrError;
024
025/**
026 * Wraps any error that might occur during interpretation of a script or expression.
027 * @since 2.0
028 */
029public class JexlException extends RuntimeException {
030    /** The point of origin for this exception. */
031    protected final transient JexlNode mark;
032    /** The debug info. */
033    protected final transient JexlInfo info;
034    /** A marker to use in NPEs stating a null operand error. */
035    public static final String NULL_OPERAND = "jexl.null";
036    /** Minimum number of characters around exception location. */
037    private static final int MIN_EXCHARLOC = 5;
038    /** Maximum number of characters around exception location. */
039    private static final int MAX_EXCHARLOC = 10;
040
041    /**
042     * Creates a new JexlException.
043     * @param node the node causing the error
044     * @param msg the error message
045     */
046    public JexlException(JexlNode node, String msg) {
047        super(msg);
048        mark = node;
049        info = node != null ? node.debugInfo() : null;
050
051    }
052
053    /**
054     * Creates a new JexlException.
055     * @param node the node causing the error
056     * @param msg the error message
057     * @param cause the exception causing the error
058     */
059    public JexlException(JexlNode node, String msg, Throwable cause) {
060        super(msg, unwrap(cause));
061        mark = node;
062        info = node != null ? node.debugInfo() : null;
063    }
064
065    /**
066     * Creates a new JexlException.
067     * @param dbg the debugging information associated
068     * @param msg the error message
069     */
070    public JexlException(JexlInfo dbg, String msg) {
071        super(msg);
072        mark = null;
073        info = dbg;
074    }
075
076    /**
077     * Creates a new JexlException.
078     * @param dbg the debugging information associated
079     * @param msg the error message
080     * @param cause the exception causing the error
081     */
082    public JexlException(JexlInfo dbg, String msg, Throwable cause) {
083        super(msg, unwrap(cause));
084        mark = null;
085        info = dbg;
086    }
087
088    /**
089     * Unwraps the cause of a throwable due to reflection. 
090     * @param xthrow the throwable
091     * @return the cause
092     */
093    private static Throwable unwrap(Throwable xthrow) {
094        if (xthrow instanceof InvocationTargetException) {
095            return ((InvocationTargetException) xthrow).getTargetException();
096        } else if (xthrow instanceof UndeclaredThrowableException) {
097            return ((UndeclaredThrowableException) xthrow).getUndeclaredThrowable();
098        } else {
099            return xthrow;
100        }
101    }
102
103    /**
104     * Accesses detailed message.
105     * @return  the message
106     * @since 2.1
107     */
108    protected String detailedMessage() {
109        return super.getMessage();
110    }
111
112    /**
113     * Formats an error message from the parser.
114     * @param prefix the prefix to the message
115     * @param expr the expression in error
116     * @return the formatted message
117     * @since 2.1
118     */
119    protected String parserError(String prefix, String expr) {
120        int begin = info.debugInfo().getColumn();
121        int end = begin + MIN_EXCHARLOC;
122        begin -= MIN_EXCHARLOC;
123        if (begin < 0) {
124            end += MIN_EXCHARLOC;
125            begin = 0;
126        }
127        int length = expr.length();
128        if (length < MAX_EXCHARLOC) {
129            return prefix + " error in '" + expr + "'";
130        } else {
131            return prefix + " error near '... "
132                    + expr.substring(begin, end > length ? length : end) + " ...'";
133        }
134    }
135
136    /**
137     * Thrown when tokenization fails.
138     * @since 2.1
139     */
140    public static class Tokenization extends JexlException {
141        /**
142         * Creates a new Tokenization exception instance.
143         * @param node the location info
144         * @param expr the expression
145         * @param cause the javacc cause
146         */
147        public Tokenization(JexlInfo node, CharSequence expr, TokenMgrError cause) {
148            super(merge(node, cause), expr.toString(), cause);
149        }
150
151        /**
152         * Merge the node info and the cause info to obtain best possible location.
153         * @param node the node
154         * @param cause the cause
155         * @return the info to use
156         */
157        private static DebugInfo merge(JexlInfo node, TokenMgrError cause) {
158            DebugInfo dbgn = node != null ? node.debugInfo() : null;
159            if (cause == null) {
160                return dbgn;
161            } else if (dbgn == null) {
162                return new DebugInfo("", cause.getLine(), cause.getColumn());
163            } else {
164                return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
165            }
166        }
167
168        /**
169         * @return the expression
170         */
171        public String getExpression() {
172            return super.detailedMessage();
173        }
174
175        @Override
176        protected String detailedMessage() {
177            return parserError("tokenization", getExpression());
178        }
179    }
180
181    /**
182     * Thrown when parsing fails.
183     * @since 2.1
184     */
185    public static class Parsing extends JexlException {
186        /**
187         * Creates a new Variable exception instance.
188         * @param node the offending ASTnode
189         * @param expr the offending source
190         * @param cause the javacc cause
191         */
192        public Parsing(JexlInfo node, CharSequence expr, ParseException cause) {
193            super(merge(node, cause), expr.toString(), cause);
194        }
195
196        /**
197         * Merge the node info and the cause info to obtain best possible location.
198         * @param node the node
199         * @param cause the cause
200         * @return the info to use
201         */
202        private static DebugInfo merge(JexlInfo node, ParseException cause) {
203            DebugInfo dbgn = node != null ? node.debugInfo() : null;
204            if (cause == null) {
205                return dbgn;
206            } else if (dbgn == null) {
207                return new DebugInfo("", cause.getLine(), cause.getColumn());
208            } else {
209                return new DebugInfo(dbgn.getName(), cause.getLine(), cause.getColumn());
210            }
211        }
212
213        /**
214         * @return the expression
215         */
216        public String getExpression() {
217            return super.detailedMessage();
218        }
219
220        @Override
221        protected String detailedMessage() {
222            return parserError("parsing", getExpression());
223        }
224    }
225
226    /**
227     * Thrown when a variable is unknown.
228     * @since 2.1
229     */
230    public static class Variable extends JexlException {
231        /**
232         * Creates a new Variable exception instance.
233         * @param node the offending ASTnode
234         * @param var the unknown variable
235         */
236        public Variable(JexlNode node, String var) {
237            super(node, var);
238        }
239
240        /**
241         * @return the variable name
242         */
243        public String getVariable() {
244            return super.detailedMessage();
245        }
246
247        @Override
248        protected String detailedMessage() {
249            return "undefined variable " + getVariable();
250        }
251    }
252
253    /**
254     * Thrown when a property is unknown.
255     * @since 2.1
256     */
257    public static class Property extends JexlException {
258        /**
259         * Creates a new Property exception instance.
260         * @param node the offending ASTnode
261         * @param var the unknown variable
262         */
263        public Property(JexlNode node, String var) {
264            super(node, var);
265        }
266
267        /**
268         * @return the property name
269         */
270        public String getProperty() {
271            return super.detailedMessage();
272        }
273
274        @Override
275        protected String detailedMessage() {
276            return "inaccessible or unknown property " + getProperty();
277        }
278    }
279
280    /**
281     * Thrown when a method or ctor is unknown, ambiguous or inaccessible.
282     * @since 2.1
283     */
284    public static class Method extends JexlException {
285        /**
286         * Creates a new Method exception instance.
287         * @param node the offending ASTnode
288         * @param name the unknown method
289         */
290        public Method(JexlNode node, String name) {
291            super(node, name);
292        }
293
294        /**
295         * @return the method name
296         */
297        public String getMethod() {
298            return super.detailedMessage();
299        }
300
301        @Override
302        protected String detailedMessage() {
303            return "unknown, ambiguous or inaccessible method " + getMethod();
304        }
305    }
306
307    /**
308     * Thrown to return a value.
309     * @since 2.1
310     */
311    protected static class Return extends JexlException {
312        /** The returned value. */
313        private final Object result;
314
315        /**
316         * Creates a new instance of Return.
317         * @param node the return node
318         * @param msg the message
319         * @param value the returned value
320         */
321        protected Return(JexlNode node, String msg, Object value) {
322            super(node, msg);
323            this.result = value;
324        }
325
326        /**
327         * @return the returned value
328         */
329        public Object getValue() {
330            return result;
331        }
332    }
333
334    /**
335     * Thrown to cancel a script execution.
336     * @since 2.1
337     */
338    protected static class Cancel extends JexlException {
339        /**
340         * Creates a new instance of Cancel.
341         * @param node the node where the interruption was detected
342         */
343        protected Cancel(JexlNode node) {
344            super(node, "execution cancelled", null);
345        }
346    }
347
348    /**
349     * Gets information about the cause of this error.
350     * <p>
351     * The returned string represents the outermost expression in error.
352     * The info parameter, an int[2] optionally provided by the caller, will be filled with the begin/end offset
353     * characters of the precise error's trigger.
354     * </p>
355     * @param offsets character offset interval of the precise node triggering the error
356     * @return a string representation of the offending expression, the empty string if it could not be determined
357     */
358    public String getInfo(int[] offsets) {
359        Debugger dbg = new Debugger();
360        if (dbg.debug(mark)) {
361            if (offsets != null && offsets.length >= 2) {
362                offsets[0] = dbg.start();
363                offsets[1] = dbg.end();
364            }
365            return dbg.data();
366        }
367        return "";
368    }
369
370    /**
371     * Detailed info message about this error.
372     * Format is "debug![begin,end]: string \n msg" where:
373     * - debug is the debugging information if it exists (@link JexlEngine.setDebug)
374     * - begin, end are character offsets in the string for the precise location of the error
375     * - string is the string representation of the offending expression
376     * - msg is the actual explanation message for this error
377     * @return this error as a string
378     */
379    @Override
380    public String getMessage() {
381        Debugger dbg = new Debugger();
382        StringBuilder msg = new StringBuilder();
383        if (info != null) {
384            msg.append(info.debugString());
385        }
386        if (dbg.debug(mark)) {
387            msg.append("![");
388            msg.append(dbg.start());
389            msg.append(",");
390            msg.append(dbg.end());
391            msg.append("]: '");
392            msg.append(dbg.data());
393            msg.append("'");
394        }
395        msg.append(' ');
396        msg.append(detailedMessage());
397        Throwable cause = getCause();
398        if (cause != null && NULL_OPERAND == cause.getMessage()) {
399            msg.append(" caused by null operand");
400        }
401        return msg.toString();
402    }
403}