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 *      https://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.FileNotFoundException;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.io.OutputStream;
027import java.io.OutputStreamWriter;
028import java.io.RandomAccessFile;
029import java.io.Reader;
030import java.io.Writer;
031import java.net.URI;
032import java.nio.charset.Charset;
033import java.nio.file.Files;
034import java.nio.file.OpenOption;
035import java.nio.file.Path;
036import java.nio.file.Paths;
037import java.nio.file.StandardOpenOption;
038import java.nio.file.spi.FileSystemProvider;
039import java.util.Arrays;
040import java.util.Objects;
041
042import org.apache.commons.io.Charsets;
043import org.apache.commons.io.IORandomAccessFile;
044import org.apache.commons.io.IOUtils;
045import org.apache.commons.io.RandomAccessFileMode;
046import org.apache.commons.io.RandomAccessFiles;
047import org.apache.commons.io.file.spi.FileSystemProviders;
048import org.apache.commons.io.input.BufferedFileChannelInputStream;
049import org.apache.commons.io.input.CharSequenceInputStream;
050import org.apache.commons.io.input.CharSequenceReader;
051import org.apache.commons.io.input.ReaderInputStream;
052import org.apache.commons.io.output.RandomAccessFileOutputStream;
053import org.apache.commons.io.output.WriterOutputStream;
054
055/**
056 * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and
057 * {@link URI}.
058 * <p>
059 * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and
060 * {@link #getPath()}.
061 * </p>
062 *
063 * @param <T> the type of instances to build.
064 * @param <B> the type of builder subclass.
065 * @since 2.12.0
066 */
067public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> {
068
069    /**
070     * A {@link RandomAccessFile} origin.
071     * <p>
072     * This origin cannot support File and Path since you cannot query a RandomAccessFile for those attributes; Use {@link IORandomAccessFileOrigin}
073     * instead.
074     * </p>
075     *
076     * @param <T> the type of instances to build.
077     * @param <B> the type of builder subclass.
078     */
079    public abstract static class AbstractRandomAccessFileOrigin<T extends RandomAccessFile, B extends AbstractRandomAccessFileOrigin<T, B>>
080            extends AbstractOrigin<T, B> {
081
082        /**
083         * A {@link RandomAccessFile} origin.
084         * <p>
085         * Starting from this origin, you can everything except a Path and a File.
086         * </p>
087         *
088         * @param origin The origin, not null.
089         */
090        public AbstractRandomAccessFileOrigin(final T origin) {
091            super(origin);
092        }
093
094        @Override
095        public byte[] getByteArray() throws IOException {
096            final long longLen = origin.length();
097            if (longLen > Integer.MAX_VALUE) {
098                throw new IllegalStateException("Origin too large.");
099            }
100            return RandomAccessFiles.read(origin, 0, (int) longLen);
101        }
102
103        @Override
104        public byte[] getByteArray(final long position, final int length) throws IOException {
105            return RandomAccessFiles.read(origin, position, length);
106        }
107
108        @Override
109        public CharSequence getCharSequence(final Charset charset) throws IOException {
110            return new String(getByteArray(), charset);
111        }
112
113        @SuppressWarnings("resource")
114        @Override
115        public InputStream getInputStream(final OpenOption... options) throws IOException {
116            return BufferedFileChannelInputStream.builder().setFileChannel(origin.getChannel()).get();
117        }
118
119        @Override
120        public OutputStream getOutputStream(final OpenOption... options) throws IOException {
121            return RandomAccessFileOutputStream.builder().setRandomAccessFile(origin).get();
122        }
123
124        @Override
125        public T getRandomAccessFile(final OpenOption... openOption) {
126            // No conversion
127            return get();
128        }
129
130        @Override
131        public Reader getReader(final Charset charset) throws IOException {
132            return new InputStreamReader(getInputStream(), Charsets.toCharset(charset));
133        }
134
135        @Override
136        public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
137            return new OutputStreamWriter(getOutputStream(options), Charsets.toCharset(charset));
138        }
139
140        @Override
141        public long size() throws IOException {
142            return origin.length();
143        }
144    }
145
146    /**
147     * A {@code byte[]} origin.
148     */
149    public static class ByteArrayOrigin extends AbstractOrigin<byte[], ByteArrayOrigin> {
150
151        /**
152         * Constructs a new instance for the given origin.
153         *
154         * @param origin The origin, not null.
155         */
156        public ByteArrayOrigin(final byte[] origin) {
157            super(origin);
158        }
159
160        @Override
161        public byte[] getByteArray() {
162            // No conversion
163            return get();
164        }
165
166        /**
167         * {@inheritDoc}
168         * <p>
169         * The {@code options} parameter is ignored since a {@code byte[]} does not need an {@link OpenOption} to be read.
170         * </p>
171         */
172        @Override
173        public InputStream getInputStream(final OpenOption... options) throws IOException {
174            return new ByteArrayInputStream(origin);
175        }
176
177        @Override
178        public Reader getReader(final Charset charset) throws IOException {
179            return new InputStreamReader(getInputStream(), Charsets.toCharset(charset));
180        }
181
182        @Override
183        public long size() throws IOException {
184            return origin.length;
185        }
186
187    }
188
189    /**
190     * A {@link CharSequence} origin.
191     */
192    public static class CharSequenceOrigin extends AbstractOrigin<CharSequence, CharSequenceOrigin> {
193
194        /**
195         * Constructs a new instance for the given origin.
196         *
197         * @param origin The origin, not null.
198         */
199        public CharSequenceOrigin(final CharSequence origin) {
200            super(origin);
201        }
202
203        @Override
204        public byte[] getByteArray() {
205            // TODO Pass in a Charset? Consider if call sites actually need this.
206            return origin.toString().getBytes(Charset.defaultCharset());
207        }
208
209        /**
210         * {@inheritDoc}
211         * <p>
212         * The {@code charset} parameter is ignored since a {@link CharSequence} does not need a {@link Charset} to be read.
213         * </p>
214         */
215        @Override
216        public CharSequence getCharSequence(final Charset charset) {
217            // No conversion
218            return get();
219        }
220
221        /**
222         * {@inheritDoc}
223         * <p>
224         * The {@code options} parameter is ignored since a {@link CharSequence} does not need an {@link OpenOption} to be read.
225         * </p>
226         */
227        @Override
228        public InputStream getInputStream(final OpenOption... options) throws IOException {
229            // TODO Pass in a Charset? Consider if call sites actually need this.
230            return CharSequenceInputStream.builder().setCharSequence(getCharSequence(Charset.defaultCharset())).get();
231        }
232
233        /**
234         * {@inheritDoc}
235         * <p>
236         * The {@code charset} parameter is ignored since a {@link CharSequence} does not need a {@link Charset} to be read.
237         * </p>
238         */
239        @Override
240        public Reader getReader(final Charset charset) throws IOException {
241            return new CharSequenceReader(get());
242        }
243
244        @Override
245        public long size() throws IOException {
246            return origin.length();
247        }
248
249    }
250
251    /**
252     * A {@link File} origin.
253     * <p>
254     * 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.
255     * </p>
256     */
257    public static class FileOrigin extends AbstractOrigin<File, FileOrigin> {
258
259        /**
260         * Constructs a new instance for the given origin.
261         *
262         * @param origin The origin, not null.
263         */
264        public FileOrigin(final File origin) {
265            super(origin);
266        }
267
268        @Override
269        public byte[] getByteArray(final long position, final int length) throws IOException {
270            try (RandomAccessFile raf = RandomAccessFileMode.READ_ONLY.create(origin)) {
271                return RandomAccessFiles.read(raf, position, length);
272            }
273        }
274
275        @Override
276        public File getFile() {
277            // No conversion
278            return get();
279        }
280
281        @Override
282        public Path getPath() {
283            return get().toPath();
284        }
285
286    }
287
288    /**
289     * An {@link InputStream} origin.
290     * <p>
291     * This origin cannot provide some of the other aspects.
292     * </p>
293     */
294    public static class InputStreamOrigin extends AbstractOrigin<InputStream, InputStreamOrigin> {
295
296        /**
297         * Constructs a new instance for the given origin.
298         *
299         * @param origin The origin, not null.
300         */
301        public InputStreamOrigin(final InputStream origin) {
302            super(origin);
303        }
304
305        @Override
306        public byte[] getByteArray() throws IOException {
307            return IOUtils.toByteArray(origin);
308        }
309
310        /**
311         * {@inheritDoc}
312         * <p>
313         * The {@code options} parameter is ignored since a {@link InputStream} does not need an {@link OpenOption} to be read.
314         * </p>
315         */
316        @Override
317        public InputStream getInputStream(final OpenOption... options) {
318            // No conversion
319            return get();
320        }
321
322        @Override
323        public Reader getReader(final Charset charset) throws IOException {
324            return new InputStreamReader(getInputStream(), Charsets.toCharset(charset));
325        }
326
327    }
328
329    /**
330     * A {@link IORandomAccessFile} origin.
331     *
332     * @since 2.18.0
333     */
334    public static class IORandomAccessFileOrigin extends AbstractRandomAccessFileOrigin<IORandomAccessFile, IORandomAccessFileOrigin> {
335
336        /**
337         * A {@link RandomAccessFile} origin.
338         *
339         * @param origin The origin, not null.
340         */
341        public IORandomAccessFileOrigin(final IORandomAccessFile origin) {
342            super(origin);
343        }
344
345        @SuppressWarnings("resource")
346        @Override
347        public File getFile() {
348            return get().getFile();
349        }
350
351        @Override
352        public Path getPath() {
353            return getFile().toPath();
354        }
355
356    }
357
358    /**
359     * An {@link OutputStream} origin.
360     * <p>
361     * This origin cannot provide some of the other aspects.
362     * </p>
363     */
364    public static class OutputStreamOrigin extends AbstractOrigin<OutputStream, OutputStreamOrigin> {
365
366        /**
367         * Constructs a new instance for the given origin.
368         *
369         * @param origin The origin, not null.
370         */
371        public OutputStreamOrigin(final OutputStream origin) {
372            super(origin);
373        }
374
375        /**
376         * {@inheritDoc}
377         * <p>
378         * The {@code options} parameter is ignored since a {@link OutputStream} does not need an {@link OpenOption} to be written.
379         * </p>
380         */
381        @Override
382        public OutputStream getOutputStream(final OpenOption... options) {
383            // No conversion
384            return get();
385        }
386
387        /**
388         * {@inheritDoc}
389         * <p>
390         * The {@code options} parameter is ignored since a {@link OutputStream} does not need an {@link OpenOption} to be written.
391         * </p>
392         */
393        @Override
394        public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
395            return new OutputStreamWriter(origin, Charsets.toCharset(charset));
396        }
397    }
398
399    /**
400     * A {@link Path} origin.
401     * <p>
402     * 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.
403     * </p>
404     */
405    public static class PathOrigin extends AbstractOrigin<Path, PathOrigin> {
406
407        /**
408         * Constructs a new instance for the given origin.
409         *
410         * @param origin The origin, not null.
411         */
412        public PathOrigin(final Path origin) {
413            super(origin);
414        }
415
416        @Override
417        public byte[] getByteArray(final long position, final int length) throws IOException {
418            return RandomAccessFileMode.READ_ONLY.apply(origin, raf -> RandomAccessFiles.read(raf, position, length));
419        }
420
421        @Override
422        public File getFile() {
423            return get().toFile();
424        }
425
426        @Override
427        public Path getPath() {
428            // No conversion
429            return get();
430        }
431
432    }
433
434    /**
435     * A {@link RandomAccessFile} origin.
436     * <p>
437     * This origin cannot support File and Path since you cannot query a RandomAccessFile for those attributes; Use {@link IORandomAccessFileOrigin}
438     * instead.
439     * </p>
440     */
441    public static class RandomAccessFileOrigin extends AbstractRandomAccessFileOrigin<RandomAccessFile, RandomAccessFileOrigin> {
442
443        /**
444         * A {@link RandomAccessFile} origin.
445         * <p>
446         * Starting from this origin, you can everything except a Path and a File.
447         * </p>
448         *
449         * @param origin The origin, not null.
450         */
451        public RandomAccessFileOrigin(final RandomAccessFile origin) {
452            super(origin);
453        }
454
455    }
456
457    /**
458     * A {@link Reader} origin.
459     * <p>
460     * This origin cannot provide conversions to other aspects.
461     * </p>
462     */
463    public static class ReaderOrigin extends AbstractOrigin<Reader, ReaderOrigin> {
464
465        /**
466         * Constructs a new instance for the given origin.
467         *
468         * @param origin The origin, not null.
469         */
470        public ReaderOrigin(final Reader origin) {
471            super(origin);
472        }
473
474        @Override
475        public byte[] getByteArray() throws IOException {
476            // TODO Pass in a Charset? Consider if call sites actually need this.
477            return IOUtils.toByteArray(origin, Charset.defaultCharset());
478        }
479
480        /**
481         * {@inheritDoc}
482         * <p>
483         * The {@code charset} parameter is ignored since a {@link Reader} does not need a {@link Charset} to be read.
484         * </p>
485         */
486        @Override
487        public CharSequence getCharSequence(final Charset charset) throws IOException {
488            return IOUtils.toString(origin);
489        }
490
491        /**
492         * {@inheritDoc}
493         * <p>
494         * The {@code options} parameter is ignored since a {@link Reader} does not need an {@link OpenOption} to be read.
495         * </p>
496         */
497        @Override
498        public InputStream getInputStream(final OpenOption... options) throws IOException {
499            // TODO Pass in a Charset? Consider if call sites actually need this.
500            return ReaderInputStream.builder().setReader(origin).setCharset(Charset.defaultCharset()).get();
501        }
502
503        /**
504         * {@inheritDoc}
505         * <p>
506         * The {@code charset} parameter is ignored since a {@link Reader} does not need a {@link Charset} to be read.
507         * </p>
508         */
509        @Override
510        public Reader getReader(final Charset charset) throws IOException {
511            // No conversion
512            return get();
513        }
514    }
515
516    /**
517     * A {@link URI} origin.
518     */
519    public static class URIOrigin extends AbstractOrigin<URI, URIOrigin> {
520
521        private static final String SCHEME_HTTPS = "https";
522        private static final String SCHEME_HTTP = "http";
523
524        /**
525         * Constructs a new instance for the given origin.
526         *
527         * @param origin The origin, not null.
528         */
529        public URIOrigin(final URI origin) {
530            super(origin);
531        }
532
533        @Override
534        public File getFile() {
535            return getPath().toFile();
536        }
537
538        @Override
539        public InputStream getInputStream(final OpenOption... options) throws IOException {
540            final URI uri = get();
541            final String scheme = uri.getScheme();
542            final FileSystemProvider fileSystemProvider = FileSystemProviders.installed().getFileSystemProvider(scheme);
543            if (fileSystemProvider != null) {
544                return Files.newInputStream(fileSystemProvider.getPath(uri), options);
545            }
546            if (SCHEME_HTTP.equalsIgnoreCase(scheme) || SCHEME_HTTPS.equalsIgnoreCase(scheme)) {
547                return uri.toURL().openStream();
548            }
549            return Files.newInputStream(getPath(), options);
550        }
551
552        @Override
553        public Path getPath() {
554            return Paths.get(get());
555        }
556    }
557
558    /**
559     * A {@link Writer} origin.
560     * <p>
561     * This origin cannot provide conversions to other aspects.
562     * </p>
563     */
564    public static class WriterOrigin extends AbstractOrigin<Writer, WriterOrigin> {
565
566        /**
567         * Constructs a new instance for the given origin.
568         *
569         * @param origin The origin, not null.
570         */
571        public WriterOrigin(final Writer origin) {
572            super(origin);
573        }
574
575        /**
576         * {@inheritDoc}
577         * <p>
578         * The {@code options} parameter is ignored since a {@link Writer} does not need an {@link OpenOption} to be written.
579         * </p>
580         */
581        @Override
582        public OutputStream getOutputStream(final OpenOption... options) throws IOException {
583            // TODO Pass in a Charset? Consider if call sites actually need this.
584            return WriterOutputStream.builder().setWriter(origin).setCharset(Charset.defaultCharset()).get();
585        }
586
587        /**
588         * {@inheritDoc}
589         * <p>
590         * The {@code charset} parameter is ignored since a {@link Writer} does not need a {@link Charset} to be written.
591         * </p>
592         * <p>
593         * The {@code options} parameter is ignored since a {@link Writer} does not need an {@link OpenOption} to be written.
594         * </p>
595         */
596        @Override
597        public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
598            // No conversion
599            return get();
600        }
601    }
602
603    /**
604     * The non-null origin.
605     */
606    final T origin;
607
608    /**
609     * Constructs a new instance for subclasses.
610     *
611     * @param origin The origin, not null.
612     */
613    protected AbstractOrigin(final T origin) {
614        this.origin = Objects.requireNonNull(origin, "origin");
615    }
616
617    /**
618     * Gets the origin.
619     *
620     * @return the origin.
621     */
622    @Override
623    public T get() {
624        return origin;
625    }
626
627    /**
628     * Gets this origin as a byte array, if possible.
629     *
630     * @return this origin as a byte array, if possible.
631     * @throws IOException                   if an I/O error occurs.
632     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
633     */
634    public byte[] getByteArray() throws IOException {
635        return Files.readAllBytes(getPath());
636    }
637
638    /**
639     * Gets a portion of this origin as a byte array, if possible.
640     *
641     * @param position the initial index of the range to be copied, inclusive.
642     * @param length   How many bytes to copy.
643     * @return this origin as a byte array, if possible.
644     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
645     * @throws ArithmeticException           if the {@code position} overflows an int
646     * @throws IOException                   if an I/O error occurs.
647     * @since 2.13.0
648     */
649    public byte[] getByteArray(final long position, final int length) throws IOException {
650        final byte[] bytes = getByteArray();
651        // Checks for int overflow.
652        final int start = Math.toIntExact(position);
653        if (start < 0 || length < 0 || start + length < 0 || start + length > bytes.length) {
654            throw new IllegalArgumentException("Couldn't read array (start: " + start + ", length: " + length + ", data length: " + bytes.length + ").");
655        }
656        return Arrays.copyOfRange(bytes, start, start + length);
657    }
658
659    /**
660     * Gets this origin as a byte array, if possible.
661     *
662     * @param charset The charset to use if conversion from bytes is needed.
663     * @return this origin as a byte array, if possible.
664     * @throws IOException                   if an I/O error occurs.
665     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
666     */
667    public CharSequence getCharSequence(final Charset charset) throws IOException {
668        return new String(getByteArray(), charset);
669    }
670
671    /**
672     * Gets this origin as a Path, if possible.
673     *
674     * @return this origin as a Path, if possible.
675     * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass.
676     */
677    public File getFile() {
678        throw new UnsupportedOperationException(
679                String.format("%s#getFile() for %s origin %s", getSimpleClassName(), origin.getClass().getSimpleName(), origin));
680    }
681
682    /**
683     * Gets this origin as an InputStream, if possible.
684     *
685     * @param options options specifying how the file is opened
686     * @return this origin as an InputStream, if possible.
687     * @throws IOException                   if an I/O error occurs.
688     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
689     */
690    public InputStream getInputStream(final OpenOption... options) throws IOException {
691        return Files.newInputStream(getPath(), options);
692    }
693
694    /**
695     * Gets this origin as an OutputStream, if possible.
696     *
697     * @param options options specifying how the file is opened
698     * @return this origin as an OutputStream, if possible.
699     * @throws IOException                   if an I/O error occurs.
700     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
701     */
702    public OutputStream getOutputStream(final OpenOption... options) throws IOException {
703        return Files.newOutputStream(getPath(), options);
704    }
705
706    /**
707     * Gets this origin as a Path, if possible.
708     *
709     * @return this origin as a Path, if possible.
710     * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass.
711     */
712    public Path getPath() {
713        throw new UnsupportedOperationException(
714                String.format("%s#getPath() for %s origin %s", getSimpleClassName(), origin.getClass().getSimpleName(), origin));
715    }
716
717    /**
718     * Gets this origin as a RandomAccessFile, if possible.
719     *
720     * @param openOption options like {@link StandardOpenOption}.
721     * @return this origin as a RandomAccessFile, if possible.
722     * @throws FileNotFoundException         See {@link RandomAccessFile#RandomAccessFile(File, String)}.
723     * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass.
724     * @since 2.18.0
725     */
726    public RandomAccessFile getRandomAccessFile(final OpenOption... openOption) throws FileNotFoundException {
727        return RandomAccessFileMode.valueOf(openOption).create(getFile());
728    }
729
730    /**
731     * Gets a new Reader on the origin, buffered by default.
732     *
733     * @param charset the charset to use for decoding, null maps to the default Charset.
734     * @return a new Reader on the origin.
735     * @throws IOException if an I/O error occurs opening the file.
736     */
737    public Reader getReader(final Charset charset) throws IOException {
738        return Files.newBufferedReader(getPath(), Charsets.toCharset(charset));
739    }
740
741    private String getSimpleClassName() {
742        return getClass().getSimpleName();
743    }
744
745    /**
746     * Gets a new Writer on the origin, buffered by default.
747     *
748     * @param charset the charset to use for encoding
749     * @param options options specifying how the file is opened
750     * @return a new Writer on the origin.
751     * @throws IOException                   if an I/O error occurs opening or creating the file.
752     * @throws UnsupportedOperationException if the origin cannot be converted to a Path.
753     */
754    public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException {
755        return Files.newBufferedWriter(getPath(), Charsets.toCharset(charset), options);
756    }
757
758    /**
759     * Gets the size of the origin, if possible.
760     *
761     * @return the size of the origin in bytes or characters.
762     * @throws IOException if an I/O error occurs.
763     * @since 2.13.0
764     */
765    public long size() throws IOException {
766        return Files.size(getPath());
767    }
768
769    @Override
770    public String toString() {
771        return getSimpleClassName() + "[" + origin.toString() + "]";
772    }
773}