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.camel.util;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.List;
022import java.util.regex.Matcher;
023import java.util.regex.Pattern;
024
025/**
026 * Helper for Camel OGNL (Object-Graph Navigation Language) expressions.
027 */
028public final class OgnlHelper {
029
030    private static final Pattern INDEX_PATTERN = Pattern.compile("^(.*)\\[(.*)\\]$");
031
032    private OgnlHelper() {
033    }
034
035    /**
036     * Tests whether or not the given String is a Camel OGNL expression.
037     * <p/>
038     * An expression is considered an OGNL expression when it contains either one of the following chars: . or [
039     *
040     * @param  expression the String
041     * @return            <tt>true</tt> if a Camel OGNL expression, otherwise <tt>false</tt>.
042     */
043    public static boolean isValidOgnlExpression(String expression) {
044        if (expression == null || expression.isEmpty()) {
045            return false;
046        }
047
048        // the brackets should come in a pair
049        int bracketBegin = StringHelper.countChar(expression, '[');
050        int bracketEnd = StringHelper.countChar(expression, ']');
051        if (bracketBegin > 0 && bracketEnd > 0) {
052            return bracketBegin == bracketEnd;
053        }
054
055        return expression.contains(".");
056    }
057
058    public static boolean isInvalidValidOgnlExpression(String expression) {
059        if (expression == null) {
060            return false;
061        }
062
063        if (expression.indexOf('.') == -1 && expression.indexOf('[') == -1 && expression.indexOf(']') == -1) {
064            return false;
065        }
066
067        // the brackets should come in pair
068        int bracketBegin = StringHelper.countChar(expression, '[');
069        int bracketEnd = StringHelper.countChar(expression, ']');
070        if (bracketBegin > 0 || bracketEnd > 0) {
071            return bracketBegin != bracketEnd;
072        }
073
074        // check for double dots
075        if (expression.contains("..")) {
076            return true;
077        }
078
079        return false;
080    }
081
082    /**
083     * Validates whether the method name is using valid java identifiers in the name Will throw
084     * {@link IllegalArgumentException} if the method name is invalid.
085     */
086    public static void validateMethodName(String method) {
087        for (int i = 0; i < method.length(); i++) {
088            char ch = method.charAt(i);
089            if (i == 0 && '.' == ch) {
090                // its a dot before a method name
091                continue;
092            }
093            if (ch == '(' || ch == '[' || ch == '.' || ch == '?') {
094                // break when method name ends and sub method or arguments begin
095                break;
096            }
097            if (i == 0 && !Character.isJavaIdentifierStart(ch)) {
098                throw new IllegalArgumentException(
099                        "Method name must start with a valid java identifier at position: 0 in method: " + method);
100            } else if (!Character.isJavaIdentifierPart(ch)) {
101                throw new IllegalArgumentException(
102                        "Method name must be valid java identifier at position: " + i + " in method: " + method);
103            }
104        }
105    }
106
107    /**
108     * Tests whether or not the given Camel OGNL expression is using the null safe operator or not.
109     *
110     * @param  ognlExpression the Camel OGNL expression
111     * @return                <tt>true</tt> if the null safe operator is used, otherwise <tt>false</tt>.
112     */
113    public static boolean isNullSafeOperator(String ognlExpression) {
114        return ognlExpression.startsWith("?");
115    }
116
117    /**
118     * Removes any leading operators from the Camel OGNL expression.
119     * <p/>
120     * Will remove any leading of the following chars: ? or .
121     *
122     * @param  ognlExpression the Camel OGNL expression
123     * @return                the Camel OGNL expression without any leading operators.
124     */
125    public static String removeLeadingOperators(String ognlExpression) {
126        if (ognlExpression.startsWith("?")) {
127            ognlExpression = ognlExpression.substring(1);
128        }
129        if (ognlExpression.startsWith(".")) {
130            ognlExpression = ognlExpression.substring(1);
131        }
132        return ognlExpression;
133    }
134
135    /**
136     * Removes any trailing operators from the Camel OGNL expression.
137     *
138     * @param  ognlExpression the Camel OGNL expression
139     * @return                the Camel OGNL expression without any trailing operators.
140     */
141    public static String removeTrailingOperators(String ognlExpression) {
142        int pos = ognlExpression.indexOf('[');
143        if (pos != -1) {
144            return ognlExpression.substring(0, pos);
145        }
146        return ognlExpression;
147    }
148
149    public static String removeOperators(String ognlExpression) {
150        return removeLeadingOperators(removeTrailingOperators(ognlExpression));
151    }
152
153    public static KeyValueHolder<String, String> isOgnlIndex(String ognlExpression) {
154        Matcher matcher = INDEX_PATTERN.matcher(ognlExpression);
155        if (matcher.matches()) {
156
157            // to avoid empty strings as we want key/value to be null in such cases
158            String key = matcher.group(1);
159            if (key != null && key.isEmpty()) {
160                key = null;
161            }
162
163            // to avoid empty strings as we want key/value to be null in such cases
164            String value = matcher.group(2);
165            if (value != null && value.isEmpty()) {
166                value = null;
167            }
168
169            return new KeyValueHolder<>(key, value);
170        }
171
172        return null;
173    }
174
175    /**
176     * Regular expression with repeating groups is a pain to get right and then nobody understands the reg exp
177     * afterwards. So we use a bit ugly/low-level Java code to split the OGNL into methods.
178     *
179     * @param  ognl                     the ognl expression
180     * @return                          a list of methods, will return an empty list, if ognl expression has no methods
181     * @throws IllegalArgumentException if the last method has a missing ending parenthesis
182     */
183    public static List<String> splitOgnl(String ognl) {
184        // return an empty list if ognl is empty
185        if (ognl == null || ognl.isEmpty() || ognl.trim().isEmpty()) {
186            return Collections.EMPTY_LIST;
187        }
188
189        List<String> methods = new ArrayList<>(4);
190        StringBuilder sb = new StringBuilder();
191
192        int j = 0; // j is used as counter per method
193        int squareBracketCnt = 0; // special to keep track if and how deep we are inside a square bracket block, eg: [foo]
194        int parenthesisBracketCnt = 0; // special to keep track if and how deep we are inside a parenthesis block, eg: bar(${body}, ${header.foo})
195
196        for (int i = 0; i < ognl.length(); i++) {
197            char ch = ognl.charAt(i);
198            // special for starting a new method
199            if (j == 0 || j == 1 && ognl.charAt(i - 1) == '?'
200                    || ch != '.' && ch != '?' && ch != ']') {
201                sb.append(ch);
202                // special if we are doing square bracket
203                if (ch == '[' && parenthesisBracketCnt == 0) {
204                    squareBracketCnt++;
205                } else if (ch == '(') {
206                    parenthesisBracketCnt++;
207                } else if (ch == ')') {
208                    parenthesisBracketCnt--;
209                }
210                j++; // advance
211            } else {
212                if (ch == '.' && squareBracketCnt == 0 && parenthesisBracketCnt == 0) {
213                    // only treat dot as a method separator if not inside a square bracket block
214                    // as dots can be used in key names when accessing maps
215
216                    // a dit denotes end of this method and a new method is to be invoked
217                    String s = sb.toString();
218
219                    // reset sb
220                    sb.setLength(0);
221
222                    // pass over ? to the new method
223                    if (s.endsWith("?")) {
224                        sb.append("?");
225                        s = s.substring(0, s.length() - 1);
226                    }
227
228                    // add the method
229                    methods.add(s);
230
231                    // reset j to begin a new method
232                    j = 0;
233                } else if (ch == ']' && parenthesisBracketCnt == 0) {
234                    // append ending ] to method name
235                    sb.append(ch);
236                    String s = sb.toString();
237
238                    // reset sb
239                    sb.setLength(0);
240
241                    // add the method
242                    methods.add(s);
243
244                    // reset j to begin a new method
245                    j = 0;
246
247                    // no more square bracket
248                    squareBracketCnt--;
249                }
250
251                // and don't lose the char if its not an ] end marker (as we already added that)
252                if (ch != ']' || parenthesisBracketCnt > 0) {
253                    sb.append(ch);
254                }
255
256                // only advance if already begun on the new method
257                if (j > 0) {
258                    j++;
259                }
260            }
261        }
262
263        // add remainder in buffer when reached end of data
264        if (sb.length() > 0) {
265            methods.add(sb.toString());
266        }
267
268        String last = methods.isEmpty() ? null : methods.get(methods.size() - 1);
269        if (parenthesisBracketCnt > 0 && last != null) {
270            // there is an unclosed parenthesis bracket on the last method, so it should end with a parenthesis
271            if (last.contains("(") && !last.endsWith(")")) {
272                throw new IllegalArgumentException("Method should end with parenthesis, was " + last);
273            }
274        }
275
276        return methods;
277    }
278
279    public static String methodAsDoubleQuotes(String ognl) {
280        StringBuilder sb = new StringBuilder();
281
282        int singleBracketCnt = 0;
283        int doubleBracketCnt = 0;
284        for (int i = 0; i < ognl.length(); i++) {
285            char ch = ognl.charAt(i);
286            char next = i < ognl.length() - 1 ? ognl.charAt(i + 1) : 0;
287
288            if (ch == '\\' && next == '\'') {
289                if (singleBracketCnt % 2 != 0) {
290                    // its an escaped single quote inside an existing quote
291                    // then unescape it
292                    sb.append('\'');
293                    // and skip over to next
294                    i++;
295                    continue;
296                }
297            }
298
299            if (doubleBracketCnt % 2 == 0 && ch == '\'') {
300                singleBracketCnt++;
301                sb.append('"');
302            } else if (singleBracketCnt % 2 == 0 && ch == '"') {
303                doubleBracketCnt++;
304                sb.append('"');
305            } else {
306                sb.append(ch);
307            }
308        }
309
310        return sb.toString();
311    }
312
313}