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.stream;
018
019import java.io.*;
020import java.lang.reflect.*;
021
022import org.apache.juneau.marshall.*;
023import org.apache.juneau.marshall.parser.*;
024
025/**
026 * O(1)-memory wrappers that adapt a fine-grained {@link TokenReader} / {@link TokenWriter} to
027 * the array-element record-stream contract.  Memory does not scale with the number of array
028 * elements — reading binds one element at a time; writing emits one element at a time.
029 *
030 * <p>
031 * For the reader: the wrapper opens an outer array via the cursor's {@link TokenReader#next()},
032 * yields elements via {@link TokenReader#read(Class)} until {@link TokenReader#canRead()} returns
033 * false (end of array), then advances past the closing {@code ]} on close.
034 *
035 * <p>
036 * For the writer: the wrapper opens an outer array via {@link TokenWriter#startArray()},
037 * forwards each {@link RecordWriter#write(Object) write(...)} call to
038 * {@link TokenWriter#object(Object)}, and emits the closing {@code ]} via
039 * {@link TokenWriter#endArray()} on close.
040 *
041 * <p>
042 * Used by FULL-capability formats whose token cursor genuinely streams (json, json5, cbor, etc.).
043 * For PARTIAL formats whose token surface doesn't exist, see
044 * {@link RecordAdapter#arrayReader(ParserSession, Object)} / {@link RecordAdapter#arrayWriter}, which
045 * are buffered.
046 */
047@SuppressWarnings({
048   "resource" // Wrapped TokenReader/TokenWriter are Closeables owned by the caller; Eclipse JDT @Owning/resource-leak warning is by design.
049})
050public final class StreamingArrayRecord {
051
052   private StreamingArrayRecord() {}
053
054   /**
055    * Wraps a {@link TokenReader} as a streaming array-element {@link RecordReader}.
056    *
057    * <p>
058    * The wrapper assumes the cursor is positioned at the start of an array on the wire.  It
059    * consumes the opening {@link TokenType#START_ARRAY} on construction and the closing
060    * {@link TokenType#END_ARRAY} on {@link RecordReader#close() close()}.
061    *
062    * @param cursor The token reader to wrap.  Must not be <jk>null</jk>.  Owned by the returned
063    *    RecordReader; closing the RecordReader closes the cursor.
064    * @return A streaming array-element {@link RecordReader}.
065    * @throws IOException If a problem occurred reading the underlying input.
066    * @throws ParseException If the input is not a JSON-style array.
067    */
068   public static RecordReader reader(TokenReader cursor) throws IOException, ParseException {
069      var t = cursor.next();
070      if (t != TokenType.START_ARRAY)
071         throw new ParseException("Expected START_ARRAY at the root of arrayRecordReader input but got %s", t);
072      return new RecordReader() {
073         @Override public boolean canRead() throws IOException, ParseException {
074            return cursor.canRead();
075         }
076         @Override public <T> T read(Class<T> type) throws IOException, ParseException {
077            return cursor.read(type);
078         }
079         @Override public <T> T read(ClassMeta<T> type) throws IOException, ParseException {
080            return cursor.read(type);
081         }
082         @Override public <T> T read(Type type, Type... args) throws IOException, ParseException {
083            return cursor.read(type, args);
084         }
085         @Override public boolean isStreaming() {
086            return true;
087         }
088         @Override public void close() throws IOException {
089            cursor.close();
090         }
091      };
092   }
093
094   /**
095    * Wraps a {@link TokenWriter} as a streaming array-element {@link RecordWriter}.
096    *
097    * <p>
098    * The wrapper emits the opening array on construction.  Each
099    * {@link RecordWriter#write(Object)} call appends one array element via
100    * {@link TokenWriter#object(Object)}.  {@link RecordWriter#close() close()} emits the
101    * closing array bracket and closes the underlying cursor.
102    *
103    * @param cursor The token writer to wrap.  Must not be <jk>null</jk>.  Owned by the returned
104    *    RecordWriter; closing the RecordWriter closes the cursor.
105    * @return A streaming array-element {@link RecordWriter}.
106    * @throws IOException If a problem occurred writing to the underlying output.
107    */
108   public static RecordWriter writer(TokenWriter cursor) throws IOException {
109      cursor.startArray();
110      return new RecordWriter() {
111         private boolean closed;
112
113         @Override public RecordWriter write(Object value) throws IOException {
114            if (closed)
115               throw new IllegalStateException("Array stream is closed.");
116            cursor.object(value);
117            return this;
118         }
119         @Override public boolean isStreaming() {
120            return true;
121         }
122         @Override public void flush() throws IOException {
123            cursor.flush();
124         }
125         @Override public void close() throws IOException {
126            if (closed)
127               return;
128            closed = true;
129            cursor.endArray();
130            cursor.close();
131         }
132      };
133   }
134}