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.io.input;
018
019import java.io.ByteArrayInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.Objects;
023
024import org.apache.commons.io.build.AbstractOrigin;
025import org.apache.commons.io.build.AbstractStreamBuilder;
026
027/**
028 * This is an alternative to {@link ByteArrayInputStream} which removes the synchronization overhead for non-concurrent access; as such this class is
029 * not thread-safe.
030 * <p>
031 * To build an instance, use {@link Builder}.
032 * </p>
033 *
034 * @see Builder
035 * @see ByteArrayInputStream
036 * @since 2.7
037 */
038//@NotThreadSafe
039public class UnsynchronizedByteArrayInputStream extends InputStream {
040
041    // @formatter:off
042    /**
043     * Builds a new {@link UnsynchronizedByteArrayInputStream}.
044     *
045     * <p>
046     * Using a Byte Array:
047     * </p>
048     * <pre>{@code
049     * UnsynchronizedByteArrayInputStream s = UnsynchronizedByteArrayInputStream.builder()
050     *   .setByteArray(byteArray)
051     *   .setOffset(0)
052     *   .setLength(byteArray.length)
053     *   .get();
054     * }
055     * </pre>
056     * <p>
057     * Using File IO:
058     * </p>
059     * <pre>{@code
060     * UnsynchronizedByteArrayInputStream s = UnsynchronizedByteArrayInputStream.builder()
061     *   .setFile(file)
062     *   .setOffset(0)
063     *   .setLength(byteArray.length)
064     *   .get();
065     * }
066     * </pre>
067     * <p>
068     * Using NIO Path:
069     * </p>
070     * <pre>{@code
071     * UnsynchronizedByteArrayInputStream s = UnsynchronizedByteArrayInputStream.builder()
072     *   .setPath(path)
073     *   .setOffset(0)
074     *   .setLength(byteArray.length)
075     *   .get();
076     * }
077     * </pre>
078     *
079     * @see #get()
080     */
081    // @formatter:on
082    public static class Builder extends AbstractStreamBuilder<UnsynchronizedByteArrayInputStream, Builder> {
083
084        private int offset;
085        private int length;
086
087        /**
088         * Constructs a builder of {@link UnsynchronizedByteArrayInputStream}.
089         */
090        public Builder() {
091            // empty
092        }
093
094        /**
095         * Builds a new {@link UnsynchronizedByteArrayInputStream}.
096         * <p>
097         * You must set an aspect that supports {@code byte[]} on this builder, otherwise, this method throws an exception.
098         * </p>
099         * <p>
100         * This builder uses the following aspects:
101         * </p>
102         * <ul>
103         * <li>{@code byte[]}</li>
104         * <li>offset</li>
105         * <li>length</li>
106         * </ul>
107         *
108         * @return a new instance.
109         * @throws UnsupportedOperationException if the origin cannot provide a {@code byte[]}.
110         * @throws IllegalStateException         if the {@code origin} is {@code null}.
111         * @throws IOException                   if an I/O error occurs converting to an {@code byte[]} using {@link AbstractOrigin#getByteArray()}.
112         * @see AbstractOrigin#getByteArray()
113         * @see #getUnchecked()
114         */
115        @Override
116        public UnsynchronizedByteArrayInputStream get() throws IOException {
117            return new UnsynchronizedByteArrayInputStream(checkOrigin().getByteArray(), offset, length);
118        }
119
120        @Override
121        public Builder setByteArray(final byte[] origin) {
122            length = Objects.requireNonNull(origin, "origin").length;
123            return super.setByteArray(origin);
124        }
125
126        /**
127         * Sets the length.
128         *
129         * @param length Must be greater or equal to 0.
130         * @return {@code this} instance.
131         */
132        public Builder setLength(final int length) {
133            if (length < 0) {
134                throw new IllegalArgumentException("length cannot be negative");
135            }
136            this.length = length;
137            return this;
138        }
139
140        /**
141         * Sets the offset.
142         *
143         * @param offset Must be greater or equal to 0.
144         * @return {@code this} instance.
145         */
146        public Builder setOffset(final int offset) {
147            if (offset < 0) {
148                throw new IllegalArgumentException("offset cannot be negative");
149            }
150            this.offset = offset;
151            return this;
152        }
153
154    }
155
156    /**
157     * The end of stream marker.
158     */
159    public static final int END_OF_STREAM = -1;
160
161    /**
162     * Constructs a new {@link Builder}.
163     *
164     * @return a new {@link Builder}.
165     */
166    public static Builder builder() {
167        return new Builder();
168    }
169
170    private static int minPosLen(final byte[] data, final int defaultValue) {
171        requireNonNegative(defaultValue, "defaultValue");
172        return Math.min(defaultValue, data.length > 0 ? data.length : defaultValue);
173    }
174
175    private static int requireNonNegative(final int value, final String name) {
176        if (value < 0) {
177            throw new IllegalArgumentException(name + " cannot be negative");
178        }
179        return value;
180    }
181
182    /**
183     * The underlying data buffer.
184     */
185    private final byte[] data;
186
187    /**
188     * End Of Data.
189     *
190     * Similar to data.length, which is the last readable offset + 1.
191     */
192    private final int eod;
193
194    /**
195     * Current offset in the data buffer.
196     */
197    private int offset;
198
199    /**
200     * The current mark (if any).
201     */
202    private int markedOffset;
203
204    /**
205     * Constructs a new byte array input stream.
206     *
207     * @param data the buffer
208     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
209     */
210    @Deprecated
211    public UnsynchronizedByteArrayInputStream(final byte[] data) {
212        this(data, data.length, 0, 0);
213    }
214
215    /**
216     * Constructs a new byte array input stream.
217     *
218     * @param data   the buffer
219     * @param offset the offset into the buffer
220     * @throws IllegalArgumentException if the offset is less than zero
221     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
222     */
223    @Deprecated
224    public UnsynchronizedByteArrayInputStream(final byte[] data, final int offset) {
225        this(data, data.length, Math.min(requireNonNegative(offset, "offset"), minPosLen(data, offset)), minPosLen(data, offset));
226    }
227
228    /**
229     * Constructs a new byte array input stream.
230     *
231     * @param data   the buffer
232     * @param offset the offset into the buffer
233     * @param length the length of the buffer
234     * @throws IllegalArgumentException if the offset or length less than zero
235     * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}.
236     */
237    @Deprecated
238    public UnsynchronizedByteArrayInputStream(final byte[] data, final int offset, final int length) {
239        requireNonNegative(offset, "offset");
240        requireNonNegative(length, "length");
241        this.data = Objects.requireNonNull(data, "data");
242        this.eod = Math.min(minPosLen(data, offset) + length, data.length);
243        this.offset = minPosLen(data, offset);
244        this.markedOffset = minPosLen(data, offset);
245    }
246
247    private UnsynchronizedByteArrayInputStream(final byte[] data, final int eod, final int offset, final int markedOffset) {
248        this.data = Objects.requireNonNull(data, "data");
249        this.eod = eod;
250        this.offset = offset;
251        this.markedOffset = markedOffset;
252    }
253
254    @Override
255    public int available() {
256        return offset < eod ? eod - offset : 0;
257    }
258
259    @SuppressWarnings("sync-override")
260    @Override
261    public void mark(final int readLimit) {
262        this.markedOffset = this.offset;
263    }
264
265    @Override
266    public boolean markSupported() {
267        return true;
268    }
269
270    @Override
271    public int read() {
272        return offset < eod ? data[offset++] & 0xff : END_OF_STREAM;
273    }
274
275    @Override
276    public int read(final byte[] dest) {
277        Objects.requireNonNull(dest, "dest");
278        return read(dest, 0, dest.length);
279    }
280
281    @Override
282    public int read(final byte[] dest, final int off, final int len) {
283        Objects.requireNonNull(dest, "dest");
284        if (off < 0 || len < 0 || off + len > dest.length) {
285            throw new IndexOutOfBoundsException();
286        }
287
288        if (offset >= eod) {
289            return END_OF_STREAM;
290        }
291
292        int actualLen = eod - offset;
293        if (len < actualLen) {
294            actualLen = len;
295        }
296        if (actualLen <= 0) {
297            return 0;
298        }
299        System.arraycopy(data, offset, dest, off, actualLen);
300        offset += actualLen;
301        return actualLen;
302    }
303
304    @SuppressWarnings("sync-override")
305    @Override
306    public void reset() {
307        this.offset = this.markedOffset;
308    }
309
310    @Override
311    public long skip(final long n) {
312        if (n < 0) {
313            throw new IllegalArgumentException("Skipping backward is not supported");
314        }
315
316        long actualSkip = eod - offset;
317        if (n < actualSkip) {
318            actualSkip = n;
319        }
320
321        offset = Math.addExact(offset, Math.toIntExact(n));
322        return actualSkip;
323    }
324}