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.hjson; 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 Hjson format into POJOs. 034 */ 035@SuppressWarnings({ 036 "java:S110", // Inheritance depth acceptable for parser session hierarchy 037 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 038 "java:S3776", // Cognitive complexity acceptable for Hjson parse logic 039 "java:S6541", // Acceptable for session implementation 040 "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 041 "unchecked" // (T) casts in doRead for generic return type 042}) 043public class HjsonParserSession extends ReaderParserSession implements RecordReadable { 044 045 /** 046 * Builder for Hjson parser session. 047 */ 048 public static class Builder extends ReaderParserSession.Builder<Builder> { 049 050 protected Builder(HjsonParser ctx) { 051 super(reqnn("ctx", ctx)); 052 } 053 054 @Override 055 public HjsonParserSession build() { 056 return new HjsonParserSession(this); 057 } 058 } 059 060 /** 061 * Creates a session builder. 062 * 063 * @param ctx The parser context. 064 * <br>Cannot be <jk>null</jk>. 065 * @return The builder. 066 */ 067 public static Builder create(HjsonParser ctx) { 068 return new Builder(reqnn("ctx", ctx)); 069 } 070 071 protected HjsonParserSession(Builder builder) { 072 super(builder); 073 } 074 075 @Override /* RecordReadable */ 076 public RecordReader readRecords(Object input) throws IOException { 077 return RecordAdapter.reader(this, input); 078 } 079 080 @Override /* RecordReadable */ 081 public boolean isRecordStreaming() { 082 return false; 083 } 084 085 @Override 086 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 087 String inputStr; 088 try { 089 inputStr = pipe.asString(); 090 } catch (IOException e) { 091 throw new ParseException(e); 092 } 093 if (inputStr == null) 094 return null; 095 var trimmed = inputStr.trim(); 096 if (trimmed.isEmpty()) 097 return null; 098 var tokenizer = new HjsonTokenizer(new StringReader(inputStr)); 099 tokenizer.skipWhitespaceAndComments(); 100 var peek = tokenizer.peek(); 101 if (peek.type() == HjsonTokenizer.TokenType.EOF) 102 return null; 103 var peekType = peek.type(); 104 // LBRACE/LBRACKET -> readValue (object/array) 105 if (peekType == HjsonTokenizer.TokenType.LBRACE || peekType == HjsonTokenizer.TokenType.LBRACKET) { 106 return (T) readValue(tokenizer, type); 107 } 108 // QUOTELESS/STRING: distinguish braceless root "key: value" from lone value "string" 109 if (peekType == HjsonTokenizer.TokenType.QUOTELESS || peekType == HjsonTokenizer.TokenType.STRING) { 110 var firstToken = tokenizer.read(); 111 var firstVal = firstToken.stringValue(); 112 tokenizer.skipWhitespaceAndComments(); 113 var nextType = tokenizer.peek().type(); 114 // Target is String/number/boolean/etc - always lone value (not key:value map) 115 if (!type.isMap() && !type.isBean()) 116 return (T) coerceMemberValue(firstVal, type); 117 // Target is Map/Bean: COLON means key:value (braceless root) 118 if (nextType == HjsonTokenizer.TokenType.COLON) 119 return (T) readRootBraceless(tokenizer, type, firstVal); 120 // No COLON - lone value (e.g. "1.0" or "true" tokenized as QUOTELESS) 121 return (T) coerceMemberValue(firstVal, type); 122 } 123 // NUMBER, TRUE, FALSE, NULL, etc - lone value; use (T) not type.cast (cast fails for boxed->primitive) 124 return (T) coerceMemberValue(readValue(tokenizer, type), type); 125 } 126 127 private Object readRootBraceless(HjsonTokenizer t, ClassMeta<?> type, String firstKey) throws IOException, ParseException, ExecutableException { 128 var result = newGenericMap(); 129 // Same per-property type threading as readObject so braceless-root beans get typed-map keys. 130 var beanMeta = (type != null && type.isBean()) ? type.getBeanMeta() : null; 131 if (t.read().type() != HjsonTokenizer.TokenType.COLON) 132 throw new ParseException(this, "Expected : after key at line %s", t.getLine()); 133 t.skipWhitespaceAndComments(); 134 var value = readValue(t, propertyType(beanMeta, firstKey, type)); 135 result.put(firstKey, value); 136 t.skipWhitespaceAndComments(); 137 if (t.peek().type() == HjsonTokenizer.TokenType.NEWLINE) 138 t.read(); 139 while (t.peek().type() != HjsonTokenizer.TokenType.EOF) { 140 var key = readKey(t); 141 t.skipWhitespaceAndComments(); 142 if (t.read().type() != HjsonTokenizer.TokenType.COLON) 143 throw new ParseException(this, "Expected : after key at line %s", t.getLine()); 144 t.skipWhitespaceAndComments(); 145 value = readValue(t, propertyType(beanMeta, key, type)); 146 result.put(key, value); 147 t.skipWhitespaceAndComments(); 148 if (t.peek().type() == HjsonTokenizer.TokenType.NEWLINE) 149 t.read(); 150 } 151 return convertToBean(result, type); 152 } 153 154 private Object readValue(HjsonTokenizer t, ClassMeta<?> type) throws IOException, ParseException, ExecutableException { 155 var tok = t.read(); 156 Object raw = switch (tok.type()) { 157 case STRING, QUOTELESS, MULTILINE -> isTrimStrings() ? tok.stringValue().trim() : tok.stringValue(); 158 case NUMBER -> tok.numberValue(); 159 case TRUE -> true; 160 case FALSE -> false; 161 case NULL -> null; 162 case LBRACE -> readObject(t, type); 163 case LBRACKET -> readArray(t, type); 164 case EOF -> null; 165 default -> throw new ParseException(this, "Unexpected token %s at line %s", tok.type(), t.getLine()); 166 }; 167 if (raw == null) 168 return null; 169 if (tok.type() == HjsonTokenizer.TokenType.LBRACE || tok.type() == HjsonTokenizer.TokenType.LBRACKET) 170 return raw; 171 return coerceMemberValue(raw, type); 172 } 173 174 private Object readObject(HjsonTokenizer t, ClassMeta<?> type) throws IOException, ParseException, ExecutableException { 175 var result = newGenericMap(); 176 // When parsing a typed Bean, look up per-property ClassMetas so that nested Map<K,V> properties 177 // see their declared key/value types and the key coercion in the inner readObject can fire 178 // (matches JsonParserSession.readIntoBeanMap2 / readIntoMap2 pattern). 179 var beanMeta = (type != null && type.isBean()) ? type.getBeanMeta() : null; 180 t.skipWhitespaceAndComments(); 181 var next = t.peek(); 182 if (next.type() == HjsonTokenizer.TokenType.RBRACE) { 183 t.read(); 184 return convertToBean(result, type); 185 } 186 while (true) { 187 var key = readKey(t); 188 t.skipWhitespaceAndComments(); 189 if (t.read().type() != HjsonTokenizer.TokenType.COLON) 190 throw new ParseException(this, "Expected : after key at line %s", t.getLine()); 191 t.skipWhitespaceAndComments(); 192 var valueType = propertyType(beanMeta, key, type); 193 var value = readValue(t, valueType); 194 result.put(key, value); 195 t.skipWhitespaceAndComments(); 196 next = t.peek(); 197 if (next.type() == HjsonTokenizer.TokenType.RBRACE) { 198 t.read(); 199 break; 200 } 201 if (next.type() == HjsonTokenizer.TokenType.COMMA || next.type() == HjsonTokenizer.TokenType.NEWLINE) { 202 t.read(); 203 t.skipWhitespaceAndComments(); 204 } 205 } 206 return convertToBean(result, type); 207 } 208 209 /** 210 * Coerces a parsed scalar value to the expected type, honoring session-aware default swaps for 211 * {@code byte[]} targets at the collection-element / top-level dispatch sites. 212 * 213 * <p>For all other targets this is a transparent wrapper over {@link #convertToMemberType} so the 214 * existing dispatch behavior is preserved. The narrow {@code byte[]} hook lets the configured 215 * {@link BinaryFormat}'s variant {@link org.apache.juneau.marshall.swaps.BinarySwap} unswap the wire-form 216 * string back into raw bytes — these dispatch sites don't go through 217 * {@code MarshalledPropertyPostProcessor}'s per-property swap install, so without this they fall 218 * through to {@code BasicConverter}'s default {@code String → byte[]} UTF-8 coercion. 219 */ 220 private Object coerceMemberValue(Object raw, ClassMeta<?> type) throws ParseException { 221 if (raw instanceof String raw2 && type != null && type.inner() == byte[].class) { 222 var swap = type.getSwap(this); 223 if (swap != null) 224 return unswap(swap, raw2, type); 225 } 226 return convertToMemberType(null, raw, type); 227 } 228 229 /** 230 * Resolves the {@link ClassMeta} to parse a bean-property value against, falling back to {@code object()} 231 * when the enclosing type isn't a bean or the key isn't a known property. Threading the property type 232 * lets typed-map properties (e.g. {@code Map<TestEnum,String>}) coerce their keys via the converter's 233 * {@code Map → Map} path. 234 * 235 * <p> 236 * Also surfaces {@link ClassMeta#isCollectionOrArray()} property types so the 237 * collection-element dispatch in {@link #readArray} threads the parent's element type into recursion — 238 * that's where {@link #coerceMemberValue} reads the {@code byte[]} target and invokes the configured 239 * {@link org.apache.juneau.marshall.swaps.BinarySwap}'s unswap on the wire-form string. Other property shapes 240 * (Optional, primitive, bean) still pass through as {@code object()} so this parser's lazy-typed 241 * conversion path keeps working unchanged. 242 */ 243 private ClassMeta<?> propertyType(BeanMeta<?> beanMeta, String key, ClassMeta<?> enclosingType) { 244 if (beanMeta != null && key != null && !key.equals(getBeanTypePropertyName(enclosingType))) { 245 var pMeta = beanMeta.getPropertyMeta(key); 246 if (pMeta != null) { 247 var cm = (ClassMeta<?>) pMeta.getBeanInfo(); 248 if (cm != null && (cm.isMap() || cm.isCollectionOrArray())) 249 return cm; 250 } 251 } 252 return object(); 253 } 254 255 private String readKey(HjsonTokenizer t) throws IOException { 256 var tok = t.read(); 257 return switch (tok.type()) { 258 case STRING, QUOTELESS, MULTILINE -> isTrimStrings() ? tok.stringValue().trim() : tok.stringValue(); 259 case NULL -> null; // null keyword used as key -> preserve as null (matches JSON parser behavior) 260 case TRUE -> "true"; 261 case FALSE -> "false"; 262 case NUMBER -> tok.numberValue().toString(); 263 default -> throw new ParseException(this, "Expected key at line %s", t.getLine()); 264 }; 265 } 266 267 private Object readArray(HjsonTokenizer t, ClassMeta<?> type) throws IOException, ParseException, ExecutableException { 268 if (type == null) 269 type = object(); 270 var result = new JsonList(); 271 t.skipWhitespaceAndComments(); 272 var next = t.peek(); 273 if (next.type() == HjsonTokenizer.TokenType.RBRACKET) { 274 t.read(); 275 return convertToCollection(result, type); 276 } 277 while (true) { 278 var eType = coalesce(type.getElementType(), object()); 279 result.add(readValue(t, eType)); 280 t.skipWhitespaceAndComments(); 281 next = t.peek(); 282 if (next.type() == HjsonTokenizer.TokenType.RBRACKET) { 283 t.read(); 284 break; 285 } 286 if (next.type() == HjsonTokenizer.TokenType.COMMA || next.type() == HjsonTokenizer.TokenType.NEWLINE) { 287 t.read(); 288 t.skipWhitespaceAndComments(); 289 } 290 } 291 return convertToCollection(result, type); 292 } 293 294 private Object convertToBean(MarshalledMap map, ClassMeta<?> type) throws ExecutableException, ParseException { 295 if (type == null) 296 type = object(); 297 var casted = cast(map, null, type); 298 if (casted != map) 299 return casted; 300 if (!type.isObject()) { 301 // If target type has a swap (e.g. surrogate D1→D2), parse JsonMap as swap class first, then unswap. 302 var swap = type.getSwap(this); 303 if (nn(swap)) { 304 var sType = swap.getSwapClassMeta(this); 305 var swapped = convertToMemberType(null, map, sType); 306 return unswap(swap, swapped, type); 307 } 308 var result = convertToMemberType(null, map, type); 309 if (type.isBean() && result != null) 310 injectAnnotations(map, result); 311 return result; 312 } 313 return map; 314 } 315 316 private void injectAnnotations(MarshalledMap map, Object bean) throws ExecutableException { 317 var bm = toBeanMap(bean); 318 for (var entry : map.entrySet()) { 319 var key = entry.getKey(); 320 var pm = bm.getPropertyMeta(key); 321 var val = (pm == null) ? null : getBeanValueSafely(bm, key); 322 if (pm == null || val == null) 323 continue; 324 var cm = (ClassMeta<?>) pm.getBeanInfo(); 325 if (cm.getNameProperty() != null) 326 setName(cm, val, key); 327 injectParentAnnotations(cm, val, entry.getValue(), bean); 328 } 329 } 330 331 /* 332 * Recursively injects @ParentProperty (and @NameProperty on map values / nested beans) into val. 333 * Intermediate collections and maps are transparent: a collection/map element's parent is 334 * parentBean (the nearest enclosing bean), skipping all intermediate containers. This keeps HJSON 335 * consistent with the streaming parsers. 336 */ 337 private void injectParentAnnotations(ClassMeta<?> cm, Object val, Object node, Object parentBean) throws ExecutableException { 338 if (val == null || cm == null) 339 return; 340 if (cm.getParentProperty() != null) 341 setParent(cm, val, parentBean); 342 if (cm.isCollectionOrArray()) { 343 var et = cm.getElementType(); 344 if (et == null || et.isObject()) 345 return; 346 var nodeList = node instanceof List<?> nl ? nl : null; 347 var i = 0; 348 for (var element : toIterable(val)) { 349 injectParentAnnotations(et, element, nodeList != null && i < nodeList.size() ? nodeList.get(i) : null, parentBean); 350 i++; 351 } 352 } else if (cm.isMap() && val instanceof Map<?,?> valMap) { 353 var vt = cm.getValueType(); 354 if (vt == null || vt.isObject()) 355 return; 356 var nodeMap = node instanceof Map<?,?> nm ? nm : null; 357 for (Map.Entry<?,?> e : valMap.entrySet()) { 358 if (vt.getNameProperty() != null) 359 setName(vt, e.getValue(), e.getKey()); 360 injectParentAnnotations(vt, e.getValue(), nodeMap != null ? nodeMap.get(e.getKey()) : null, parentBean); 361 } 362 } else if (cm.isBean() && !(val instanceof Map) && node instanceof MarshalledMap nodeMap) { 363 injectAnnotations(nodeMap, val); 364 } 365 } 366 367 private static Iterable<?> toIterable(Object val) { 368 if (val instanceof Collection<?> c) 369 return c; 370 if (val instanceof Object[] a) 371 return Arrays.asList(a); 372 return List.of(); 373 } 374 375 private static Object getBeanValueSafely(BeanMap<?> bm, String key) { 376 try { 377 return bm.get(key); 378 } catch (@SuppressWarnings("unused") Exception e) { 379 return null; 380 } 381 } 382 383 private Object convertToCollection(MarshalledList list, ClassMeta<?> type) throws ExecutableException { 384 if (type == null) 385 type = object(); 386 var eType = coalesce(type.getElementType(), object()); 387 var converted = newGenericList(); 388 for (var item : list) 389 converted.add(convertToMemberType(null, item, eType)); 390 // For array types, convert to the appropriate array 391 if (type.inner() != null && type.inner().isArray()) 392 return toArray(type, converted); 393 // For specific collection subtypes (e.g. Vector, LinkedList), convert via MarshallingSession 394 if (!type.isObject()) 395 return convertToMemberType(null, converted, type); 396 return converted; 397 } 398}