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 static org.apache.juneau.commons.utils.Shorts.*;
020
021import java.io.*;
022import java.lang.reflect.*;
023import java.util.*;
024
025import org.apache.juneau.marshall.*;
026import org.apache.juneau.marshall.parser.*;
027import org.apache.juneau.marshall.serializer.*;
028
029/**
030 * Static factories that produce buffered {@link RecordReader} / {@link RecordWriter} instances for
031 * formats whose token-stream surface is whole-value-only (no native {@link TokenReader} /
032 * {@link TokenWriter}).
033 *
034 * <p>
035 * The cursors are created from the <b>live session</b> ({@link ParserSession} /
036 * {@link SerializerSession}), so they honor the same per-invocation configuration (swaps, bean
037 * filters, locale/timezone, schema) as {@code parse(...)}/{@code serialize(...)}.
038 *
039 * <p>
040 * Reduces per-format override boilerplate to a one-liner on the session class:
041 * <p class='bjava'>
042 *    <ja>@Override</ja>
043 *    <jk>public</jk> RecordReader readRecords(Object <jv>input</jv>) <jk>throws</jk> IOException {
044 *       <jk>return</jk> RecordAdapter.<jsm>reader</jsm>(<jk>this</jk>, <jv>input</jv>);
045 *    }
046 *
047 *    <ja>@Override</ja>
048 *    <jk>public</jk> RecordWriter writeRecords(Object <jv>output</jv>) <jk>throws</jk> IOException {
049 *       <jk>return</jk> RecordAdapter.<jsm>writer</jsm>(<jk>this</jk>, <jv>output</jv>);
050 *    }
051 * </p>
052 *
053 * <p>
054 * All four cursors report {@link RecordReader#isStreaming() isStreaming()} == <jk>false</jk> (they
055 * buffer) and obey one resource-ownership rule: {@link Closeable#close() close()} closes the
056 * input/output it was handed when it is {@link Closeable}.  Multi-rooted formats that need
057 * different semantics (jsonl, csv, sse) implement their own cursor.
058 */
059@SuppressWarnings({
060   "resource" // RecordReader/RecordWriter returned here are Closeables owned by the caller; Eclipse JDT @Owning warning is by design.
061})
062public final class RecordAdapter {
063
064   private RecordAdapter() {
065      // Static-method facade.
066   }
067
068   /** Closes the handed input/output when it is {@link Closeable} (shared resource-ownership rule). */
069   private static void closeIfCloseable(Object io) throws IOException {
070      if (io instanceof Closeable io2)
071         io2.close();
072   }
073
074   /**
075    * Creates a single-shot {@link RecordReader} that delegates {@code read(...)} calls to the
076    * session's {@link ParserSession#read(Object, Class) read(...)} entry point.
077    *
078    * @param session The format's live parser session.  Must not be <jk>null</jk>.
079    * @param input The input to bind on the first {@code read(...)} call.
080    * @return A new {@link RecordReader}.
081    */
082   public static RecordReader reader(ParserSession session, Object input) {
083      return new RecordReader() {
084         private boolean exhausted;
085
086         @Override public boolean canRead() {
087            return !exhausted;
088         }
089
090         @Override public <T> T read(Class<T> type) throws IOException, ParseException {
091            checkNotExhausted();
092            exhausted = true;
093            return session.read(input, type);
094         }
095
096         @Override public <T> T read(ClassMeta<T> type) throws IOException, ParseException {
097            checkNotExhausted();
098            exhausted = true;
099            return session.read(input, type);
100         }
101
102         @Override public <T> T read(Type type, Type... args) throws IOException, ParseException {
103            checkNotExhausted();
104            exhausted = true;
105            return session.read(input, type, args);
106         }
107
108         @Override public boolean isStreaming() {
109            return false;
110         }
111
112         @Override public void close() throws IOException {
113            exhausted = true;
114            closeIfCloseable(input);
115         }
116
117         private void checkNotExhausted() {
118            if (exhausted)
119               throw new IllegalStateException("Cursor has already produced its single root value.");
120         }
121      };
122   }
123
124   /**
125    * Creates an array-element {@link RecordReader} backed by a buffered list parse.
126    *
127    * <p>
128    * Equivalent to calling {@code session.read(input, List.class, Object.class)} once and
129    * iterating the result.  This is correct but NOT streaming &mdash; the whole array is
130    * materialized.  Format-specific implementations may override
131    * {@link ArrayRecordReadable#readArrayRecords(Object)} directly for actual streaming.
132    *
133    * @param session The format's live parser session.  Must not be <jk>null</jk>.
134    * @param input The input to bind on the first read.
135    * @return A new {@link RecordReader} that yields one record per array element.
136    * @throws IOException If a problem occurred reading the input.
137    */
138   @SuppressWarnings({
139      "unchecked" // The List<?> returned by session.read(...) is cast to List<Object>; its elements are only read back as Object
140   })
141   public static RecordReader arrayReader(ParserSession session, Object input) throws IOException {
142      List<?> all;
143      try {
144         all = session.read(input, List.class, Object.class);
145      } catch (ParseException e) {
146         throw new IOException(e);
147      }
148      var iter = (all == null ? Collections.<Object>emptyList() : (List<Object>) all).iterator();
149      return new RecordReader() {
150         @Override public boolean canRead() {
151            return iter.hasNext();
152         }
153         @Override public <T> T read(Class<T> type) {
154            return session.convertToType(nextElement(), type);
155         }
156         @Override public <T> T read(ClassMeta<T> type) {
157            return session.convertToType(nextElement(), type);
158         }
159         @Override public <T> T read(Type type, Type... args) {
160            return session.<T>convertToType(nextElement(), type, args);
161         }
162         @Override public boolean isStreaming() {
163            return false;
164         }
165         @Override public void close() throws IOException {
166            closeIfCloseable(input);
167         }
168         private Object nextElement() {
169            if (!iter.hasNext())
170               throw new IllegalStateException("Array stream is exhausted.");
171            return iter.next();
172         }
173      };
174   }
175
176   /**
177    * Creates an array-element {@link RecordWriter} that buffers each {@link RecordWriter#write(Object)
178    * write(...)} call and emits the whole list on {@link RecordWriter#close()}.
179    *
180    * <p>
181    * This is correct but NOT streaming &mdash; the writes are accumulated in memory.
182    * Format-specific implementations may override
183    * {@link ArrayRecordWritable#writeArrayRecords(Object)} directly for actual streaming.
184    *
185    * @param session The format's live serializer session.  Must not be <jk>null</jk>.
186    * @param output The output to write to.
187    * @return A new {@link RecordWriter} that accepts N elements and emits an N-element array.
188    */
189   public static RecordWriter arrayWriter(SerializerSession session, Object output) {
190      return new RecordWriter() {
191         private final List<Object> buf = l();
192         private boolean closed;
193
194         @Override public RecordWriter write(Object value) {
195            if (closed)
196               throw new IllegalStateException("Array stream is closed.");
197            buf.add(value);
198            return this;
199         }
200         @Override public boolean isStreaming() {
201            return false;
202         }
203         @Override public void flush() throws IOException {
204            if (output instanceof Flushable output2)
205               output2.flush();
206         }
207         @Override public void close() throws IOException {
208            if (closed)
209               return;
210            closed = true;
211            try {
212               session.write(buf, output);
213            } catch (SerializeException e) {
214               throw new IOException(e);
215            }
216            closeIfCloseable(output);
217         }
218      };
219   }
220
221   /**
222    * Creates a single-shot {@link RecordWriter} that delegates {@code write(Object)} calls to the
223    * session's {@link SerializerSession#write(Object, Object) write(...)} entry point.
224    *
225    * @param session The format's live serializer session.  Must not be <jk>null</jk>.
226    * @param output The output to write to.
227    * @return A new {@link RecordWriter}.
228    */
229   public static RecordWriter writer(SerializerSession session, Object output) {
230      return new RecordWriter() {
231         private boolean written;
232
233         @Override public RecordWriter write(Object value) throws IOException {
234            if (written)
235               throw new IllegalStateException("Cursor has already accepted its single root value.");
236            written = true;
237            try {
238               session.write(value, output);
239            } catch (SerializeException e) {
240               throw new IOException(e);
241            }
242            return this;
243         }
244
245         @Override public boolean isStreaming() {
246            return false;
247         }
248
249         @Override public void flush() throws IOException {
250            if (output instanceof Flushable output2)
251               output2.flush();
252         }
253
254         @Override public void close() throws IOException {
255            closeIfCloseable(output);
256         }
257      };
258   }
259}