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
019
020import java.io.*;
021import java.math.*;
022
023import org.apache.juneau.marshall.stream.*;
024import static org.apache.juneau.commons.utils.Shorts.*;
025
026/**
027 * Reference implementation of the public {@link TokenWriter} surface for the CBOR format
028 * (RFC 8949).
029 *
030 * <p>
031 * Wraps a {@link CborOutputStream} and emits **indefinite-length** containers
032 * (<c>0xBF</c> for maps, <c>0x9F</c> for arrays, terminated by <c>0xFF</c> BREAK) so that the
033 * caller doesn't have to know element counts up front &mdash; a natural fit for streaming
034 * generation.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not thread safe.
038 *    <li>{@code byte[]} values dispatch to native CBOR major-type-2 byte strings via
039 *       {@link CborOutputStream#appendBinary(byte[])}, not to a base64 string fallback.
040 *    <li>The writer is purely structural; object swaps and {@code @Schema} annotations are NOT
041 *       applied at the token layer (see {@link TokenWriter#object(Object)} for the bridge that
042 *       does apply them).
043 * </ul>
044 */
045@SuppressWarnings({
046   "resource" // The writer's underlying OutputStream is owned by the caller via try-with-resources on the writer itself; Eclipse JDT flags the inner stream as unclosed but that's by design.
047})
048public class CborTokenWriter implements TokenWriter {
049
050   /**
051    * Cursor-level settings honored by {@link CborTokenWriter}.
052    *
053    * @param walk The {@link org.apache.juneau.marshall.stream.PojoWalker.Options walk options} used by
054    *    {@link #object(Object)} when walking a value graph.
055    */
056   public record Settings(PojoWalker.Options walk) {
057
058      /** Default canonical setting: default walk options. */
059      public static final Settings DEFAULT = new Settings(PojoWalker.Options.DEFAULT);
060   }
061
062   private final CborOutputStream out;
063   private final OutputStream raw;
064   private final Closeable owned;
065   private final Settings settings;
066
067   // Container stack: 0 = array, 1 = map; tracked depth lets us validate end-kind matches.
068   private boolean[] stackIsMap = new boolean[16];
069   private int depth;
070   // Map state: track whether we're emitting a key (true) or a value (false) at the current
071   // map level.
072   private boolean[] stackAwaitingKey = new boolean[16];
073
074   private boolean closed;
075
076   /**
077    * Constructor with default settings.
078    *
079    * @param out The {@link OutputStream} to emit CBOR to.  Must not be <jk>null</jk>.
080    */
081   public CborTokenWriter(OutputStream out) {
082      this(out, null, Settings.DEFAULT);
083   }
084
085   /**
086    * Constructor with explicit settings.
087    *
088    * @param out The {@link OutputStream} to emit CBOR to.  Must not be <jk>null</jk>.
089    * @param settings The settings.  Must not be <jk>null</jk>.
090    */
091   public CborTokenWriter(OutputStream out, Settings settings) {
092      this(out, null, settings);
093   }
094
095   private CborTokenWriter(OutputStream out, Closeable owned, Settings settings) {
096      reqnn("out", out);
097      reqnn("settings", settings);
098      this.raw = out;
099      this.out = new CborOutputStream(out);
100      this.owned = owned;
101      this.settings = settings;
102   }
103
104   @Override /* TokenWriter */
105   public TokenWriter startObject() throws IOException {
106      assertOpen();
107      preValueWrite();
108      // 0xBF = indefinite-length map start.
109      raw.write(0xBF);
110      pushContainer(true);
111      return this;
112   }
113
114   @Override /* TokenWriter */
115   public TokenWriter endObject() throws IOException {
116      assertOpen();
117      popContainer(true);
118      // 0xFF = BREAK (closes both indefinite arrays and maps).
119      raw.write(0xFF);
120      afterValue();
121      return this;
122   }
123
124   @Override /* TokenWriter */
125   public TokenWriter startArray() throws IOException {
126      assertOpen();
127      preValueWrite();
128      // 0x9F = indefinite-length array start.
129      raw.write(0x9F);
130      pushContainer(false);
131      return this;
132   }
133
134   @Override /* TokenWriter */
135   public TokenWriter endArray() throws IOException {
136      assertOpen();
137      popContainer(false);
138      raw.write(0xFF);
139      afterValue();
140      return this;
141   }
142
143   @Override /* TokenWriter */
144   public TokenWriter fieldName(String name) throws IOException {
145      assertOpen();
146      reqnn("name", name);
147      if (depth == 0 || !stackIsMap[depth - 1])
148         throw new IllegalStateException("field called outside an object");
149      if (!stackAwaitingKey[depth - 1])
150         throw new IllegalStateException("field called twice without an intervening value");
151      out.appendString(name);
152      stackAwaitingKey[depth - 1] = false;
153      return this;
154   }
155
156   @Override /* TokenWriter */
157   public TokenWriter string(String value) throws IOException {
158      assertOpen();
159      if (value == null)
160         return nil();
161      preValueWrite();
162      out.appendString(value);
163      afterValue();
164      return this;
165   }
166
167   @Override /* TokenWriter */
168   public TokenWriter number(Number value) throws IOException {
169      assertOpen();
170      if (value == null)
171         return nil();
172      preValueWrite();
173      out.appendNumber(value);
174      afterValue();
175      return this;
176   }
177
178   @Override /* TokenWriter */
179   public TokenWriter number(long value) throws IOException {
180      assertOpen();
181      preValueWrite();
182      out.appendLong(value);
183      afterValue();
184      return this;
185   }
186
187   @Override /* TokenWriter */
188   public TokenWriter number(double value) throws IOException {
189      assertOpen();
190      preValueWrite();
191      out.appendDouble(value);
192      afterValue();
193      return this;
194   }
195
196   @Override /* TokenWriter */
197   public TokenWriter number(BigDecimal value) throws IOException {
198      assertOpen();
199      if (value == null)
200         return nil();
201      preValueWrite();
202      out.appendDouble(value.doubleValue());
203      afterValue();
204      return this;
205   }
206
207   @Override /* TokenWriter */
208   public TokenWriter number(BigInteger value) throws IOException {
209      assertOpen();
210      if (value == null)
211         return nil();
212      preValueWrite();
213      // Delegates to appendNumber so values outside the signed-long range are emitted losslessly
214      // (native CBOR integer up to ±2^64, decimal string beyond) rather than throwing on overflow.
215      out.appendNumber(value);
216      afterValue();
217      return this;
218   }
219
220   @Override /* TokenWriter */
221   public TokenWriter bool(boolean value) throws IOException {
222      assertOpen();
223      preValueWrite();
224      out.appendBoolean(value);
225      afterValue();
226      return this;
227   }
228
229   @Override /* TokenWriter */
230   public TokenWriter nil() throws IOException {
231      assertOpen();
232      preValueWrite();
233      out.appendNull();
234      afterValue();
235      return this;
236   }
237
238   @Override /* TokenWriter */
239   public TokenWriter binary(byte[] value) throws IOException {
240      assertOpen();
241      if (value == null)
242         return nil();
243      preValueWrite();
244      out.appendBinary(value);
245      afterValue();
246      return this;
247   }
248
249   @Override /* TokenWriter */
250   public CborTokenWriter writeTag(long tagNumber) throws IOException {
251      assertOpen();
252      // A tag is a prefix on the next value emit; it does NOT consume a map-key/value or
253      // array-element slot.  Skip preValueWrite()/afterValue() — the wrapped value emit that
254      // follows owns the state transition.
255      out.writeTag(tagNumber);
256      return this;
257   }
258
259   @Override /* TokenWriter */
260   public CborTokenWriter writeSimple(int value) throws IOException {
261      assertOpen();
262      preValueWrite();
263      out.writeSimple(value);
264      afterValue();
265      return this;
266   }
267
268   @Override /* TokenWriter */
269   public TokenWriter object(Object value) throws IOException {
270      assertOpen();
271      PojoWalker.walk(this, value, settings.walk);
272      return this;
273   }
274
275   @Override /* TokenWriter */
276   public boolean isStreaming() {
277      return true;
278   }
279
280   @Override /* TokenWriter */
281   public void flush() throws IOException {
282      raw.flush();
283   }
284
285   @Override /* TokenWriter */
286   public void close() throws IOException {
287      if (closed)
288         return;
289      closed = true;
290      try {
291         raw.flush();
292      } finally {
293         if (owned != null)
294            owned.close();
295      }
296   }
297
298   // =================================================================================
299   // Helpers
300   // =================================================================================
301
302   /**
303    * Throws an {@link IOException} if this writer has been {@link #close() closed}.
304    *
305    * @throws IOException If this writer is closed.
306    */
307   private void assertOpen() throws IOException {
308      if (closed)
309         throw new IOException("Token writer is closed.");
310   }
311
312   private void preValueWrite() {
313      if (depth == 0)
314         return;
315      if (stackIsMap[depth - 1] && stackAwaitingKey[depth - 1])
316         throw new IllegalStateException(
317            "Value emitted at map-key position without preceding field(...).");
318   }
319
320   private void afterValue() {
321      if (depth == 0)
322         return;
323      if (stackIsMap[depth - 1])
324         stackAwaitingKey[depth - 1] = true;  // next emit inside this map should be a key
325   }
326
327   @SuppressWarnings({
328      "javabugs:S6466" // The stack arrays start at 16 and double whenever depth reaches their length, so depth always indexes a live slot.
329   })
330   private void pushContainer(boolean isMap) {
331      if (depth == stackIsMap.length) {
332         var n = depth * 2;
333         var nm = new boolean[n];
334         var nk = new boolean[n];
335         System.arraycopy(stackIsMap, 0, nm, 0, depth);
336         System.arraycopy(stackAwaitingKey, 0, nk, 0, depth);
337         stackIsMap = nm;
338         stackAwaitingKey = nk;
339      }
340      stackIsMap[depth] = isMap;
341      stackAwaitingKey[depth] = isMap;  // map starts awaiting a key
342      depth++;
343   }
344
345   private void popContainer(boolean expectMap) {
346      if (depth == 0)
347         throw new IllegalStateException("end-container called with no matching start-container");
348      var actualIsMap = stackIsMap[depth - 1];
349      if (actualIsMap != expectMap)
350         throw new IllegalStateException(
351            "end-container kind mismatch: expected " + (expectMap ? "object" : "array") +
352            ", got " + (actualIsMap ? "object" : "array"));
353      depth--;
354   }
355}