001package io.jstach.jstachio.output;
002
003import java.io.IOException;
004import java.io.OutputStream;
005import java.nio.charset.Charset;
006
007import org.eclipse.jdt.annotation.Nullable;
008
009import io.jstach.jstachio.Output.CloseableEncodedOutput;
010
011/**
012 * This output will {@linkplain #limit() limit} buffering by byte count and then fallback
013 * to pushing to the downstream output type of <code>T</code> once limit is exceeded. If
014 * the limit is not exceeded then the buffered data will be replayed and pushed when
015 * {@linkplain #close() closed}. <em>Consequently this output strategy is a better fit for
016 * integration of blocking APIs such as Servlet based frameworks where the data is pushed
017 * instead of pulled. If pulling is more desired (non blocking code generally prefers a
018 * pull approach) than {@link BufferedEncodedOutput} is a better fit but requires the
019 * entire output be buffered.</em>
020 * <p>
021 * The output <code>T</code> is generally lazily created once and only once by calling and
022 * the total size buffered will be passed if under limit. If the limit is exceeded than
023 * 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 *
032 * @author agentgt
033 * @param <T> the downstream output type
034 * @param <E> the exception type that can be thrown while writing to the output type
035 * @apiNote This class is not thread safe.
036 * @see BufferedEncodedOutput#limit(int, OutputFactory)
037 * @see ThresholdEncodedOutput
038 */
039public sealed interface LimitEncodedOutput<T, E extends Exception>
040                extends CloseableEncodedOutput<E>permits AbstractLimitEncodedOutput, ThresholdEncodedOutput {
041
042        /**
043         * Buffer limit
044         * @return limit buffer to this amount of bytes
045         */
046        public int limit();
047
048        /**
049         * Current amount of bytes written.
050         * @return number of bytes written.
051         */
052        public int size();
053
054        /**
055         * The created consumer. Maybe <code>null</code> but on successful close should not
056         * be. <strong>This is not to create the consumer but to fetch it after processing has
057         * finished</strong> since the consumer is created on demand.
058         * @return created consumer
059         */
060        public @Nullable T consumer();
061
062}
063
064/**
065 * This is purposely not public at the moment.
066 *
067 * @author agentgt
068 */
069abstract non-sealed class AbstractLimitEncodedOutput implements LimitEncodedOutput<OutputStream, IOException> {
070
071        private final BufferedEncodedOutput buffer;
072
073        protected final int limit;
074
075        protected @Nullable OutputStream consumer;
076
077        protected int size = 0;
078
079        protected final Charset charset;
080
081        protected AbstractLimitEncodedOutput(BufferedEncodedOutput buffer, int limit) {
082                super();
083                this.buffer = buffer;
084                this.limit = limit;
085                this.charset = buffer.charset();
086        }
087
088        @Override
089        public void write(byte[] chunk) throws IOException {
090                int length = chunk.length;
091                OutputStream c = this.consumer;
092                if (c != null) {
093                        c.write(chunk);
094                }
095                else if ((length + size) > limit) {
096                        /*
097                         * We have exceeded the threshold
098                         */
099                        c = this.consumer = createConsumer(-1);
100                        buffer.transferTo(c);
101                        c.write(chunk);
102                }
103                else {
104                        buffer.write(chunk);
105                }
106                size += length;
107
108        }
109
110        @Override
111        public int size() {
112                return size;
113        }
114
115        @Override
116        public int limit() {
117                return limit;
118        }
119
120        @Override
121        public @Nullable OutputStream consumer() {
122                return consumer;
123        }
124
125        /**
126         * Creates the consumer. If size is not <code>-1</code> than the entire output has
127         * been buffered and can be safely used to set <code>Content-Length</code> before
128         * creating the actual consumer (often an OutputStream).
129         * @param size <code>-1</code> indicates that the output is larger than the limit.
130         * @return the created consumer
131         * @throws E an error while creating the consumer
132         */
133        protected abstract OutputStream createConsumer(int size) throws IOException;
134
135        /**
136         * Writes to a consumer.
137         * @param consumer the consumer created from {@link #createConsumer(int)}.
138         * @param bytes data to be written
139         * @throws E if an error happens while using the consumer.
140         */
141        protected void write(OutputStream consumer, byte[] bytes) throws IOException {
142                consumer.write(bytes);
143        }
144
145        /**
146         * Called to close the consumer. Implementations can decide whether or not to really
147         * close the consumer.
148         * @param consumer to be closed or not
149         * @throws IOException if an error happens while closing.
150         */
151        protected void close(OutputStream consumer) throws IOException {
152                consumer.close();
153        }
154
155        @Override
156        public Charset charset() {
157                return charset;
158        }
159
160        @Override
161        public void append(CharSequence s) throws IOException {
162                append(s.toString());
163        }
164
165        @Override
166        public void append(String s) throws IOException {
167                write(s.getBytes(charset));
168        }
169
170        /**
171         * If the limit is not exceeded then the buffered data will be replayed and pushed
172         * when closed. Regardless {@link #close(OutputStream)} will be called on the output
173         * like object.
174         * @throws IOException if an error happens while creating or closing the downstream
175         * output
176         */
177        @Override
178        public void close() throws IOException {
179                var c = this.consumer;
180                if (c == null) {
181                        this.consumer = c = createConsumer(size);
182                        this.buffer.transferTo(c);
183                }
184                close(c);
185                this.consumer = null;
186        }
187
188}