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}