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.time.*; 025import java.util.*; 026import java.util.concurrent.*; 027 028import org.apache.juneau.commons.bean.*; 029import org.apache.juneau.commons.collections.*; 030import org.apache.juneau.marshall.*; 031import org.apache.juneau.marshall.serializer.*; 032import org.apache.juneau.marshall.stream.*; 033 034/** 035 * Serializes POJO models to CBOR (RFC 8949). 036 * 037 * <h5 class='section'>Media types:</h5> 038 * 039 * Handles <c>Accept</c> types: <bc>application/cbor</bc> 040 * <p> 041 * Produces <c>Content-Type</c> types: <bc>application/cbor</bc> 042 * 043 * <h5 class='topic'>Description</h5> 044 * <p> 045 * Java-to-CBOR type mapping: 046 * <ul class='spaced-list'> 047 * <li>Maps and beans → CBOR map (major type 5) 048 * <li>Collections and arrays → CBOR array (major type 4) 049 * <li>{@link String} / {@link Enum} → CBOR text string (major type 3, UTF-8) 050 * <li>{@link Boolean} → CBOR simple values 20/21 (<c>0xF4</c>/<c>0xF5</c>) 051 * <li>{@link Number} (int, long, float, double) → CBOR compact integers (major types 0/1) or IEEE 754 floats (major type 7) 052 * <li>{@link Date} / {@link Calendar} / {@link Instant} → ISO 8601 string 053 * <li>{@link Duration} → ISO 8601 duration string 054 * <li>{@code byte[]} → CBOR byte string (major type 2) 055 * <li>{@code null} → CBOR simple value 22 (<c>0xF6</c>) 056 * <li>All other types → fallback to {@code toString()} as string 057 * </ul> 058 * 059 * <h5 class='section'>Behavior-specific subclasses:</h5> 060 * <ul class='spaced-list'> 061 * <li>{@link SpacedHex} — Spaced-hex text output for debugging 062 * <li>{@link Base64} — Base64 text output 063 * </ul> 064 * 065 * <h5 class='section'>Example:</h5> 066 * <p class='bjava'> 067 * <jc>// Serialize a bean to CBOR bytes using the default serializer</jc> 068 * byte[] <jv>cbor</jv> = CborSerializer.<jsf>DEFAULT</jsf>.write(<jv>myBean</jv>); 069 * 070 * <jc>// Custom serializer with bean type properties enabled</jc> 071 * CborSerializer <jv>s</jv> = CborSerializer.<jsm>create</jsm>().addBeanTypes().build(); 072 * byte[] <jv>cbor</jv> = <jv>s</jv>.write(<jv>myBean</jv>); 073 * </p> 074 * 075 * <h5 class='section'>Round-trip notes:</h5> 076 * <ul class='spaced-list'> 077 * <li>{@link BigInteger} — emitted losslessly as a native CBOR integer when it fits CBOR's integer 078 * range ({@code -2^64 .. 2^64-1}), otherwise as a lossless decimal string. 079 * <li>{@link BigDecimal} — emitted as a lossless decimal string (native CBOR has no decimal number 080 * head without semantic tags). 081 * <li>The high-level databind path emits definite-length containers; the streaming 082 * {@link CborTokenWriter} emits indefinite-length containers (both are valid RFC 8949). 083 * <li>Half-precision float (0xF9) not produced; always uses float32 (0xFA) or float64 (0xFB). 084 * <li>Semantic tags (major type 6) not emitted unless {@link Builder#useTags(boolean) useTags(true)}. 085 * </ul> 086 * 087 * <h5 class='section'>Notes:</h5><ul> 088 * <li class='note'>This class is thread safe and reusable. 089 * </ul> 090 * 091 * <h5 class='section'>See Also:</h5><ul> 092 * <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/Cbor">CBOR</a> 093 * <li class='link'><a class="doclink" href="https://www.rfc-editor.org/rfc/rfc8949.html">RFC 8949</a> 094 * </ul> 095 */ 096@SuppressWarnings({ 097 "java:S110", // Inheritance depth acceptable for this class hierarchy 098 "java:S1192", // Duplicated literals (argument/property names) read more clearly inline than as constants 099 "java:S9149", // Per-format static factories intentionally shadow the parent's. 100 "resource" // Closeable resources are owned by the caller's serializer session; Eclipse JDT @Owning warning is by design. 101}) 102public class CborSerializer extends OutputStreamSerializer implements CborMetaProvider, TokenWritable, ArrayRecordWritable { 103 104 /** Default serializer, BASE64 string output. */ 105 public static class Base64 extends CborSerializer { 106 107 /** 108 * Constructor. 109 * 110 * @param builder The builder for this object. 111 */ 112 public Base64(Builder builder) { 113 super(reqnn("builder", builder).binaryFormat(BinaryFormat.BASE64)); 114 } 115 } 116 117 /** 118 * Builder class. 119 */ 120 public static class Builder extends OutputStreamSerializer.Builder<Builder> { 121 122 private static final Cache<HashKey,CborSerializer> CACHE = Cache.of(HashKey.class, CborSerializer.class).build(); 123 124 private boolean addBeanTypesCbor; 125 private boolean useTags; 126 127 /** 128 * Constructor, default settings. 129 */ 130 protected Builder() { 131 produces("application/cbor"); 132 addBeanTypesCbor = env("CborSerializer.addBeanTypesCbor", false); 133 useTags = env("CborSerializer.useTags", false); 134 } 135 136 /** 137 * Copy constructor. 138 * 139 * @param copyFrom The builder to copy from. 140 */ 141 protected Builder(Builder copyFrom) { 142 super(reqnn("copyFrom", copyFrom)); 143 addBeanTypesCbor = copyFrom.addBeanTypesCbor; 144 useTags = copyFrom.useTags; 145 } 146 147 /** 148 * Copy constructor. 149 * 150 * @param copyFrom The serializer to copy from. 151 */ 152 protected Builder(CborSerializer copyFrom) { 153 super(reqnn("copyFrom", copyFrom)); 154 addBeanTypesCbor = copyFrom.addBeanTypesCbor; 155 useTags = copyFrom.useTags; 156 } 157 158 /** 159 * Add <js>"_type"</js> properties when needed. 160 * 161 * @return This object. 162 */ 163 public Builder addBeanTypesCbor() { 164 return addBeanTypesCbor(true); 165 } 166 167 /** 168 * Same as {@link #addBeanTypesCbor()} but allows you to explicitly specify the value. 169 * 170 * @param value The value for this setting. 171 * @return This object. 172 */ 173 public Builder addBeanTypesCbor(boolean value) { 174 addBeanTypesCbor = value; 175 return this; 176 } 177 178 /** 179 * Use CBOR semantic tags for dates and other typed values. 180 * 181 * @return This object. 182 */ 183 public Builder useTags() { 184 return useTags(true); 185 } 186 187 /** 188 * Same as {@link #useTags()} but allows you to explicitly specify the value. 189 * 190 * @param value The value for this setting. 191 * @return This object. 192 */ 193 public Builder useTags(boolean value) { 194 useTags = value; 195 return this; 196 } 197 198 @Override /* Overridden from Context.Builder<?> */ 199 public Builder copy() { 200 return new Builder(this); 201 } 202 203 @Override /* Overridden from Context.Builder<?> */ 204 public CborSerializer build() { 205 return cache(CACHE).build(CborSerializer.class); 206 } 207 208 @Override /* Overridden from Context.Builder<?> */ 209 public HashKey hashKey() { 210 return HashKey.of(super.hashKey(), addBeanTypesCbor, useTags); 211 } 212 } 213 214 /** Default serializer, spaced-hex string output. */ 215 public static class SpacedHex extends CborSerializer { 216 217 /** 218 * Constructor. 219 * 220 * @param builder The builder for this object. 221 */ 222 public SpacedHex(Builder builder) { 223 super(builder.binaryFormat(BinaryFormat.SPACED_HEX)); 224 } 225 } 226 227 /** Default serializer, all default settings. */ 228 public static final CborSerializer DEFAULT = new CborSerializer(create()); 229 /** Default serializer, spaced-hex string output. */ 230 public static final CborSerializer DEFAULT_SPACED_HEX = new SpacedHex(create()); 231 /** Default serializer, BASE64 string output. */ 232 public static final CborSerializer DEFAULT_BASE64 = new Base64(create()); 233 234 /** 235 * Creates a new builder for this object. 236 * 237 * @return A new builder. 238 */ 239 public static Builder create() { 240 return new Builder(); 241 } 242 243 protected final boolean addBeanTypesCbor; 244 protected final boolean useTags; 245 246 private final Map<BeanPropertyMeta,CborBeanPropertyMeta> cborBeanPropertyMetas = new ConcurrentHashMap<>(); 247 private final Map<ClassMeta<?>,CborClassMeta> cborClassMetas = new ConcurrentHashMap<>(); 248 249 /** 250 * Constructor. 251 * 252 * @param builder The builder for this object. 253 */ 254 public CborSerializer(Builder builder) { 255 super(builder); 256 this.addBeanTypesCbor = builder.addBeanTypesCbor; 257 this.useTags = builder.useTags; 258 } 259 260 @Override /* Overridden from Context */ 261 public Builder copy() { 262 return new Builder(this); 263 } 264 265 @Override /* Overridden from Context */ 266 public CborSerializerSession.Builder createSession() { 267 return CborSerializerSession.create(this); 268 } 269 270 @Override /* Overridden from CborMetaProvider */ 271 public CborBeanPropertyMeta getCborBeanPropertyMeta(BeanPropertyMeta bpm) { 272 if (bpm == null) 273 return CborBeanPropertyMeta.DEFAULT; 274 return cborBeanPropertyMetas.computeIfAbsent(bpm, k -> new CborBeanPropertyMeta(k.getDelegateFor(), this)); 275 } 276 277 @Override /* Overridden from CborMetaProvider */ 278 public CborClassMeta getCborClassMeta(ClassMeta<?> cm) { 279 return cborClassMetas.computeIfAbsent(cm, k -> new CborClassMeta(k, this)); 280 } 281 282 @Override /* Overridden from Context */ 283 public CborSerializerSession getSession() { 284 return createSession().build(); 285 } 286 287 /** 288 * Convenience delegator that opens a {@link CborTokenWriter} over the output using 289 * <b>default session arguments</b> (mirrors {@link #write(Object)}). 290 * 291 * <p> 292 * The real implementation lives on {@link CborSerializerSession#writeTokens(Object)}. 293 * Callers that need request-derived configuration (locale, timezone, schema, swaps) should 294 * call {@link #createSession()} and invoke 295 * {@link CborSerializerSession#writeTokens(Object)} on the built session instead. 296 * 297 * @param output The output. Accepts {@link OutputStream} or {@link File}. 298 * @return A new {@link CborTokenWriter}. 299 * @throws IOException If the output type is not supported or could not be opened. 300 */ 301 @Override /* TokenWritable */ 302 public TokenWriter writeTokens(Object output) throws IOException { 303 return getSession().writeTokens(output); 304 } 305 306 @Override 307 protected final boolean isAddBeanTypes() { 308 return addBeanTypesCbor || super.isAddBeanTypes(); 309 } 310 311 @Override /* Overridden from OutputStreamSerializer */ 312 protected FluentMap<String,Object> properties() { 313 return super.properties() 314 .a("addBeanTypesCbor", addBeanTypesCbor) 315 .a("useTags", useTags); 316 } 317 318 /** 319 * Convenience delegator for the streaming array-element {@link RecordWriter} (uses default 320 * session args; see {@link #writeTokens(Object)}). Real impl on 321 * {@link CborSerializerSession#writeArrayRecords(Object)}. 322 * 323 * @param output The output. 324 * @return A new element-streamed {@link RecordWriter}. 325 * @throws IOException If a problem occurred opening the underlying output. 326 */ 327 @Override /* ArrayRecordWritable */ 328 public RecordWriter writeArrayRecords(Object output) throws IOException { 329 return getSession().writeArrayRecords(output); 330 } 331}