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.CharSequenceInputStream; 043import org.apache.commons.io.input.CharSequenceReader; 044import org.apache.commons.io.input.ReaderInputStream; 045import org.apache.commons.io.output.WriterOutputStream; 046 047/** 048 * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and 049 * {@link URI}. 050 * <p> 051 * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and 052 * {@link #getPath()}. 053 * </p> 054 * 055 * @param <T> the type of instances to build. 056 * @param <B> the type of builder subclass. 057 * @since 2.12.0 058 */ 059public abstract class AbstractOrigin<T, B extends AbstractOrigin<T, B>> extends AbstractSupplier<T, B> { 060 061 /** 062 * A {@code byte[]} origin. 063 */ 064 public static class ByteArrayOrigin extends AbstractOrigin<byte[], ByteArrayOrigin> { 065 066 /** 067 * Constructs a new instance for the given origin. 068 * 069 * @param origin The origin. 070 */ 071 public ByteArrayOrigin(final byte[] origin) { 072 super(origin); 073 } 074 075 @Override 076 public byte[] getByteArray() { 077 // No conversion 078 return get(); 079 } 080 081 /** 082 * {@inheritDoc} 083 * <p> 084 * The {@code options} parameter is ignored since a {@code byte[]} does not need an {@link OpenOption} to be read. 085 * </p> 086 */ 087 @Override 088 public InputStream getInputStream(final OpenOption... options) throws IOException { 089 return new ByteArrayInputStream(origin); 090 } 091 092 @Override 093 public Reader getReader(final Charset charset) throws IOException { 094 return new InputStreamReader(getInputStream(), charset); 095 } 096 097 @Override 098 public long size() throws IOException { 099 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}