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 <= 0) { 051 * return 1024 * 32; 052 * } 053 * return limit; 054 * } 055 * 056 * @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}