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}