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.cbor; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020import static org.apache.juneau.commons.utils.SystemUtils.*; 021 022import java.io.*; 023import java.math.*; 024import java.util.*; 025import java.util.concurrent.*; 026 027import org.apache.juneau.commons.bean.*; 028import org.apache.juneau.commons.collections.*; 029import org.apache.juneau.marshall.*; 030import org.apache.juneau.marshall.parser.*; 031import org.apache.juneau.marshall.stream.*; 032 033/** 034 * Parses CBOR streams (RFC 8949) into POJO models. 035 * 036 * <h5 class='topic'>Media types</h5> 037 * 038 * Handles <c>Content-Type</c> types: <bc>application/cbor</bc> 039 * 040 * <h5 class='topic'>Description</h5> 041 * <p> 042 * CBOR-to-Java type mapping: 043 * <ul class='spaced-list'> 044 * <li>CBOR map (major type 5) → bean or {@link Map} 045 * <li>CBOR array (major type 4) → {@link Collection} or Java array 046 * <li>CBOR text string (major type 3) → {@link String}; coerced to types parseable from string 047 * <li>CBOR unsigned/negative integer (major types 0/1) → {@link Integer} or {@link Long} 048 * <li>CBOR float (major type 7, 0xFA/0xFB) → {@link Float} or {@link Double} 049 * <li>CBOR half-precision float (major type 7, 0xF9) → {@link Float} (converted) 050 * <li>CBOR boolean (major type 7, 0xF4/0xF5) → {@link Boolean} 051 * <li>CBOR null (major type 7, 0xF6) → {@code null} 052 * <li>CBOR byte string (major type 2) → {@code byte[]} 053 * <li>CBOR semantic tag (major type 6) → tag ignored; tagged value parsed normally 054 * </ul> 055 * 056 * <h5 class='section'>Round-trip notes:</h5> 057 * <ul class='spaced-list'> 058 * <li>Integers (major types 0/1) surface by target type: a {@link BigInteger} field carries the full 059 * unsigned-64-bit magnitude; any other numeric type keeps the raw 64-bit bits as a {@code long}. 060 * <li>{@link BigInteger} / {@link BigDecimal} round-trip losslessly (big integers decode from native 061 * CBOR integers or, beyond {@code ±2^64}, from their decimal string; {@link BigDecimal} decodes 062 * from its decimal string). 063 * <li>Indefinite-length CBOR (RFC 8949 §3.2) is supported: arrays, maps, and chunked text/byte 064 * strings all decode. 065 * <li>Semantic tags are read and discarded on the databind path; tag number does not affect parsed 066 * type. Enable {@link Builder#nativeMode() nativeMode} to surface tags/simple values on the 067 * {@link TokenReader} cursor. 068 * </ul> 069 * 070 * <h5 class='section'>Notes:</h5><ul> 071 * <li class='note'>This class is thread safe and reusable. 072 * </ul> 073 * 074 * <h5 class='section'>See Also:</h5><ul> 075 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Cbor">CBOR</a> 076 * <li class='link'><a class="doclink" href="https://www.rfc-editor.org/rfc/rfc8949.html">RFC 8949</a> 077 * </ul> 078 */ 079@SuppressWarnings({ 080 "java:S110", // Inheritance depth acceptable for this class hierarchy 081 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 082 "java:S9149", // Per-format static factories intentionally shadow the parent's. 083 "resource" // Closeable resources are owned by the caller's parser session; Eclipse JDT @Owning warning is by design. 084}) 085public class CborParser extends InputStreamParser implements CborMetaProvider, TokenReadable, ArrayRecordReadable { 086 087 private static final int DEFAULT_MAX_LENGTH = CborInputStream.DEFAULT_MAX_LENGTH; 088 089 /** Default parser, string input encoded as BASE64. */ 090 public static class Base64 extends CborParser { 091 092 /** 093 * Constructor. 094 * 095 * @param builder The builder for this object. 096 */ 097 public Base64(Builder builder) { 098 super(builder.binaryFormat(BinaryFormat.BASE64)); 099 } 100 } 101 102 /** 103 * Builder class. 104 */ 105 public static class Builder extends InputStreamParser.Builder<Builder> { 106 107 private static final Cache<HashKey,CborParser> CACHE = Cache.of(HashKey.class, CborParser.class).build(); 108 109 private boolean nativeMode; 110 private int maxLength = DEFAULT_MAX_LENGTH; 111 112 /** 113 * Constructor, default settings. 114 */ 115 protected Builder() { 116 consumes("application/cbor"); 117 nativeMode = env("CborParser.nativeMode", false); 118 maxLength = env("CborParser.maxLength", DEFAULT_MAX_LENGTH); 119 } 120 121 /** 122 * Copy constructor. 123 * 124 * @param copyFrom The builder to copy from. 125 */ 126 protected Builder(Builder copyFrom) { 127 super(reqnn("copyFrom", copyFrom)); 128 nativeMode = copyFrom.nativeMode; 129 maxLength = copyFrom.maxLength; 130 } 131 132 /** 133 * Copy constructor. 134 * 135 * @param copyFrom The parser to copy from. 136 */ 137 protected Builder(CborParser copyFrom) { 138 super(reqnn("copyFrom", copyFrom)); 139 nativeMode = copyFrom.nativeMode; 140 maxLength = copyFrom.maxLength; 141 } 142 143 /** 144 * Surfaces CBOR semantic tags and simple values as token-level metadata on 145 * {@link TokenReader} cursors returned by {@link CborParserSession#readTokens(Object)}. 146 * 147 * <p> 148 * When enabled, tags accumulate on the cursor's tag stack (visible via 149 * {@link TokenReader#getTagCount()} / {@link TokenReader#getTag(int)}) and simple values 150 * surface their int via {@link TokenReader#getSimpleValue()} on 151 * {@link TokenType#VALUE_NULL} tokens. When disabled (default), tags are silently 152 * unwrapped and simple values collapse to {@link TokenType#VALUE_NULL} with no metadata. 153 * 154 * <p> 155 * This is a token-cursor-level setting and does not affect the high-level POJO databind 156 * path ({@link CborParser#read(Object, Class)}); that path always discards tags. 157 * 158 * @return This object. 159 */ 160 public Builder nativeMode() { 161 return nativeMode(true); 162 } 163 164 /** 165 * Same as {@link #nativeMode()} but allows you to explicitly specify the value. 166 * 167 * @param value The value for this setting. 168 * @return This object. 169 */ 170 public Builder nativeMode(boolean value) { 171 nativeMode = value; 172 return this; 173 } 174 175 /** 176 * The maximum allowed wire-declared length for CBOR byte/text strings and array/map element counts. 177 * 178 * <p> 179 * Guards against malformed or adversarial input where a small payload declares a huge length or 180 * element count, which would otherwise trigger {@link OutOfMemoryError}. Malformed lengths are 181 * reported as a clean parse error instead. 182 * 183 * @param value The maximum length. Default is 16 MiB. Values ≤ 0 disable the cap. 184 * @return This object. 185 */ 186 public Builder maxLength(int value) { 187 maxLength = value; 188 return this; 189 } 190 191 @Override /* Overridden from Context.Builder<?> */ 192 public CborParser build() { 193 return cache(CACHE).build(CborParser.class); 194 } 195 196 @Override /* Overridden from Context.Builder<?> */ 197 public Builder copy() { 198 return new Builder(this); 199 } 200 201 @Override /* Overridden from Context.Builder<?> */ 202 public HashKey hashKey() { 203 return HashKey.of(super.hashKey(), nativeMode, maxLength); 204 } 205 } 206 207 /** Default parser, string input encoded as spaced-hex. */ 208 public static class SpacedHex extends CborParser { 209 210 /** 211 * Constructor. 212 * 213 * @param builder The builder for this object. 214 */ 215 public SpacedHex(Builder builder) { 216 super(builder.binaryFormat(BinaryFormat.SPACED_HEX)); 217 } 218 } 219 220 /** Default parser, all default settings. */ 221 public static final CborParser DEFAULT = new CborParser(create()); 222 /** Default parser with binary-native opt-in mode enabled (CBOR tags / simple values surface as token-level metadata). */ 223 public static final CborParser DEFAULT_NATIVE = new CborParser(create().nativeMode()); 224 /** Default parser, string input encoded as spaced-hex. */ 225 public static final CborParser DEFAULT_SPACED_HEX = new SpacedHex(create()); 226 /** Default parser, string input encoded as BASE64. */ 227 public static final CborParser DEFAULT_BASE64 = new Base64(create()); 228 229 /** 230 * Creates a new builder for this object. 231 * 232 * @return A new builder. 233 */ 234 public static Builder create() { 235 return new Builder(); 236 } 237 238 protected final boolean nativeMode; 239 240 private final int maxLength; 241 242 private final Map<ClassMeta<?>,CborClassMeta> cborClassMetas = new ConcurrentHashMap<>(); 243 private final Map<BeanPropertyMeta,CborBeanPropertyMeta> cborBeanPropertyMetas = new ConcurrentHashMap<>(); 244 245 /** 246 * Constructor. 247 * 248 * @param builder The builder for this object. 249 */ 250 public CborParser(Builder builder) { 251 super(builder); 252 this.nativeMode = builder.nativeMode; 253 this.maxLength = builder.maxLength; 254 } 255 256 /** 257 * Returns <jk>true</jk> if token-cursor native mode is enabled. 258 * 259 * @return <jk>true</jk> if native mode is enabled, else <jk>false</jk>. 260 */ 261 public boolean isNativeMode() { 262 return nativeMode; 263 } 264 265 /** 266 * Returns the maximum allowed wire-declared length for byte/text strings and array/map element counts. 267 * 268 * @return The maximum length. 269 */ 270 public int getMaxLength() { 271 return maxLength; 272 } 273 274 @Override /* Overridden from Context */ 275 public Builder copy() { 276 return new Builder(this); 277 } 278 279 @Override /* Overridden from Context */ 280 public CborParserSession.Builder createSession() { 281 return CborParserSession.create(this); 282 } 283 284 @Override /* Overridden from CborMetaProvider */ 285 public CborBeanPropertyMeta getCborBeanPropertyMeta(BeanPropertyMeta bpm) { 286 if (bpm == null) 287 return CborBeanPropertyMeta.DEFAULT; 288 return cborBeanPropertyMetas.computeIfAbsent(bpm, k -> new CborBeanPropertyMeta(k.getDelegateFor(), this)); 289 } 290 291 @Override /* Overridden from CborMetaProvider */ 292 public CborClassMeta getCborClassMeta(ClassMeta<?> cm) { 293 return cborClassMetas.computeIfAbsent(cm, k -> new CborClassMeta(k, this)); 294 } 295 296 @Override /* Overridden from Context */ 297 public CborParserSession getSession() { 298 return createSession().build(); 299 } 300 301 /** 302 * Convenience delegator that opens a {@link CborTokenReader} over the input using 303 * <b>default session arguments</b> (mirrors {@link #read(Object, Class)}). 304 * 305 * <p> 306 * The real implementation lives on {@link CborParserSession#readTokens(Object)}. Callers 307 * that need request-derived configuration (locale, timezone, schema, swaps) should call 308 * {@link #createSession()} and invoke {@link CborParserSession#readTokens(Object)} on the 309 * built session instead. 310 * 311 * @param input The input. 312 * @return A new {@link CborTokenReader}. 313 * @throws IOException If a problem occurred opening the underlying input. 314 */ 315 @Override /* TokenReadable */ 316 public TokenReader readTokens(Object input) throws IOException { 317 return getSession().readTokens(input); 318 } 319 320 /** 321 * Convenience delegator for the streaming array-element {@link RecordReader} (uses default 322 * session args; see {@link #readTokens(Object)}). Real impl on 323 * {@link CborParserSession#readArrayRecords(Object)}. 324 * 325 * @param input The input. 326 * @return A new element-streamed {@link RecordReader}. 327 * @throws IOException If a problem occurred opening the underlying input. 328 */ 329 @Override /* ArrayRecordReadable */ 330 public RecordReader readArrayRecords(Object input) throws IOException { 331 return getSession().readArrayRecords(input); 332 } 333 334 @Override /* Overridden from InputStreamParser */ 335 protected FluentMap<String,Object> properties() { 336 return super.properties() 337 .a("nativeMode", nativeMode); 338 } 339}