001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 the ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.lib.commons.io; 018 019import java.io.InputStream; 020import java.util.List; 021 022import org.conqat.lib.commons.assertion.CCSMAssert; 023import org.conqat.lib.commons.collections.CollectionUtils; 024 025/** 026 * A stream that provides functionality for reading from a list of byte arrays, 027 * also called chunks. 028 */ 029public class ChunkInputStream extends InputStream { 030 /** The internal chunks to read from. */ 031 private final List<byte[]> chunks; 032 033 /** The chunk's index that is to be read after the current chunk. */ 034 private int nextChunkIndex; 035 036 /** The remaining number of bytes to read. */ 037 private int remaining; 038 039 /** The chunk from which is currently read. */ 040 private byte[] currentChunk; 041 042 /** The offset in the current chunk to read from. */ 043 private int currentOffset; 044 045 /** 046 * Creates a new ChunkInputStream with the given chunks. 047 */ 048 public ChunkInputStream(List<byte[]> chunks) { 049 this(chunks, getLastChunkSize(chunks)); 050 } 051 052 /** 053 * Retrieves the last chunk's size or zero if the given chunk list is empty. 054 * 055 * @param chunks 056 * the chunks 057 * @return the last chunk's size 058 */ 059 private static int getLastChunkSize(List<byte[]> chunks) { 060 CCSMAssert.isNotNull(chunks); 061 if (chunks.isEmpty()) { 062 return 0; 063 } 064 return CollectionUtils.getLast(chunks).length; 065 } 066 067 /** 068 * Creates a new ChunkInputStream with the given chunks. If the input chunks 069 * are the output of a ChunkOutputStream, the last chunk is probably smaller 070 * than its actual byte array size. Therefore the lastChunkSize can be 071 * specified additionally. 072 * 073 * @param chunks 074 * the chunks to read from, the list must not contain null values 075 * or empty arrays 076 * @param lastChunkSize 077 * the real size of the last chunk 078 */ 079 public ChunkInputStream(List<byte[]> chunks, int lastChunkSize) { 080 CCSMAssert.isNotNull(chunks); 081 082 this.chunks = chunks; 083 this.currentChunk = null; 084 085 for (byte[] chunk : this.chunks) { 086 remaining += chunk.length; 087 } 088 089 if (!this.chunks.isEmpty()) { 090 this.currentChunk = this.chunks.get(0); 091 byte[] lastChunk = CollectionUtils.getLast(this.chunks); 092 CCSMAssert.isTrue(lastChunkSize > 0 && lastChunkSize <= lastChunk.length, 093 "lastChunkSize must be in range ]0,lastChunk.length]"); 094 095 remaining -= lastChunk.length - lastChunkSize; 096 } 097 098 nextChunkIndex = 1; 099 currentOffset = 0; 100 } 101 102 /** {@inheritDoc} */ 103 @Override 104 public int read() { 105 updateChunk(); 106 if (remaining <= 0) { 107 return -1; 108 } 109 110 remaining--; 111 return ByteArrayUtils.unsignedByte(currentChunk[currentOffset++]); 112 } 113 114 /** {@inheritDoc} */ 115 @Override 116 public int read(byte[] b, int offset, int length) { 117 CCSMAssert.isNotNull(b); 118 CCSMAssert.isFalse(offset < 0 || length < 0 || length > b.length - offset, "invalid offset/length"); 119 120 if (length == 0) { 121 return 0; 122 } 123 124 if (remaining <= 0) { 125 return -1; 126 } 127 128 int alreadyRead = 0; 129 while (remaining > 0 && alreadyRead < length) { 130 updateChunk(); 131 132 // calculate how many bytes must/can be read from the current chunk 133 int readNow = Math.min(currentChunk.length - currentOffset, length - alreadyRead); 134 135 System.arraycopy(currentChunk, currentOffset, b, offset + alreadyRead, readNow); 136 137 currentOffset += readNow; 138 alreadyRead += readNow; 139 remaining -= readNow; 140 } 141 return alreadyRead; 142 } 143 144 /** {@inheritDoc} */ 145 @Override 146 public int available() { 147 return remaining; 148 } 149 150 /** {@inheritDoc} */ 151 @Override 152 public void close() { 153 // do nothing 154 } 155 156 /** 157 * Updates the current chunk. If the current offset is at the end of the 158 * current chunk, switch to the next one. Sets the current chunk to null if 159 * there is no next chunk. 160 */ 161 private void updateChunk() { 162 if (currentChunk != null && currentOffset == currentChunk.length) { 163 if (nextChunkIndex < chunks.size()) { 164 currentChunk = chunks.get(nextChunkIndex++); 165 currentOffset = 0; 166 } else { 167 currentChunk = null; 168 } 169 } 170 } 171}