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