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    *      https://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.Charsets;
43  import org.apache.commons.io.IORandomAccessFile;
44  import org.apache.commons.io.IOUtils;
45  import org.apache.commons.io.RandomAccessFileMode;
46  import org.apache.commons.io.RandomAccessFiles;
47  import org.apache.commons.io.file.spi.FileSystemProviders;
48  import org.apache.commons.io.input.BufferedFileChannelInputStream;
49  import org.apache.commons.io.input.CharSequenceInputStream;
50  import org.apache.commons.io.input.CharSequenceReader;
51  import org.apache.commons.io.input.ReaderInputStream;
52  import org.apache.commons.io.output.RandomAccessFileOutputStream;
53  import org.apache.commons.io.output.WriterOutputStream;
54  
55  /**
56   * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and
57   * {@link URI}.
58   * <p>
59   * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and
60   * {@link #getPath()}.
61   * </p>
62   *
63   * @param <T> the type of instances to build.
64   * @param <B> the type of builder subclass.
65   * @since 2.12.0
66   */
67  public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> {
68  
69      /**
70       * A {@link RandomAccessFile} origin.
71       * <p>
72       * This origin cannot support File and Path since you cannot query a RandomAccessFile for those attributes; Use {@link IORandomAccessFileOrigin}
73       * instead.
74       * </p>
75       *
76       * @param <T> the type of instances to build.
77       * @param <B> the type of builder subclass.
78       */
79      public abstract static class AbstractRandomAccessFileOrigin<T extends RandomAccessFile, B extends AbstractRandomAccessFileOrigin<T, B>>
80              extends AbstractOrigin<T, B> {
81  
82          /**
83           * A {@link RandomAccessFile} origin.
84           * <p>
85           * Starting from this origin, you can everything except a Path and a File.
86           * </p>
87           *
88           * @param origin The origin, not null.
89           */
90          public AbstractRandomAccessFileOrigin(final T origin) {
91              super(origin);
92          }
93  
94          @Override
95          public byte[] getByteArray() throws IOException {
96              final long longLen = origin.length();
97              if (longLen > Integer.MAX_VALUE) {
98                  throw new IllegalStateException("Origin too large.");
99              }
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 }