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.juneau.marshall.hocon; 018 019import static org.apache.juneau.commons.utils.ObjectUtils.*; 020import static org.apache.juneau.commons.utils.Shorts.*; 021 022import java.io.*; 023import java.util.*; 024 025import org.apache.juneau.commons.bean.*; 026import org.apache.juneau.commons.reflect.*; 027import org.apache.juneau.marshall.*; 028import org.apache.juneau.marshall.collections.*; 029import org.apache.juneau.marshall.parser.*; 030import org.apache.juneau.marshall.stream.*; 031 032/** 033 * Session for parsing HOCON format into POJOs. 034 */ 035@SuppressWarnings({ 036 "java:S110", // Inheritance depth acceptable for this class hierarchy. 037 "java:S135", // readArrayOrConcat uses two distinct breaks — one for standard HOCON terminators, one for unquoted keys after arrays/objects; merging them would obscure intent 038 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 039 "java:S3776", // Cognitive complexity acceptable for this specific logic. 040 "java:S6541", // Brain method acceptable for this parser state machine. 041 "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 042 "unchecked" // Type erasure on reflective/generic cast; element type is verified at call site. 043}) 044public class HoconParserSession extends ReaderParserSession implements RecordReadable { 045 046 private static final String[] EMPTY_PATH = new String[0]; 047 048 /** 049 * Builder for HOCON parser session. 050 */ 051 public static class Builder extends ReaderParserSession.Builder<Builder> { 052 053 private final HoconParser hoconParser; 054 055 protected Builder(HoconParser ctx) { 056 super(reqnn("ctx", ctx)); 057 this.hoconParser = ctx; 058 } 059 060 @Override 061 public HoconParserSession build() { 062 return new HoconParserSession(this); 063 } 064 } 065 066 /** 067 * Creates a session builder. 068 * 069 * @param ctx The parser context. 070 * <br>Cannot be <jk>null</jk>. 071 * @return The builder. 072 */ 073 public static Builder create(HoconParser ctx) { 074 return new Builder(reqnn("ctx", ctx)); 075 } 076 077 private final HoconParser hoconParser; 078 079 protected HoconParserSession(Builder builder) { 080 super(builder); 081 hoconParser = builder.hoconParser; 082 } 083 084 @Override /* RecordReadable */ 085 public RecordReader readRecords(Object input) throws IOException { 086 return RecordAdapter.reader(this, input); 087 } 088 089 @Override /* RecordReadable */ 090 public boolean isRecordStreaming() { 091 return false; 092 } 093 094 @Override 095 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 096 String inputStr; 097 try { 098 inputStr = pipe.asString(); 099 } catch (IOException e) { 100 throw new ParseException(e); 101 } 102 if (inputStr == null) 103 return null; 104 var trimmed = inputStr.trim(); 105 if (trimmed.isEmpty()) 106 return null; 107 var tokenizer = new HoconTokenizer(new StringReader(inputStr)); 108 var root = readRoot(tokenizer); 109 if (root == null) 110 return null; 111 if (hoconParser.resolveSubstitutions) { 112 var resolver = new HoconResolver(root); 113 resolver.resolve(); 114 } 115 var map = hoconToMap(root, type); 116 return (T) convertToBean(map, type); 117 } 118 119 private HoconValue.HoconObject readRoot(HoconTokenizer t) throws IOException, ParseException { 120 t.skipWhitespaceAndComments(); 121 var peek = t.peek(); 122 if (peek.type() == HoconTokenizer.TokenType.EOF) 123 return null; 124 if (peek.type() == HoconTokenizer.TokenType.LBRACE) { 125 t.read(); 126 return readObject(t, null, new String[0]); 127 } 128 return readRootBraceless(t); 129 } 130 131 private HoconValue.HoconObject readRootBraceless(HoconTokenizer t) throws IOException, ParseException { 132 var root = new HoconValue.HoconObject(); 133 while (t.peek().type() != HoconTokenizer.TokenType.EOF) { 134 var path = readPath(t); 135 if (path == null || path.length == 0) 136 break; 137 t.skipWhitespaceAndComments(); 138 var next = t.peek(); 139 if (next.type() == HoconTokenizer.TokenType.LBRACE) { 140 t.read(); 141 var nested = readObject(t, root, path); 142 var existing = root.getPath(path); 143 if (existing instanceof HoconValue.HoconObject existingObj) 144 existingObj.merge(nested); 145 else 146 root.setPath(path, nested); 147 } else if (next.type() == HoconTokenizer.TokenType.PLUS_EQUALS) { 148 t.read(); 149 t.skipWhitespaceAndComments(); 150 var value = readValueOrConcat(t); 151 var existing = root.getPath(path); 152 if (existing instanceof HoconValue.HoconArray arr) 153 arr.add(value); 154 else { 155 var newArr = new HoconValue.HoconArray(); 156 if (existing != null) 157 newArr.add(existing); 158 newArr.add(value); 159 root.setPath(path, newArr); 160 } 161 } else if (next.type() == HoconTokenizer.TokenType.EQUALS || next.type() == HoconTokenizer.TokenType.COLON) { 162 t.read(); 163 t.skipWhitespaceAndComments(); 164 var value = readValueOrConcat(t); 165 value = resolveSelfRefConcatIfNeeded(value, path, root); 166 root.setPath(path, value); 167 } else { 168 throw new ParseException(this, "Expected =, : or brace at line %s", t.getLine()); 169 } 170 t.skipWhitespaceAndComments(); 171 if (t.peek().type() == HoconTokenizer.TokenType.NEWLINE) 172 t.read(); 173 } 174 return root; 175 } 176 177 private String[] readPath(HoconTokenizer t) throws IOException, ParseException { 178 var tok = t.read(); 179 String first = switch (tok.type()) { 180 case EOF -> null; 181 case UNQUOTED_STRING -> isTrimStrings() ? tok.stringValue().trim() : tok.stringValue(); 182 case QUOTED_STRING, TRIPLE_QUOTED -> isTrimStrings() ? tok.stringValue().trim() : tok.stringValue(); 183 case NULL -> null; 184 case TRUE -> "true"; 185 case FALSE -> "false"; 186 case NUMBER -> tok.numberValue().toString(); 187 default -> throw new ParseException(this, "Expected key at line %s", t.getLine()); 188 }; 189 if (first == null) 190 return EMPTY_PATH; 191 var path = new ArrayList<String>(); 192 if (tok.type() == HoconTokenizer.TokenType.UNQUOTED_STRING && first.contains(".") && !first.startsWith(".")) 193 path.addAll(Arrays.asList(first.split("\\.", -1))); 194 else { 195 var comp = first.startsWith(".") ? first.substring(1) : first; 196 if (!comp.isEmpty()) 197 path.add(comp); 198 t.skipWhitespaceAndComments(); 199 while (t.peek().type() == HoconTokenizer.TokenType.UNQUOTED_STRING 200 || t.peek().type() == HoconTokenizer.TokenType.QUOTED_STRING 201 || t.peek().type() == HoconTokenizer.TokenType.TRIPLE_QUOTED) { 202 var next = t.read().stringValue(); 203 var part = next.startsWith(".") ? next.substring(1) : next; 204 if (!part.isEmpty()) 205 path.add(part); 206 t.skipWhitespaceAndComments(); 207 } 208 } 209 return path.toArray(String[]::new); 210 } 211 212 /** 213 * Parses a value, supporting HOCON concatenation: adjacent values on the same line 214 * are concatenated (strings) or merged (objects/arrays). 215 */ 216 private HoconValue readValueOrConcat(HoconTokenizer t) throws IOException, ParseException { 217 var values = new ArrayList<HoconValue>(); 218 values.add(readValue(t)); 219 while (true) { 220 t.skipWhitespaceAndCommentsExceptNewlines(); 221 var peek = t.peekNoSkip(); 222 if (peek.type() == HoconTokenizer.TokenType.NEWLINE 223 || peek.type() == HoconTokenizer.TokenType.COMMA 224 || peek.type() == HoconTokenizer.TokenType.RBRACE 225 || peek.type() == HoconTokenizer.TokenType.RBRACKET 226 || peek.type() == HoconTokenizer.TokenType.EQUALS 227 || peek.type() == HoconTokenizer.TokenType.PLUS_EQUALS 228 || peek.type() == HoconTokenizer.TokenType.EOF) 229 break; 230 // HOCON: newline ends concatenation; unquoted string after array/object is next key, not concat 231 var last = values.isEmpty() ? null : values.get(values.size() - 1); 232 if (peek.type() == HoconTokenizer.TokenType.UNQUOTED_STRING 233 && (last instanceof HoconValue.HoconArray || last instanceof HoconValue.HoconObject)) 234 break; 235 values.add(readValue(t)); 236 } 237 if (values.size() == 1) 238 return values.get(0); 239 return combineConcat(values); 240 } 241 242 private static HoconValue combineConcat(List<HoconValue> values) { 243 if (values.size() == 1) 244 return values.get(0); 245 var allArrays = values.stream().allMatch(v -> v instanceof HoconValue.HoconArray); 246 var allObjects = values.stream().allMatch(v -> v instanceof HoconValue.HoconObject); 247 if (allArrays) { 248 var first = (HoconValue.HoconArray) values.get(0); 249 for (var i = 1; i < values.size(); i++) 250 first.concat((HoconValue.HoconArray) values.get(i)); 251 return first; 252 } 253 if (allObjects) { 254 var first = (HoconValue.HoconObject) values.get(0); 255 for (var i = 1; i < values.size(); i++) 256 first.merge((HoconValue.HoconObject) values.get(i)); 257 return first; 258 } 259 var concat = new HoconValue.HoconConcat(); 260 for (var v : values) 261 concat.add(v); 262 return concat; 263 } 264 265 private static String[] concatenatePaths(String[] prefix, String[] path) { 266 var result = new String[prefix.length + path.length]; 267 System.arraycopy(prefix, 0, result, 0, prefix.length); 268 System.arraycopy(path, 0, result, prefix.length, path.length); 269 return result; 270 } 271 272 private static HoconValue resolveSelfRefConcatIfNeeded(HoconValue value, String[] fullPath, 273 HoconValue.HoconObject root) throws ParseException { 274 if (!(value instanceof HoconValue.HoconConcat concat)) 275 return value; 276 var pathStr = String.join(".", fullPath); 277 if (!concat.referencesPath(pathStr)) 278 return value; 279 var existing = root.getPath(fullPath); 280 if (existing == null) 281 return value; 282 var resolver = new HoconResolver(root); 283 java.util.function.Function<String,HoconValue> lookup = p -> p.equals(pathStr) ? existing : resolver.lookup(p); 284 return resolver.resolveConcatWithLookup(concat, lookup); 285 } 286 287 private HoconValue readValue(HoconTokenizer t) throws IOException, ParseException { 288 var tok = t.peek(); 289 return switch (tok.type()) { 290 case UNQUOTED_STRING, QUOTED_STRING, TRIPLE_QUOTED -> { 291 t.read(); 292 var s = isTrimStrings() ? tok.stringValue().trim() : tok.stringValue(); 293 yield new HoconValue.HoconString(s); 294 } 295 case NUMBER -> { 296 t.read(); 297 yield new HoconValue.HoconNumber(tok.numberValue()); 298 } 299 case TRUE -> { 300 t.read(); 301 yield new HoconValue.HoconBoolean(true); 302 } 303 case FALSE -> { 304 t.read(); 305 yield new HoconValue.HoconBoolean(false); 306 } 307 case NULL -> { 308 t.read(); 309 yield new HoconValue.HoconNull(); 310 } 311 case LBRACE -> { 312 t.read(); 313 yield readObject(t, null, new String[0]); 314 } 315 case LBRACKET -> { 316 t.read(); 317 yield readArray(t); 318 } 319 case SUBSTITUTION, OPT_SUBSTITUTION -> { 320 var st = t.read(); 321 yield new HoconValue.HoconSubstitution(st.stringValue(), tok.type() == HoconTokenizer.TokenType.OPT_SUBSTITUTION); 322 } 323 case EOF -> null; 324 default -> throw new ParseException(this, "Unexpected token %s at line %s", tok.type(), t.getLine()); 325 }; 326 } 327 328 private HoconValue.HoconObject readObject(HoconTokenizer t, HoconValue.HoconObject root, String[] pathPrefix) 329 throws IOException, ParseException { 330 var obj = new HoconValue.HoconObject(); 331 var effectiveRoot = or(root, obj); 332 t.skipWhitespaceAndComments(); 333 while (t.peek().type() != HoconTokenizer.TokenType.RBRACE && t.peek().type() != HoconTokenizer.TokenType.EOF) { 334 var path = readPath(t); 335 if (path == null || path.length == 0) 336 break; 337 t.skipWhitespaceAndComments(); 338 var next = t.peek(); 339 if (next.type() == HoconTokenizer.TokenType.LBRACE) { 340 t.read(); 341 var nested = readObject(t, effectiveRoot, concatenatePaths(pathPrefix, path)); 342 var existing = obj.getPath(path); 343 if (existing instanceof HoconValue.HoconObject existingObj) 344 existingObj.merge(nested); 345 else 346 obj.setPath(path, nested); 347 } else if (next.type() == HoconTokenizer.TokenType.PLUS_EQUALS) { 348 t.read(); 349 t.skipWhitespaceAndComments(); 350 var value = readValueOrConcat(t); 351 var existing = obj.getPath(path); 352 if (existing instanceof HoconValue.HoconArray arr) 353 arr.add(value); 354 else { 355 var newArr = new HoconValue.HoconArray(); 356 if (existing != null) 357 newArr.add(existing); 358 newArr.add(value); 359 obj.setPath(path, newArr); 360 } 361 } else if (next.type() == HoconTokenizer.TokenType.EQUALS || next.type() == HoconTokenizer.TokenType.COLON) { 362 t.read(); 363 t.skipWhitespaceAndComments(); 364 var value = readValueOrConcat(t); 365 var fullPath = concatenatePaths(pathPrefix, path); 366 value = resolveSelfRefConcatIfNeeded(value, fullPath, effectiveRoot); 367 obj.setPath(path, value); 368 } else { 369 throw new ParseException(this, "Expected =, : or brace at line %s", t.getLine()); 370 } 371 // skipWhitespaceAndComments is a no-op while a token is cached (see HoconTokenizer), so a 372 // closing `}` already peeked by readArrayOrConcat's concat loop is preserved here and the 373 // loop terminates naturally via the while-condition above rather than chewing through a 374 // newline that belongs to an enclosing scope. 375 t.skipWhitespaceAndComments(); 376 if (t.peek().type() == HoconTokenizer.TokenType.COMMA || t.peek().type() == HoconTokenizer.TokenType.NEWLINE) 377 t.read(); 378 t.skipWhitespaceAndComments(); 379 } 380 if (t.peek().type() == HoconTokenizer.TokenType.RBRACE) 381 t.read(); 382 return obj; 383 } 384 385 private HoconValue.HoconArray readArray(HoconTokenizer t) throws IOException, ParseException { 386 var arr = new HoconValue.HoconArray(); 387 t.skipWhitespaceAndComments(); 388 while (t.peek().type() != HoconTokenizer.TokenType.RBRACKET && t.peek().type() != HoconTokenizer.TokenType.EOF) { 389 // Elements inside [ ... ] are separated by COMMA or NEWLINE (handled below), so each 390 // readArrayOrConcat call returns a single element. HOCON array-concatenation 391 // (`[a,b] [c,d]` ≡ `[a,b,c,d]`) is performed inside readArrayOrConcat when adjacent 392 // arrays appear without a separator; the result is already a flattened HoconArray at 393 // that point. Here we must add the element as-is so nested arrays like 394 // `[[1,2,3], [4,5,6]]` (with separators) stay nested rather than flattening. 395 arr.add(readValueOrConcat(t)); 396 // readArrayOrConcat's internal concat loop calls peekNoSkip(), which eagerly consumes the 397 // closing-bracket char from the underlying reader and stashes it as peeked=RBRACKET. 398 // skipWhitespaceAndComments is a no-op while a token is cached (see HoconTokenizer), so it 399 // will NOT read past the cached `]` and eat a following newline — a newline that is either 400 // the in-array separator for the NEXT element of an enclosing array or a meaningful boundary 401 // for an outer scope. This prevents adjacent newline-separated inner arrays like 402 // `[[1,2]\n[3,4]]` from being silently re-merged via HOCON array-concatenation above us. 403 // The loop terminates on the cached `]` via the while-condition above. 404 t.skipWhitespaceAndComments(); 405 if (t.peek().type() == HoconTokenizer.TokenType.COMMA || t.peek().type() == HoconTokenizer.TokenType.NEWLINE) 406 t.read(); 407 t.skipWhitespaceAndComments(); 408 } 409 if (t.peek().type() == HoconTokenizer.TokenType.RBRACKET) 410 t.read(); 411 return arr; 412 } 413 414 private Object hoconToMap(HoconValue val, ClassMeta<?> type) throws ParseException { 415 if (val == null) 416 return null; 417 // STRING values targeting byte[] consult the configured BinaryFormat's variant BinarySwap 418 // before falling back to the default String → byte[] UTF-8 coercion that the bean binder 419 // would otherwise apply at the collection-element / top-level dispatch site. The 420 // bean-property path is unaffected because the per-property MPP install hides the type-level 421 // default swap behind the per-property swap on the BeanPropertyMeta. 422 // 423 // At BinaryFormat.NOT_SET BinarySwap.match returns 0 and no swap fires, but 424 // HoconSerializerSession emits byte[] at NOT_SET as a base64 string (to sidestep HOCON's 425 // array-concatenation flattening for nested int-array wire forms). Decode that here as the 426 // symmetric fallback so List<byte[]> and top-level byte[] round-trip at NOT_SET. 427 if (val.getType() == HoconValue.Type.STRING && type != null && type.inner() == byte[].class) { 428 var s = ((HoconValue.HoconString) val).getValue(); 429 var swap = type.getSwap(this); 430 if (swap != null) 431 return unswap(swap, s, type); 432 return Base64.getDecoder().decode(s); 433 } 434 // Look up bean property metas so nested typed-map properties (Map<K,V> with non-String K) can have 435 // their keys coerced via the converter's Map→Map path before the bean binds them. Collection/Array 436 // property types are also surfaced here so the ARRAY branch can thread the parent's element type 437 // into recursion and the byte[] swap dispatch above can fire at the right depth. Other property 438 // shapes (Optional, primitive, bean) still pass through as object() so the lazy-typed conversion 439 // path stays in place. 440 var beanMeta = (type != null && type.isBean()) ? type.getBeanMeta() : null; 441 var result = switch (val.getType()) { 442 case STRING -> ((HoconValue.HoconString) val).getValue(); 443 case NUMBER -> ((HoconValue.HoconNumber) val).getValue(); 444 case BOOLEAN -> ((HoconValue.HoconBoolean) val).getValue(); 445 case NULL -> null; 446 case OBJECT -> { 447 var obj = (HoconValue.HoconObject) val; 448 var map = newGenericMap(); 449 for (var e : obj.getMembers().entrySet()) { 450 var key = e.getKey(); 451 ClassMeta<?> childType = object(); 452 if (beanMeta != null && key != null && !key.equals(getBeanTypePropertyName(type))) { 453 var pMeta = beanMeta.getPropertyMeta(key); 454 if (pMeta != null) { 455 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 456 if (cm != null && (cm.isMap() || cm.isCollectionOrArray())) 457 childType = cm; 458 } 459 } 460 map.put(key, hoconToMap(e.getValue(), childType)); 461 } 462 yield map; 463 } 464 case ARRAY -> { 465 var arr = (HoconValue.HoconArray) val; 466 var list = newGenericList(); 467 var elType = (type != null && type.isCollectionOrArray()) ? coalesce(type.getElementType(), object()) : object(); 468 for (var el : arr.getElements()) { 469 list.add(hoconToMap(el, elType)); 470 } 471 yield list; 472 } 473 default -> null; 474 }; 475 // If the enclosing target is a typed Map (e.g. Map<TestEnum,String>), coerce keys / values 476 // via the converter's findMapConversion so the bean property binder gets a properly-keyed map. 477 if (result instanceof Map<?,?> result2 && type != null && type.isMap() 478 && type.getKeyType() != null && !type.getKeyType().isObject()) 479 return convertToMemberType(null, result2, type); 480 return result; 481 } 482 483 private Object convertToBean(Object map, ClassMeta<?> type) throws ExecutableException, ParseException { 484 if (map == null) 485 return null; 486 if (type == null) 487 type = object(); 488 var casted = cast((MarshalledMap) map, null, type); 489 if (casted != map) 490 return casted; 491 if (!type.isObject()) { 492 var swap = type.getSwap(this); 493 if (nn(swap)) { 494 var sType = swap.getSwapClassMeta(this); 495 var swapped = convertToMemberType(null, map, sType); 496 return unswap(swap, swapped, type); 497 } 498 var result = convertToMemberType(null, map, type); 499 if (type.isBean() && result != null) 500 injectAnnotations((MarshalledMap) map, result); 501 return result; 502 } 503 return map; 504 } 505 506 private void injectAnnotations(MarshalledMap map, Object bean) throws ExecutableException { 507 var bm = toBeanMap(bean); 508 for (var entry : map.entrySet()) { 509 var key = entry.getKey(); 510 var pm = bm.getPropertyMeta(key); 511 var val = pm == null ? null : getBeanValueSafely(bm, key); 512 if (pm == null || val == null) 513 continue; 514 var cm = (ClassMeta<?>) pm.getBeanInfo(); 515 if (cm.getNameProperty() != null) 516 setName(cm, val, key); 517 injectParentAnnotations(cm, val, entry.getValue(), bean); 518 } 519 } 520 521 /* 522 * Recursively injects @ParentProperty (and @NameProperty on map values / nested beans) into val. 523 * Intermediate collections and maps are transparent: a collection/map element's parent is 524 * parentBean (the nearest enclosing bean), skipping all intermediate containers. This keeps HOCON 525 * consistent with the streaming parsers. 526 */ 527 private void injectParentAnnotations(ClassMeta<?> cm, Object val, Object node, Object parentBean) throws ExecutableException { 528 if (val == null || cm == null) 529 return; 530 if (cm.getParentProperty() != null) 531 setParent(cm, val, parentBean); 532 if (cm.isCollectionOrArray()) { 533 var et = cm.getElementType(); 534 if (et == null || et.isObject()) 535 return; 536 var nodeList = node instanceof List<?> nl ? nl : null; 537 var i = 0; 538 for (var element : toIterable(val)) { 539 injectParentAnnotations(et, element, nodeList != null && i < nodeList.size() ? nodeList.get(i) : null, parentBean); 540 i++; 541 } 542 } else if (cm.isMap() && val instanceof Map<?,?> valMap) { 543 var vt = cm.getValueType(); 544 if (vt == null || vt.isObject()) 545 return; 546 var nodeMap = node instanceof Map<?,?> nm ? nm : null; 547 for (Map.Entry<?,?> e : valMap.entrySet()) { 548 if (vt.getNameProperty() != null) 549 setName(vt, e.getValue(), e.getKey()); 550 injectParentAnnotations(vt, e.getValue(), nodeMap != null ? nodeMap.get(e.getKey()) : null, parentBean); 551 } 552 } else if (cm.isBean() && !(val instanceof Map) && node instanceof MarshalledMap nodeMap) { 553 injectAnnotations(nodeMap, val); 554 } 555 } 556 557 private static Iterable<?> toIterable(Object val) { 558 if (val instanceof Collection<?> c) 559 return c; 560 if (val instanceof Object[] a) 561 return Arrays.asList(a); 562 return List.of(); 563 } 564 565 private static Object getBeanValueSafely(BeanMap<?> bm, String key) { 566 try { 567 return bm.get(key); 568 } catch (@SuppressWarnings("unused") Exception e) { 569 return null; 570 } 571 } 572}