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