001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  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.apache.commons.imaging.common.bytesource;
018
019import java.io.BufferedInputStream;
020import java.io.ByteArrayOutputStream;
021import java.io.IOException;
022import java.io.InputStream;
023import java.util.Objects;
024
025import org.apache.commons.imaging.common.BinaryFunctions;
026
027public class ByteSourceInputStream extends ByteSource {
028    private static final int BLOCK_SIZE = 1024;
029
030    private final InputStream is;
031    private CacheBlock cacheHead;
032    private byte[] readBuffer;
033    private long streamLength = -1;
034
035    public ByteSourceInputStream(final InputStream is, final String fileName) {
036        super(fileName);
037        this.is = new BufferedInputStream(is);
038    }
039
040    private class CacheBlock {
041        public final byte[] bytes;
042        private CacheBlock next;
043        private boolean triedNext;
044
045        CacheBlock(final byte[] bytes) {
046            this.bytes = bytes;
047        }
048
049        public CacheBlock getNext() throws IOException {
050            if (null != next) {
051                return next;
052            }
053            if (triedNext) {
054                return null;
055            }
056            triedNext = true;
057            next = readBlock();
058            return next;
059        }
060
061    }
062
063    private CacheBlock readBlock() throws IOException {
064        if (null == readBuffer) {
065            readBuffer = new byte[BLOCK_SIZE];
066        }
067
068        final int read = is.read(readBuffer);
069        if (read < 1) {
070            return null;
071        }
072        if (read < BLOCK_SIZE) {
073            // return a copy.
074            final byte[] result = new byte[read];
075            System.arraycopy(readBuffer, 0, result, 0, read);
076            return new CacheBlock(result);
077        }
078        // return current buffer.
079        final byte[] result = readBuffer;
080        readBuffer = null;
081        return new CacheBlock(result);
082    }
083
084    private CacheBlock getFirstBlock() throws IOException {
085        if (null == cacheHead) {
086            cacheHead = readBlock();
087        }
088        return cacheHead;
089    }
090
091    private class CacheReadingInputStream extends InputStream {
092        private CacheBlock block;
093        private boolean readFirst;
094        private int blockIndex;
095
096        @Override
097        public int read() throws IOException {
098            if (null == block) {
099                if (readFirst) {
100                    return -1;
101                }
102                block = getFirstBlock();
103                readFirst = true;
104            }
105
106            if (block != null && blockIndex >= block.bytes.length) {
107                block = block.getNext();
108                blockIndex = 0;
109            }
110
111            if (null == block) {
112                return -1;
113            }
114
115            if (blockIndex >= block.bytes.length) {
116                return -1;
117            }
118
119            return 0xff & block.bytes[blockIndex++];
120        }
121
122        @Override
123        public int read(final byte[] array, final int off, final int len) throws IOException {
124            // first section copied verbatim from InputStream
125            Objects.requireNonNull(array, "array");
126            if ((off < 0) || (off > array.length) || (len < 0)
127                    || ((off + len) > array.length) || ((off + len) < 0)) {
128                throw new IndexOutOfBoundsException();
129            }
130            if (len == 0) {
131                return 0;
132            }
133
134            // optimized block read
135
136            if (null == block) {
137                if (readFirst) {
138                    return -1;
139                }
140                block = getFirstBlock();
141                readFirst = true;
142            }
143
144            if (block != null && blockIndex >= block.bytes.length) {
145                block = block.getNext();
146                blockIndex = 0;
147            }
148
149            if (null == block) {
150                return -1;
151            }
152
153            if (blockIndex >= block.bytes.length) {
154                return -1;
155            }
156
157            final int readSize = Math.min(len, block.bytes.length - blockIndex);
158            System.arraycopy(block.bytes, blockIndex, array, off, readSize);
159            blockIndex += readSize;
160            return readSize;
161        }
162
163        @Override
164        public long skip(final long n) throws IOException {
165
166            long remaining = n;
167
168            if (n <= 0) {
169                return 0;
170            }
171
172            while (remaining > 0) {
173                // read the first block
174                if (null == block) {
175                    if (readFirst) {
176                        return -1;
177                    }
178                    block = getFirstBlock();
179                    readFirst = true;
180                }
181
182                // get next block
183                if (block != null && blockIndex >= block.bytes.length) {
184                    block = block.getNext();
185                    blockIndex = 0;
186                }
187
188                if (null == block) {
189                    break;
190                }
191
192                if (blockIndex >= block.bytes.length) {
193                    break;
194                }
195
196                final int readSize = Math.min((int) Math.min(BLOCK_SIZE, remaining), block.bytes.length - blockIndex);
197
198                blockIndex += readSize;
199                remaining -= readSize;
200            }
201
202            return n - remaining;
203        }
204
205    }
206
207    @Override
208    public InputStream getInputStream() throws IOException {
209        return new CacheReadingInputStream();
210    }
211
212    @Override
213    public byte[] getBlock(final long blockStart, final int blockLength) throws IOException {
214        // We include a separate check for int overflow.
215        if ((blockStart < 0) || (blockLength < 0)
216                || (blockStart + blockLength < 0)
217                || (blockStart + blockLength > getLength())) {
218            throw new IOException("Could not read block (block start: "
219                    + blockStart + ", block length: " + blockLength
220                    + ", data length: " + streamLength + ").");
221        }
222
223        final InputStream cis = getInputStream();
224        BinaryFunctions.skipBytes(cis, blockStart);
225
226        final byte[] bytes = new byte[blockLength];
227        int total = 0;
228        while (true) {
229            final int read = cis.read(bytes, total, bytes.length - total);
230            if (read < 1) {
231                throw new IOException("Could not read block.");
232            }
233            total += read;
234            if (total >= blockLength) {
235                return bytes;
236            }
237        }
238    }
239
240    @Override
241    public long getLength() throws IOException {
242        if (streamLength >= 0) {
243            return streamLength;
244        }
245
246        final InputStream cis = getInputStream();
247        long result = 0;
248        long skipped;
249        while ((skipped = cis.skip(1024)) > 0) {
250            result += skipped;
251        }
252        streamLength = result;
253        return result;
254    }
255
256    @Override
257    public byte[] getAll() throws IOException {
258        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
259
260        CacheBlock block = getFirstBlock();
261        while (block != null) {
262            baos.write(block.bytes);
263            block = block.getNext();
264        }
265        return baos.toByteArray();
266    }
267
268    @Override
269    public String getDescription() {
270        return "Inputstream: '" + getFileName() + "'";
271    }
272
273}