    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    }