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 * https://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.commons.jexl3; 018 019import java.io.BufferedReader; 020import java.io.IOException; 021import java.io.InputStream; 022import java.io.InputStreamReader; 023import java.io.Reader; 024import java.lang.reflect.InvocationTargetException; 025import java.lang.reflect.Method; 026import java.math.MathContext; 027import java.nio.charset.Charset; 028import java.nio.charset.StandardCharsets; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.LinkedHashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.function.Predicate; 035 036import org.apache.commons.jexl3.introspection.JexlPermissions; 037import org.apache.commons.jexl3.introspection.JexlUberspect; 038 039/** 040 * Loads a YAML configuration file and applies it to a {@link JexlBuilder}. 041 * 042 * <p>Quick start:</p> 043 * <pre> 044 * try (InputStream in = getClass().getResourceAsStream("/jexl.yaml")) { 045 * JexlEngine engine = JexlConfigLoader.load(in).create(); 046 * } 047 * </pre> 048 * 049 * <p>The loader understands a simple YAML subset: top-level scalars, one level of section nesting, 050 * and list items. No external YAML library is required. Inline comments ({@code #} after 051 * whitespace), empty lines, and single/double-quoted string values are supported.</p> 052 * 053 * <h2>Top-level scalar keys</h2> 054 * <p>These map directly to the matching {@link JexlBuilder} setter. Boolean values accept 055 * {@code true/false}, {@code yes/no}, or {@code on/off}. Example:</p> 056 * <pre> 057 * strict: true # JexlBuilder.strict(true) 058 * silent: false # JexlBuilder.silent(false) 059 * safe: true # JexlBuilder.safe(true) — enable safe-navigation ?. 060 * cancellable: false # JexlBuilder.cancellable(false) 061 * antish: true # JexlBuilder.antish(true) — resolve ant-style dotted names 062 * lexical: true # JexlBuilder.lexical(true) 063 * lexicalShade: false # JexlBuilder.lexicalShade(false) 064 * strictInterpolation: false 065 * booleanLogical: false 066 * debug: true # include source location in exceptions 067 * cache: 512 # expression cache size (entries) 068 * cacheThreshold: 64 # max expression length cached 069 * stackOverflow: 512 # max recursion depth 070 * collectMode: 1 # variable collection mode (0=off, 1=on) 071 * charset: UTF-8 # source charset for script text 072 * strategy: JEXL_STRATEGY # property-resolver order: JEXL_STRATEGY or MAP_STRATEGY 073 * </pre> 074 * 075 * <h2>{@code permissions:} section</h2> 076 * <p>Controls which classes and members are visible to scripts.</p> 077 * <pre> 078 * permissions: 079 * base: SECURE # NONE (default) | SECURE | RESTRICTED | UNRESTRICTED 080 * rules: # list of JexlPermissions DSL strings composed on top of base 081 * - "com.example.api +{}" # allow whole package 082 * - "com.example.api +{ Foo{} }" # allow specific class 083 * - "java.util +{ -Formatter { Formatter(); } }" # deny one constructor 084 * classes: # explicit fully-qualified class names to allow (ClassPermissions) 085 * - com.example.api.Foo 086 * - com.example.api.Bar 087 * logging: my.logger # optional: wrap in LoggingPermissions; value = logger name 088 * # bare "logging:" with no value uses the default logger 089 * </pre> 090 * <p>See {@link JexlPermissions#parse(String...)} for the DSL syntax, 091 * {@link JexlPermissions#SECURE}, {@link JexlPermissions#RESTRICTED}, 092 * {@link JexlPermissions#logging()} for the logging wrapper.</p> 093 * 094 * <h2>{@code features:} section</h2> 095 * <p>Controls which syntactic constructs are available at parse time. Each key is the name of 096 * a {@link JexlFeatures} boolean setter method; unknown keys are silently ignored.</p> 097 * <pre> 098 * features: 099 * # 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 */ 249public 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}