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