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.FileNotFoundException; 023import java.io.IOException; 024import java.io.InputStream; 025import java.io.InputStreamReader; 026import java.io.OutputStream; 027import java.io.OutputStreamWriter; 028import java.io.RandomAccessFile; 029import java.io.Reader; 030import java.io.Writer; 031import java.net.URI; 032import java.nio.charset.Charset; 033import java.nio.file.Files; 034import java.nio.file.OpenOption; 035import java.nio.file.Path; 036import java.nio.file.Paths; 037import java.nio.file.StandardOpenOption; 038import java.nio.file.spi.FileSystemProvider; 039import java.util.Arrays; 040import java.util.Objects; 041 042import org.apache.commons.io.IORandomAccessFile; 043import org.apache.commons.io.IOUtils; 044import org.apache.commons.io.RandomAccessFileMode; 045import org.apache.commons.io.RandomAccessFiles; 046import org.apache.commons.io.file.spi.FileSystemProviders; 047import org.apache.commons.io.input.BufferedFileChannelInputStream; 048import org.apache.commons.io.input.CharSequenceInputStream; 049import org.apache.commons.io.input.CharSequenceReader; 050import org.apache.commons.io.input.ReaderInputStream; 051import org.apache.commons.io.output.RandomAccessFileOutputStream; 052import org.apache.commons.io.output.WriterOutputStream; 053 054/** 055 * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and 056 * {@link URI}. 057 * <p> 058 * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and 059 * {@link #getPath()}. 060 * </p> 061 * 062 * @param <T> the type of instances to build. 063 * @param <B> the type of builder subclass. 064 * @since 2.12.0 065 */ 066public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> { 067 068 /** 069 * A {@link RandomAccessFile} origin. 070 * <p> 071 * This origin cannot support File and Path since you cannot query a RandomAccessFile for those attributes; Use {@link IORandomAccessFileOrigin} 072 * instead. 073 * </p> 074 * 075 * @param <T> the type of instances to build. 076 * @param <B> the type of builder subclass. 077 */ 078 public static abstract class AbstractRandomAccessFileOrigin<T extends RandomAccessFile, B extends AbstractRandomAccessFileOrigin<T, B>> 079 extends AbstractOrigin<T, B> { 080 081 /** 082 * A {@link RandomAccessFile} origin. 083 * <p> 084 * Starting from this origin, you can everything except a Path and a File. 085 * </p> 086 * 087 * @param origin The origin. 088 */ 089 public AbstractRandomAccessFileOrigin(final T origin) { 090 super(origin); 091 } 092 093 @Override 094 public byte[] getByteArray() throws IOException { 095 final long longLen = origin.length(); 096 if (longLen > Integer.MAX_VALUE) { 097 throw new IllegalStateException("Origin too large."); 098 } 099 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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 a subclass. 609 * 610 * @param origin The origin. 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}