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.msgpack;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.marshall.msgpack.DataType.*;
021
022import java.io.*;
023import java.math.*;
024import java.util.*;
025
026import org.apache.juneau.commons.bean.*;
027import org.apache.juneau.commons.reflect.*;
028import org.apache.juneau.marshall.*;
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 MsgPackParser}.
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/MessagePackSupport">MessagePack</a>
042
043 * </ul>
044 */
045@SuppressWarnings({
046   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
047   "rawtypes", // Raw types necessary for generic type handling
048   "resource", // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
049   "unchecked" // Type erasure requires unchecked casts
050})
051public class MsgPackParserSession extends InputStreamParserSession implements TokenReadable, ArrayRecordReadable {
052
053   /**
054    * Builder class.
055    */
056   public static class Builder extends InputStreamParserSession.Builder<Builder> {
057
058      final boolean nativeMode;
059      final int maxLength;
060
061      /**
062       * Constructor
063       *
064       * @param ctx The context creating this session.
065       *    <br>Cannot be <jk>null</jk>.
066       */
067      protected Builder(MsgPackParser ctx) {
068         super(reqnn("ctx", ctx));
069         this.nativeMode = ctx.isNativeMode();
070         this.maxLength = ctx.getMaxLength();
071      }
072
073      @Override
074      public MsgPackParserSession build() {
075         return new MsgPackParserSession(this);
076      }
077
078   }
079
080   /**
081    * Creates a new builder for this object.
082    *
083    * @param ctx The context creating this session.
084    *    <br>Cannot be <jk>null</jk>.
085    * @return A new builder.
086    */
087   public static Builder create(MsgPackParser ctx) {
088      return new Builder(reqnn("ctx", ctx));
089   }
090
091   private final boolean nativeMode;
092   private final int maxLength;
093
094   /**
095    * Constructor.
096    *
097    * @param builder The builder for this object.
098    */
099   protected MsgPackParserSession(Builder builder) {
100      super(builder);
101      this.nativeMode = builder.nativeMode;
102      this.maxLength = builder.maxLength;
103   }
104
105   /**
106    * Opens a low-level pull-parser cursor over a MessagePack document, bound to this live session.
107    *
108    * <h5 class='section'>Builder properties honored:</h5>
109    * <c>autoCloseStreams</c>, <c>unbuffered</c>.
110    *
111    * <h5 class='section'>Builder properties NOT honored:</h5>
112    * <c>listener</c>, <c>debugOutputLines</c>, <c>consumes</c>, swaps, {@code @Schema}.
113    *
114    * @param input The input.
115    * @return A new {@link MsgPackTokenReader}.
116    * @throws IOException If a problem occurred opening the underlying input.
117    */
118   @SuppressWarnings({
119      "java:S2095" // ParserPipe lifecycle is transferred to the returned MsgPackTokenReader, which closes it via its own close(); the caller owns the cursor via try-with-resources.
120   })
121   @Override /* TokenReadable */
122   public TokenReader readTokens(Object input) throws IOException {
123      var pipe = new ParserPipe(input, isDebug(), isAutoCloseStreams(), isUnbuffered(), null);
124      return new MsgPackTokenReader(pipe, this).setNativeMode(nativeMode).setMaxLength(maxLength);
125   }
126
127   /**
128    * Streaming array-element {@link RecordReader} backed by {@link MsgPackTokenReader}.
129    *
130    * <p>
131    * Memory is O(1) in the array length &mdash; the wrapper opens the outer MessagePack array via
132    * the token cursor and binds one element at a time, exactly mirroring the CBOR cursor.  The
133    * returned cursor reports {@link RecordReader#isStreaming()} == <jk>true</jk>.
134    *
135    * @param input The input.
136    * @return A new element-streamed {@link RecordReader}.
137    * @throws IOException If a problem occurred opening the underlying input.
138    */
139   @Override /* ArrayRecordReadable */
140   public RecordReader readArrayRecords(Object input) throws IOException {
141      try {
142         return StreamingArrayRecord.reader(readTokens(input));
143      } catch (ParseException e) {
144         throw new IOException(e);
145      }
146   }
147
148   /*
149    * Workhorse entry point.  Wraps readAnything0 with the shared ParserSession recursion-depth guard so that
150    * an adversarial deeply-nested document fails with a ParseException instead of a StackOverflowError.  The
151    * token-cursor path is iterative and unaffected.
152    */
153   <T> T readAnything(ClassMeta<?> eType, MsgPackInputStream is, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
154      enterParseDepth();
155      try {
156         return readAnything0(eType, is, outer, pMeta);
157      } finally {
158         exitParseDepth();
159      }
160   }
161
162   /*
163    * Workhorse method.
164    */
165   @SuppressWarnings({
166      "java:S3776", // Cognitive complexity acceptable for this specific logic
167      "java:S6541" // Single-threaded session contexts do not require synchronization
168   })
169   private <T> T readAnything0(ClassMeta<?> eType, MsgPackInputStream is, Object outer, BeanPropertyMeta pMeta) throws IOException, ParseException, ExecutableException {
170
171      if (eType == null)
172         eType = object();
173      var swap = (ObjectSwap<T,Object>)eType.getSwap(this);
174      var builder = (BuilderSwap<T,Object>)eType.getBuilderSwap(this);
175      ClassMeta<?> sType = null;
176      if (nn(builder))
177         sType = builder.getBuilderClassMeta(this);
178      else if (nn(swap))
179         sType = swap.getSwapClassMeta(this);
180      else
181         sType = eType;
182
183      if (sType.isOptional())
184         return (T)o(readAnything(eType.getElementType(), is, outer, pMeta));
185
186      setCurrentClass(sType);
187
188      Object o = null;
189      DataType dt = is.readDataType();
190      var rawLength = is.readLength();
191      // G8: array32/map32/str32/bin32/ext32 lengths are 32-bit unsigned (up to 2^32-1); a value in
192      // [2^31, 2^32-1] would truncate to a negative int.  Java containers/arrays cannot exceed int range.
193      if (rawLength > Integer.MAX_VALUE)
194         throw new ParseException(this, "MessagePack length %s exceeds the maximum supported size of %s.", rawLength, Integer.MAX_VALUE);
195      int length = (int)rawLength;
196
197      // Bound a definite array/map element count up front.  This is the databind path (elements
198      // accumulate into a collection below); the O(1)-memory streaming cursor (MsgPackTokenReader) is
199      // intentionally exempt so it can still emit tokens for large length-prefixed containers.  Byte/
200      // text-string LENGTH caps remain active on every path via readBinary.
201      if (dt == ARRAY || dt == MAP)
202         is.checkLength(length, dt == ARRAY ? "array" : "map");
203
204      if (dt != DataType.NULL) {
205         if (dt == BOOLEAN)
206            o = is.readBoolean();
207         else if (dt == INT)
208            o = is.readInt();
209         else if (dt == LONG) {
210            var l = is.readLong();
211            // G3: a uint64 above Long.MAX_VALUE comes back as a negative signed long.  Mirror the
212            // protobuf R5 cross-format decision: a long target keeps the raw bits, a BigInteger target
213            // carries the full unsigned magnitude (lossless widen).
214            if (is.isUint64() && sType.inner() == BigInteger.class)
215               o = new BigInteger(Long.toUnsignedString(l));
216            else
217               o = l;
218         } else if (dt == FLOAT)
219            o = is.readFloat();
220         else if (dt == DOUBLE)
221            o = is.readDouble();
222         else if (dt == STRING)
223            o = trim(is.readString());
224         else if (dt == BIN)
225            o = is.readBinary();
226         else if (dt == EXT)
227            // G1: always consume the ext payload (mirrors the token cursor) and surface it as byte[].
228            // Without this branch the payload bytes are left in the stream, desyncing the next element.
229            o = is.readBinary();
230         else if (dt == ARRAY && sType.isObject()) {
231            var jl = newGenericList();
232            for (var i = 0; i < length; i++)
233               jl.add(readAnything(object(), is, outer, pMeta));
234            o = jl;
235         } else if (dt == MAP && sType.isObject()) {
236            var jm = newGenericMap();
237            for (var i = 0; i < length; i++)
238               jm.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, jm, pMeta));
239            o = cast(jm, pMeta, eType);
240         }
241
242         if (sType.isObject()) {
243            // Do nothing.
244         } else if (nn(builder) || sType.canCreateNewBean(outer)) {
245            if (dt == MAP) {
246               BeanMap m = builder == null ? newBeanMap(outer, sType.inner()) : toBeanMap(builder.create(this, eType));
247               var pb = swapParentBean(m.getBean(false));
248               try {
249                  for (var i = 0; i < length; i++) {
250                     String pName = readAnything(string(), is, m.getBean(false), null);
251                     var bpm = m.getPropertyMeta(pName);
252                     if (bpm == null) {
253                        if (pName.equals(getBeanTypePropertyName(eType)))
254                           readAnything(string(), is, null, null);
255                        else
256                           onUnknownProperty(pName, m, readAnything(string(), is, null, null));
257                     } else {
258                        var cm = (ClassMeta<?>) bpm.getBeanInfo();
259                        Object value = readAnything(cm, is, m.getBean(false), bpm);
260                        setName(cm, value, pName);
261                        try {
262                           bpm.set(m, pName, value);
263                        } catch (BeanRuntimeException e) {
264                           onBeanSetterException(pMeta, e);
265                           throw e;
266                        }
267                     }
268                  }
269               } finally {
270                  swapParentBean(pb);
271               }
272               o = builder == null ? checkRequired(m).getBean() : builder.build(this, checkRequired(m).getBean(), eType);
273            } else {
274               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
275            }
276         } else if (sType.isMap()) {
277            if (dt == MAP) {
278               Map m = (sType.canCreateNewInstance(outer) ? (Map)sType.newInstance(outer) : newGenericMap(sType));
279               for (var i = 0; i < length; i++) {
280                  Object key = readAnything(sType.getKeyType(), is, outer, pMeta);
281                  var vt = sType.getValueType();
282                  Object value = readAnything(vt, is, m, pMeta);
283                  setName(vt, value, key);
284                  m.put(key, value);
285               }
286               o = m;
287            } else {
288               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
289            }
290         } else if (sType.isBoolean() || sType.isCharSequence() || sType.isChar() || sType.isNumber() || sType.isByteArray()) {
291            // Merged scalar tier: one wire-type (BOOLEAN/INT/LONG/FLOAT/DOUBLE/STRING/BIN) covers
292            // many scalar Java types — the read already happened above and convertToType narrows.
293            // A byte[]-targeted string element is a BinaryFormat-encoded payload (mirroring the
294            // serializer) rather than a literal string - decode it back to bytes directly, since
295            // the generic String-to-byte[] conversion doesn't know about BinaryFormat.
296            var binaryFormat = getBinaryFormat();
297            if (dt == STRING && sType.isByteArray() && binaryFormat != BinaryFormat.NOT_SET)
298               o = binaryFormat.parse((String)o);
299            else
300               o = convertToType(o, sType);
301         } else if (sType.isDate()) {
302            o = readDate(String.valueOf(o), sType);
303         } else if (sType.isCalendar()) {
304            o = readCalendar(String.valueOf(o), sType);
305         } else if (sType.isTemporal()) {
306            o = readTemporal(String.valueOf(o), sType);
307         } else if (sType.isDuration()) {
308            o = readDuration(String.valueOf(o));
309         } else if (sType.isPeriod()) {
310            o = readPeriod(String.valueOf(o));
311         } else if (sType.canCreateNewInstanceFromString(outer) && dt == STRING) {
312            o = sType.newInstanceFromString(outer, o == null ? "" : o.toString());
313         } else if (sType.isCollection()) {
314            if (dt == MAP) {
315               var m = newGenericMap();
316               for (var i = 0; i < length; i++)
317                  m.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, m, pMeta));
318               o = cast(m, pMeta, eType);
319            } else if (dt == ARRAY) {
320               Collection l = (sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
321               for (var i = 0; i < length; i++)
322                  l.add(readAnything(sType.getElementType(), is, l, pMeta));
323               o = l;
324            } else {
325               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
326            }
327         } else if (sType.isArray() || sType.isArgs()) {
328            if (dt == MAP) {
329               var m = newGenericMap();
330               for (var i = 0; i < length; i++)
331                  m.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, m, pMeta));
332               o = cast(m, pMeta, eType);
333            } else if (dt == ARRAY) {
334               Collection l = (sType.isCollection() && sType.canCreateNewInstance(outer) ? (Collection)sType.newInstance() : newGenericList());
335               for (var i = 0; i < length; i++)
336                  l.add(readAnything(sType.isArgs() ? sType.getArg(i) : sType.getElementType(), is, l, pMeta));
337               o = toArray(sType, l);
338            } else {
339               throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
340            }
341         } else if (dt == MAP) {
342            var m = newGenericMap();
343            for (var i = 0; i < length; i++)
344               m.put((String)readAnything(string(), is, outer, pMeta), readAnything(object(), is, m, pMeta));
345            if (m.containsKey(getBeanTypePropertyName(eType)))
346               o = cast(m, pMeta, eType);
347            else if (nn(sType.getProxyInvocationHandler()))
348               o = checkRequired(newBeanMap(outer, sType.inner()).load(m)).getBean();
349            else
350               throw new ParseException(this, "Class '%s' could not be instantiated.  Reason: '%s'", cn(sType), sType.getNotABeanReason());
351         } else {
352            throw new ParseException(this, "Invalid data type %s encountered for parse type %s", dt, sType);
353         }
354      }
355
356      if (nn(swap) && nn(o))
357         o = unswap(swap, o, eType);
358
359      if (nn(parentBean()))
360         setParent(eType, o, parentBean());
361
362      return (T)o;
363   }
364
365   @Override /* Overridden from ParserSession */
366   protected <T> T doRead(ParserPipe pipe, ClassMeta<T> type) throws IOException, ParseException, ExecutableException {
367      try (MsgPackInputStream is = new MsgPackInputStream(pipe)) {
368         is.setMaxLength(maxLength);
369         return readAnything(type, is, getOuter(), null);
370      }
371   }
372}