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