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 -> x + 1
123 * fatArrow: true # fat-arrow lambdas x => 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 }