View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InputStreamReader;
23  import java.io.Reader;
24  import java.lang.reflect.InvocationTargetException;
25  import java.lang.reflect.Method;
26  import java.math.MathContext;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.util.ArrayList;
30  import java.util.Collections;
31  import java.util.LinkedHashMap;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.function.Predicate;
35  
36  import org.apache.commons.jexl3.introspection.JexlPermissions;
37  import org.apache.commons.jexl3.introspection.JexlUberspect;
38  
39  /**
40   * Loads a YAML configuration file and applies it to a {@link JexlBuilder}.
41   *
42   * <p>Quick start:</p>
43   * <pre>
44   * try (InputStream in = getClass().getResourceAsStream("/jexl.yaml")) {
45   *     JexlEngine engine = JexlConfigLoader.load(in).create();
46   * }
47   * </pre>
48   *
49   * <p>The loader understands a simple YAML subset: top-level scalars, one level of section nesting,
50   * and list items.  No external YAML library is required.  Inline comments ({@code #} after
51   * whitespace), empty lines, and single/double-quoted string values are supported.</p>
52   *
53   * <h2>Top-level scalar keys</h2>
54   * <p>These map directly to the matching {@link JexlBuilder} setter.  Boolean values accept
55   * {@code true/false}, {@code yes/no}, or {@code on/off}.  Example:</p>
56   * <pre>
57   * strict: true         # JexlBuilder.strict(true)
58   * silent: false        # JexlBuilder.silent(false)
59   * safe: true           # JexlBuilder.safe(true)   — enable safe-navigation ?.
60   * cancellable: false   # JexlBuilder.cancellable(false)
61   * antish: true         # JexlBuilder.antish(true) — resolve ant-style dotted names
62   * lexical: true        # JexlBuilder.lexical(true)
63   * lexicalShade: false  # JexlBuilder.lexicalShade(false)
64   * strictInterpolation: false
65   * booleanLogical: false
66   * debug: true          # include source location in exceptions
67   * cache: 512           # expression cache size (entries)
68   * cacheThreshold: 64   # max expression length cached
69   * stackOverflow: 512   # max recursion depth
70   * collectMode: 1       # variable collection mode (0=off, 1=on)
71   * charset: UTF-8       # source charset for script text
72   * strategy: JEXL_STRATEGY   # property-resolver order: JEXL_STRATEGY or MAP_STRATEGY
73   * </pre>
74   *
75   * <h2>{@code permissions:} section</h2>
76   * <p>Controls which classes and members are visible to scripts.</p>
77   * <pre>
78   * permissions:
79   *   base: SECURE           # NONE (default) | SECURE | RESTRICTED | UNRESTRICTED
80   *   rules:                 # list of JexlPermissions DSL strings composed on top of base
81   *     - "com.example.api +{}"           # allow whole package
82   *     - "com.example.api +{ Foo{} }"    # allow specific class
83   *     - "java.util +{ -Formatter { Formatter(); } }"  # deny one constructor
84   *   classes:               # explicit fully-qualified class names to allow (ClassPermissions)
85   *     - com.example.api.Foo
86   *     - com.example.api.Bar
87   *   logging: my.logger     # optional: wrap in LoggingPermissions; value = logger name
88   *                          # bare "logging:" with no value uses the default logger
89   * </pre>
90   * <p>See {@link JexlPermissions#parse(String...)} for the DSL syntax,
91   * {@link JexlPermissions#SECURE}, {@link JexlPermissions#RESTRICTED},
92   * {@link JexlPermissions#logging()} for the logging wrapper.</p>
93   *
94   * <h2>{@code features:} section</h2>
95   * <p>Controls which syntactic constructs are available at parse time.  Each key is the name of
96   * a {@link JexlFeatures} boolean setter method; unknown keys are silently ignored.</p>
97   * <pre>
98   * features:
99   *   # Script structure
100  *   script: true             # multi-statement scripts (vs. single-expression mode)
101  *   localVar: true           # local variable declarations (var x = ...)
102  *   lambda: true             # lambda / named-function definitions
103  *   loops: true              # for / while / do-while loops
104  *   newInstance: true        # new(...) constructor calls
105  *
106  *   # Side-effects
107  *   sideEffect: true         # any assignment or modification (=, +=, ...)
108  *   sideEffectGlobal: true   # assignment to context / global variables
109  *
110  *   # Literals and operators
111  *   structuredLiteral: true  # array [], map {}, set {} literals; ranges a..b
112  *   arrayReferenceExpr: true # non-constant array index expressions (x[f()])
113  *   methodCall: true         # method calls on objects (obj.method())
114  *   annotation: true         # @annotation statements
115  *   pragma: true             # #pragma directives
116  *   pragmaAnywhere: true     # allow #pragma anywhere (not just at the top)
117  *   namespacePragma: true    # #pragma jexl.namespace.ns ... syntax
118  *   namespaceIdentifier: true# ns:fun(...) compact namespace call syntax
119  *   importPragma: true       # #pragma jexl.import ... syntax
120  *
121  *   # Lambda arrow styles
122  *   thinArrow: true          # thin-arrow lambdas  x -&gt; x + 1
123  *   fatArrow: true           # fat-arrow lambdas   x =&gt; x + 1
124  *
125  *   # Variable capture semantics
126  *   constCapture: true       # captured variables are read-only (Java-style)
127  *   referenceCapture: false  # captured variables are pass-by-reference (ECMAScript-style)
128  *
129  *   # Lexical scoping (also settable at top level via 'lexical:' / 'lexicalShade:')
130  *   lexical: true
131  *   lexicalShade: false
132  *
133  *   # Misc
134  *   comparatorNames: true    # allow 'gt', 'lt', 'ge', 'le', 'eq', 'ne' as operator aliases
135  *   ambiguousStatement: true # allow statements that are syntactically ambiguous
136  *   ignoreTemplatePrefix: false
137  *
138  *   # Reserved names (list — cannot be used as local variable or parameter names)
139  *   reservedNames:
140  *     - try
141  *     - catch
142  *     - class
143  * </pre>
144  *
145  * <h2>{@code arithmetic:} section</h2>
146  * <p>Selects and configures the {@link JexlArithmetic} implementation.</p>
147  * <pre>
148  * arithmetic:
149  *   clazz: org.apache.commons.jexl3.JexlArithmetic  # fully-qualified class (default)
150  *   strict: true       # strict arithmetic (true = default); passed to the constructor
151  *   mathContext: DECIMAL64   # java.math.MathContext field: DECIMAL32 | DECIMAL64 | DECIMAL128 | UNLIMITED
152  *   mathScale: 10      # BigDecimal scale; requires mathContext; -1 = use context default
153  * </pre>
154  * <p>When {@code mathContext} is present the constructor {@code (boolean, MathContext, int)} is used;
155  * otherwise {@code (boolean)} is used.  The class must be on the classpath.</p>
156  *
157  * <h2>{@code namespaces:} section</h2>
158  * <p>Maps namespace prefixes to fully-qualified class names.  The class is loaded and passed to
159  * {@link JexlBuilder#namespaces(java.util.Map)}.  Its static (or instance) methods become
160  * callable as {@code prefix:methodName(args)} from scripts.</p>
161  * <pre>
162  * namespaces:
163  *   math: java.lang.Math          # math:abs(-1) etc.
164  *   str:  com.example.StringUtils # str:trim(x) etc.
165  * </pre>
166  *
167  * <h2>{@code imports:} section</h2>
168  * <p>A list of package or class names passed to {@link JexlBuilder#imports(java.util.Collection)}.
169  * Imported packages allow unqualified class names in {@code new(...)} and type references.</p>
170  * <pre>
171  * imports:
172  *   - java.lang
173  *   - java.util
174  *   - com.example.api
175  * </pre>
176  *
177  * <h2>{@code pragmas:} section</h2>
178  * <p>A list of allowed pragma key patterns, turned into the predicate passed to
179  * {@link JexlFeatures#pragmaTest(java.util.function.Predicate)}.  A pattern ending with {@code *}
180  * matches any key starting with what precedes it; any other pattern must match the key exactly.
181  * Scripts declaring a pragma whose key matches no pattern fail to parse with a
182  * {@link JexlException.Feature}.  The section is an allow-list: when present with no items, no pragma is
183  * allowed; when absent, all pragmas are allowed (subject to the {@code pragma}, {@code importPragma} and
184  * {@code namespacePragma} features).  The list applies to JEXL's own pragmas too: {@code jexl.options},
185  * {@code jexl.import}, {@code jexl.namespace.*} and {@code jexl.module.*} must be listed to be allowed.
186  * A single pattern may also be written inline
187  * ({@code pragmas: dlm.*}); entries that are not {@code - pattern} items, or flow-style lists, are
188  * rejected with an {@link IllegalArgumentException}.</p>
189  * <pre>
190  * pragmas:
191  *   - dlm.*              # any pragma key starting with 'dlm.'
192  *   - jexl.import        # exactly 'jexl.import'
193  *   - jexl.namespace.*
194  * </pre>
195  *
196  * <h2>Complete annotated example</h2>
197  * <p>Every flag is listed explicitly so the configuration does not depend on any library default
198  * (which may change between releases).  The {@code features:} block below reproduces the pre-3.7
199  * feature set ({@link JexlFeatures#createDefault()}).</p>
200  * <pre>
201  * # Production engine — explicit permissions + legacy feature set
202  * strict: true
203  * safe: false
204  * cache: 512
205  *
206  * permissions:
207  *   base: RESTRICTED
208  *   rules:
209  *     - "com.example.api +{}"
210  *   logging: com.example.jexl.permissions  # log allow/deny at INFO once per element
211  *
212  * features:
213  *   script: true
214  *   localVar: true
215  *   lambda: true
216  *   loops: true
217  *   newInstance: true
218  *   sideEffect: true
219  *   sideEffectGlobal: true
220  *   structuredLiteral: true
221  *   arrayReferenceExpr: true
222  *   methodCall: true
223  *   annotation: true
224  *   pragma: true
225  *   pragmaAnywhere: true
226  *   namespacePragma: true
227  *   importPragma: true
228  *   namespaceIdentifier: false
229  *   thinArrow: true
230  *   fatArrow: false
231  *   constCapture: false
232  *   referenceCapture: false
233  *   lexical: false
234  *   lexicalShade: false
235  *   comparatorNames: true
236  *   ambiguousStatement: false
237  *   ignoreTemplatePrefix: false
238  *
239  * namespaces:
240  *   math: java.lang.Math
241  *
242  * imports:
243  *   - java.lang
244  *   - java.util
245  * </pre>
246  *
247  * @since 3.7.0
248  */
249 public final class JexlConfigLoader {
250 
251     private JexlConfigLoader() { }
252 
253     /**
254      * Loads configuration from a YAML {@link InputStream} (UTF-8) into a {@link JexlBuilder}.
255      *
256      * @param in YAML input; the caller is responsible for closing it
257      * @return A configured JexlBuilder
258      * @throws IOException Thrown if the stream cannot be read
259      */
260     public static JexlBuilder load(final InputStream in) throws IOException {
261         return load(new InputStreamReader(in, StandardCharsets.UTF_8));
262     }
263 
264     /**
265      * Loads configuration from a YAML {@link Reader} into a {@link JexlBuilder}.
266      *
267      * @param reader YAML input; the caller is responsible for closing it
268      * @return A configured JexlBuilder
269      * @throws IOException Thrown if the reader cannot be read
270      */
271     public static JexlBuilder load(final Reader reader) throws IOException {
272         final JexlBuilder builder = new JexlBuilder();
273         final JexlFeatures features = new JexlFeatures();
274         parse(reader instanceof BufferedReader ? (BufferedReader) reader
275             : new BufferedReader(reader), builder, features);
276         return builder.features(features);
277     }
278 
279     /**
280      * Convenience: loads YAML from {@code in} and creates the engine in one call.
281      *
282      * @param in YAML input; the caller is responsible for closing it
283      * @return A new JexlEngine configured from the YAML
284      * @throws IOException Thrown if the stream cannot be read
285      */
286     public static JexlEngine engine(final InputStream in) throws IOException {
287         return load(in).create();
288     }
289 
290     // =========================================================================
291     //  Parser
292     // =========================================================================
293 
294     /**
295      * Minimal line-by-line YAML parser that handles the subset needed for JEXL configuration:
296      * top-level key:value scalars, one-level section blocks, and list items within sections.
297      */
298     private static void parse(final BufferedReader reader,
299                               final JexlBuilder builder,
300                               final JexlFeatures features) throws IOException {
301         String section = null;
302         Map<String, Object> sectionData = null;
303         List<String> currentList = null;
304         String currentListKey = null;
305 
306         for (String raw; (raw = reader.readLine()) != null; ) {
307             final int indent = leadingSpaces(raw);
308             final String stripped = ltrim(raw);
309             if (stripped.isEmpty() || stripped.charAt(0) == '#') {
310                 continue;
311             }
312             final String line = stripComment(stripped);
313             if (line.isEmpty()) {
314                 continue;
315             }
316 
317             if (indent == 0) {
318                 // back to top level — flush any open section
319                 if (section != null) {
320                     if (currentList != null && currentListKey != null) {
321                         sectionData.put(currentListKey, currentList);
322                     }
323                     flushSection(section, sectionData, builder, features);
324                     section = null;
325                     sectionData = null;
326                     currentList = null;
327                     currentListKey = null;
328                 }
329 
330                 final int colon = line.indexOf(':');
331                 final String key = colon < 0 ? line : rtrim(line.substring(0, colon));
332                 final String val = colon < 0 ? "" : ltrim(line.substring(colon + 1));
333 
334                 if (val.isEmpty()) {
335                     section = key;
336                     sectionData = new LinkedHashMap<>();
337                 } else if ("pragmas".equals(key)) {
338                     // a single pattern on the same line
339                     features.pragmaTest(parsePragmaPatterns(Collections.singletonList(unquote(val))));
340                 } else {
341                     applyTopLevel(key, val, builder);
342                 }
343 
344             } else {
345                 // inside a section
346                 if (section == null) {
347                     continue;
348                 }
349 
350                 if (line.charAt(0) == '-') {
351                     // list item
352                     final String item = unquote(ltrim(line.substring(1)));
353                     if (currentList == null) {
354                         currentList = new ArrayList<>();
355                         currentListKey = section;
356                     }
357                     currentList.add(item);
358                 } else {
359                     final int colon = line.indexOf(':');
360                     final String key = colon < 0 ? line : rtrim(line.substring(0, colon));
361                     final String val = colon < 0 ? "" : ltrim(line.substring(colon + 1));
362 
363                     if (val.isEmpty()) {
364                         // nested list-key start (e.g. "rules:")
365                         if (currentList != null && currentListKey != null) {
366                             sectionData.put(currentListKey, currentList);
367                         }
368                         currentListKey = key;
369                         currentList = new ArrayList<>();
370                     } else {
371                         // scalar within section — close any pending list first
372                         if (currentList != null && currentListKey != null) {
373                             sectionData.put(currentListKey, currentList);
374                             currentList = null;
375                             currentListKey = null;
376                         }
377                         sectionData.put(key, val);
378                     }
379                 }
380             }
381         }
382 
383         // flush the last section
384         if (section != null) {
385             if (currentList != null && currentListKey != null) {
386                 sectionData.put(currentListKey, currentList);
387             }
388             flushSection(section, sectionData, builder, features);
389         }
390     }
391 
392     // =========================================================================
393     //  Section dispatchers
394     // =========================================================================
395 
396     @SuppressWarnings("unchecked")
397     private static void flushSection(final String section,
398                                      final Map<String, Object> data,
399                                      final JexlBuilder builder,
400                                      final JexlFeatures features) {
401         switch (section) {
402             case "permissions":
403                 applyPermissions(data, builder);
404                 break;
405             case "namespaces":
406                 applyNamespaces(data, builder);
407                 break;
408             case "arithmetic":
409                 applyArithmetic(data, builder);
410                 break;
411             case "features":
412                 applyFeatures(data, features);
413                 break;
414             case "imports": {
415                 final Object list = data.get(section);
416                 if (list instanceof List) {
417                     builder.imports((List<String>) list);
418                 }
419                 break;
420             }
421             case "pragmas": {
422                 // only list items are expected; anything else is a configuration mistake
423                 for (final String key : data.keySet()) {
424                     if (!section.equals(key)) {
425                         throw new IllegalArgumentException(
426                             "pragmas: expects a list of '- pattern' items, found '" + key + "'");
427                     }
428                 }
429                 // an allow-list: a section with no items allows no pragma
430                 final Object list = data.get(section);
431                 features.pragmaTest(parsePragmaPatterns(list instanceof List
432                     ? (List<String>) list
433                     : Collections.emptyList()));
434                 break;
435             }
436             default:
437                 break;
438         }
439     }
440 
441     private static void applyTopLevel(final String key,
442                                       final String value,
443                                       final JexlBuilder builder) {
444         switch (key) {
445             case "strict":              builder.strict(parseBool(value)); break;
446             case "silent":              builder.silent(parseBool(value)); break;
447             case "safe":                builder.safe(parseBool(value)); break;
448             case "cancellable":         builder.cancellable(parseBool(value)); break;
449             case "antish":              builder.antish(parseBool(value)); break;
450             case "lexical":             builder.lexical(parseBool(value)); break;
451             case "lexicalShade":        builder.lexicalShade(parseBool(value)); break;
452             case "strictInterpolation": builder.strictInterpolation(parseBool(value)); break;
453             case "booleanLogical":      builder.booleanLogical(parseBool(value)); break;
454             case "debug":               builder.debug(parseBool(value)); break;
455             case "cache":               builder.cache(parseInt(value)); break;
456             case "cacheThreshold":      builder.cacheThreshold(parseInt(value)); break;
457             case "stackOverflow":       builder.stackOverflow(parseInt(value)); break;
458             case "collectMode":         builder.collectMode(parseInt(value)); break;
459             case "charset":             builder.charset(Charset.forName(value)); break;
460             case "strategy":            builder.strategy(parseStrategy(value)); break;
461             default:
462                 break;
463         }
464     }
465 
466     @SuppressWarnings("unchecked")
467     private static void applyPermissions(final Map<String, Object> data,
468                                          final JexlBuilder builder) {
469         final Object baseVal = data.get("base");
470         // absent or "NONE" → deny-everything base (build from scratch with rules/classes)
471         JexlPermissions perms = "UNRESTRICTED".equals(baseVal) ? JexlPermissions.UNRESTRICTED
472             : "SECURE".equals(baseVal) ? JexlPermissions.SECURE
473             : "RESTRICTED".equals(baseVal) ? JexlPermissions.RESTRICTED
474             : JexlPermissions.NONE;
475         final Object rules = data.get("rules");
476         if (rules instanceof List && !((List<?>) rules).isEmpty()) {
477             final List<String> ruleList = (List<String>) rules;
478             perms = perms.compose(ruleList.toArray(new String[0]));
479         }
480         final Object classes = data.get("classes");
481         if (classes instanceof List && !((List<?>) classes).isEmpty()) {
482             perms = new JexlPermissions.ClassPermissions(perms, (List<String>) classes);
483         }
484         // optional logging wrapper, outermost so it reports the effective decisions;
485         // a String value names the logger, a bare "logging:" uses the default logger
486         final Object logging = data.get("logging");
487         if (logging != null) {
488             final String name = logging instanceof String ? (String) logging : "";
489             perms = name.isEmpty() ? perms.logging() : perms.logging(name);
490         }
491         builder.permissions(perms);
492     }
493 
494     private static void applyNamespaces(final Map<String, Object> data,
495                                         final JexlBuilder builder) {
496         final Map<String, Object> ns = new LinkedHashMap<>(data.size());
497         for (final Map.Entry<String, Object> e : data.entrySet()) {
498             if (e.getValue() instanceof String) {
499                 try {
500                     ns.put(e.getKey(), Class.forName((String) e.getValue()));
501                 } catch (final ClassNotFoundException ex) {
502                     throw new IllegalArgumentException(
503                         "namespace class not found: " + e.getValue(), ex);
504                 }
505             }
506         }
507         if (!ns.isEmpty()) {
508             builder.namespaces(ns);
509         }
510     }
511 
512     private static void applyArithmetic(final Map<String, Object> data,
513                                         final JexlBuilder builder) {
514         final String className = data.containsKey("clazz")
515             ? (String) data.get("clazz")
516             : JexlArithmetic.class.getName();
517         final boolean astrict = !data.containsKey("strict") || parseBool((String) data.get("strict"));
518         final Class<?> clazz;
519         try {
520             clazz = Class.forName(className);
521         } catch (final ClassNotFoundException e) {
522             throw new IllegalArgumentException("arithmetic class not found: " + className, e);
523         }
524         try {
525             if (data.containsKey("mathContext")) {
526                 final MathContext mc = (MathContext)
527                     MathContext.class.getField((String) data.get("mathContext")).get(null);
528                 final int scale = data.containsKey("mathScale")
529                     ? parseInt((String) data.get("mathScale"))
530                     : -1;
531                 builder.arithmetic((JexlArithmetic) clazz
532                     .getConstructor(boolean.class, MathContext.class, int.class)
533                     .newInstance(astrict, mc, scale));
534             } else {
535                 builder.arithmetic((JexlArithmetic) clazz
536                     .getConstructor(boolean.class)
537                     .newInstance(astrict));
538             }
539         } catch (final ReflectiveOperationException e) {
540             throw new IllegalArgumentException(
541                 "cannot instantiate arithmetic class " + className, e);
542         }
543     }
544 
545     @SuppressWarnings("unchecked")
546     private static void applyFeatures(final Map<String, Object> data,
547                                       final JexlFeatures features) {
548         for (final Map.Entry<String, Object> e : data.entrySet()) {
549             final String key = e.getKey();
550             final Object val = e.getValue();
551             if ("reservedNames".equals(key) && val instanceof List) {
552                 features.reservedNames((List<String>) val);
553             } else if (val instanceof String) {
554                 try {
555                     final Method m = JexlFeatures.class.getMethod(key, boolean.class);
556                     m.invoke(features, parseBool((String) val));
557                 } catch (final NoSuchMethodException ignored) {
558                     // unknown feature key — skip
559                 } catch (final IllegalAccessException | InvocationTargetException ex) {
560                     throw new IllegalArgumentException("cannot set feature " + key, ex);
561                 }
562             }
563         }
564     }
565 
566     // =========================================================================
567     //  Parsing helpers
568     // =========================================================================
569 
570     private static JexlUberspect.ResolverStrategy parseStrategy(final String name) {
571         return "MAP_STRATEGY".equals(name)
572             ? JexlUberspect.MAP_STRATEGY
573             : JexlUberspect.JEXL_STRATEGY;
574     }
575 
576     /**
577      * Creates a pragma key predicate from a list of patterns.
578      *
579      * @see JexlFeatures#pragmaPatterns(java.util.Collection)
580      */
581     private static Predicate<String> parsePragmaPatterns(final List<String> patterns) {
582         for (final String pattern : patterns) {
583             final String trimmed = pattern.trim();
584             // flow-style lists ([a, b] or a, b) are not supported
585             if (trimmed.startsWith("[") || trimmed.indexOf(',') >= 0) {
586                 throw new IllegalArgumentException(
587                     "pragmas: expects one pattern per '- pattern' item, found '" + trimmed + "'");
588             }
589         }
590         return JexlFeatures.pragmaPatterns(patterns);
591     }
592 
593     private static boolean parseBool(final String s) {
594         return "true".equalsIgnoreCase(s) || "yes".equalsIgnoreCase(s) || "on".equalsIgnoreCase(s);
595     }
596 
597     private static int parseInt(final String s) {
598         return Integer.parseInt(s.trim());
599     }
600 
601     private static int leadingSpaces(final String s) {
602         int i = 0;
603         while (i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t')) {
604             i++;
605         }
606         return i;
607     }
608 
609     private static String ltrim(final String s) {
610         int i = 0;
611         while (i < s.length() && Character.isWhitespace(s.charAt(i))) {
612             i++;
613         }
614         return i == 0 ? s : s.substring(i);
615     }
616 
617     private static String rtrim(final String s) {
618         int i = s.length();
619         while (i > 0 && Character.isWhitespace(s.charAt(i - 1))) {
620             i--;
621         }
622         return i == s.length() ? s : s.substring(0, i);
623     }
624 
625     private static String stripComment(final String s) {
626         boolean inDouble = false;
627         boolean inSingle = false;
628         for (int i = 0; i < s.length(); i++) {
629             final char c = s.charAt(i);
630             if (c == '"' && !inSingle) {
631                 inDouble = !inDouble;
632             } else if (c == '\'' && !inDouble) {
633                 inSingle = !inSingle;
634             } else if (c == '#' && !inDouble && !inSingle
635                 && i > 0 && Character.isWhitespace(s.charAt(i - 1))) {
636                 return rtrim(s.substring(0, i));
637             }
638         }
639         return s;
640     }
641 
642     private static String unquote(final String s) {
643         if (s.length() >= 2) {
644             final char first = s.charAt(0);
645             final char last = s.charAt(s.length() - 1);
646             if ((first == '"' && last == '"') || (first == '\'' && last == '\'')) {
647                 return s.substring(1, s.length() - 1);
648             }
649         }
650         return s;
651     }
652 }