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