View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.io.build;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.File;
22  import java.io.FileNotFoundException;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.OutputStream;
27  import java.io.OutputStreamWriter;
28  import java.io.RandomAccessFile;
29  import java.io.Reader;
30  import java.io.Writer;
31  import java.net.URI;
32  import java.nio.charset.Charset;
33  import java.nio.file.Files;
34  import java.nio.file.OpenOption;
35  import java.nio.file.Path;
36  import java.nio.file.Paths;
37  import java.nio.file.StandardOpenOption;
38  import java.nio.file.spi.FileSystemProvider;
39  import java.util.Arrays;
40  import java.util.Objects;
41  
42  import org.apache.commons.io.IORandomAccessFile;
43  import org.apache.commons.io.IOUtils;
44  import org.apache.commons.io.RandomAccessFileMode;
45  import org.apache.commons.io.RandomAccessFiles;
46  import org.apache.commons.io.file.spi.FileSystemProviders;
47  import org.apache.commons.io.input.BufferedFileChannelInputStream;
48  import org.apache.commons.io.input.CharSequenceInputStream;
49  import org.apache.commons.io.input.CharSequenceReader;
50  import org.apache.commons.io.input.ReaderInputStream;
51  import org.apache.commons.io.output.RandomAccessFileOutputStream;
52  import org.apache.commons.io.output.WriterOutputStream;
53  
54  /**
55   * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and
56   * {@link URI}.
57   * <p>
58   * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and
59   * {@link #getPath()}.
60   * </p>
61   *
62   * @param <T> the type of instances to build.
63   * @param <B> the type of builder subclass.
64   * @since 2.12.0
65   */
66  public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> {
67  
68      /**
69       * A {@link RandomAccessFile} origin.
70       * <p>
71       * This origin cannot support File and Path since you cannot query a RandomAccessFile for those attributes; Use {@link IORandomAccessFileOrigin}
72       * instead.
73       * </p>
74       *
75       * @param <T> the type of instances to build.
76       * @param <B> the type of builder subclass.
77       */
78      public abstract static class AbstractRandomAccessFileOrigin<T extends RandomAccessFile, B extends AbstractRandomAccessFileOrigin<T, B>>
79              extends AbstractOrigin<T, B> {
80  
81          /**
82           * A {@link RandomAccessFile} origin.
83           * <p>
84           * Starting from this origin, you can everything except a Path and a File.
85           * </p>
86           *
87           * @param origin The origin, not null.
88           */
89          public AbstractRandomAccessFileOrigin(final T origin) {
90              super(origin);
91          }
92  
93          @Override
94          public byte[] getByteArray() throws IOException {
95              final long longLen = origin.length();
96              if (longLen > Integer.MAX_VALUE) {
97                  throw new IllegalStateException("Origin too large.");
98              }
99              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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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, not null.
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 subclasses.
609      *
610      * @param origin The origin, not null.
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 }