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