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}