    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 */
    017
    018package org.apache.commons.io.build;
    019
    020import java.io.ByteArrayInputStream;
    021import java.io.File;
    022import java.io.IOException;
    023import java.io.InputStream;
    024import java.io.InputStreamReader;
    025import java.io.OutputStream;
    026import java.io.OutputStreamWriter;
    027import java.io.RandomAccessFile;
    028import java.io.Reader;
    029import java.io.Writer;
    030import java.net.URI;
    031import java.nio.charset.Charset;
    032import java.nio.file.Files;
    033import java.nio.file.OpenOption;
    034import java.nio.file.Path;
    035import java.nio.file.Paths;
    036import java.util.Arrays;
    037import java.util.Objects;
    038
    039import org.apache.commons.io.IOUtils;
    040import org.apache.commons.io.RandomAccessFileMode;
    041import org.apache.commons.io.RandomAccessFiles;
    042import org.apache.commons.io.input.ReaderInputStream;
    043import org.apache.commons.io.output.WriterOutputStream;
    044
    045/**
    046 * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and
    047 * {@link URI}.
    048 * <p>
    049 * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and
    050 * {@link #getPath()}.
    051 * </p>
    052 *
    053 * @param <T> the type of instances to build.
    054 * @param <B> the type of builder subclass.
    055 * @since 2.12.0
    056 */
    057public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> {
    058
    059    /**
    060     * A {@code byte[]} origin.
    061     */
    062    public static class ByteArrayOrigin extends AbstractOrigin<byte[], ByteArrayOrigin> {
    063
    064        /**
    065         * Constructs a new instance for the given origin.
    066         *
    067         * @param origin The origin.
    068         */
    069        public ByteArrayOrigin(final byte[] origin) {
    070            super(origin);
    071        }
    072
    073        @Override
    074        public byte[] getByteArray() {
    075            // No conversion
    076            return get();
    077        }
    078
    079        @Override
    080        public InputStream getInputStream(final OpenOption... options) throws IOException {
    081            return new ByteArrayInputStream(origin);
    082        }
    083
    084        @Override
    085        public Reader getReader(final Charset charset) throws IOException {
    086            return new InputStreamReader(getInputStream(), charset);
    087        }
    088
    089        @Override
    090        public long size() throws IOException {
    091            return origin.length;
    092        }
    093
    094    }
    095
    096    /**
    097     * A {@link CharSequence} origin.
    098     */
    099    public static class CharSequenceOrigin extends AbstractOrigin<CharSequence, CharSequenceOrigin> {
    100
    101        /**
    102         * Constructs a new instance for the given origin.
    103         *
    104         * @param origin The origin.
    105         */
    106        public CharSequenceOrigin(final CharSequence origin) {
    107            super(origin);
    108        }
    109
    110        @Override
    111        public byte[] getByteArray() {
    112            // TODO Pass in a Charset? Consider if call sites actually need this.
    113            return origin.toString().getBytes(Charset.defaultCharset());
    114        }
    115
    116        @Override
    117        public CharSequence getCharSequence(final Charset charset) {
    118            // No conversion
    119            return get();
    120        }
    121
    122        @Override
    123        public InputStream getInputStream(final OpenOption... options) throws IOException {
    124            // TODO Pass in a Charset? Consider if call sites actually need this.
    125            return new ByteArrayInputStream(origin.toString().getBytes(Charset.defaultCharset()));
    126            // Needs [IO-795] CharSequenceInputStream.reset() only works once.
    127            // return CharSequenceInputStream.builder().setCharSequence(getCharSequence(Charset.defaultCharset())).get();
    128        }
    129
    130        @Override
    131        public Reader getReader(final Charset charset) throws IOException {
    132            return new InputStreamReader(getInputStream(), charset);
    133        }
    134
    135        @Override
    136        public long size() throws IOException {
    137            return origin.length();
    138        }
    139
    140    }
    141
    142    /**
    143     * A {@link File} origin.
    144     * <p>
    145     * Starting from this origin, you can get a byte array, a file, an input stream, an output stream, a path, a reader, and a writer.
    146     * </p>
    147     */
    148    public static class FileOrigin extends AbstractOrigin<File, FileOrigin> {
    149
    150        /**
    151         * Constructs a new instance for the given origin.
    152         *
    153         * @param origin The origin.
    154         */
    155        public FileOrigin(final File origin) {
    156            super(origin);
    157        }
    158
    159        @Override
    160        public byte[] getByteArray(final long position, final int length) throws IOException {
    161            try (RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(origin)) {
    162                return RandomAccessFiles.read(raf, position, length);
    163            }
    164        }
    165
    166        @Override
    167        public File getFile() {
    168            // No conversion
    169            return get();
    170        }
    171
    172        @Override
    173        public Path getPath() {
    174            return get().toPath();
    175        }
    176
    177    }
    178
    179    /**
    180     * An {@link InputStream} origin.
    181     * <p>
    182     * This origin cannot provide some of the other aspects.
    183     * </p>
    184     */
    185    public static class InputStreamOrigin extends AbstractOrigin<InputStream, InputStreamOrigin> {
    186
    187        /**
    188         * Constructs a new instance for the given origin.
    189         *
    190         * @param origin The origin.
    191         */
    192        public InputStreamOrigin(final InputStream origin) {
    193            super(origin);
    194        }
    195
    196        @Override
    197        public byte[] getByteArray() throws IOException {
    198            return IOUtils.toByteArray(origin);
    199        }
    200
    201        @Override
    202        public InputStream getInputStream(final OpenOption... options) {
    203            // No conversion
    204            return get();
    205        }
    206
    207        @Override
    208        public Reader getReader(final Charset charset) throws IOException {
    209            return new InputStreamReader(getInputStream(), charset);
    210        }
    211
    212    }
    213
    214    /**
    215     * An {@link OutputStream} origin.
    216     * <p>
    217     * This origin cannot provide some of the other aspects.
    218     * </p>
    219     */
    220    public static class OutputStreamOrigin extends AbstractOrigin<OutputStream, OutputStreamOrigin> {
    221
    222        /**
    223         * Constructs a new instance for the given origin.
    224         *
    225         * @param origin The origin.
    226         */
    227        public OutputStreamOrigin(final OutputStream origin) {
    228            super(origin);
    229        }
    230
    231        @Override
    232        public OutputStream getOutputStream(final OpenOption... options) {
    233            // No conversion
    234            return get();
    235        }
    236
    237        @Override
    238        public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
    239            return new OutputStreamWriter(origin, charset);
    240        }
    241    }
    242
    243    /**
    244     * A {@link Path} origin.
    245     * <p>
    246     * Starting from this origin, you can get a byte array, a file, an input stream, an output stream, a path, a reader, and a writer.
    247     * </p>
    248     */
    249    public static class PathOrigin extends AbstractOrigin<Path, PathOrigin> {
    250
    251        /**
    252         * Constructs a new instance for the given origin.
    253         *
    254         * @param origin The origin.
    255         */
    256        public PathOrigin(final Path origin) {
    257            super(origin);
    258        }
    259
    260        @Override
    261        public byte[] getByteArray(final long position, final int length) throws IOException {
    262            try (RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(origin)) {
    263                return RandomAccessFiles.read(raf, position, length);
    264            }
    265        }
    266
    267        @Override
    268        public File getFile() {
    269            return get().toFile();
    270        }
    271
    272        @Override
    273        public Path getPath() {
    274            // No conversion
    275            return get();
    276        }
    277
    278    }
    279
    280    /**
    281     * An {@link Reader} origin.
    282     * <p>
    283     * This origin cannot provide other aspects.
    284     * </p>
    285     */
    286    public static class ReaderOrigin extends AbstractOrigin<Reader, ReaderOrigin> {
    287
    288        /**
    289         * Constructs a new instance for the given origin.
    290         *
    291         * @param origin The origin.
    292         */
    293        public ReaderOrigin(final Reader origin) {
    294            super(origin);
    295        }
    296
    297        @Override
    298        public byte[] getByteArray() throws IOException {
    299            // TODO Pass in a Charset? Consider if call sites actually need this.
    300            return IOUtils.toByteArray(origin, Charset.defaultCharset());
    301        }
    302
    303        @Override
    304        public CharSequence getCharSequence(final Charset charset) throws IOException {
    305            return IOUtils.toString(origin);
    306        }
    307
    308        @Override
    309        public InputStream getInputStream(final OpenOption... options) throws IOException {
    310            // TODO Pass in a Charset? Consider if call sites actually need this.
    311            return ReaderInputStream.builder().setReader(origin).setCharset(Charset.defaultCharset()).get();
    312        }
    313
    314        @Override
    315        public Reader getReader(final Charset charset) throws IOException {
    316            // No conversion
    317            return get();
    318        }
    319    }
    320
    321    /**
    322     * A {@link URI} origin.
    323     */
    324    public static class URIOrigin extends AbstractOrigin<URI, URIOrigin> {
    325
    326        /**
    327         * Constructs a new instance for the given origin.
    328         *
    329         * @param origin The origin.
    330         */
    331        public URIOrigin(final URI origin) {
    332            super(origin);
    333        }
    334
    335        @Override
    336        public File getFile() {
    337            return getPath().toFile();
    338        }
    339
    340        @Override
    341        public Path getPath() {
    342            return Paths.get(get());
    343        }
    344
    345    }
    346
    347    /**
    348     * An {@link Writer} origin.
    349     * <p>
    350     * This origin cannot provide other aspects.
    351     * </p>
    352     */
    353    public static class WriterOrigin extends AbstractOrigin<Writer, WriterOrigin> {
    354
    355        /**
    356         * Constructs a new instance for the given origin.
    357         *
    358         * @param origin The origin.
    359         */
    360        public WriterOrigin(final Writer origin) {
    361            super(origin);
    362        }
    363
    364        @Override
    365        public OutputStream getOutputStream(final OpenOption... options) throws IOException {
    366            // TODO Pass in a Charset? Consider if call sites actually need this.
    367            return WriterOutputStream.builder().setWriter(origin).setCharset(Charset.defaultCharset()).get();
    368        }
    369
    370        @Override
    371        public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
    372            // No conversion
    373            return get();
    374        }
    375    }
    376
    377    /**
    378     * The non-null origin.
    379     */
    380    final T origin;
    381
    382    /**
    383     * Constructs a new instance for a subclass.
    384     *
    385     * @param origin The origin.
    386     */
    387    protected AbstractOrigin(final T origin) {
    388        this.origin = Objects.requireNonNull(origin, "origin");
    389    }
    390
    391    /**
    392     * Gets the origin.
    393     *
    394     * @return the origin.
    395     */
    396    @Override
    397    public T get() {
    398        return origin;
    399    }
    400
    401    /**
    402     * Gets this origin as a byte array, if possible.
    403     *
    404     * @return this origin as a byte array, if possible.
    405     * @throws IOException                   if an I/O error occurs.
    406     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    407     */
    408    public byte[] getByteArray() throws IOException {
    409        return Files.readAllBytes(getPath());
    410    }
    411
    412    /**
    413     * Gets this origin as a byte array, if possible.
    414     *
    415     * @param position the initial index of the range to be copied, inclusive.
    416     * @param length   How many bytes to copy.
    417     * @return this origin as a byte array, if possible.
    418     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    419     * @throws ArithmeticException           if the {@code position} overflows an int
    420     * @throws IOException                   if an I/O error occurs.
    421     * @since 2.13.0
    422     */
    423    public byte[] getByteArray(final long position, final int length) throws IOException {
    424        final byte[] bytes = getByteArray();
    425        // Checks for int overflow.
    426        final int start = Math.toIntExact(position);
    427        if (start < 0 || length < 0 || start + length < 0 || start + length > bytes.length) {
    428            throw new IllegalArgumentException("Couldn't read array (start: " + start + ", length: " + length + ", data length: " + bytes.length + ").");
    429        }
    430        return Arrays.copyOfRange(bytes, start, start + length);
    431    }
    432
    433    /**
    434     * Gets this origin as a byte array, if possible.
    435     *
    436     * @param charset The charset to use if conversion from bytes is needed.
    437     * @return this origin as a byte array, if possible.
    438     * @throws IOException                   if an I/O error occurs.
    439     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    440     */
    441    public CharSequence getCharSequence(final Charset charset) throws IOException {
    442        return new String(getByteArray(), charset);
    443    }
    444
    445    /**
    446     * Gets this origin as a Path, if possible.
    447     *
    448     * @return this origin as a Path, if possible.
    449     * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass.
    450     */
    451    public File getFile() {
    452        throw new UnsupportedOperationException(
    453                String.format("%s#getFile() for %s origin %s", getClass().getSimpleName(), origin.getClass().getSimpleName(), origin));
    454    }
    455
    456    /**
    457     * Gets this origin as an InputStream, if possible.
    458     *
    459     * @param options options specifying how the file is opened
    460     * @return this origin as an InputStream, if possible.
    461     * @throws IOException                   if an I/O error occurs.
    462     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    463     */
    464    public InputStream getInputStream(final OpenOption... options) throws IOException {
    465        return Files.newInputStream(getPath(), options);
    466    }
    467
    468    /**
    469     * Gets this origin as an OutputStream, if possible.
    470     *
    471     * @param options options specifying how the file is opened
    472     * @return this origin as an OutputStream, if possible.
    473     * @throws IOException                   if an I/O error occurs.
    474     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    475     */
    476    public OutputStream getOutputStream(final OpenOption... options) throws IOException {
    477        return Files.newOutputStream(getPath(), options);
    478    }
    479
    480    /**
    481     * Gets this origin as a Path, if possible.
    482     *
    483     * @return this origin as a Path, if possible.
    484     * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass.
    485     */
    486    public Path getPath() {
    487        throw new UnsupportedOperationException(
    488                String.format("%s#getPath() for %s origin %s", getClass().getSimpleName(), origin.getClass().getSimpleName(), origin));
    489    }
    490
    491    /**
    492     * Gets a new Reader on the origin, buffered by default.
    493     *
    494     * @param charset the charset to use for decoding
    495     * @return a new Reader on the origin.
    496     * @throws IOException if an I/O error occurs opening the file.
    497     */
    498    public Reader getReader(final Charset charset) throws IOException {
    499        return Files.newBufferedReader(getPath(), charset);
    500    }
    501
    502    /**
    503     * Gets a new Writer on the origin, buffered by default.
    504     *
    505     * @param charset the charset to use for encoding
    506     * @param options options specifying how the file is opened
    507     * @return a new Writer on the origin.
    508     * @throws IOException                   if an I/O error occurs opening or creating the file.
    509     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
    510     */
    511    public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
    512        return Files.newBufferedWriter(getPath(), charset, options);
    513    }
    514
    515    /**
    516     * Gets the size of the origin, if possible.
    517     *
    518     * @return the size of the origin in bytes or characters.
    519     * @throws IOException if an I/O error occurs.
    520     * @since 2.13.0
    521     */
    522    public long size() throws IOException {
    523        return Files.size(getPath());
    524    }
    525
    526    @Override
    527    public String toString() {
    528        return getClass().getSimpleName() + "[" + origin.toString() + "]";
    529    }
    530}