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.bson; 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.*; 031import org.apache.juneau.marshall.swap.spi.*; 032 033/** 034 * Session object that lives for the duration of a single use of {@link BsonParser}. 035 * 036 * <h5 class='section'>Notes:</h5><ul> 037 * <li class='warn'>This class is not thread safe and is typically discarded after one use. 038 * </ul> 039 * 040 * <h5 class='section'>See Also:</h5><ul> 041 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Bson">BSON</a> 042 * </ul> 043 */ 044@SuppressWarnings({ 045 "java:S125", // Explanatory comments contain BSON/JSON syntax (e.g. {"value":x}) that Sonar misreads as commented-out code 046 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 047 "java:S3776", // readDocument() branches on map/bean/Optional/swap targets and element types in one pass 048 "java:S6541", // readDocument() is the single BSON document-to-object dispatch and is long by design 049 "rawtypes", // readDocument()/readArray() build raw Map and Collection instances whose element types are only known at runtime from ClassMeta 050 "resource", // RecordReader returned by RecordAdapter is a Closeable owned by the caller; Eclipse JDT @Owning warning is by design. 051 "unchecked" // (T) results of readDocument()/doRead() are built from the ClassMeta eType, so the cast matches the requested type 052}) 053public class BsonParserSession extends InputStreamParserSession implements RecordReadable, ArrayRecordReadable { 054 055 private static final String BSON_VALUE_KEY = "value"; 056 057 /** 058 * Builder class. 059 */ 060 public static class Builder extends InputStreamParserSession.Builder<Builder> { 061 062 private final String nullKeyString; 063 private final int maxLength; 064 065 /** 066 * Constructor 067 * 068 * @param ctx The context creating this session. 069 * <br>Cannot be <jk>null</jk>. 070 */ 071 protected Builder(BsonParser ctx) { 072 super(reqnn("ctx", ctx)); 073 nullKeyString = ctx.getNullKeyString(); 074 maxLength = ctx.getMaxLength(); 075 } 076 077 @Override 078 public BsonParserSession build() { 079 return new BsonParserSession(this); 080 } 081 } 082 083 /** 084 * Creates a new builder for a {@link BsonParserSession}. 085 * 086 * @param ctx The BSON parser context creating this session. 087 * <br>Cannot be <jk>null</jk>. 088 * @return A new builder. 089 */ 090 public static Builder create(BsonParser ctx) { 091 return new Builder(reqnn("ctx", ctx)); 092 } 093 094 private final String nullKeyString; 095 private final int maxLength; 096 097 protected BsonParserSession(Builder builder) { 098 super(builder); 099 nullKeyString = builder.nullKeyString; 100 maxLength = builder.maxLength; 101 } 102 103 private String trimKey(String name) { 104 return isTrimStrings() && nn(name) ? name.trim() : name; 105 } 106 107 private Object readTypedValue(BsonInputStream is, int elementType, ClassMeta<?> targetType, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 108 Object o = switch (elementType) { 109 case 0x01 -> is.readDouble(); 110 case 0x02 -> trim(is.readString()); 111 case 0x03 -> readDocument(is, targetType, outer, pMeta); 112 case 0x04 -> readArray(is, targetType, outer, pMeta); 113 case 0x05 -> is.readBinary(); 114 case 0x07 -> is.readObjectId(); 115 case 0x08 -> is.readBoolean(); 116 case 0x09 -> is.readDateTime(); 117 case 0x0A -> null; 118 case 0x10 -> is.readInt32(); 119 case 0x12 -> is.readInt64(); 120 case 0x13 -> is.readDecimal128(); 121 default -> { 122 is.skipValue(elementType); 123 yield null; 124 } 125 }; 126 // A byte[]-targeted string element is a BinaryFormat-encoded payload (mirroring the serializer) 127 // rather than a literal string value - decode it back to bytes. 128 if (elementType == 0x02 && nn(targetType) && targetType.isByteArray()) { 129 var binaryFormat = getBinaryFormat(); 130 if (binaryFormat != BinaryFormat.NOT_SET) 131 o = binaryFormat.parse((String)o); 132 } 133 if (nn(parentBean()) && nn(o)) 134 setParent(targetType, o, parentBean()); 135 return o; 136 } 137 138 private <T> T readDocument(BsonInputStream is, ClassMeta<?> eType, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 139 is.readDocumentSize(); 140 if (eType == null) 141 eType = object(); 142 143 var swap = (ObjectSwap<T,Object>)eType.getSwap(this); 144 var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this); 145 ClassMeta<?> sType; 146 if (nn(builder)) 147 sType = builder.getBuilderClassMeta(this); 148 else if (nn(swap)) 149 sType = swap.getSwapClassMeta(this); 150 else 151 sType = eType; 152 153 // Do not recurse for Optional here - BSON wraps non-bean/map roots as {"value":x}. 154 // We must parse the document first, then unwrap via the map.size()==1 / "value" path below. 155 var wrapInOptional = eType.isOptional(); 156 if (sType.isOptional()) 157 sType = eType.getElementType(); 158 159 setCurrentClass(sType); 160 161 Object result; 162 // Skip map path for Optional - we need fallback to unwrap {"value":x} 163 if (!eType.isOptional() && sType.isMap()) { 164 var map = sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType); 165 // Coerce string keys to the declared key type when non-String (e.g. Map<TestEnum,String>) 166 // so map.get(EnumConstant) works. Matches the JSON-family readIntoMap2 pattern. 167 var keyType = sType.getKeyType(); 168 var coerceKeys = nn(keyType) && !keyType.isObject() && !keyType.isString(); 169 while (!is.isDocumentEnd()) { 170 var et = is.readElementType(); 171 var name = is.readElementName(); 172 var key = name.equals(nullKeyString) ? null : trimKey(name); 173 var value = readTypedValue(is, et, sType.getValueType(), map, pMeta); 174 setName(sType.getValueType(), value, key); 175 var resolvedKey = coerceKeys ? convertAttrToType(map, key, keyType) : key; 176 map.put(resolvedKey, value); 177 } 178 is.readDocumentTerminator(); 179 var raw = map instanceof MarshalledMap map2 ? cast(map2, pMeta, eType) : map; 180 // Convert JsonMap to target map type (TreeMap, LinkedHashMap, etc.) when needed 181 if (eType.isMap() && raw instanceof Map raw2 && !eType.inner().isInstance(raw)) 182 result = convertToMemberType(null, raw2, eType); 183 else 184 result = raw; 185 } else if (!eType.isOptional() && (nn(builder) || sType.canCreateNewBean(outer))) { 186 var beanMap = builder == null ? newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType)); 187 var pb = swapParentBean(beanMap.getBean(false)); 188 try { 189 while (!is.isDocumentEnd()) { 190 var et = is.readElementType(); 191 var name = is.readElementName(); 192 var key = trimKey(name); 193 var bpm = beanMap.getPropertyMeta(key); 194 Object value; 195 if (name.equals(getBeanTypePropertyName(eType))) { 196 value = readTypedValue(is, et, string(), null, null); 197 if (nn(value)) 198 beanMap = applyTypeProperty(beanMap, value.toString(), eType); 199 } else if (bpm != null) { 200 var bcm = (ClassMeta<?>) bpm.getBeanInfo(); 201 value = readTypedValue(is, et, bcm, beanMap.getBean(false), bpm); 202 setName(bcm, value, key); 203 try { 204 bpm.set(beanMap, key, value); 205 } catch (BeanRuntimeException e) { 206 onBeanSetterException(nn(pMeta) ? pMeta : bpm, e); 207 throw e; 208 } 209 } else { 210 onUnknownProperty(key, beanMap, readTypedValue(is, et, object(), null, null)); 211 } 212 } 213 } finally { 214 swapParentBean(pb); 215 } 216 is.readDocumentTerminator(); 217 result = builder == null ? checkRequired(beanMap).getBean() : builder.build(this, checkRequired(beanMap).getBean(), eType); 218 } else { 219 // Fallback: read document elements. Handles scalar, array, Optional roots (all wrapped as {"value":x}). 220 // Do NOT call readArray here - at root the next bytes are type+name of first element, not array doc. 221 var map = newGenericMap(); 222 while (!is.isDocumentEnd()) { 223 var et = is.readElementType(); 224 var name = is.readElementName(); 225 var key = name.equals(nullKeyString) ? null : trimKey(name); 226 map.put(key, readTypedValue(is, et, BSON_VALUE_KEY.equals(name) ? eType : object(), map, null)); 227 } 228 is.readDocumentTerminator(); 229 // Unwrap {"value":x} when target is Optional or when scalar-like (BSON root wrap convention) 230 if (map.size() == 1 && map.containsKey(BSON_VALUE_KEY)) { 231 var wrapped = map.get(BSON_VALUE_KEY); 232 if (eType.isOptional() || (!sType.isMap() && !sType.isBean() && !sType.isObject()) 233 || (wrapped == null && sType.isObject())) { 234 result = convertToMemberType(null, wrapped, eType); 235 if (eType.isOptional()) 236 wrapInOptional = false; // convertToMemberType already wrapped 237 } else { 238 var castResult = cast(map, pMeta, eType); 239 result = (eType.isMap() && castResult instanceof Map && !eType.inner().isInstance(castResult)) 240 ? convertToMemberType(null, castResult, eType) : castResult; 241 } 242 } else { 243 var castResult = cast(map, pMeta, eType); 244 result = (eType.isMap() && castResult instanceof Map && !eType.inner().isInstance(castResult)) 245 ? convertToMemberType(null, castResult, eType) : castResult; 246 } 247 } 248 // Final fallback: when target is Optional and we have map with "value", unwrap 249 if (eType.isOptional() && result instanceof Map result2 && result2.size() == 1 && result2.containsKey(BSON_VALUE_KEY)) { 250 result = convertToMemberType(null, result2.get(BSON_VALUE_KEY), eType); 251 wrapInOptional = false; // convertToMemberType already wrapped 252 } 253 // Convert JsonMap to target Map type (TreeMap, LinkedHashMap, etc.) when needed 254 if (eType.isMap() && result instanceof Map m2 && !eType.inner().isInstance(result)) 255 result = convertToMemberType(null, m2, eType); 256 // Unswap when result is in swapped form (e.g. JsonMap for bean with Map swap, String for Class). 257 // Skip when convertToMemberType already produced normal form (e.g. Class from String). 258 if (nn(swap) && nn(result) && swap.isSwappedObject(result)) 259 result = unswap(swap, result, eType); 260 return (T)(wrapInOptional ? o(result) : result); 261 } 262 263 private BeanMap<?> applyTypeProperty(BeanMap<?> beanMap, String typeName, ClassMeta<?> eType) throws ExecutableException { 264 var cm = getBeanRegistry().getClassMeta(typeName); 265 if (nn(cm) && nn(eType) && eType.isAssignableFrom(cm)) 266 return toBeanMap(newBean(cm.inner())); 267 return beanMap; 268 } 269 270 @SuppressWarnings({ 271 "java:S1172", // pMeta kept for API consistency with other readXxx methods 272 "unused" // pMeta kept for API consistency with other readXxx methods 273 }) 274 private Object readArray(BsonInputStream is, ClassMeta<?> eType, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException { 275 is.readDocumentSize(); 276 if (eType == null) 277 eType = object(); 278 279 var sType = eType; 280 // Do not recurse for Optional - parse array elements with inner type, wrap at end 281 if (sType.isOptional()) 282 sType = eType.getElementType(); 283 284 // Array types (String[], int[][], etc.) must use a Collection to gather elements; toArray() converts at the end. 285 // ClassMeta.newInstance() for array types returns an empty array, not a Collection - never use it for arrays. 286 // Only use sType.newInstance() for actual Collection types (List, Set, etc.). 287 Collection coll; 288 if (sType.isArray() || sType.isArgs() || sType.inner().isArray()) 289 coll = newGenericList(); 290 else { 291 var instance = sType.canCreateNewInstance(outer) ? sType.newInstance(outer) : null; 292 coll = (instance instanceof Collection instance2) ? instance2 : newGenericList(); 293 } 294 var elementType = coalesce(sType.getElementType(), object()); 295 296 while (!is.isDocumentEnd()) { 297 var et = is.readElementType(); 298 is.readElementName(); 299 coll.add(readTypedValue(is, et, elementType, coll, null)); 300 } 301 is.readDocumentTerminator(); 302 303 Object result = sType.isArray() || sType.isArgs() ? toArray(sType, coll) : coll; 304 return eType.isOptional() ? o(result) : result; 305 } 306 307 @Override 308 protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException { 309 try (var is = new BsonInputStream(pipe)) { 310 is.setMaxLength(maxLength); 311 return (T)readDocument(is, type, getOuter(), null); 312 } 313 } 314 315 /** 316 * Opens a whole-value pull-parser cursor over a BSON document, bound to this live session. 317 * {@link RecordReader#read(Class) read(...)} delegates to the polymorphic 318 * {@link ParserSession#read(Object, Class)} entry point. 319 * 320 * @param input The input. 321 * @return A new {@link RecordReader} cursor. 322 * @throws IOException If a problem occurred opening the underlying input. 323 */ 324 @Override /* RecordReadable */ 325 public RecordReader readRecords(Object input) throws IOException { 326 return RecordAdapter.reader(this, input); 327 } 328 329 /** 330 * Buffered array-element {@link RecordReader} for BSON, bound to this live session. Calls 331 * {@code parse(input, List.class, Object.class)} once and iterates the result. 332 * 333 * @param input The input. 334 * @return A buffered {@link RecordReader}. 335 * @throws IOException If a problem occurred reading the input. 336 */ 337 @Override /* ArrayRecordReadable */ 338 public RecordReader readArrayRecords(Object input) throws IOException { 339 return RecordAdapter.arrayReader(this, input); 340 } 341 342 /** 343 * The BSON record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 344 * 345 * @return Always <jk>false</jk>. 346 */ 347 @Override /* RecordReadable */ 348 public boolean isRecordStreaming() { return false; } 349 350 /** 351 * The BSON array-record cursor is buffered/{@link RecordAdapter}-backed, not O(1) streaming. 352 * 353 * @return Always <jk>false</jk>. 354 */ 355 @Override /* ArrayRecordReadable */ 356 public boolean isArrayRecordStreaming() { return false; } 357}