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