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}