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 — 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 — 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}