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