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.Shorts.*;
020
021import java.io.*;
022import java.lang.reflect.*;
023
024import org.apache.juneau.marshall.*;
025import org.apache.juneau.marshall.parser.*;
026import org.apache.juneau.marshall.stream.*;
027
028/**
029 * Reference implementation of the public {@link TokenReader} surface for the MessagePack format.
030 *
031 * <p>
032 * Backed by the existing internal {@link MsgPackInputStream} plumbing.  MessagePack containers
033 * are length-prefixed; this cursor tracks the remaining-element-count per level on a stack and
034 * emits END_* when the count is exhausted.
035 *
036 * <h5 class='section'>Notes:</h5><ul>
037 *    <li class='warn'>This class is not thread safe.
038 *    <li>The cursor is a true O(1)-memory streaming cursor ({@link #isStreaming()} == <jk>true</jk>).
039 * </ul>
040 */
041@SuppressWarnings({
042   "resource" // The cursor's underlying ParserPipe is owned by the caller via try-with-resources on the cursor itself; Eclipse JDT flags the inner pipe as unclosed but that's by design.
043})
044public class MsgPackTokenReader implements TokenReader {
045
046   private final MsgPackInputStream is;
047   private final ParserPipe pipe;
048   private final MsgPackParserSession session;
049
050   private TokenType currentToken = TokenType.NOT_AVAILABLE;
051   private boolean ended;
052
053   // Container stack (parallel arrays).
054   private long[] stackRemaining = new long[16];
055   private boolean[] stackIsMap = new boolean[16];
056   private boolean[] stackAwaitingKey = new boolean[16];
057   private int depth;
058
059   // Scratch for current value.
060   private String currentString;
061   private String currentNumberLexeme;
062   private Number currentNumber;
063   private boolean currentBoolean;
064   private byte[] currentBinary;
065
066   // Binary-native opt-in state (175ad).  Defaults preserve the normalize-by-default contract:
067   // EXT payloads surface as VALUE_BINARY with no metadata.  When nativeMode is true, the EXT
068   // branch additionally captures the signed type byte into 'extType' and sets
069   // 'nativeKind' = MSGPACK_EXT.
070   private boolean nativeMode;
071   private BinaryNativeKind nativeKind = BinaryNativeKind.NONE;
072   private int extType;
073
074   /**
075    * Constructor with default settings.
076    *
077    * @param pipe The parser input pipe.  Must not be <jk>null</jk>.
078    * @throws IOException If a problem occurred opening the underlying stream.
079    */
080   public MsgPackTokenReader(ParserPipe pipe) throws IOException {
081      this(pipe, null);
082   }
083
084   /**
085    * Constructor used by {@link MsgPackParserSession#readTokens(Object)} to plumb the calling
086    * session through.
087    *
088    * @param pipe The parser input pipe.  Must not be <jk>null</jk>.
089    * @param session The {@link MsgPackParserSession} for {@link #read(Class)} delegation,
090    *    or <jk>null</jk> to disable {@code read}.
091    * @throws IOException If a problem occurred opening the underlying stream.
092    */
093   public MsgPackTokenReader(ParserPipe pipe, MsgPackParserSession session) throws IOException {
094      this.pipe = pipe;
095      this.is = new MsgPackInputStream(pipe);
096      this.session = session;
097   }
098
099   /**
100    * Enables or disables binary-native opt-in mode.
101    *
102    * <p>
103    * When enabled, MsgPack {@code ext} tokens carry the signed type byte via
104    * {@link #getExtType()}.  When disabled (default), the payload still surfaces as
105    * {@link TokenType#VALUE_BINARY} but the type byte is dropped &mdash; identical to the
106    * pre-native-mode behavior.
107    *
108    * @param value <jk>true</jk> to enable native mode.
109    * @return This object.
110    */
111   public MsgPackTokenReader setNativeMode(boolean value) {
112      nativeMode = value;
113      return this;
114   }
115
116   /**
117    * Sets the maximum allowed wire-declared length (in bytes) for binary/string payloads read by this cursor.
118    *
119    * <p>
120    * The cursor is intentionally NOT bounded by an element-count cap for arrays/maps &mdash; it is an
121    * O(1)-memory streaming cursor and never materializes containers, so it accepts large length-prefixed
122    * containers.  Values {@code <= 0} disable the maximum-length cap (the negative-length check still
123    * applies).
124    *
125    * @param value The maximum length in bytes.
126    * @return This object.
127    */
128   public MsgPackTokenReader setMaxLength(int value) {
129      is.setMaxLength(value);
130      return this;
131   }
132
133   // ==============================================================================================
134   // State-machine summary.  MsgPack containers are length-prefixed (no indefinite-length
135   // encoding), so next() reads the data-type tag via MsgPackInputStream.readDataType() and
136   // dispatches based on: (1) whether we're at end-of-input, (2) whether the current container's
137   // remaining element count has reached zero (emit END_*), and (3) within a map, whether we're
138   // at a key position or value position.
139   //
140   // Per-level container state lives on parallel stacks (stackRemaining + stackIsMap +
141   // stackAwaitingKey, indexed by depth).  Each emit decrements stackRemaining[top]; map-level
142   // emits also flip stackAwaitingKey[top] between key and value positions.
143   // ==============================================================================================
144   @Override /* TokenReader */
145   public TokenType next() throws IOException, ParseException {
146      if (ended) {
147         currentToken = TokenType.END_OF_STREAM;
148         return currentToken;
149      }
150      currentString = null;
151      currentNumberLexeme = null;
152      currentNumber = null;
153      currentBinary = null;
154      nativeKind = BinaryNativeKind.NONE;
155
156      // Inside a container at element-count zero: emit END_*.
157      if (depth > 0 && stackRemaining[depth - 1] == 0) {
158         popContainer();
159         return currentToken;
160      }
161
162      DataType dt;
163      try {
164         dt = is.readDataType();
165      } catch (IOException e) {
166         if (depth == 0) {
167            ended = true;
168            currentToken = TokenType.END_OF_STREAM;
169            return currentToken;
170         }
171         throw e;
172      }
173
174      // Map key position: emit FIELD_NAME.
175      if (depth > 0 && stackIsMap[depth - 1] && stackAwaitingKey[depth - 1]) {
176         currentToken = TokenType.FIELD_NAME;
177         currentString = readScalarAsString(dt);
178         consumedOneElement();
179         return currentToken;
180      }
181
182      switch (dt) {
183         case ARRAY -> {
184            pushContainer(false, is.readLength());
185            currentToken = TokenType.START_ARRAY;
186         }
187         case MAP -> {
188            var n = is.readLength();
189            // Pair-count -> element-count.  Guard the doubling so a malformed/overflowing count cannot
190            // wrap the element-count bookkeeping negative (the streaming cursor is intentionally exempt
191            // from the maxLength element-count cap, which is enforced on the databind path).
192            if (n < 0 || n > Long.MAX_VALUE / 2)
193               throw new ParseException("Invalid MessagePack map length: %s", n);
194            pushContainer(true, n * 2);
195            currentToken = TokenType.START_OBJECT;
196         }
197         case INT -> {
198            var v = (long) is.readInt();
199            currentNumber = v;
200            currentNumberLexeme = Long.toString(v);
201            currentToken = TokenType.VALUE_NUMBER;
202            consumedOneElement();
203         }
204         case LONG -> {
205            var v = is.readLong();
206            currentNumber = v;
207            currentNumberLexeme = Long.toString(v);
208            currentToken = TokenType.VALUE_NUMBER;
209            consumedOneElement();
210         }
211         case FLOAT -> {
212            var v = (double) is.readFloat();
213            currentNumber = v;
214            currentNumberLexeme = Double.toString(v);
215            currentToken = TokenType.VALUE_NUMBER;
216            consumedOneElement();
217         }
218         case DOUBLE -> {
219            var v = is.readDouble();
220            currentNumber = v;
221            currentNumberLexeme = Double.toString(v);
222            currentToken = TokenType.VALUE_NUMBER;
223            consumedOneElement();
224         }
225         case BOOLEAN -> {
226            currentBoolean = is.readBoolean();
227            currentToken = TokenType.VALUE_BOOLEAN;
228            consumedOneElement();
229         }
230         case NULL -> {
231            currentToken = TokenType.VALUE_NULL;
232            consumedOneElement();
233         }
234         case STRING -> {
235            currentString = is.readString();
236            currentToken = TokenType.VALUE_STRING;
237            consumedOneElement();
238         }
239         case BIN -> {
240            currentBinary = is.readBinary();
241            currentToken = TokenType.VALUE_BINARY;
242            consumedOneElement();
243         }
244         case EXT -> {
245            // Q3 default: normalize to common scalars; the extension payload surfaces as
246            // VALUE_BINARY.  In native mode (175ad), the signed ext type byte is also
247            // captured for retrieval via getExtType().
248            if (nativeMode) {
249               extType = (byte) is.getExtType();
250               nativeKind = BinaryNativeKind.MSGPACK_EXT;
251            }
252            currentBinary = is.readBinary();
253            currentToken = TokenType.VALUE_BINARY;
254            consumedOneElement();
255         }
256         default -> throw new ParseException("Unexpected MsgPack data type: %s", dt);
257      }
258      return currentToken;
259   }
260
261   private String readScalarAsString(DataType dt) throws IOException, ParseException {
262      switch (dt) {
263         case STRING:    return is.readString();
264         case INT:       return Long.toString(is.readInt());
265         case LONG:      return Long.toString(is.readLong());
266         case BIN: {
267            var b = is.readBinary();
268            return new String(b, java.nio.charset.StandardCharsets.UTF_8);
269         }
270         case BOOLEAN:   return Boolean.toString(is.readBoolean());
271         case FLOAT:     return Float.toString(is.readFloat());
272         case DOUBLE:    return Double.toString(is.readDouble());
273         default:
274            throw new ParseException("Cannot use MsgPack data type %s as a map key", dt);
275      }
276   }
277
278   private void pushContainer(boolean isMap, long remaining) {
279      if (depth == stackRemaining.length) {
280         var n = depth * 2;
281         var nr = new long[n];
282         var nm = new boolean[n];
283         var nk = new boolean[n];
284         System.arraycopy(stackRemaining, 0, nr, 0, depth);
285         System.arraycopy(stackIsMap, 0, nm, 0, depth);
286         System.arraycopy(stackAwaitingKey, 0, nk, 0, depth);
287         stackRemaining = nr;
288         stackIsMap = nm;
289         stackAwaitingKey = nk;
290      }
291      stackRemaining[depth] = remaining;
292      stackIsMap[depth] = isMap;
293      stackAwaitingKey[depth] = isMap;
294      depth++;
295   }
296
297   private void popContainer() {
298      depth--;
299      currentToken = stackIsMap[depth] ? TokenType.END_OBJECT : TokenType.END_ARRAY;
300      consumedOneElement();
301   }
302
303   private void consumedOneElement() {
304      if (depth == 0)
305         return;
306      var top = depth - 1;
307      stackRemaining[top]--;
308      if (stackIsMap[top])
309         stackAwaitingKey[top] = !stackAwaitingKey[top];
310   }
311
312   @Override /* TokenReader */
313   public TokenType getCurrentToken() { return currentToken; }
314
315   @Override /* TokenReader */
316   public int getDepth() { return depth; }
317
318   @Override /* TokenReader */
319   public String getFieldName() {
320      if (currentToken != TokenType.FIELD_NAME)
321         throw isex("Current token is not FIELD_NAME (was %s)", currentToken);
322      return currentString;
323   }
324
325   @Override /* TokenReader */
326   public String getString() {
327      switch (currentToken) {
328         case VALUE_STRING:  return currentString;
329         case FIELD_NAME:    return currentString;
330         case VALUE_NUMBER:  return currentNumberLexeme;
331         case VALUE_BOOLEAN: return Boolean.toString(currentBoolean);
332         case VALUE_NULL:    return null;
333         default: throw isex("Current token does not have a string view (was %s)", currentToken);
334      }
335   }
336
337   @Override /* TokenReader */
338   public Number getNumber() throws ParseException {
339      if (currentToken != TokenType.VALUE_NUMBER)
340         throw isex("Current token is not VALUE_NUMBER (was %s)", currentToken);
341      return currentNumber;
342   }
343
344   @Override /* TokenReader */
345   public String getNumberLexeme() {
346      if (currentToken != TokenType.VALUE_NUMBER)
347         throw isex("Current token is not VALUE_NUMBER (was %s)", currentToken);
348      return currentNumberLexeme;
349   }
350
351   @Override /* TokenReader */
352   public boolean getBool() {
353      if (currentToken != TokenType.VALUE_BOOLEAN)
354         throw isex("Current token is not VALUE_BOOLEAN (was %s)", currentToken);
355      return currentBoolean;
356   }
357
358   @Override /* TokenReader */
359   public byte[] getBinary() {
360      if (currentToken != TokenType.VALUE_BINARY)
361         throw isex("Current token is not VALUE_BINARY (was %s)", currentToken);
362      return cp(currentBinary);
363   }
364
365   @Override /* TokenReader */
366   public boolean canRead() throws IOException, ParseException {
367      return !ended && !(depth > 0 && stackRemaining[depth - 1] == 0);
368   }
369
370   @Override /* TokenReader */
371   public <T> T read(Class<T> type) throws IOException, ParseException {
372      return read(session().getClassMeta(type));
373   }
374
375   @Override /* TokenReader */
376   public <T> T read(ClassMeta<T> type) throws IOException, ParseException {
377      var s = session();
378      if (!canRead())
379         throw new IllegalStateException(MSG_READ_NON_VALUE_STATE);
380      try {
381         T o = s.readAnything(type, is, null, null);
382         consumedOneElement();
383         currentToken = TokenType.NOT_AVAILABLE;
384         return o;
385      } catch (Exception e) {
386         if (e instanceof IOException e2) throw e2;
387         if (e instanceof ParseException e2) throw e2;
388         throw new ParseException(s, "read failed: %s", e.getMessage());
389      }
390   }
391
392   @Override /* TokenReader */
393   public <T> T read(Type type, Type... args) throws IOException, ParseException {
394      return read(session().<T>getClassMeta(type, args));
395   }
396
397   private MsgPackParserSession session() {
398      if (session == null)
399         throw new UnsupportedOperationException(
400            "read is not available on this MsgPackTokenReader (no session attached). " +
401            "Open the cursor via MsgPackParser.readTokens(...) to enable read.");
402      return session;
403   }
404
405   @Override /* TokenReader */
406   public void skipChildren() throws IOException, ParseException {
407      if (currentToken != TokenType.START_OBJECT && currentToken != TokenType.START_ARRAY)
408         return;
409      var startDepth = depth - 1;
410      while (depth > startDepth) {
411         var t = next();
412         if (t == TokenType.END_OF_STREAM)
413            throw new ParseException("Unexpected end of input while skipping children");
414      }
415   }
416
417   @Override /* TokenReader */
418   public boolean isStreaming() { return true; }
419
420   @Override /* TokenReader */
421   public void close() throws IOException { pipe.close(); }
422
423   @Override /* TokenReader */
424   public BinaryNativeKind getNativeKind() {
425      return nativeKind;
426   }
427
428   @Override /* TokenReader */
429   public int getTagCount() {
430      return 0;
431   }
432
433   @Override /* TokenReader */
434   public long getTag(int index) {
435      throw new IndexOutOfBoundsException("Tag index " + index + " out of bounds for tagCount 0");
436   }
437
438   @Override /* TokenReader */
439   public int getExtType() {
440      if (nativeKind != BinaryNativeKind.MSGPACK_EXT)
441         throw isex("Current token is not a MsgPack ext (nativeKind=%s)", nativeKind);
442      return extType;
443   }
444
445   @Override /* TokenReader */
446   public int getSimpleValue() {
447      throw new IllegalStateException("MsgPack cursor has no CBOR simple value.");
448   }
449}