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