001package io.jstach.jstachio.output;
002
003import java.io.IOException;
004import java.io.OutputStream;
005import java.nio.charset.Charset;
006import java.util.ArrayList;
007import java.util.List;
008
009import org.eclipse.jdt.annotation.Nullable;
010
011/**
012 * This abstract output will {@linkplain #limit limit} buffering by byte count and then
013 * fallback to pushing to the downstream output type of <code>T</code> once limit is
014 * exceeded. If the limit is not exceeded then the buffered data will be replayed and
015 * pushed when {@linkplain #close() closed}. <em>Consequently this output strategy is a
016 * better fit for integration of blocking APIs such as Servlet based frameworks where the
017 * data is pushed instead of pulled. If pulling is more desired (non blocking code
018 * generally prefers a pull approach) than {@link BufferedEncodedOutput} is a better fit
019 * but requires the entire output be buffered.</em>
020 * <p>
021 * The output <code>T</code> is lazily created once and only once by calling
022 * {@link #createConsumer(int)} and the total size buffered will be passed if under limit.
023 * If the limit is exceeded than the size passed will be <code>-1</code>.
024 * <p>
025 * <strong>For this implementation to work {@link #close()} must be called and thus a
026 * try-with-resource is recommended regardless if the downstream consumer needs to be
027 * closed or not! </strong>
028 * <p>
029 * The total buffered amount of data is not guaranteed to be exactly at the limit even if
030 * the total output is greater than the limit.
031 * <p>
032 * The advantages to letting this implementation do the buffering instead of the
033 * downstream framework is saving memory in that the pre-encoded parts of the template are
034 * just pointers and are not copied multiple times. Since this output instance will be
035 * doing the buffering it is important to minimize downstream buffering by letting the
036 * framework know that it does not need to buffer.
037 *
038 * <h2>Example for Servlet output:</h2> <pre><code class="language-java">
039 * class ServletThresholdEncodedOutput extends ThresholdEncodedOutput.OutputStreamThresholdEncodedOutput {
040 *
041 *     private final HttpServletResponse response;
042 *
043 *     public ServletThresholdEncodedOutput(Charset charset, HttpServletResponse response) {
044 *         super(charset, calculateLimit(response));
045 *         this.response = response;
046 *     }
047 *
048 *     private static int calculateLimit(HttpServletResponse response) {
049 *         int limit = response.getBufferSize();
050 *         if (limit &lt;= 0) {
051 *             return 1024 * 32;
052 *         }
053 *         return limit;
054 *     }
055 *
056 *     &#64;Override
057 *     protected OutputStream createConsumer(int size) throws IOException {
058 *         if (size > -1) {
059 *             response.setContentLength(size);
060 *              // It is already all in memory so we do not need a buffer.
061 *             response.setBufferSize(0);
062 *         }
063 *         return response.getOutputStream();
064 *     }
065 *
066 * }
067 * </code> </pre>
068 *
069 * @author agentgt
070 * @param <T> the downstream output type
071 * @param <E> the exception type that can be thrown while writing to the output type
072 * @apiNote This class is not thread safe.
073 * @see OutputStreamThresholdEncodedOutput
074 */
075public abstract non-sealed class ThresholdEncodedOutput<T, E extends Exception> implements LimitEncodedOutput<T, E> {
076
077        private final List<byte[]> chunks;
078
079        private final Charset charset;
080
081        private int size = 0;
082
083        /**
084         * The maximum number of bytes to buffer.
085         */
086        protected final int limit;
087
088        private @Nullable T consumer;
089
090        /**
091         * Create with charset and limit.
092         * @param charset the encoding to use.
093         * @param limit the amount of total bytes to limit buffering however the total
094         * buffered amount of data is not guaranteed to be exactly at the limit even if the
095         * total output is greater than the limit.
096         */
097        protected ThresholdEncodedOutput(Charset charset, int limit) {
098                chunks = new ArrayList<>();
099                this.charset = charset;
100                this.limit = limit;
101        }
102
103        /**
104         * Writes to a consumer.
105         * @param consumer the consumer created from {@link #createConsumer(int)}.
106         * @param bytes data to be written
107         * @throws E if an error happens while using the consumer.
108         */
109        protected abstract void write(T consumer, byte[] bytes) throws E;
110
111        /**
112         * Creates the consumer. If size is not <code>-1</code> than the entire output has
113         * been buffered and can be safely used to set <code>Content-Length</code> before
114         * creating the actual consumer (often an OutputStream).
115         * @param size <code>-1</code> indicates that the output is larger than the limit.
116         * @return the created consumer
117         * @throws E an error while creating the consumer
118         */
119        protected abstract T createConsumer(int size) throws E;
120
121        /**
122         * Called to close the consumer. Implementations can decide whether or not to really
123         * close the consumer.
124         * @param consumer to be closed or not
125         * @throws E if an error happens while closing.
126         */
127        protected abstract void close(T consumer) throws E;
128
129        @Override
130        public void write(byte[] bytes) throws E {
131                addChunk(bytes);
132        }
133
134        private void addChunk(byte[] chunk) throws E {
135                int length = chunk.length;
136                @Nullable
137                T c = this.consumer;
138                if (c != null) {
139                        write(c, chunk);
140                }
141                else if ((length + size) > limit) {
142                        /*
143                         * We have exceeded the threshold
144                         */
145                        T _consumer;
146                        this.consumer = _consumer = createConsumer(-1);
147                        chunks.add(chunk);
148                        drain(_consumer);
149                }
150                else {
151                        chunks.add(chunk);
152                }
153                size += length;
154        }
155
156        private void drain(T consumer) throws E {
157                for (var chunk : chunks) {
158                        write(consumer, chunk);
159                }
160        }
161
162        @Override
163        public @Nullable T consumer() {
164                return this.consumer;
165        }
166
167        @Override
168        public Charset charset() {
169                return charset;
170        }
171
172        /**
173         * If the limit is not exceeded then the buffered data will be replayed and pushed
174         * when closed. Regardless {@link #close(Object)} will be called on the output like
175         * object.
176         * @throws E if an error happens while creating or closing the downstream output
177         */
178        @Override
179        public void close() throws E {
180                /*
181                 * This little ceremony is because of null checkers. For whatever reason c cannot
182                 * be flow coerced into nonnull.
183                 */
184                @Nullable
185                T c = this.consumer;
186                T _consumer;
187                if (c == null) {
188                        this.consumer = _consumer = createConsumer(size);
189                        drain(_consumer);
190                }
191                else {
192                        _consumer = c;
193                }
194                close(_consumer);
195                this.consumer = null;
196        }
197
198        /**
199         * This is the current written length.
200         * @return current written length
201         */
202        @Override
203        public int size() {
204                return size;
205        }
206
207        @Override
208        public int limit() {
209                return limit;
210        }
211
212        @Override
213        public void append(CharSequence s) throws E {
214                append(s.toString());
215        }
216
217        @Override
218        public void append(String s) throws E {
219                write(s.getBytes(charset));
220        }
221
222        /**
223         * An OutputStream backed buffer limited encoded output. This partial implementation
224         * will cascade {@link #close()} to the OutputStream similar to OutputStream
225         * decorators in the JDK.
226         *
227         * @author agentgt
228         */
229        public abstract static class OutputStreamThresholdEncodedOutput
230                        extends ThresholdEncodedOutput<OutputStream, IOException> {
231
232                /**
233                 * Create with charset and limit.
234                 * @param charset the encoding to use.
235                 * @param limit the amount of total bytes to limit buffering however the total
236                 * buffered amount of data is not guaranteed to be exactly at the limit even if
237                 * the total output is greater than the limit.
238                 */
239                protected OutputStreamThresholdEncodedOutput(Charset charset, int limit) {
240                        super(charset, limit);
241                }
242
243                @Override
244                protected void write(OutputStream consumer, byte[] bytes) throws IOException {
245                        consumer.write(bytes);
246                }
247
248                @Override
249                protected void close(OutputStream consumer) throws IOException {
250                        consumer.close();
251                }
252
253        }
254
255}