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}