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}