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 ≤ 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}