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     */
017    package org.apache.commons.jexl2;
018    
019    import java.lang.reflect.InvocationTargetException;
020    import java.lang.reflect.UndeclaredThrowableException;
021    import org.apache.commons.jexl2.parser.JexlNode;
022    import org.apache.commons.jexl2.parser.ParseException;
023    import 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     */
029    public 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    }