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.commons.utils.SystemUtils.*;
021
022import java.io.*;
023import java.util.*;
024import java.util.concurrent.*;
025
026import org.apache.juneau.commons.bean.*;
027import org.apache.juneau.commons.collections.*;
028import org.apache.juneau.marshall.*;
029import org.apache.juneau.marshall.parser.*;
030import org.apache.juneau.marshall.stream.*;
031
032/**
033 * Parses a MessagePack stream into a POJO model.
034 *
035 * <h5 class='topic'>Media types</h5>
036 *
037 * Handles <c>Content-Type</c> types:  <bc>application/msgpack, octal/msgpack</bc>
038 *
039 * <p>
040 * The legacy media type <bc>octal/msgpack</bc> is retained as a backward-compatibility alias so
041 * that existing wire values still decode. New consumers should prefer the RFC-standard
042 * <bc>application/msgpack</bc> value.
043 *
044 * <h5 class='topic'>Description</h5>
045 * <p>
046 * MessagePack-to-Java type mapping:
047 * <ul class='spaced-list'>
048 *    <li>MessagePack map → bean or {@link Map}
049 *    <li>MessagePack array → {@link Collection} or Java array
050 *    <li>MessagePack string → {@link String}; coerced to types parseable from string
051 *    <li>MessagePack boolean → {@link Boolean}
052 *    <li>MessagePack int → {@link Integer}; coerced to {@link Long}, {@link Short}, {@link Byte} as needed
053 *    <li>MessagePack float → {@link Float} or {@link Double}
054 *    <li>MessagePack binary → {@code byte[]}
055 *    <li>MessagePack nil → {@code null}
056 * </ul>
057 *
058 * <h5 class='section'>Notes:</h5><ul>
059 *    <li class='note'>This class is thread safe and reusable.
060 * </ul>
061 *
062 * <h5 class='section'>See Also:</h5><ul>
063 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/MessagePackSupport">MessagePack</a>
064
065 * </ul>
066 */
067@SuppressWarnings({
068   "java:S110", // Inheritance depth acceptable for this class hierarchy
069   "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants
070   "java:S9149", // Per-format static factories intentionally shadow the parent's.
071   "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design.
072})
073public class MsgPackParser extends InputStreamParser implements MsgPackMetaProvider, TokenReadable, ArrayRecordReadable {
074
075   private static final int DEFAULT_MAX_LENGTH = MsgPackInputStream.DEFAULT_MAX_LENGTH;
076
077   /** Default parser, string input encoded as BASE64. */
078   public static class Base64 extends MsgPackParser {
079
080      /**
081       * Constructor.
082       *
083       * @param builder The builder for this object.
084       */
085      public Base64(Builder builder) {
086         super(builder.binaryFormat(BinaryFormat.BASE64));
087      }
088   }
089
090   /**
091    * Builder class.
092    */
093   public static class Builder extends InputStreamParser.Builder<Builder> {
094
095      private static final Cache<HashKey,MsgPackParser> CACHE = Cache.of(HashKey.class, MsgPackParser.class).build();
096
097      private boolean nativeMode;
098      private int maxLength = DEFAULT_MAX_LENGTH;
099
100      /**
101       * Constructor, default settings.
102       */
103      protected Builder() {
104         consumes("application/msgpack,octal/msgpack");
105         nativeMode = env("MsgPackParser.nativeMode", false);
106         maxLength = env("MsgPackParser.maxLength", DEFAULT_MAX_LENGTH);
107      }
108
109      /**
110       * Copy constructor.
111       *
112       * @param copyFrom The builder to copy from.
113       *    <br>Cannot be <jk>null</jk>.
114       */
115      protected Builder(Builder copyFrom) {
116         super(reqnn("copyFrom", copyFrom));
117         nativeMode = copyFrom.nativeMode;
118         maxLength = copyFrom.maxLength;
119      }
120
121      /**
122       * Copy constructor.
123       *
124       * @param copyFrom The bean to copy from.
125       *    <br>Cannot be <jk>null</jk>.
126       */
127      protected Builder(MsgPackParser copyFrom) {
128         super(reqnn("copyFrom", copyFrom));
129         nativeMode = copyFrom.nativeMode;
130         maxLength = copyFrom.maxLength;
131      }
132
133      /**
134       * Surfaces the MsgPack {@code ext} type byte as token-level metadata on
135       * {@link TokenReader} cursors returned by {@link MsgPackParserSession#readTokens(Object)}.
136       *
137       * <p>
138       * When enabled, ext tokens carry the signed type byte (visible via
139       * {@link TokenReader#getExtType()}) on {@link TokenType#VALUE_BINARY} tokens.  When
140       * disabled (default), the payload still surfaces as {@link TokenType#VALUE_BINARY} but
141       * the type byte is dropped.
142       *
143       * <p>
144       * This is a token-cursor-level setting and does not affect the high-level POJO databind
145       * path ({@link MsgPackParser#read(Object, Class)}); that path always discards the type
146       * byte.
147       *
148       * @return This object.
149       */
150      public Builder nativeMode() {
151         return nativeMode(true);
152      }
153
154      /**
155       * Same as {@link #nativeMode()} but allows you to explicitly specify the value.
156       *
157       * @param value The value for this setting.
158       * @return This object.
159       */
160      public Builder nativeMode(boolean value) {
161         nativeMode = value;
162         return this;
163      }
164
165      /**
166       * The maximum allowed wire-declared length (in bytes) for MessagePack binary and string payloads.
167       *
168       * <p>
169       * Guards against malformed or adversarial input where a small payload declares a huge length, which
170       * would otherwise trigger {@link OutOfMemoryError}.  Malformed lengths are reported as a clean parse
171       * error instead.
172       *
173       * @param value The maximum length in bytes.  Default is 16 MiB.  Values &le; 0 disable the cap.
174       * @return This object.
175       */
176      public Builder maxLength(int value) {
177         maxLength = value;
178         return this;
179      }
180
181      @Override /* Overridden from Context.Builder<?> */
182      public MsgPackParser build() {
183         return cache(CACHE).build(MsgPackParser.class);
184      }
185
186      @Override /* Overridden from Context.Builder<?> */
187      public Builder copy() {
188         return new Builder(this);
189      }
190
191      @Override /* Overridden from Context.Builder<?> */
192      public HashKey hashKey() {
193         return HashKey.of(super.hashKey(), nativeMode, maxLength);
194      }
195   }
196
197   /** Default parser, string input encoded as spaced-hex. */
198   public static class SpacedHex extends MsgPackParser {
199
200      /**
201       * Constructor.
202       *
203       * @param builder The builder for this object.
204       */
205      public SpacedHex(Builder builder) {
206         super(builder.binaryFormat(BinaryFormat.SPACED_HEX));
207      }
208   }
209
210   /** Default parser, all default settings.*/
211   public static final MsgPackParser DEFAULT = new MsgPackParser(create());
212   /** Default parser with binary-native opt-in mode enabled (MsgPack {@code ext} type byte surfaces as token-level metadata). */
213   public static final MsgPackParser DEFAULT_NATIVE = new MsgPackParser(create().nativeMode());
214   /** Default parser, all default settings, string input encoded as spaced-hex.*/
215   public static final MsgPackParser DEFAULT_SPACED_HEX = new SpacedHex(create());
216
217   /** Default parser, all default settings, string input encoded as BASE64.*/
218   public static final MsgPackParser DEFAULT_BASE64 = new Base64(create());
219
220   /**
221    * Creates a new builder for this object.
222    *
223    * @return A new builder.
224    */
225   public static Builder create() {
226      return new Builder();
227   }
228
229   protected final boolean nativeMode;
230
231   private final int maxLength;
232
233   private final Map<ClassMeta<?>,MsgPackClassMeta> msgPackClassMetas = new ConcurrentHashMap<>();
234   private final Map<BeanPropertyMeta,MsgPackBeanPropertyMeta> msgPackBeanPropertyMetas = new ConcurrentHashMap<>();
235
236   /**
237    * Constructor.
238    *
239    * @param builder The builder for this object.
240    */
241   public MsgPackParser(Builder builder) {
242      super(builder);
243      this.nativeMode = builder.nativeMode;
244      this.maxLength = builder.maxLength;
245   }
246
247   /**
248    * Returns <jk>true</jk> if token-cursor native mode is enabled.
249    *
250    * @return <jk>true</jk> if native mode is enabled, else <jk>false</jk>.
251    */
252   public boolean isNativeMode() {
253      return nativeMode;
254   }
255
256   /**
257    * Returns the maximum allowed wire-declared length (in bytes) for binary and string payloads.
258    *
259    * @return The maximum length in bytes.
260    */
261   public int getMaxLength() {
262      return maxLength;
263   }
264
265   @Override /* Overridden from Context */
266   public Builder copy() {
267      return new Builder(this);
268   }
269
270   @Override /* Overridden from Context */
271   public MsgPackParserSession.Builder createSession() {
272      return MsgPackParserSession.create(this);
273   }
274
275   @Override /* Overridden from MsgPackMetaProvider */
276   public MsgPackBeanPropertyMeta getMsgPackBeanPropertyMeta(BeanPropertyMeta bpm) {
277      if (bpm == null)
278         return MsgPackBeanPropertyMeta.DEFAULT;
279      return msgPackBeanPropertyMetas.computeIfAbsent(bpm, k -> new MsgPackBeanPropertyMeta(k.getDelegateFor(), this));
280   }
281
282   @Override /* Overridden from MsgPackMetaProvider */
283   public MsgPackClassMeta getMsgPackClassMeta(ClassMeta<?> cm) {
284      return msgPackClassMetas.computeIfAbsent(cm, k -> new MsgPackClassMeta(k, this));
285   }
286
287   @Override /* Overridden from Context */
288   public MsgPackParserSession getSession() { return createSession().build(); }
289
290   /**
291    * Convenience delegator that opens a {@link MsgPackTokenReader} over the input using
292    * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}).
293    *
294    * <p>
295    * The real implementation lives on {@link MsgPackParserSession#readTokens(Object)}.  Callers
296    * that need request-derived configuration (locale, timezone, schema, swaps) should call
297    * {@link #createSession()} and invoke {@link MsgPackParserSession#readTokens(Object)} on the
298    * built session instead.
299    *
300    * @param input The input.
301    * @return A new {@link MsgPackTokenReader}.
302    * @throws IOException If a problem occurred opening the underlying input.
303    */
304   @Override /* TokenReadable */
305   public TokenReader readTokens(Object input) throws IOException {
306      return getSession().readTokens(input);
307   }
308
309   /**
310    * Convenience delegator for the array-element {@link RecordReader} (uses default session args;
311    * see {@link #readTokens(Object)}).  Real impl on
312    * {@link MsgPackParserSession#readArrayRecords(Object)}.
313    *
314    * @param input The input.
315    * @return A new array-element {@link RecordReader}.
316    * @throws IOException If a problem occurred opening the underlying input.
317    */
318   @Override /* ArrayRecordReadable */
319   public RecordReader readArrayRecords(Object input) throws IOException {
320      return getSession().readArrayRecords(input);
321   }
322
323   @Override /* Overridden from InputStreamParser */
324   protected FluentMap<String,Object> properties() {
325      return super.properties()
326         .a("nativeMode", nativeMode);
327   }
328}