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