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.ObjectUtils.*;
020import static org.apache.juneau.commons.utils.Shorts.*;
021
022import java.io.*;
023import java.math.*;
024
025import org.apache.juneau.marshall.stream.*;
026
027/**
028 * Reference implementation of the public {@link TokenWriter} surface for the MessagePack format.
029 *
030 * <p>
031 * MessagePack containers are length-prefixed (no indefinite-length encoding); the writer therefore
032 * <b>buffers</b> each open container's body bytes until {@link #endObject()} / {@link #endArray()}
033 * is called, then writes the length-prefix header followed by the buffered bytes.  Memory cost
034 * is O(largest open container).
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 MsgPack {@code bin}, not to a base64 string fallback.
039 * </ul>
040 */
041@SuppressWarnings({
042   "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.
043})
044public class MsgPackTokenWriter implements TokenWriter {
045
046   /**
047    * Cursor-level settings honored by {@link MsgPackTokenWriter}.
048    *
049    * @param walk The {@link PojoWalker.Options walk options} used by {@link #object(Object)}.
050    */
051   @SuppressWarnings({
052      "javadoc" // Record components are documented in the enclosing Javadoc; per-component tags are omitted.
053   })
054   public record Settings(PojoWalker.Options walk) {
055
056      /** Default canonical setting: default walk options. */
057      public static final Settings DEFAULT = new Settings(PojoWalker.Options.DEFAULT);
058   }
059
060   private final OutputStream finalOut;
061   private final Closeable owned;
062   private final Settings settings;
063
064   // Per-level buffers.  The active write target is the top of this stack; if depth==0 we write
065   // directly to finalOut.
066   private final java.util.ArrayDeque<ByteArrayOutputStream> buffers = new java.util.ArrayDeque<>();
067   // Per-level container kind: true=map, false=array.
068   private final java.util.ArrayDeque<Boolean> isMapStack = new java.util.ArrayDeque<>();
069   // Per-level element-count (for arrays = number of values emitted; for maps = number of pairs).
070   // For maps, incremented once per field() (= once per pair).
071   private final java.util.ArrayDeque<Integer> elementCount = new java.util.ArrayDeque<>();
072   // Per-level: for maps, true when the next emit should be a field (key), false after the
073   // field when the next emit should be the value.
074   private final java.util.ArrayDeque<Boolean> awaitingKey = new java.util.ArrayDeque<>();
075
076   private boolean closed;
077
078   /**
079    * Constructor with default settings.
080    *
081    * @param out The {@link OutputStream} to emit MsgPack to.  Must not be <jk>null</jk>.
082    */
083   public MsgPackTokenWriter(OutputStream out) {
084      this(out, null, Settings.DEFAULT);
085   }
086
087   /**
088    * Constructor with explicit settings.
089    *
090    * @param out The {@link OutputStream} to emit MsgPack to.  Must not be <jk>null</jk>.
091    * @param settings The settings.  Must not be <jk>null</jk>.
092    */
093   public MsgPackTokenWriter(OutputStream out, Settings settings) {
094      this(out, null, settings);
095   }
096
097   private MsgPackTokenWriter(OutputStream out, Closeable owned, Settings settings) {
098      reqnn("out", out);
099      reqnn("settings", settings);
100      this.finalOut = out;
101      this.owned = owned;
102      this.settings = settings;
103   }
104
105   /** The active write target for child bytes (top buffer or finalOut). */
106   private OutputStream activeOut() {
107      return buffers.isEmpty() ? finalOut : buffers.peek();
108   }
109
110   @Override /* TokenWriter */
111   public TokenWriter startObject() throws IOException {
112      assertOpen();
113      // Push a new buffer; child writes go there until endObject.
114      buffers.push(new ByteArrayOutputStream());
115      isMapStack.push(true);
116      elementCount.push(0);
117      awaitingKey.push(true);
118      return this;
119   }
120
121   @Override /* TokenWriter */
122   public TokenWriter endObject() throws IOException {
123      assertOpen();
124      if (buffers.isEmpty() || !isTrue(isMapStack.peek()))
125         throw new IllegalStateException("endObject called with no matching startObject");
126      var body = buffers.pop();
127      var count = elementCount.pop();
128      isMapStack.pop();
129      awaitingKey.pop();
130      // Write the map header to whatever is now the active stream, then the buffered body.
131      var header = new MsgPackOutputStream(activeOut());
132      header.startMap(count);
133      body.writeTo(activeOut());
134      afterValue();
135      return this;
136   }
137
138   @Override /* TokenWriter */
139   public TokenWriter startArray() throws IOException {
140      assertOpen();
141      buffers.push(new ByteArrayOutputStream());
142      isMapStack.push(false);
143      elementCount.push(0);
144      awaitingKey.push(false);
145      return this;
146   }
147
148   @Override /* TokenWriter */
149   public TokenWriter endArray() throws IOException {
150      assertOpen();
151      if (buffers.isEmpty() || isTrue(isMapStack.peek()))
152         throw new IllegalStateException("endArray called with no matching startArray");
153      var body = buffers.pop();
154      var count = elementCount.pop();
155      isMapStack.pop();
156      awaitingKey.pop();
157      var header = new MsgPackOutputStream(activeOut());
158      header.startArray(count);
159      body.writeTo(activeOut());
160      afterValue();
161      return this;
162   }
163
164   @Override /* TokenWriter */
165   public TokenWriter fieldName(String name) throws IOException {
166      assertOpen();
167      reqnn("name", name);
168      if (isMapStack.isEmpty() || !isTrue(isMapStack.peek()))
169         throw new IllegalStateException("field called outside an object");
170      if (!isTrue(awaitingKey.peek()))
171         throw new IllegalStateException("field called twice without an intervening value");
172      new MsgPackOutputStream(activeOut()).appendString(name);
173      // Replace top of awaitingKey with false (we just wrote the key; expect a value next).
174      awaitingKey.pop();
175      awaitingKey.push(false);
176      // Increment pair count once (per pair) — happens at the field, not the value.
177      var c = elementCount.pop();
178      elementCount.push(c + 1);
179      return this;
180   }
181
182   @Override /* TokenWriter */
183   public TokenWriter string(String value) throws IOException {
184      assertOpen();
185      if (value == null)
186         return nil();
187      preValueCheck();
188      new MsgPackOutputStream(activeOut()).appendString(value);
189      afterValue();
190      return this;
191   }
192
193   @Override /* TokenWriter */
194   public TokenWriter number(Number value) throws IOException {
195      assertOpen();
196      if (value == null)
197         return nil();
198      preValueCheck();
199      new MsgPackOutputStream(activeOut()).appendNumber(value);
200      afterValue();
201      return this;
202   }
203
204   @Override /* TokenWriter */
205   public TokenWriter number(long value) throws IOException {
206      assertOpen();
207      preValueCheck();
208      new MsgPackOutputStream(activeOut()).appendLong(value);
209      afterValue();
210      return this;
211   }
212
213   @Override /* TokenWriter */
214   public TokenWriter number(double value) throws IOException {
215      assertOpen();
216      preValueCheck();
217      new MsgPackOutputStream(activeOut()).appendDouble(value);
218      afterValue();
219      return this;
220   }
221
222   @Override /* TokenWriter */
223   public TokenWriter number(BigDecimal value) throws IOException {
224      assertOpen();
225      if (value == null)
226         return nil();
227      preValueCheck();
228      new MsgPackOutputStream(activeOut()).appendDouble(value.doubleValue());
229      afterValue();
230      return this;
231   }
232
233   @Override /* TokenWriter */
234   public TokenWriter number(BigInteger value) throws IOException {
235      assertOpen();
236      if (value == null)
237         return nil();
238      preValueCheck();
239      // Align with the databind serializer path (G4): long-range -> INT64, [2^63,2^64-1] -> UINT64,
240      // otherwise throw — rather than the previous longValueExact() which threw on the representable
241      // [2^63,2^64-1] range.
242      new MsgPackOutputStream(activeOut()).appendBigInteger(value);
243      afterValue();
244      return this;
245   }
246
247   @Override /* TokenWriter */
248   public TokenWriter bool(boolean value) throws IOException {
249      assertOpen();
250      preValueCheck();
251      new MsgPackOutputStream(activeOut()).appendBoolean(value);
252      afterValue();
253      return this;
254   }
255
256   @Override /* TokenWriter */
257   public TokenWriter nil() throws IOException {
258      assertOpen();
259      preValueCheck();
260      new MsgPackOutputStream(activeOut()).appendNull();
261      afterValue();
262      return this;
263   }
264
265   @Override /* TokenWriter */
266   public TokenWriter binary(byte[] value) throws IOException {
267      assertOpen();
268      if (value == null)
269         return nil();
270      preValueCheck();
271      new MsgPackOutputStream(activeOut()).appendBinary(value);
272      afterValue();
273      return this;
274   }
275
276   @Override /* TokenWriter */
277   public MsgPackTokenWriter writeExt(int type, byte[] payload) throws IOException {
278      assertOpen();
279      if (payload == null)
280         throw new IllegalArgumentException("ext payload must not be null");
281      preValueCheck();
282      new MsgPackOutputStream(activeOut()).writeExt(type, payload);
283      afterValue();
284      return this;
285   }
286
287   @Override /* TokenWriter */
288   public TokenWriter object(Object value) throws IOException {
289      assertOpen();
290      PojoWalker.walk(this, value, settings.walk);
291      return this;
292   }
293
294   @Override /* TokenWriter */
295   public boolean isStreaming() { return true; }
296
297   @Override /* TokenWriter */
298   public void flush() throws IOException { finalOut.flush(); }
299
300   @Override /* TokenWriter */
301   public void close() throws IOException {
302      if (closed)
303         return;
304      closed = true;
305      try {
306         finalOut.flush();
307      } finally {
308         if (owned != null)
309            owned.close();
310      }
311   }
312
313   /**
314    * Throws an {@link IOException} if this writer has been {@link #close() closed}.
315    *
316    * @throws IOException If this writer is closed.
317    */
318   private void assertOpen() throws IOException {
319      if (closed)
320         throw new IOException("Token writer is closed.");
321   }
322
323   private void preValueCheck() {
324      // At map-key position, value emits are illegal — caller must call field() first.
325      if (!isMapStack.isEmpty() && isTrue(isMapStack.peek()) && isTrue(awaitingKey.peek()))
326         throw new IllegalStateException(
327            "Value emitted at map-key position without preceding field(...).");
328   }
329
330   private void afterValue() {
331      if (isMapStack.isEmpty())
332         return;
333      // After emitting a value:
334      // - In a map, flip awaitingKey back to true (next emit is the next pair's key).
335      // - In an array, increment element count.
336      if (isTrue(isMapStack.peek())) {
337         awaitingKey.pop();
338         awaitingKey.push(true);
339      } else {
340         var c = elementCount.pop();
341         elementCount.push(c + 1);
342      }
343   }
344}