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.parser;
018
019import static org.apache.juneau.commons.utils.Shorts.*;
020import static org.apache.juneau.commons.utils.StringUtils.*;
021
022import java.io.*;
023import java.util.*;
024
025import org.apache.juneau.commons.io.*;
026
027/**
028 * Similar to a {@link PushbackReader} with a pushback buffer of 1 character.
029 *
030 * <p>
031 * Code is optimized to work with a 1 character buffer.
032 *
033 * <p>
034 * Additionally keeps track of current line and column number, and provides the ability to set mark points and capture
035 * characters from the previous mark point.
036 *
037 * <h5 class='section'>Notes:</h5><ul>
038 *    <li class='warn'>This class is not thread safe.
039 * </ul>
040 *
041 * <h5 class='section'>See Also:</h5><ul>
042 *    <li class='link'><a class="doclink" href="https://juneau.apache.org/docs/topics/SerializersAndParsers">Serializers and Parsers</a>
043
044 * </ul>
045 */
046@SuppressWarnings({
047   "java:S115", // Constants use UPPER_snakeCase convention (e.g., MSG_bufferUnderflow)
048   "java:S1192", // Duplicated literals read more clearly inline than as constants.
049   "resource" // ParserReader is managed by caller
050})
051public class ParserReader extends Reader implements Positionable {
052
053   /**
054    * Maximum number of previously-read characters preserved as unread lookback when the buffer is
055    * refilled <em>without</em> an active {@link #mark()}. Originally 10, which was sufficient for
056    * JSON's short tokens but too small for YAML's indent-aware parser: a deeply-nested OpenAPI 3.1
057    * document can have indent depth well past 10 chars on a single line, and the YAML parser's
058    * {@code skipBlanksAndCountIndent} / {@code unreadSpaces} pair (and similar peek-then-unread
059    * patterns in block-scalar handling) tries to unread the entire indent at once. When the indent
060    * read crossed the buffer boundary, the rest underflowed. 256 chars covers any realistic YAML
061    * document indent depth (128 levels at 2-space indent) while still leaving 768 chars per
062    * 1024-char refill for forward-progress reads.
063    */
064   private static final int UNMARKED_LOOKBACK_CHARS = 256;
065
066   /** Wrapped reader */
067   protected final Reader r;
068
069   private char[] buff;       // Internal character buffer
070   private int line = 1;      // Current line number
071   private int column;        // Current column number
072   private int iCurrent;      // Current pointer into character buffer
073   private int iMark = -1;    // Mark position in buffer
074   private int iEnd;          // The last good character position in the buffer
075   private boolean endReached;
076   private BitSet holes;      // Deleted positions (relative to iMark); null unless an escape was collapsed in the current mark
077   private final boolean unbuffered;
078
079   /**
080    * Constructor.
081    *
082    * @param pipe The parser input.
083    *    <br>Must not be <jk>null</jk>.
084    * @throws IOException Thrown by underlying stream.
085    */
086   public ParserReader(ParserPipe pipe) throws IOException {
087      this.unbuffered = pipe.unbuffered;
088      if (pipe.isString()) {
089         String in = pipe.getInputAsString();
090         this.r = new CharSequenceReader(in);
091         this.buff = new char[in.length() < 1024 ? in.length() : 1024];
092      } else {
093         Reader reader = pipe.getReader();
094         if (reader instanceof ParserReader reader2)
095            this.r = reader2.r;
096         else
097            this.r = reader;
098         this.buff = new char[1024];
099      }
100      pipe.setPositionable(this);
101   }
102
103   /**
104    * No-op.
105    *
106    * <p>
107    * Input readers are closed in the {@link ParserPipe} class.
108    *
109    * @throws IOException If a problem occurred trying to read from the reader.
110    */
111   @Override /* Overridden from Reader */
112   public void close() throws IOException {
113      // No-op
114   }
115
116   /**
117    * Trims off the last character in the marking buffer.
118    *
119    * <p>
120    * Useful for removing escape characters from sequences.
121    *
122    * @return This object.
123    */
124   public final ParserReader delete() {
125      return delete(1);
126   }
127
128   /**
129    * Trims off the specified number of last characters in the marking buffer.
130    * Useful for removing escape characters from sequences.
131    *
132    * @param count The number of characters to delete.
133    * @return This object.
134    */
135   public final ParserReader delete(int count) {
136      markHoles(iCurrent - count, iCurrent);
137      return this;
138   }
139
140   /**
141    * Returns the contents of the reusable character buffer as a string, and resets the buffer for next usage.
142    *
143    * @return The contents of the reusable character buffer as a string.
144    */
145   public final String getMarked() { return getMarked(0, 0); }
146
147   /**
148    * Same as {@link #getMarked()} except allows you to specify offsets into the buffer.
149    *
150    * <p>
151    * For example, to return the marked string, but trim the first and last characters, call the following:
152    * <p class='bjava'>
153    *    getFromMarked(1, -1);
154    * </p>
155    *
156    * @param offsetStart The offset of the start position.
157    * @param offsetEnd The offset of the end position.
158    * @return The contents of the reusable character buffer as a string.
159    */
160   public final String getMarked(int offsetStart, int offsetEnd) {
161      int offset = 0;
162
163      // Holes are positions removed by delete()/replace() that we need to get rid of now.
164      // They are tracked by position (not by sentinel character) so real DEL characters are preserved.
165      if (holes != null) {
166         for (var i = iMark; i < iCurrent; i++) {
167            if (holes.get(i - iMark))
168               offset++;
169            else
170               buff[i - offset] = buff[i];
171         }
172         holes = null;
173      }
174      int start = iMark + offsetStart;
175      int len = iCurrent - iMark + offsetEnd - offsetStart - offset;
176      var s = new String(buff, start, len);
177      iMark = -1;
178      return s;
179   }
180
181   @Override /* Overridden from Positionable */
182   public Position getPosition() { return new Position(line, column); }
183
184   /**
185    * Start buffering the calls to read() so that the text can be gathered from the mark point on calling {@code getFromMarked()}.
186    */
187   public final void mark() {
188      iMark = iCurrent;
189      holes = null;
190   }
191
192   /**
193    * Reads a numeric string from the specified reader.
194    *
195    * @return The parsed number string.
196    * @throws IOException Thrown by underlying stream.
197    */
198   public String readNumberString() throws IOException {
199      mark();
200      int c = 0;
201      while (true) {
202         c = read();
203         if (c == -1 || ! isNumberChar((char)c)) {
204            if (c != -1)
205               unread();
206            break;
207         }
208      }
209      return getMarked();
210   }
211
212   /**
213    * Peeks the next character in the stream.
214    *
215    * <p>
216    * This is equivalent to doing a {@code read()} followed by an {@code unread()}.
217    *
218    * @return The peeked character, or (char)-1 if the end of the stream has been reached.
219    * @throws IOException If a problem occurred trying to read from the reader.
220    */
221   public final int peek() throws IOException {
222      int c = read();
223      if (c != -1)
224         unread();
225      return c;
226   }
227
228   /**
229    * Same as {@link #peek()} but skips over any whitespace characters.
230    *
231    * <p>
232    * This is equivalent to doing a {@code read()} followed by an {@code unread()}.
233    *
234    * @return The peeked character, or (char)-1 if the end of the stream has been reached.
235    * @throws IOException If a problem occurred trying to read from the reader.
236    */
237   public final int peekSkipWs() throws IOException {
238      while (true) {
239         var c = read();
240         var isWs = Character.isWhitespace(c);
241         if (c != -1 && ! isWs)
242            unread();
243         if (! isWs)
244            return c;
245      }
246   }
247
248   /**
249    * Reads a single character.
250    *
251    * <p>
252    * Note that this method does NOT process extended unicode characters (i.e. characters above 0x10000), but rather
253    * returns them as two <jk>char</jk>s.
254    * Use {@link #readCodePoint()} to ensure proper handling of extended unicode.
255    *
256    * @return The character read, or -1 if the end of the stream has been reached.
257    * @throws IOException If a problem occurred trying to read from the reader.
258    */
259   @Override /* Overridden from Reader */
260   public final int read() throws IOException {
261      int c = readFromBuff();
262      if (c == -1)
263         return -1;
264      if (c == '\n') {
265         line++;
266         column = 0;
267      } else {
268         column++;
269      }
270      return c;
271   }
272
273   /**
274    * Subclasses can override this method to provide additional filtering.
275    *
276    * <p>
277    * Default implementation simply calls the same method on the underlying reader.
278    */
279   @Override /* Overridden from Reader */
280   public int read(char[] cbuf, int off, int len) throws IOException {
281      return r.read(cbuf, off, unbuffered ? 1 : len);
282   }
283
284   /**
285    * Read the specified number of characters off the stream.
286    *
287    * @param num The number of characters to read.
288    * @return The characters packaged as a String.
289    * @throws IOException If a problem occurred trying to read from the reader.
290    */
291   public final String read(int num) throws IOException {
292      var c = new char[num];
293      for (var i = 0; i < num; i++) {
294         var c2 = read();
295         if (c2 == -1)
296            return new String(c, 0, i);
297         c[i] = (char)c2;
298      }
299      return new String(c);
300   }
301
302   /**
303    * Same as {@link #read()} but detects and combines extended unicode characters (characters above 0x10000).
304    *
305    * @return The character read, or -1 if the end of the stream has been reached.
306    * @throws IOException If a problem occurred trying to read from the reader.
307    */
308   public final int readCodePoint() throws IOException {
309      int c = read();
310
311      // Characters that take up 2 chars.
312      if (c >= 0xd800 && c <= 0xdbff) {
313         var low = read();
314         if (low >= 0xdc00 && low <= 0xdfff)
315            c = 0x10000 + ((c - 0xd800) << 10) + (low - 0xdc00);
316      }
317
318      return c;
319   }
320
321   /**
322    * Same as {@link #read()} but skips over any whitespace characters.
323    *
324    * @return The first non-whitespace character, or -1 if the end of stream reached.
325    * @throws IOException Thrown by underlying stream.
326    */
327   public final int readSkipWs() throws IOException {
328      while (true) {
329         var c = read();
330         if (c == -1 || ! Character.isWhitespace(c))
331            return c;
332      }
333   }
334
335   /**
336    * Replace the last read character in the buffer with the specified character.
337    *
338    * @param c The new character.
339    * @return This object.
340    * @throws IOException Thrown by underlying stream.
341    */
342   public final ParserReader replace(char c) throws IOException {
343      return replace(c, 1);
344   }
345
346   /**
347    * Replaces the last character in the marking buffer with the specified character.
348    *
349    * <p>
350    * <c>offset</c> must be at least <c>1</c> for normal characters, and <c>2</c> for extended
351    * unicode characters in order for the replacement to fit into the buffer.
352    *
353    * @param c The new character.
354    * @param offset The offset.
355    * @return This object.
356    * @throws IOException Thrown by underlying stream.
357    */
358   public final ParserReader replace(int c, int offset) throws IOException {
359      if (c < 0x10000) {
360         if (offset < 1)
361            throw ioex("Buffer underflow.");
362         clearHoles(iCurrent - offset, iCurrent - offset + 1);
363         buff[iCurrent - offset] = (char)c;
364      } else {
365         if (offset < 2)
366            throw ioex("Buffer underflow.");
367         c -= 0x10000;
368         clearHoles(iCurrent - offset, iCurrent - offset + 2);
369         buff[iCurrent - offset] = (char)(0xd800 + (c >> 10));
370         buff[iCurrent - offset + 1] = (char)(0xdc00 + (c & 0x3ff));
371         offset--;
372      }
373      // Mark the gap as deleted.
374      if (offset > 1)
375         markHoles(iCurrent - offset + 1, iCurrent);
376      return this;
377   }
378
379   /*
380    * Un-deletes [from,to) because a replacement character is being written there.
381    */
382   private void clearHoles(int from, int to) {
383      if (holes != null && iMark >= 0)
384         holes.clear(Math.max(from - iMark, 0), Math.max(to - iMark, 0));
385   }
386
387   /*
388    * Records [from,to) as deleted; positions are stored relative to iMark so they survive buffer shifts.
389    */
390   private void markHoles(int from, int to) {
391      if (iMark < 0)
392         return;
393      if (holes == null)
394         holes = new BitSet();
395      holes.set(Math.max(from - iMark, 0), Math.max(to - iMark, 0));
396   }
397
398   /**
399    * Pushes the last read character back into the stream.
400    *
401    * @return This object.
402    * @throws IOException If a problem occurred trying to read from the reader.
403    */
404   public ParserReader unread() throws IOException {
405      if (iCurrent <= 0)
406         throw ioex("Buffer underflow.");
407      iCurrent--;
408      if (column == 0)
409         line--;
410      else
411         column--;
412      return this;
413   }
414
415   @SuppressWarnings({
416      "java:S3776" // Cognitive complexity acceptable for buffer reading logic
417   })
418   private final int readFromBuff() throws IOException {
419      while (iCurrent >= iEnd) {
420         if (endReached)
421            return -1;
422
423         // If there's still space at the end of this buffer, fill it.
424         // Make sure there's at least 2 character spaces free for extended unicode characters.
425         if (iEnd + 1 < buff.length) {
426            int x = read(buff, iCurrent, buff.length - iEnd);
427            if (x == -1) {
428               endReached = true;
429               return -1;
430            }
431            iEnd += x;
432
433         } else {
434            // If we're currently marking, then we want to copy from the current mark point
435            // to the beginning of the buffer and then fill in the remainder of buffer.
436            if (iMark >= 0) {
437
438               // If we're marking from the beginning of the array, we double the size of the
439               // buffer.  This isn't likely to occur often.
440               if (iMark == 0) {
441                  var buff2 = new char[buff.length << 1];
442                  System.arraycopy(buff, 0, buff2, 0, buff.length);
443                  buff = buff2;
444
445                  // Otherwise, we copy what's currently marked to the beginning of the buffer.
446               } else {
447                  int copyBuff = iMark;
448                  System.arraycopy(buff, copyBuff, buff, 0, buff.length - copyBuff);
449                  iCurrent -= copyBuff;
450                  iMark -= copyBuff;
451               }
452               int expected = buff.length - iCurrent;
453
454               int x = read(buff, iCurrent, expected);
455               if (x == -1) {
456                  endReached = true;
457                  iEnd = iCurrent;
458                  return -1;
459               }
460               iEnd = iCurrent + x;
461            } else {
462               // Copy the last UNMARKED_LOOKBACK_CHARS chars in the buffer to the beginning so
463               // callers can still unread() that far across a buffer-boundary refill. See the
464               // constant's javadoc for why 256 (not 10) — widened to handle YAML's deep-indent
465               // peek-then-unread patterns where a single line's indent can exceed 10 chars.
466               int copyBuff = Math.min(iCurrent, UNMARKED_LOOKBACK_CHARS);
467               System.arraycopy(buff, iCurrent - copyBuff, buff, 0, copyBuff);
468
469               // Number of characters we expect to copy on the next read.
470               int expected = buff.length - copyBuff;
471               int x = read(buff, copyBuff, expected);
472               iCurrent = copyBuff;
473               if (x == -1) {
474                  endReached = true;
475                  iEnd = iCurrent;
476                  return -1;
477               }
478               iEnd = iCurrent + x;
479            }
480         }
481      }
482      return buff[iCurrent++];
483   }
484}