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}