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