1 /* Jackson JSON-processor.
2  *
3  * Copyright (c) 2007- Tatu Saloranta, tatu.saloranta@iki.fi
4  */
5 
6 package com.fasterxml.jackson.core.util;
7 
8 import java.io.OutputStream;
9 import java.util.*;
10 
11 /**
12  * Helper class that is similar to {@link java.io.ByteArrayOutputStream}
13  * in usage, but more geared to Jackson use cases internally.
14  * Specific changes include segment storage (no need to have linear
15  * backing buffer, can avoid reallocations, copying), as well API
16  * not based on {@link java.io.OutputStream}. In short, a very much
17  * specialized builder object.
18  *<p>
19  * Also implements {@link OutputStream} to allow
20  * efficient aggregation of output content as a byte array, similar
21  * to how {@link java.io.ByteArrayOutputStream} works, but somewhat more
22  * efficiently for many use cases.
23  *<p>
24  * NOTE: maximum size limited to Java Array maximum, 2 gigabytes: this
25  * because usage pattern is to collect content for a `byte[]` and so although
26  * theoretically this builder can aggregate more content it will not be usable
27  * as things are. Behavior may be improved if we solve the access problem.
28  */
29 public final class ByteArrayBuilder extends OutputStream
30 {
31     public final static byte[] NO_BYTES = new byte[0];
32 
33     // Size of the first block we will allocate.
34     private final static int INITIAL_BLOCK_SIZE = 500;
35 
36     // Maximum block size we will use for individual non-aggregated blocks.
37     // For 2.10, let's limit to using 128k chunks (was 256k up to 2.9)
38     private final static int MAX_BLOCK_SIZE = (1 << 17);
39 
40     final static int DEFAULT_BLOCK_ARRAY_SIZE = 40;
41 
42     // Optional buffer recycler instance that we can use for allocating the first block.
43     private final BufferRecycler _bufferRecycler;
44     private final LinkedList<byte[]> _pastBlocks = new LinkedList<byte[]>();
45 
46     // Number of bytes within byte arrays in {@link _pastBlocks}.
47     private int _pastLen;
48     private byte[] _currBlock;
49     private int _currBlockPtr;
50 
ByteArrayBuilder()51     public ByteArrayBuilder() { this(null); }
ByteArrayBuilder(BufferRecycler br)52     public ByteArrayBuilder(BufferRecycler br) { this(br, INITIAL_BLOCK_SIZE); }
ByteArrayBuilder(int firstBlockSize)53     public ByteArrayBuilder(int firstBlockSize) { this(null, firstBlockSize); }
54 
ByteArrayBuilder(BufferRecycler br, int firstBlockSize)55     public ByteArrayBuilder(BufferRecycler br, int firstBlockSize) {
56         _bufferRecycler = br;
57         _currBlock = (br == null) ? new byte[firstBlockSize] : br.allocByteBuffer(BufferRecycler.BYTE_WRITE_CONCAT_BUFFER);
58     }
59 
ByteArrayBuilder(BufferRecycler br, byte[] initialBlock, int initialLen)60     private ByteArrayBuilder(BufferRecycler br, byte[] initialBlock, int initialLen) {
61         _bufferRecycler = null;
62         _currBlock = initialBlock;
63         _currBlockPtr = initialLen;
64     }
65 
fromInitial(byte[] initialBlock, int length)66     public static ByteArrayBuilder fromInitial(byte[] initialBlock, int length) {
67         return new ByteArrayBuilder(null, initialBlock, length);
68     }
69 
reset()70     public void reset() {
71         _pastLen = 0;
72         _currBlockPtr = 0;
73 
74         if (!_pastBlocks.isEmpty()) {
75             _pastBlocks.clear();
76         }
77     }
78 
79     /**
80      * @since 2.9
81      */
size()82     public int size() {
83         return _pastLen + _currBlockPtr;
84     }
85 
86     /**
87      * Clean up method to call to release all buffers this object may be
88      * using. After calling the method, no other accessors can be used (and
89      * attempt to do so may result in an exception)
90      */
release()91     public void release() {
92         reset();
93         if (_bufferRecycler != null && _currBlock != null) {
94             _bufferRecycler.releaseByteBuffer(BufferRecycler.BYTE_WRITE_CONCAT_BUFFER, _currBlock);
95             _currBlock = null;
96         }
97     }
98 
append(int i)99     public void append(int i) {
100         if (_currBlockPtr >= _currBlock.length) {
101             _allocMore();
102         }
103         _currBlock[_currBlockPtr++] = (byte) i;
104     }
105 
appendTwoBytes(int b16)106     public void appendTwoBytes(int b16) {
107         if ((_currBlockPtr + 1) < _currBlock.length) {
108             _currBlock[_currBlockPtr++] = (byte) (b16 >> 8);
109             _currBlock[_currBlockPtr++] = (byte) b16;
110         } else {
111             append(b16 >> 8);
112             append(b16);
113         }
114     }
115 
appendThreeBytes(int b24)116     public void appendThreeBytes(int b24) {
117         if ((_currBlockPtr + 2) < _currBlock.length) {
118             _currBlock[_currBlockPtr++] = (byte) (b24 >> 16);
119             _currBlock[_currBlockPtr++] = (byte) (b24 >> 8);
120             _currBlock[_currBlockPtr++] = (byte) b24;
121         } else {
122             append(b24 >> 16);
123             append(b24 >> 8);
124             append(b24);
125         }
126     }
127 
128     /**
129      * @since 2.9
130      */
appendFourBytes(int b32)131     public void appendFourBytes(int b32) {
132         if ((_currBlockPtr + 3) < _currBlock.length) {
133             _currBlock[_currBlockPtr++] = (byte) (b32 >> 24);
134             _currBlock[_currBlockPtr++] = (byte) (b32 >> 16);
135             _currBlock[_currBlockPtr++] = (byte) (b32 >> 8);
136             _currBlock[_currBlockPtr++] = (byte) b32;
137         } else {
138             append(b32 >> 24);
139             append(b32 >> 16);
140             append(b32 >> 8);
141             append(b32);
142         }
143     }
144 
145     /**
146      * Method called when results are finalized and we can get the
147      * full aggregated result buffer to return to the caller
148      */
toByteArray()149     public byte[] toByteArray()
150     {
151         int totalLen = _pastLen + _currBlockPtr;
152 
153         if (totalLen == 0) { // quick check: nothing aggregated?
154             return NO_BYTES;
155         }
156         byte[] result = new byte[totalLen];
157         int offset = 0;
158 
159         for (byte[] block : _pastBlocks) {
160             int len = block.length;
161             System.arraycopy(block, 0, result, offset, len);
162             offset += len;
163         }
164         System.arraycopy(_currBlock, 0, result, offset, _currBlockPtr);
165         offset += _currBlockPtr;
166         if (offset != totalLen) { // just a sanity check
167             throw new RuntimeException("Internal error: total len assumed to be "+totalLen+", copied "+offset+" bytes");
168         }
169         // Let's only reset if there's sizable use, otherwise will get reset later on
170         if (!_pastBlocks.isEmpty()) {
171             reset();
172         }
173         return result;
174     }
175 
176     /*
177     /**********************************************************
178     /* Non-stream API (similar to TextBuffer)
179     /**********************************************************
180      */
181 
182     /**
183      * Method called when starting "manual" output: will clear out
184      * current state and return the first segment buffer to fill
185      */
resetAndGetFirstSegment()186     public byte[] resetAndGetFirstSegment() {
187         reset();
188         return _currBlock;
189     }
190 
191     /**
192      * Method called when the current segment buffer is full; will
193      * append to current contents, allocate a new segment buffer
194      * and return it
195      */
finishCurrentSegment()196     public byte[] finishCurrentSegment() {
197         _allocMore();
198         return _currBlock;
199     }
200 
201     /**
202      * Method that will complete "manual" output process, coalesce
203      * content (if necessary) and return results as a contiguous buffer.
204      *
205      * @param lastBlockLength Amount of content in the current segment
206      * buffer.
207      *
208      * @return Coalesced contents
209      */
completeAndCoalesce(int lastBlockLength)210     public byte[] completeAndCoalesce(int lastBlockLength) {
211         _currBlockPtr = lastBlockLength;
212         return toByteArray();
213     }
214 
getCurrentSegment()215     public byte[] getCurrentSegment() { return _currBlock; }
setCurrentSegmentLength(int len)216     public void setCurrentSegmentLength(int len) { _currBlockPtr = len; }
getCurrentSegmentLength()217     public int getCurrentSegmentLength() { return _currBlockPtr; }
218 
219     /*
220     /**********************************************************
221     /* OutputStream implementation
222     /**********************************************************
223      */
224 
225     @Override
write(byte[] b)226     public void write(byte[] b) {
227         write(b, 0, b.length);
228     }
229 
230     @Override
write(byte[] b, int off, int len)231     public void write(byte[] b, int off, int len)
232     {
233         while (true) {
234             int max = _currBlock.length - _currBlockPtr;
235             int toCopy = Math.min(max, len);
236             if (toCopy > 0) {
237                 System.arraycopy(b, off, _currBlock, _currBlockPtr, toCopy);
238                 off += toCopy;
239                 _currBlockPtr += toCopy;
240                 len -= toCopy;
241             }
242             if (len <= 0) break;
243             _allocMore();
244         }
245     }
246 
247     @Override
write(int b)248     public void write(int b) {
249         append(b);
250     }
251 
close()252     @Override public void close() { /* NOP */ }
flush()253     @Override public void flush() { /* NOP */ }
254 
255     /*
256     /**********************************************************
257     /* Internal methods
258     /**********************************************************
259      */
260 
_allocMore()261     private void _allocMore()
262     {
263         final int newPastLen = _pastLen + _currBlock.length;
264 
265         // 13-Feb-2016, tatu: As per [core#351] let's try to catch problem earlier;
266         //     for now we are strongly limited by 2GB limit of Java arrays
267         if (newPastLen < 0) {
268             throw new IllegalStateException("Maximum Java array size (2GB) exceeded by `ByteArrayBuilder`");
269         }
270 
271         _pastLen = newPastLen;
272 
273         /* Let's allocate block that's half the total size, except
274          * never smaller than twice the initial block size.
275          * The idea is just to grow with reasonable rate, to optimize
276          * between minimal number of chunks and minimal amount of
277          * wasted space.
278          */
279         int newSize = Math.max((_pastLen >> 1), (INITIAL_BLOCK_SIZE + INITIAL_BLOCK_SIZE));
280         // plus not to exceed max we define...
281         if (newSize > MAX_BLOCK_SIZE) {
282             newSize = MAX_BLOCK_SIZE;
283         }
284         _pastBlocks.add(_currBlock);
285         _currBlock = new byte[newSize];
286         _currBlockPtr = 0;
287     }
288 }
289