001package io.jstach.jstachio.output; 002 003import java.io.IOException; 004import java.io.OutputStream; 005import java.nio.ByteBuffer; 006import java.nio.charset.Charset; 007import java.nio.charset.StandardCharsets; 008import java.util.Arrays; 009 010/** 011 * 012 * A custom OutputStream that is designed for generating bytes from pre-encoded output as 013 * well as reused <em>carefully</em> either by threadlocals or some other pooling 014 * mechanism. 015 * <p> 016 * If the buffer is to be reused {@link #close()} should be called first before it is used 017 * or after every time it is used and {@link #toBuffer()} should be called to get a 018 * correct view of the internal buffer. 019 * <p> 020 * This is basically the same as Joobys Rockers byte buffer but as an OutputStream because 021 * JStachio wants that interface. 022 * 023 * Consequently this code was heavily inspired from 024 * <a href="https://github.com/jooby-project/jooby">Jooby's</a> custom Rocker Output. 025 * <p> 026 * Apache License Version 2.0 https://jooby.io/LICENSE.txt Copyright 2014 Edgar Espina 027 * 028 * @author agentgt 029 * @author jknack 030 */ 031public class ByteBufferedOutputStream extends OutputStream implements ByteBufferEncodedOutput { 032 033 /** Default buffer size: <code>4k</code>. */ 034 public static final int BUFFER_SIZE = 4096; 035 036 /** 037 * The maximum size of array to allocate. Some VMs reserve some header words in an 038 * array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested 039 * array size exceeds VM limit 040 */ 041 private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 042 043 /** The buffer where data is stored. */ 044 protected byte[] buf; 045 046 /** The number of valid bytes in the buffer. */ 047 protected int count; 048 049 /** 050 * The expected charset of the output. 051 */ 052 protected final Charset charset; 053 054 /** 055 * Creates buffered stream of given size. 056 * @param bufferSize initial size. 057 */ 058 public ByteBufferedOutputStream(int bufferSize) { 059 this(bufferSize, StandardCharsets.UTF_8); 060 } 061 062 /** 063 * Creates buffered Output of given size and given charset if used as a Jstachio 064 * Output. 065 * @param bufferSize initial size. 066 * @param charset the charset of the output 067 */ 068 public ByteBufferedOutputStream(int bufferSize, Charset charset) { 069 this.buf = new byte[bufferSize]; 070 this.charset = charset; 071 } 072 073 /** 074 * Creates buffered stream of buffer initial size: {@value #BUFFER_SIZE}. 075 */ 076 public ByteBufferedOutputStream() { 077 this(BUFFER_SIZE); 078 } 079 080 void reset() { 081 count = 0; 082 } 083 084 @Override 085 public void close() { 086 this.reset(); 087 } 088 089 @Override 090 public Charset charset() { 091 return this.charset; 092 } 093 094 @Override 095 public void write(byte[] bytes) { 096 int len = bytes.length; 097 ensureCapacity(count + len); 098 System.arraycopy(bytes, 0, buf, count, len); 099 count += len; 100 } 101 102 @Override 103 public void write(byte[] bytes, int off, int len) { 104 ensureCapacity(count + len); 105 System.arraycopy(bytes, off, buf, count, len); 106 count += len; 107 } 108 109 @Override 110 public void append(String s) { 111 write(s.getBytes(this.charset)); 112 } 113 114 /** 115 * How many bytes have been written so far. 116 * @return 0 if empty, otherwise how many bytes so far 117 */ 118 @Override 119 public int size() { 120 return count; 121 } 122 123 /** 124 * Copy internal byte array into a new array. 125 * @return Byte array. 126 */ 127 @Override 128 public byte[] toByteArray() { 129 byte[] array = new byte[count]; 130 System.arraycopy(buf, 0, array, 0, count); 131 return array; 132 } 133 134 /** 135 * Get a view of the internal byte buffer. <strong>Care must be taken if this instance 136 * is to be reused in multithreaded environment!</strong> 137 * @return Byte buffer. 138 */ 139 public ByteBuffer toBuffer() { 140 return ByteBuffer.wrap(buf, 0, count); 141 } 142 143 @Override 144 public ByteBuffer asByteBuffer() { 145 return toBuffer(); 146 } 147 148 private void ensureCapacity(int minCapacity) { 149 // overflow-conscious code 150 if (minCapacity - buf.length > 0) { 151 grow(minCapacity); 152 } 153 } 154 155 /** 156 * Increases the capacity to ensure that it can hold at least the number of elements 157 * specified by the minimum capacity argument. 158 * @param minCapacity the desired minimum capacity 159 */ 160 private void grow(int minCapacity) { 161 // overflow-conscious code 162 int oldCapacity = buf.length; 163 int newCapacity = oldCapacity << 1; 164 if (newCapacity - minCapacity < 0) { 165 newCapacity = minCapacity; 166 } 167 if (newCapacity - MAX_ARRAY_SIZE > 0) { 168 newCapacity = hugeCapacity(minCapacity); 169 } 170 buf = Arrays.copyOf(buf, newCapacity); 171 } 172 173 private static int hugeCapacity(int minCapacity) { 174 if (minCapacity < 0) { 175 throw new OutOfMemoryError(); 176 } 177 return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; 178 } 179 180 @Override 181 public void write(int b) { 182 throw new UnsupportedOperationException("expecting only write(byte[])"); 183 } 184 185 @Override 186 public void transferTo(OutputStream stream) throws IOException { 187 stream.write(buf, 0, count); 188 } 189 190 @Override 191 public <E extends Exception> void accept(OutputConsumer<E> consumer) throws E { 192 consumer.accept(buf, 0, count); 193 } 194 195 @Override 196 public boolean isReusable() { 197 return true; 198 } 199 200}