diff --git a/.asf.yaml b/.asf.yaml index 5ea25ac8897..33a8dd8c4ff 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -23,7 +23,8 @@ notifications: pullrequests: issues@commons.apache.org jira_options: link label jobs: notifications@commons.apache.org - issues_bot_dependabot: notifications@commons.apache.org - pullrequests_bot_dependabot: notifications@commons.apache.org + # commits_bot_dependabot: dependabot@commons.apache.org + issues_bot_dependabot: dependabot@commons.apache.org + pullrequests_bot_dependabot: dependabot@commons.apache.org issues_bot_codecov-commenter: notifications@commons.apache.org pullrequests_bot_codecov-commenter: notifications@commons.apache.org diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index e17973cb0c5..4cbe168c3e8 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -22,7 +22,9 @@ Thanks for your contribution to [Apache Commons](https://commons.apache.org/)! Y Before you push a pull request, review this list: - [ ] Read the [contribution guidelines](CONTRIBUTING.md) for this project. +- [ ] Read the [ASF Generative Tooling Guidance](https://www.apache.org/legal/generative-tooling.html) if you use Artificial Intelligence (AI). +- [ ] I used AI to create any part of, or all of, this pull request. - [ ] Run a successful build using the default [Maven](https://maven.apache.org/) goal with `mvn`; that's `mvn` on the command line by itself. -- [ ] Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible but is a best-practice. +- [ ] Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best practice. - [ ] Write a pull request description that is detailed enough to understand what the pull request does, how, and why. -- [ ] Each commit in the pull request should have a meaningful subject line and body. Note that commits might be squashed by a maintainer on merge. +- [ ] Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4f6d2427a9c..276e14f41de 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -50,10 +50,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -62,7 +62,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # 3.29.2 + uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # 3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -73,7 +73,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@181d5eefc20863364f96762470ba6f862bdef56b # 3.29.2 + uses: github/codeql-action/autobuild@0499de31b99561a6d14a36a5f662c2a54f91beee # 3.29.5 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -87,4 +87,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # 3.29.2 + uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # 3.29.5 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 33573cf948f..a657a4ae2cd 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -26,6 +26,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - name: 'Dependency Review PR' - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + uses: actions/dependency-review-action@40c09b7dc99638e5ddb0bfd91c1673effc064d8a # v4.8.1 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 24ba38ed379..ee6b9c99a12 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -18,6 +18,8 @@ name: Java CI on: workflow_dispatch: push: + branches: + - 'master' paths-ignore: - '**/workflows/*.yml' - '!**/workflows/maven.yml' @@ -37,7 +39,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-13] - java: [ 8, 11, 17, 21 ] + java: [ 8, 11, 17, 21, 25 ] experimental: [false] # Keep the same parameter order as the matrix above include: @@ -46,40 +48,29 @@ jobs: java: 21 experimental: false deploy: true - # Experimental builds: Java 24-ea + # Experimental builds: Java 26-ea - os: ubuntu-latest - java: 24 + java: 26-ea experimental: true - os: windows-latest - java: 24 + java: 26-ea experimental: true - os: macos-latest - java: 24 + java: 26-ea experimental: true - # Experimental builds: Java 24-ea - - os: ubuntu-latest - java: 25-ea - experimental: true - - os: windows-latest - java: 25-ea - experimental: true - - os: macos-latest - java: 25-ea - experimental: true - fail-fast: false - + fail-fast: false steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: | ${{ runner.os }}-maven- - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1 + uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0 with: distribution: 'temurin' java-version: ${{ matrix.java }} diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 6ffc5bf007a..3215897f03b 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -42,12 +42,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # 2.4.2 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # 2.4.3 with: results_file: results.sarif results_format: sarif @@ -59,13 +59,13 @@ jobs: publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # 5.0.0 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@181d5eefc20863364f96762470ba6f862bdef56b # 3.29.2 + uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # 3.29.5 with: sarif_file: results.sarif diff --git a/README.md b/README.md index bb31ce854a6..36a82594a8e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Apache Commons IO [![Java CI](https://github.com/apache/commons-io/actions/workflows/maven.yml/badge.svg)](https://github.com/apache/commons-io/actions/workflows/maven.yml) [![Maven Central](https://img.shields.io/maven-central/v/commons-io/commons-io?label=Maven%20Central)](https://search.maven.org/artifact/commons-io/commons-io) -[![Javadocs](https://javadoc.io/badge/commons-io/commons-io/2.20.0.svg)](https://javadoc.io/doc/commons-io/commons-io/2.20.0) +[![Javadocs](https://javadoc.io/badge/commons-io/commons-io/2.21.0.svg)](https://javadoc.io/doc/commons-io/commons-io/2.21.0) [![CodeQL](https://github.com/apache/commons-io/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/apache/commons-io/actions/workflows/codeql-analysis.yml) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/apache/commons-io/badge)](https://api.securityscorecards.dev/projects/github.com/apache/commons-io) @@ -69,7 +69,7 @@ Alternatively, you can pull it from the central Maven repositories: commons-io commons-io - 2.20.0 + 2.21.0 ``` @@ -90,7 +90,7 @@ There are some guidelines which will make applying PRs easier for us: + Respect the existing code style for each file. + Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change. + Provide JUnit tests for your changes and make sure your changes don't break any existing tests by running `mvn`. -+ Before you pushing a PR, run `mvn` (by itself), this runs the default goal, which contains all build checks. ++ Before you push a PR, run `mvn` (without arguments). This runs the default goal which contains all build checks. + To see the code coverage report, regardless of coverage failures, run `mvn clean site -Dcommons.jacoco.haltOnFailure=false -Pjacoco` If you plan to contribute on a regular basis, please consider filing a [contributor license agreement](https://www.apache.org/licenses/#clas). diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 1f30f8b0f5a..3f84b517499 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,4 +1,82 @@ +Apache Commons IO 2.21.0 Release Notes + +The Apache Commons IO team is pleased to announce the release of Apache Commons IO 2.21.0. + +Introduction +------------ + +The Apache Commons IO library contains utility classes, stream implementations, file filters, +file comparators, endian transformation classes, and much more. + +Version 2.21.0: Java 8 or later is required. + +New features +------------ + +o FileUtils#byteCountToDisplaySize() supports Zettabyte, Yottabyte, Ronnabyte and Quettabyte #763. Thanks to strangelookingnerd, Gary Gregory. +o Add org.apache.commons.io.FileUtils.ONE_RB #763. Thanks to strangelookingnerd, Gary Gregory. +o Add org.apache.commons.io.FileUtils.ONE_QB #763. Thanks to strangelookingnerd, Gary Gregory. +o Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(byte[], int, int, long). Thanks to Gary Gregory. +o Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(byte[], long). Thanks to Gary Gregory. +o Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(int, long). Thanks to Gary Gregory. +o Add length unit support in FileSystem limits. Thanks to Piotr P. Karwasz. +o Add IOUtils.toByteArray(InputStream, int, int) for safer chunked reading with size validation. Thanks to Piotr P. Karwasz. +o Add org.apache.commons.io.file.PathUtils.getPath(String, String). Thanks to Gary Gregory. +o Add org.apache.commons.io.channels.ByteArraySeekableByteChannel. Thanks to Gary Gregory. +o Add IOIterable.asIterable(). Thanks to Gary Gregory. +o Add NIO channel support to `AbstractStreamBuilder`. Thanks to Piotr P. Karwasz. +o Add CloseShieldChannel to close-shielded NIO Channels #786. Thanks to Piotr P. Karwasz. +o Added IOUtils.checkFromIndexSize as a Java 8 backport of Objects.checkFromIndexSize #790. Thanks to Piotr P. Karwasz. + +Fixed Bugs +---------- + +o When testing on Java 21 and up, enable -XX:+EnableDynamicAgentLoading. Thanks to Gary Gregory. +o When testing on Java 24 and up, don't fail FileUtilsListFilesTest for a different behavior in the JRE. Thanks to Gary Gregory. +o ValidatingObjectInputStream does not validate dynamic proxy interfaces. Thanks to Stanislav Fort, Gary Gregory. +o BoundedInputStream.getRemaining() now reports Long.MAX_VALUE instead of 0 when no limit is set. Thanks to Piotr P. Karwasz. +o BoundedInputStream.available() correctly accounts for the maximum read limit. Thanks to Piotr P. Karwasz. +o Deprecate IOUtils.readFully(InputStream, int) in favor of toByteArray(InputStream, int). Thanks to Gary Gregory, Piotr P. Karwasz. +o IOUtils.toByteArray(InputStream) now throws IOException on byte array overflow. Thanks to Piotr P. Karwasz. +o Javadoc general improvements. Thanks to Gary Gregory, Piotr P. Karwasz. +o IOUtils.toByteArray() now throws EOFException when not enough data is available #796. Thanks to Piotr P. Karwasz. +o Fix IOUtils.skip() usage in concurrent scenarios. Thanks to Piotr P. Karwasz. +o [javadoc] Fix XmlStreamReader Javadoc to indicate the correct class that is built #806. Thanks to J Hawkins. + +Changes +------- + +o Bump org.apache.commons:commons-parent from 85 to 91 #774, #783, #808. Thanks to Gary Gregory, Dependabot. +o [test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0. Thanks to Gary Gregory. +o [test] Bump commons.bytebuddy.version from 1.17.6 to 1.17.8 #769. Thanks to Gary Gregory, Dependabot. +o [test] Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0. Thanks to Gary Gregory. + +Removed +------- + +o Inline private constant field ProxyInputStream.exceptionHandler #780. Thanks to Piotr P. Karwasz. +Commons IO 2.7 and up requires Java 8 or above. +Commons IO 2.6 requires Java 7 or above. +Commons IO 2.3 through 2.5 requires Java 6 or above. +Commons IO 2.2 requires Java 5 or above. +Commons IO 1.4 requires Java 1.3 or above. + +Historical list of changes: https://commons.apache.org/proper/commons-io/changes.html + +For complete information on Apache Commons IO, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons IO website: + +https://commons.apache.org/proper/commons-io/ + +Download page: https://commons.apache.org/proper/commons-io/download_io.cgi + +Have fun! +-Apache Commons Team + +------------------------------------------------------------------------------ + + Apache Commons IO 2.20.0 Release Notes The Apache Commons IO team is pleased to announce the release of Apache Commons IO 2.20.0. diff --git a/pom.xml b/pom.xml index d545a1e0de3..59e438881c2 100644 --- a/pom.xml +++ b/pom.xml @@ -19,12 +19,12 @@ org.apache.commons commons-parent - 85 + 91 4.0.0 commons-io commons-io - 2.20.0 + 2.21.0 Apache Commons IO 2002 @@ -47,7 +47,7 @@ file comparators, endian transformation classes, and much more. scm:git:https://gitbox.apache.org/repos/asf/commons-io.git scm:git:https://gitbox.apache.org/repos/asf/commons-io.git https://gitbox.apache.org/repos/asf?p=commons-io.git - rel/commons-io-2.20.0 + rel/commons-io-2.21.0 GitHub @@ -87,19 +87,19 @@ file comparators, endian transformation classes, and much more. com.google.jimfs jimfs - 1.3.0 + 1.3.1 test org.apache.commons commons-lang3 - 3.18.0 + 3.19.0 test commons-codec commons-codec - 1.18.0 + 1.19.0 test @@ -115,11 +115,11 @@ file comparators, endian transformation classes, and much more. io org.apache.commons.io RC1 - 2.19.0 - 2.20.0 - 2.20.1 + 2.20.0 + 2.21.0 + 2.21.1 - 2025-07-14T21:18:06Z + 2025-11-04T20:17:29Z (requires Java 8) IO 12310477 @@ -146,7 +146,7 @@ file comparators, endian transformation classes, and much more. https://svn.apache.org/repos/infra/websites/production/commons/content/proper/commons-io/ site-content - 1.17.6 + 1.17.8 false true @@ -158,6 +158,8 @@ file comparators, endian transformation classes, and much more. 0.85 0.90 0.85 + + @@ -217,7 +219,7 @@ file comparators, endian transformation classes, and much more. false - ${argLine} -Xmx25M + ${argLine} -Xmx25M ${EnableDynamicAgentLoading} **/*Test*.class @@ -345,6 +347,15 @@ file comparators, endian transformation classes, and much more. 8 + + java21-up + + [21,) + + + -XX:+EnableDynamicAgentLoading + + benchmark diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 0080206dcc3..23a7d3b40d6 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -45,7 +45,43 @@ The type attribute can be add,update,fix,remove. Apache Commons IO Release Notes - + + + When testing on Java 21 and up, enable -XX:+EnableDynamicAgentLoading. + When testing on Java 24 and up, don't fail FileUtilsListFilesTest for a different behavior in the JRE. + ValidatingObjectInputStream does not validate dynamic proxy interfaces. + BoundedInputStream.getRemaining() now reports Long.MAX_VALUE instead of 0 when no limit is set. + BoundedInputStream.available() correctly accounts for the maximum read limit. + Deprecate IOUtils.readFully(InputStream, int) in favor of toByteArray(InputStream, int). + IOUtils.toByteArray(InputStream) now throws IOException on byte array overflow. + Javadoc general improvements. + IOUtils.toByteArray() now throws EOFException when not enough data is available #796. + Fix IOUtils.skip() usage in concurrent scenarios. + [javadoc] Fix XmlStreamReader Javadoc to indicate the correct class that is built #806. + + FileUtils#byteCountToDisplaySize() supports Zettabyte, Yottabyte, Ronnabyte and Quettabyte #763. + Add org.apache.commons.io.FileUtils.ONE_RB #763. + Add org.apache.commons.io.FileUtils.ONE_QB #763. + Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(byte[], int, int, long). + Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(byte[], long). + Add org.apache.commons.io.output.ProxyOutputStream.writeRepeat(int, long). + Add length unit support in FileSystem limits. + Add IOUtils.toByteArray(InputStream, int, int) for safer chunked reading with size validation. + Add org.apache.commons.io.file.PathUtils.getPath(String, String). + Add org.apache.commons.io.channels.ByteArraySeekableByteChannel. + Add IOIterable.asIterable(). + Add NIO channel support to `AbstractStreamBuilder`. + Add CloseShieldChannel to close-shielded NIO Channels #786. + Added IOUtils.checkFromIndexSize as a Java 8 backport of Objects.checkFromIndexSize #790. + + Bump org.apache.commons:commons-parent from 85 to 91 #774, #783, #808. + [test] Bump commons-codec:commons-codec from 1.18.0 to 1.19.0. + [test] Bump commons.bytebuddy.version from 1.17.6 to 1.17.8 #769. + [test] Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.19.0. + + Inline private constant field ProxyInputStream.exceptionHandler #780. + + [javadoc] Rename parameter of ProxyOutputStream.write(int) #740. CopyDirectoryVisitor ignores fileFilter #743. diff --git a/src/conf/maven-pmd-plugin.xml b/src/conf/maven-pmd-plugin.xml index 215ac295ede..9d51e182ff9 100644 --- a/src/conf/maven-pmd-plugin.xml +++ b/src/conf/maven-pmd-plugin.xml @@ -78,7 +78,6 @@ under the License. - diff --git a/src/conf/spotbugs-exclude-filter.xml b/src/conf/spotbugs-exclude-filter.xml index 24382785a47..bf35c1f1c9b 100644 --- a/src/conf/spotbugs-exclude-filter.xml +++ b/src/conf/spotbugs-exclude-filter.xml @@ -111,4 +111,9 @@ + + + + + diff --git a/src/main/java/org/apache/commons/io/ByteBuffers.java b/src/main/java/org/apache/commons/io/ByteBuffers.java index b8841433e3b..91efd8ed3e2 100644 --- a/src/main/java/org/apache/commons/io/ByteBuffers.java +++ b/src/main/java/org/apache/commons/io/ByteBuffers.java @@ -62,7 +62,7 @@ public static ByteBuffer littleEndian(final ByteBuffer allocate) { * * @param capacity The new buffer's capacity, in bytes. * @return The new byte buffer. - * @throws IllegalArgumentException If the capacity is negative. + * @throws IllegalArgumentException If the {@code capacity} is negative. */ public static ByteBuffer littleEndian(final int capacity) { return littleEndian(ByteBuffer.allocate(capacity)); diff --git a/src/main/java/org/apache/commons/io/ByteOrderMark.java b/src/main/java/org/apache/commons/io/ByteOrderMark.java index 0067f7d2d6f..9624a0ba8b4 100644 --- a/src/main/java/org/apache/commons/io/ByteOrderMark.java +++ b/src/main/java/org/apache/commons/io/ByteOrderMark.java @@ -40,7 +40,7 @@ * * @see org.apache.commons.io.input.BOMInputStream * @see Wikipedia: Byte Order Mark - * @see W3C: Autodetection of Character Encodings + * @see W3C: Autodetection of Character Encodings * (Non-Normative) * @since 2.0 */ diff --git a/src/main/java/org/apache/commons/io/CopyUtils.java b/src/main/java/org/apache/commons/io/CopyUtils.java index ad426952e4e..bb9ac470c75 100644 --- a/src/main/java/org/apache/commons/io/CopyUtils.java +++ b/src/main/java/org/apache/commons/io/CopyUtils.java @@ -29,6 +29,8 @@ import java.io.Writer; import java.nio.charset.Charset; +import org.apache.commons.io.IOUtils.ScratchChars; + /** * This class provides static utility methods for buffered * copying between sources ({@link InputStream}, {@link Reader}, @@ -44,7 +46,7 @@ * released when the associated Stream is garbage-collected. It is not a good * idea to rely on this mechanism. For a good overview of the distinction * between "memory management" and "resource management", see - * this + * this * UnixReview article. *

* For byte-to-char methods, a {@code copy} variant allows the encoding @@ -146,7 +148,7 @@ public static void copy(final byte[] input, final Writer output) throws IOExcept * @param input the byte array to read from * @param output the {@link Writer} to write to * @param encoding The name of a supported character encoding. See the - * IANA + * IANA * Charset Registry for a list of valid encoding types. * @throws IOException In case of an I/O problem */ @@ -179,7 +181,7 @@ public static int copy(final InputStream input, final OutputStream output) throw * Copies and convert bytes from an {@link InputStream} to chars on a * {@link Writer}. *

- * This method uses the virtual machine's {@link Charset#defaultCharset() default charset} for byte-to-char conversion. + * This method uses the virtual machine's {@linkplain Charset#defaultCharset() default charset} for byte-to-char conversion. *

* * @param input the {@link InputStream} to read from @@ -204,7 +206,7 @@ public static void copy( * @param input the {@link InputStream} to read from * @param output the {@link Writer} to write to * @param encoding The name of a supported character encoding. See the - * IANA + * IANA * Charset Registry for a list of valid encoding types. * @throws IOException In case of an I/O problem */ @@ -221,7 +223,7 @@ public static void copy( * Serialize chars from a {@link Reader} to bytes on an * {@link OutputStream}, and flush the {@link OutputStream}. *

- * This method uses the virtual machine's {@link Charset#defaultCharset() default charset} for byte-to-char conversion. + * This method uses the virtual machine's {@linkplain Charset#defaultCharset() default charset} for byte-to-char conversion. *

* * @param input the {@link Reader} to read from @@ -249,7 +251,7 @@ public static void copy( * @param input the {@link Reader} to read from * @param output the {@link OutputStream} to write to * @param encoding The name of a supported character encoding. See the - * IANA + * IANA * Charset Registry for a list of valid encoding types. * @throws IOException In case of an I/O problem * @since 2.5 @@ -278,14 +280,16 @@ public static int copy( final Reader input, final Writer output) throws IOException { - final char[] buffer = IOUtils.getScratchCharArray(); - int count = 0; - int n; - while (EOF != (n = input.read(buffer))) { - output.write(buffer, 0, n); - count += n; + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + final char[] buffer = scratch.array(); + int count = 0; + int n; + while (EOF != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; } - return count; } /** @@ -293,7 +297,7 @@ public static int copy( * {@link OutputStream}, and * flush the {@link OutputStream}. *

- * This method uses the virtual machine's {@link Charset#defaultCharset() default charset} for byte-to-char conversion. + * This method uses the virtual machine's {@linkplain Charset#defaultCharset() default charset} for byte-to-char conversion. *

* * @param input the {@link String} to read from @@ -323,7 +327,7 @@ public static void copy( * @param input the {@link String} to read from * @param output the {@link OutputStream} to write to * @param encoding The name of a supported character encoding. See the - * IANA + * IANA * Charset Registry for a list of valid encoding types. * @throws IOException In case of an I/O problem * @since 2.5 diff --git a/src/main/java/org/apache/commons/io/EndianUtils.java b/src/main/java/org/apache/commons/io/EndianUtils.java index 9fa1913c731..6b4d58eeba3 100644 --- a/src/main/java/org/apache/commons/io/EndianUtils.java +++ b/src/main/java/org/apache/commons/io/EndianUtils.java @@ -26,7 +26,7 @@ /** * Helps with reading and writing primitive numeric types ({@code short}, * {@code int}, {@code long}, {@code float}, and {@code double}) that are - * encoded in little endian using two's complement or unsigned representations. + * encoded in little-endian using two's complement or unsigned representations. *

* Different computer architectures have different conventions for * byte ordering. In "Little Endian" architectures (e.g. X86), @@ -35,8 +35,8 @@ * (e.g. Motorola 680X0), the situation is reversed. * Most methods and classes throughout Java — e.g. {@code DataInputStream} and * {@code Double.longBitsToDouble()} — assume data is laid out - * in big endian order with the most significant byte first. - * The methods in this class read and write data in little endian order, + * in big-endian order with the most significant byte first. + * The methods in this class read and write data in little-endian order, * generally by reversing the bytes and then using the * regular Java methods to convert the swapped bytes to a primitive type. *

@@ -63,7 +63,7 @@ private static int read(final InputStream input) throws IOException { } /** - * Reads a little endian {@code double} value from a byte array at a given offset. + * Reads a little-endian {@code double} value from a byte array at a given offset. * * @param data source byte array * @param offset starting offset in the byte array @@ -75,7 +75,7 @@ public static double readSwappedDouble(final byte[] data, final int offset) { } /** - * Reads a little endian {@code double} value from an InputStream. + * Reads a little-endian {@code double} value from an InputStream. * * @param input source InputStream * @return the value just read @@ -86,7 +86,7 @@ public static double readSwappedDouble(final InputStream input) throws IOExcepti } /** - * Reads a little endian {@code float} value from a byte array at a given offset. + * Reads a little-endian {@code float} value from a byte array at a given offset. * * @param data source byte array * @param offset starting offset in the byte array @@ -98,7 +98,7 @@ public static float readSwappedFloat(final byte[] data, final int offset) { } /** - * Reads a little endian {@code float} value from an InputStream. + * Reads a little-endian {@code float} value from an InputStream. * * @param input source InputStream * @return the value just read @@ -109,7 +109,7 @@ public static float readSwappedFloat(final InputStream input) throws IOException } /** - * Reads a little endian {@code int} value from a byte array at a given offset. + * Reads a little-endian {@code int} value from a byte array at a given offset. * * @param data source byte array * @param offset starting offset in the byte array @@ -125,7 +125,7 @@ public static int readSwappedInteger(final byte[] data, final int offset) { } /** - * Reads a little endian {@code int} value from an InputStream. + * Reads a little-endian {@code int} value from an InputStream. * * @param input source InputStream * @return the value just read @@ -140,7 +140,7 @@ public static int readSwappedInteger(final InputStream input) throws IOException } /** - * Reads a little endian {@code long} value from a byte array at a given offset. + * Reads a little-endian {@code long} value from a byte array at a given offset. * * @param data source byte array * @param offset starting offset in the byte array @@ -155,7 +155,7 @@ public static long readSwappedLong(final byte[] data, final int offset) { } /** - * Reads a little endian {@code long} value from an InputStream. + * Reads a little-endian {@code long} value from an InputStream. * * @param input source InputStream * @return the value just read @@ -170,7 +170,7 @@ public static long readSwappedLong(final InputStream input) throws IOException { } /** - * Reads a little endian {@code short} value from a byte array at a given offset. + * Reads a little-endian {@code short} value from a byte array at a given offset. * * @param data source byte array * @param offset starting offset in the byte array @@ -183,7 +183,7 @@ public static short readSwappedShort(final byte[] data, final int offset) { } /** - * Reads a little endian {@code short} value from an InputStream. + * Reads a little-endian {@code short} value from an InputStream. * * @param input source InputStream * @return the value just read @@ -194,7 +194,7 @@ public static short readSwappedShort(final InputStream input) throws IOException } /** - * Reads a little endian unsigned integer (32-bit) value from a byte array at a given + * Reads a little-endian unsigned integer (32-bit) value from a byte array at a given * offset. * * @param data source byte array @@ -212,7 +212,7 @@ public static long readSwappedUnsignedInteger(final byte[] data, final int offse } /** - * Reads a little endian unsigned integer (32-bit) from an InputStream. + * Reads a little-endian unsigned integer (32-bit) from an InputStream. * * @param input source InputStream * @return the value just read @@ -229,7 +229,7 @@ public static long readSwappedUnsignedInteger(final InputStream input) throws IO } /** - * Reads an unsigned short (16-bit) value from a byte array in little endian order at a given + * Reads an unsigned short (16-bit) value from a byte array in little-endian order at a given * offset. * * @param data source byte array @@ -243,7 +243,7 @@ public static int readSwappedUnsignedShort(final byte[] data, final int offset) } /** - * Reads an unsigned short (16-bit) from an InputStream in little endian order. + * Reads an unsigned short (16-bit) from an InputStream in little-endian order. * * @param input source InputStream * @return the value just read @@ -257,7 +257,7 @@ public static int readSwappedUnsignedShort(final InputStream input) throws IOExc } /** - * Converts a {@code double} value from big endian to little endian + * Converts a {@code double} value from big-endian to little-endian * and vice versa. That is, it converts the {@code double} to bytes, * reverses the bytes, and then reinterprets those bytes as a new {@code double}. * This can be useful if you have a number that was read from the @@ -271,7 +271,7 @@ public static double swapDouble(final double value) { } /** - * Converts a {@code float} value from big endian to little endian and vice versa. + * Converts a {@code float} value from big-endian to little-endian and vice versa. * * @param value value to convert * @return the converted value @@ -281,7 +281,7 @@ public static float swapFloat(final float value) { } /** - * Converts an {@code int} value from big endian to little endian and vice versa. + * Converts an {@code int} value from big-endian to little-endian and vice versa. * * @param value value to convert * @return the converted value @@ -295,7 +295,7 @@ public static int swapInteger(final int value) { } /** - * Converts a {@code long} value from big endian to little endian and vice versa. + * Converts a {@code long} value from big-endian to little-endian and vice versa. * * @param value value to convert * @return the converted value @@ -313,7 +313,7 @@ public static long swapLong(final long value) { } /** - * Converts a {@code short} value from big endian to little endian and vice versa. + * Converts a {@code short} value from big-endian to little-endian and vice versa. * * @param value value to convert * @return the converted value @@ -338,7 +338,7 @@ private static void validateByteArrayOffset(final byte[] data, final int offset, } /** - * Writes the 8 bytes of a {@code double} to a byte array at a given offset in little endian order. + * Writes the 8 bytes of a {@code double} to a byte array at a given offset in little-endian order. * * @param data target byte array * @param offset starting offset in the byte array @@ -350,7 +350,7 @@ public static void writeSwappedDouble(final byte[] data, final int offset, final } /** - * Writes the 8 bytes of a {@code double} to an output stream in little endian order. + * Writes the 8 bytes of a {@code double} to an output stream in little-endian order. * * @param output target OutputStream * @param value value to write @@ -361,7 +361,7 @@ public static void writeSwappedDouble(final OutputStream output, final double va } /** - * Writes the 4 bytes of a {@code float} to a byte array at a given offset in little endian order. + * Writes the 4 bytes of a {@code float} to a byte array at a given offset in little-endian order. * * @param data target byte array * @param offset starting offset in the byte array @@ -373,7 +373,7 @@ public static void writeSwappedFloat(final byte[] data, final int offset, final } /** - * Writes the 4 bytes of a {@code float} to an output stream in little endian order. + * Writes the 4 bytes of a {@code float} to an output stream in little-endian order. * * @param output target OutputStream * @param value value to write @@ -384,7 +384,7 @@ public static void writeSwappedFloat(final OutputStream output, final float valu } /** - * Writes the 4 bytes of an {@code int} to a byte array at a given offset in little endian order. + * Writes the 4 bytes of an {@code int} to a byte array at a given offset in little-endian order. * * @param data target byte array * @param offset starting offset in the byte array @@ -400,7 +400,7 @@ public static void writeSwappedInteger(final byte[] data, final int offset, fina } /** - * Writes the 4 bytes of an {@code int} to an output stream in little endian order. + * Writes the 4 bytes of an {@code int} to an output stream in little-endian order. * * @param output target OutputStream * @param value value to write @@ -414,7 +414,7 @@ public static void writeSwappedInteger(final OutputStream output, final int valu } /** - * Writes the 8 bytes of a {@code long} to a byte array at a given offset in little endian order. + * Writes the 8 bytes of a {@code long} to a byte array at a given offset in little-endian order. * * @param data target byte array * @param offset starting offset in the byte array @@ -434,7 +434,7 @@ public static void writeSwappedLong(final byte[] data, final int offset, final l } /** - * Writes the 8 bytes of a {@code long} to an output stream in little endian order. + * Writes the 8 bytes of a {@code long} to an output stream in little-endian order. * * @param output target OutputStream * @param value value to write @@ -452,7 +452,7 @@ public static void writeSwappedLong(final OutputStream output, final long value) } /** - * Writes the 2 bytes of a {@code short} to a byte array at a given offset in little endian order. + * Writes the 2 bytes of a {@code short} to a byte array at a given offset in little-endian order. * * @param data target byte array * @param offset starting offset in the byte array @@ -466,7 +466,7 @@ public static void writeSwappedShort(final byte[] data, final int offset, final } /** - * Writes the 2 bytes of a {@code short} to an output stream using little endian encoding. + * Writes the 2 bytes of a {@code short} to an output stream using little-endian encoding. * * @param output target OutputStream * @param value value to write diff --git a/src/main/java/org/apache/commons/io/FileSystem.java b/src/main/java/org/apache/commons/io/FileSystem.java index 0e0ddf05aff..4169e1a53b1 100644 --- a/src/main/java/org/apache/commons/io/FileSystem.java +++ b/src/main/java/org/apache/commons/io/FileSystem.java @@ -17,6 +17,16 @@ package org.apache.commons.io; +import static java.nio.charset.StandardCharsets.UTF_16; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.nio.charset.CoderResult; +import java.nio.charset.CodingErrorAction; +import java.text.BreakIterator; import java.util.Arrays; import java.util.Locale; import java.util.Objects; @@ -36,7 +46,12 @@ public enum FileSystem { /** * Generic file system. */ - GENERIC(4096, false, false, Integer.MAX_VALUE, Integer.MAX_VALUE, new int[] { 0 }, new String[] {}, false, false, '/'), + GENERIC(4096, false, false, 1020, 1024 * 1024, new int[] { + // @formatter:off + // ASCII NUL + 0 + // @formatter:on + }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES), /** * Linux file system. @@ -48,7 +63,7 @@ public enum FileSystem { 0, '/' // @formatter:on - }, new String[] {}, false, false, '/'), + }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES), /** * MacOS file system. @@ -61,7 +76,7 @@ public enum FileSystem { '/', ':' // @formatter:on - }, new String[] {}, false, false, '/'), + }, new String[] {}, false, false, '/', NameLengthStrategy.BYTES), /** * Windows file system. @@ -78,7 +93,7 @@ public enum FileSystem { */ // @formatter:off WINDOWS(4096, false, true, - 255, 32000, // KEEP THIS ARRAY SORTED! + 255, 32767, // KEEP THIS ARRAY SORTED! new int[] { // KEEP THIS ARRAY SORTED! // ASCII NUL @@ -95,13 +110,127 @@ public enum FileSystem { "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", "LPT\u00b2", "LPT\u00b3", "LPT\u00b9", // Superscript 2 3 1 in that order "NUL", "PRN" - }, true, true, '\\'); + }, true, true, '\\', NameLengthStrategy.UTF16_CODE_UNITS); // @formatter:on /** - *

+ * Strategy for measuring and truncating file or path names in different units. + * Implementations measure length and can truncate to a specified limit. + */ + enum NameLengthStrategy { + /** Length measured as encoded bytes. */ + BYTES { + @Override + int getLength(final CharSequence value, final Charset charset) { + final CharsetEncoder enc = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + try { + return enc.encode(CharBuffer.wrap(value)).remaining(); + } catch (final CharacterCodingException e) { + // Unencodable, does not fit any byte limit. + return Integer.MAX_VALUE; + } + } + + @Override + CharSequence truncate(final CharSequence value, final int limit, final Charset charset) { + final CharsetEncoder encoder = charset.newEncoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT); + if (!encoder.canEncode(value)) { + throw new IllegalArgumentException("The value " + value + " cannot be encoded using " + charset.name()); + } + // Fast path: if even the worst-case expansion fits, we're done. + if (value.length() <= Math.floor(limit / encoder.maxBytesPerChar())) { + return value; + } + // Slow path: encode into a fixed-size byte buffer. + // 1. Compute length of extension in bytes (if any). + final CharSequence[] parts = splitExtension(value); + final int extensionLength = getLength(parts[1], charset); + if (extensionLength > 0 && extensionLength >= limit) { + // Extension itself does not fit + throw new IllegalArgumentException("The extension of " + value + " is too long to fit within " + limit + " bytes"); + } + // 2. Compute the character part that fits within the remaining byte budget. + final ByteBuffer byteBuffer = ByteBuffer.allocate(limit - extensionLength); + final CharBuffer charBuffer = CharBuffer.wrap(parts[0]); + // Encode until the first character that would exceed the byte budget. + final CoderResult cr = encoder.encode(charBuffer, byteBuffer, true); + if (cr.isUnderflow()) { + // Entire candidate fit within maxFileNameLength bytes. + return value; + } + final CharSequence truncated = safeTruncate(value, charBuffer.position()); + return extensionLength == 0 ? truncated : truncated.toString() + parts[1]; + } + }, + + /** Length measured as UTF-16 code units (i.e., {@code CharSequence.length()}). */ + UTF16_CODE_UNITS { + @Override + int getLength(final CharSequence value, final Charset charset) { + return value.length(); + } + + @Override + CharSequence truncate(final CharSequence value, final int limit, final Charset charset) { + if (!UTF_16.newEncoder().canEncode(value)) { + throw new IllegalArgumentException("The value " + value + " can not be encoded using " + UTF_16.name()); + } + // Fast path: no truncation needed. + if (value.length() <= limit) { + return value; + } + // Slow path: truncate to limit. + // 1. Compute length of extension in chars (if any). + final CharSequence[] parts = splitExtension(value); + final int extensionLength = parts[1].length(); + if (extensionLength > 0 && extensionLength >= limit) { + // Extension itself does not fit + throw new IllegalArgumentException("The extension of " + value + " is too long to fit within " + limit + " characters"); + } + // 2. Truncate the non-extension part and append the extension (if any). + final CharSequence truncated = safeTruncate(value, limit - extensionLength); + return extensionLength == 0 ? truncated : truncated.toString() + parts[1]; + } + }; + + /** + * Gets the measured length in this strategy’s unit. + * + * @param value The value to measure, not null. + * @param charset The charset to use when measuring in bytes. + * @return The length in this strategy’s unit. + */ + abstract int getLength(CharSequence value, Charset charset); + + /** + * Tests if the measured length is less or equal the {@code limit}. + * + * @param value The value to measure, not null. + * @param limit The limit to compare to. + * @param charset The charset to use when measuring in bytes. + * @return {@code true} if the measured length is less or equal the {@code limit}, {@code false} otherwise. + */ + final boolean isWithinLimit(final CharSequence value, final int limit, final Charset charset) { + return getLength(value, charset) <= limit; + } + + /** + * Truncates to {@code limit} in this strategy’s unit (no-op if already within limit). + * + * @param value The value to truncate, not null. + * @param limit The limit to truncate to. + * @param charset The charset to use when measuring in bytes. + * @return The truncated value, not null. + */ + abstract CharSequence truncate(CharSequence value, int limit, Charset charset); + } + + /** * Is {@code true} if this is Linux. - *

*

* The field will return {@code false} if {@code OS_NAME} is {@code null}. *

@@ -109,9 +238,7 @@ public enum FileSystem { private static final boolean IS_OS_LINUX = getOsMatchesName("Linux"); /** - *

* Is {@code true} if this is Mac. - *

*

* The field will return {@code false} if {@code OS_NAME} is {@code null}. *

@@ -124,9 +251,7 @@ public enum FileSystem { private static final String OS_NAME_WINDOWS_PREFIX = "Windows"; /** - *

* Is {@code true} if this is Windows. - *

*

* The field will return {@code false} if {@code OS_NAME} is {@code null}. *

@@ -177,9 +302,7 @@ private static boolean getOsMatchesName(final String osNamePrefix) { } /** - *

* Gets a System property, defaulting to {@code null} if the property cannot be read. - *

*

* If a {@link SecurityException} is caught, the return value is {@code null} and a message is written to * {@code System.err}. @@ -200,74 +323,16 @@ private static String getSystemProperty(final String property) { } } - /** - * Copied from Apache Commons Lang CharSequenceUtils. - * - * Returns the index within {@code cs} of the first occurrence of the - * specified character, starting the search at the specified index. - *

- * If a character with value {@code searchChar} occurs in the - * character sequence represented by the {@code cs} - * object at an index no smaller than {@code start}, then - * the index of the first such occurrence is returned. For values - * of {@code searchChar} in the range from 0 to 0xFFFF (inclusive), - * this is the smallest value k such that: - *

- *
-     * (this.charAt(k) == searchChar) && (k >= start)
-     * 
- * is true. For other values of {@code searchChar}, it is the - * smallest value k such that: - *
-     * (this.codePointAt(k) == searchChar) && (k >= start)
-     * 
- *

- * is true. In either case, if no such character occurs in {@code cs} - * at or after position {@code start}, then - * {@code -1} is returned. - *

- *

- * There is no restriction on the value of {@code start}. If it - * is negative, it has the same effect as if it were zero: the entire - * {@link CharSequence} may be searched. If it is greater than - * the length of {@code cs}, it has the same effect as if it were - * equal to the length of {@code cs}: {@code -1} is returned. - *

- *

All indices are specified in {@code char} values - * (Unicode code units). - *

- * - * @param cs the {@link CharSequence} to be processed, not null - * @param searchChar the char to be searched for - * @param start the start index, negative starts at the string start - * @return the index where the search char was found, -1 if not found - * @since 3.6 updated to behave more like {@link String} + /* + * Finds the index of the first dot in a CharSequence. */ - private static int indexOf(final CharSequence cs, final int searchChar, int start) { + private static int indexOfFirstDot(final CharSequence cs) { if (cs instanceof String) { - return ((String) cs).indexOf(searchChar, start); - } - final int sz = cs.length(); - if (start < 0) { - start = 0; + return ((String) cs).indexOf('.'); } - if (searchChar < Character.MIN_SUPPLEMENTARY_CODE_POINT) { - for (int i = start; i < sz; i++) { - if (cs.charAt(i) == searchChar) { - return i; - } - } - return -1; - } - //supplementary characters (LANG1300) - if (searchChar <= Character.MAX_CODE_POINT) { - final char[] chars = Character.toChars(searchChar); - for (int i = start; i < sz - 1; i++) { - final char high = cs.charAt(i); - final char low = cs.charAt(i + 1); - if (high == chars[0] && low == chars[1]) { - return i; - } + for (int i = 0; i < cs.length(); i++) { + if (cs.charAt(i) == '.') { + return i; } } return -1; @@ -304,6 +369,42 @@ private static String replace(final String path, final char oldChar, final char return path == null ? null : path.replace(oldChar, newChar); } + /** + * Truncates a string respecting grapheme cluster boundaries. + * + * @param value The value to truncate. + * @param limit The maximum length. + * @return The truncated value. + * @throws IllegalArgumentException If the first grapheme cluster is longer than the limit. + */ + private static CharSequence safeTruncate(final CharSequence value, final int limit) { + if (value.length() <= limit) { + return value; + } + final BreakIterator boundary = BreakIterator.getCharacterInstance(Locale.ROOT); + final String text = value.toString(); + boundary.setText(text); + final int end = boundary.preceding(limit + 1); + assert end != BreakIterator.DONE; + if (end == 0) { + final String limitMessage = limit <= 1 ? "1 character" : limit + " characters"; + throw new IllegalArgumentException("The value " + value + " can not be truncated to " + limitMessage + + " without breaking the first codepoint or grapheme cluster"); + } + return text.substring(0, end); + } + static CharSequence[] splitExtension(final CharSequence value) { + final int index = indexOfFirstDot(value); + // An initial dot is not an extension + return index < 1 + ? new CharSequence[] {value, ""} + : new CharSequence[] {value.subSequence(0, index), value.subSequence(index, value.length())}; + } + static CharSequence trimExtension(final CharSequence cs) { + final int index = indexOfFirstDot(cs); + // An initial dot is not an extension + return index < 1 ? cs : cs.subSequence(0, index); + } private final int blockSize; private final boolean casePreserving; private final boolean caseSensitive; @@ -312,10 +413,15 @@ private static String replace(final String path, final char oldChar, final char private final int maxPathLength; private final String[] reservedFileNames; private final boolean reservedFileNamesExtensions; + private final boolean supportsDriveLetter; + private final char nameSeparator; + private final char nameSeparatorOther; + private final NameLengthStrategy nameLengthStrategy; + /** * Constructs a new instance. * @@ -326,13 +432,15 @@ private static String replace(final String path, final char oldChar, final char * @param maxPathLength The maximum length of the path to a file. This can include folders. * @param illegalFileNameChars Illegal characters for this file system. * @param reservedFileNames The reserved file names. - * @param reservedFileNamesExtensions TODO + * @param reservedFileNamesExtensions The reserved file name extensions. * @param supportsDriveLetter Whether this file system support driver letters. * @param nameSeparator The name separator, '\\' on Windows, '/' on Linux. + * @param nameLengthStrategy The strategy for measuring and truncating file and path names. */ FileSystem(final int blockSize, final boolean caseSensitive, final boolean casePreserving, final int maxFileLength, final int maxPathLength, final int[] illegalFileNameChars, - final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, final char nameSeparator) { + final String[] reservedFileNames, final boolean reservedFileNamesExtensions, final boolean supportsDriveLetter, + final char nameSeparator, final NameLengthStrategy nameLengthStrategy) { this.blockSize = blockSize; this.maxFileNameLength = maxFileLength; this.maxPathLength = maxPathLength; @@ -345,10 +453,12 @@ private static String replace(final String path, final char oldChar, final char this.supportsDriveLetter = supportsDriveLetter; this.nameSeparator = nameSeparator; this.nameSeparatorOther = FilenameUtils.flipSeparator(nameSeparator); + this.nameLengthStrategy = nameLengthStrategy; } /** * Gets the file allocation block size in bytes. + * * @return the file allocation block size in bytes. * @since 2.12.0 */ @@ -380,23 +490,66 @@ public int[] getIllegalFileNameCodePoints() { } /** - * Gets the maximum length for file names. The file name does not include folders. + * Gets the maximum length for file names (excluding any folder path). + * + *

+ * This limit applies only to the file name itself, excluding any parent directories. + *

+ * + *

+ * The value is expressed in Java {@code char} units (UTF-16 code units). + *

+ * + *

+ * Note: Because many file systems enforce limits in bytes using a specific encoding rather than in UTF-16 code units, a name that + * fits this limit may still be rejected by the underlying file system. + *

+ * + *

+ * Use {@link #isLegalFileName} to check whether a given name is valid for the current file system and charset. + *

* - * @return the maximum length for file names. + *

+ * However, any file name longer than this limit is guaranteed to be invalid on the current file system. + *

+ * + * @return the maximum file name length in characters. */ public int getMaxFileNameLength() { return maxFileNameLength; } /** - * Gets the maximum length of the path to a file. This can include folders. + * Gets the maximum length for file paths (may include folders). + * + *

+ * This value is inclusive of all path components and separators. For a limit of each path component see {@link #getMaxFileNameLength()}. + *

+ * + *

+ * The value is expressed in Java {@code char} units (UTF-16 code units) and represents the longest path that can be safely passed to Java + * {@link java.io.File} and {@link java.nio.file.Path} APIs. + *

+ * + *

+ * Note: many operating systems and file systems enforce path length limits in bytes using a specific encoding, rather than in + * UTF-16 code units. As a result, a path that fits within this limit may still be rejected by the underlying platform. + *

+ * + *

+ * Conversely, any path longer than this limit is guaranteed to fail with at least some operating system API calls. + *

* - * @return the maximum length of the path to a file. + * @return the maximum file path length in characters. */ public int getMaxPathLength() { return maxPathLength; } + NameLengthStrategy getNameLengthStrategy() { + return nameLengthStrategy; + } + /** * Gets the name separator, '\\' on Windows, '/' on Linux. * @@ -438,7 +591,7 @@ public boolean isCaseSensitive() { * Tests if the given character is illegal in a file name, {@code false} otherwise. * * @param c - * the character to test + * the character to test. * @return {@code true} if the given character is illegal in a file name, {@code false} otherwise. */ private boolean isIllegalFileNameChar(final int c) { @@ -446,29 +599,53 @@ private boolean isIllegalFileNameChar(final int c) { } /** - * Tests if a candidate file name (without a path) such as {@code "filename.ext"} or {@code "filename"} is a - * potentially legal file name. If the file name length exceeds {@link #getMaxFileNameLength()}, or if it contains - * an illegal character then the check fails. + * Tests if a candidate file name (without a path) is a legal file name. + * + *

Takes a file name like {@code "filename.ext"} or {@code "filename"} and checks:

+ *
    + *
  • if the file name length is legal
  • + *
  • if the file name is not a reserved file name
  • + *
  • if the file name does not contain illegal characters
  • + *
* * @param candidate - * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} - * @return {@code true} if the candidate name is legal + * A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}. + * @return {@code true} if the candidate name is legal. */ public boolean isLegalFileName(final CharSequence candidate) { - if (candidate == null || candidate.length() == 0 || candidate.length() > maxFileNameLength) { - return false; - } - if (isReservedFileName(candidate)) { - return false; - } - return candidate.chars().noneMatch(this::isIllegalFileNameChar); + return isLegalFileName(candidate, Charset.defaultCharset()); + } + + /** + * Tests if a candidate file name (without a path) is a legal file name. + * + *

Takes a file name like {@code "filename.ext"} or {@code "filename"} and checks:

+ *
    + *
  • if the file name length is legal
  • + *
  • if the file name is not a reserved file name
  • + *
  • if the file name does not contain illegal characters
  • + *
+ * + * @param candidate + * A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}. + * @param charset + * The charset to use when the file name length is measured in bytes. + * @return {@code true} if the candidate name is legal. + * @since 2.21.0 + */ + public boolean isLegalFileName(final CharSequence candidate, final Charset charset) { + return candidate != null + && candidate.length() != 0 + && nameLengthStrategy.isWithinLimit(candidate, getMaxFileNameLength(), charset) + && !isReservedFileName(candidate) + && candidate.chars().noneMatch(this::isIllegalFileNameChar); } /** * Tests whether the given string is a reserved file name. * * @param candidate - * the string to test + * the string to test. * @return {@code true} if the given string is a reserved file name. */ public boolean isReservedFileName(final CharSequence candidate) { @@ -479,8 +656,8 @@ public boolean isReservedFileName(final CharSequence candidate) { /** * Converts all separators to the Windows separator of backslash. * - * @param path the path to be changed, null ignored - * @return the updated path + * @param path the path to be changed, null ignored. + * @return the updated path. * @since 2.12.0 */ public String normalizeSeparators(final String path) { @@ -504,30 +681,56 @@ public boolean supportsDriveLetter() { } /** - * Converts a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} to a legal file - * name. Illegal characters in the candidate name are replaced by the {@code replacement} character. If the file - * name length exceeds {@link #getMaxFileNameLength()}, then the name is truncated to - * {@link #getMaxFileNameLength()}. + * Converts a candidate file name (without a path) to a legal file name. + * + *

Takes a file name like {@code "filename.ext"} or {@code "filename"} and:

+ *
    + *
  • replaces illegal characters by the given replacement character
  • + *
  • truncates the name to {@link #getMaxFileNameLength()} if necessary
  • + *
* * @param candidate - * a candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"} + * A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}. * @param replacement - * Illegal characters in the candidate name are replaced by this character - * @return a String without illegal characters - */ - public String toLegalFileName(final String candidate, final char replacement) { + * Illegal characters in the candidate name are replaced by this character. + * @param charset + * The charset to use when the file name length is measured in bytes. + * @return a String without illegal characters. + * @since 2.21.0 + */ + public String toLegalFileName(final CharSequence candidate, final char replacement, final Charset charset) { + Objects.requireNonNull(candidate, "candidate"); + if (candidate.length() == 0) { + throw new IllegalArgumentException("The candidate file name is empty"); + } if (isIllegalFileNameChar(replacement)) { // %s does not work properly with NUL throw new IllegalArgumentException(String.format("The replacement character '%s' cannot be one of the %s illegal characters: %s", replacement == '\0' ? "\\0" : replacement, name(), Arrays.toString(illegalFileNameChars))); } - final String truncated = candidate.length() > maxFileNameLength ? candidate.substring(0, maxFileNameLength) : candidate; + final CharSequence truncated = nameLengthStrategy.truncate(candidate, getMaxFileNameLength(), charset); final int[] array = truncated.chars().map(i -> isIllegalFileNameChar(i) ? replacement : i).toArray(); return new String(array, 0, array.length); } - CharSequence trimExtension(final CharSequence cs) { - final int index = indexOf(cs, '.', 0); - return index < 0 ? cs : cs.subSequence(0, index); + + /** + * Converts a candidate file name (without a path) to a legal file name. + * + *

Takes a file name like {@code "filename.ext"} or {@code "filename"} and:

+ *
    + *
  • replaces illegal characters by the given replacement character
  • + *
  • truncates the name to {@link #getMaxFileNameLength()} if necessary
  • + *
+ * + * @param candidate + * A candidate file name (without a path) like {@code "filename.ext"} or {@code "filename"}. + * @param replacement + * Illegal characters in the candidate name are replaced by this character. + * @return a String without illegal characters. + */ + public String toLegalFileName(final String candidate, final char replacement) { + return toLegalFileName(candidate, replacement, Charset.defaultCharset()); } + } diff --git a/src/main/java/org/apache/commons/io/FileUtils.java b/src/main/java/org/apache/commons/io/FileUtils.java index ffd59f92ab5..523c2c1fd26 100644 --- a/src/main/java/org/apache/commons/io/FileUtils.java +++ b/src/main/java/org/apache/commons/io/FileUtils.java @@ -194,13 +194,27 @@ public class FileUtils { /** * The number of bytes in a zettabyte. */ - public static final BigInteger ONE_ZB = BigInteger.valueOf(ONE_KB).multiply(BigInteger.valueOf(ONE_EB)); + public static final BigInteger ONE_ZB = ONE_KB_BI.multiply(ONE_EB_BI); /** * The number of bytes in a yottabyte. */ public static final BigInteger ONE_YB = ONE_KB_BI.multiply(ONE_ZB); + /** + * The number of bytes in a ronnabyte. + * + * @since 2.21.0 + */ + public static final BigInteger ONE_RB = ONE_KB_BI.multiply(ONE_YB); + + /** + * The number of bytes in a quettabyte. + * + * @since 2.21.0 + */ + public static final BigInteger ONE_QB = ONE_KB_BI.multiply(ONE_RB); + /** * An empty array of type {@link File}. */ @@ -217,7 +231,7 @@ public class FileUtils { *

* * @param size the number of bytes - * @return a human-readable display value (includes units - EB, PB, TB, GB, MB, KB or bytes) + * @return a human-readable display value (includes units - QB, RB, YB, ZB, EB, PB, TB, GB, MB, KB or bytes) * @throws NullPointerException if the given {@link BigInteger} is {@code null}. * @see IO-226 - should the rounding be changed? * @since 2.4 @@ -226,8 +240,15 @@ public class FileUtils { public static String byteCountToDisplaySize(final BigInteger size) { Objects.requireNonNull(size, "size"); final String displaySize; - - if (size.divide(ONE_EB_BI).compareTo(BigInteger.ZERO) > 0) { + if (size.divide(ONE_QB).compareTo(BigInteger.ZERO) > 0) { + displaySize = size.divide(ONE_QB) + " QB"; + } else if (size.divide(ONE_RB).compareTo(BigInteger.ZERO) > 0) { + displaySize = size.divide(ONE_RB) + " RB"; + } else if (size.divide(ONE_YB).compareTo(BigInteger.ZERO) > 0) { + displaySize = size.divide(ONE_YB) + " YB"; + } else if (size.divide(ONE_ZB).compareTo(BigInteger.ZERO) > 0) { + displaySize = size.divide(ONE_ZB) + " ZB"; + } else if (size.divide(ONE_EB_BI).compareTo(BigInteger.ZERO) > 0) { displaySize = size.divide(ONE_EB_BI) + " EB"; } else if (size.divide(ONE_PB_BI).compareTo(BigInteger.ZERO) > 0) { displaySize = size.divide(ONE_PB_BI) + " PB"; @@ -2082,12 +2103,12 @@ public static boolean isSymlink(final File file) { * All files found are filtered by an IOFileFilter. *

* - * @param directory the directory to search in + * @param directory The directory to search. * @param fileFilter filter to apply when finding files. * @param dirFilter optional filter to apply when finding subdirectories. * If this parameter is {@code null}, subdirectories will not be included in the * search. Use TrueFileFilter.INSTANCE to match all directories. - * @return an iterator of {@link File} for the matching files + * @return an iterator of {@link File} for the matching files. * @see org.apache.commons.io.filefilter.FileFilterUtils * @see org.apache.commons.io.filefilter.NameFileFilter * @since 1.2 @@ -2103,11 +2124,11 @@ public static Iterator iterateFiles(final File directory, final IOFileFilt * The resulting iterator MUST be consumed in its entirety in order to close its underlying stream. *

* - * @param directory the directory to search in - * @param extensions an array of extensions, for example, {"java","xml"}. If this + * @param directory The directory to search. + * @param extensions an array of extensions, for example, {"java", "xml"}. If this * parameter is {@code null}, all files are returned. - * @param recursive if true all subdirectories are searched as well - * @return an iterator of {@link File} with the matching files + * @param recursive if true all subdirectories are searched as well. + * @return an iterator of {@link File} with the matching files. * @since 1.2 */ public static Iterator iterateFiles(final File directory, final String[] extensions, final boolean recursive) { @@ -2127,12 +2148,12 @@ public static Iterator iterateFiles(final File directory, final String[] e * The resulting iterator includes the subdirectories themselves. *

* - * @param directory the directory to search in + * @param directory The directory to search. * @param fileFilter filter to apply when finding files. * @param dirFilter optional filter to apply when finding subdirectories. * If this parameter is {@code null}, subdirectories will not be included in the * search. Use TrueFileFilter.INSTANCE to match all directories. - * @return an iterator of {@link File} for the matching files + * @return an iterator of {@link File} for the matching files. * @see org.apache.commons.io.filefilter.FileFilterUtils * @see org.apache.commons.io.filefilter.NameFileFilter * @since 2.2 @@ -2213,8 +2234,8 @@ public static long lastModifiedUnchecked(final File file) { /** * Returns an Iterator for the lines in a {@link File} using the default encoding for the VM. * - * @param file the file to open for input, must not be {@code null} - * @return an Iterator of the lines in the file, never {@code null} + * @param file the file to open for input, must not be {@code null}. + * @return an Iterator of the lines in the file, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some * other reason cannot be opened for reading. @@ -2242,7 +2263,7 @@ public static LineIterator lineIterator(final File file) throws IOException { * try { * while (it.hasNext()) { * String line = it.nextLine(); - * /// do something with line + * // do something with line * } * } finally { * LineIterator.closeQuietly(iterator); @@ -2253,8 +2274,8 @@ public static LineIterator lineIterator(final File file) throws IOException { * underlying stream is closed. *

* - * @param file the file to open for input, must not be {@code null} - * @param charsetName the name of the requested charset, {@code null} means platform default + * @param file the file to open for input, must not be {@code null}. + * @param charsetName the name of the requested charset, {@code null} means platform default. * @return a LineIterator for lines in the file, never {@code null}; MUST be closed by the caller. * @throws NullPointerException if file is {@code null}. * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some @@ -2328,7 +2349,7 @@ private static File[] listFiles(final File directory, final FileFilter fileFilte *

*

* An example: If you want to search through all directories called - * "temp" you pass in {@code FileFilterUtils.NameFileFilter("temp")} + * "temp" you pass in {@code FileFilterUtils.NameFileFilter("temp")}. *

*

* Another common usage of this method is find files in a directory @@ -2336,13 +2357,13 @@ private static File[] listFiles(final File directory, final FileFilter fileFilte * in {@code FileFilterUtils.makeCVSAware(null)}. *

* - * @param directory the directory to search in + * @param directory The directory to search. * @param fileFilter filter to apply when finding files. Must not be {@code null}, * use {@link TrueFileFilter#INSTANCE} to match all files in selected directories. * @param dirFilter optional filter to apply when finding subdirectories. * If this parameter is {@code null}, subdirectories will not be included in the * search. Use {@link TrueFileFilter#INSTANCE} to match all directories. - * @return a collection of {@link File} with the matching files + * @return a collection of {@link File} with the matching files. * @see org.apache.commons.io.filefilter.FileFilterUtils * @see org.apache.commons.io.filefilter.NameFileFilter */ @@ -2359,9 +2380,10 @@ public static Collection listFiles(final File directory, final IOFileFilte * @param files The list to add found Files, not null. * @param recursive Whether or not to recurse into subdirectories. * @param filter How to filter files, not null. + * @return The given list. */ @SuppressWarnings("null") - private static void listFiles(final File directory, final List files, final boolean recursive, final FilenameFilter filter) { + private static List listFiles(final File directory, final List files, final boolean recursive, final FilenameFilter filter) { final File[] listFiles = directory.listFiles(); if (listFiles != null) { // Only allocate if you must. @@ -2377,24 +2399,21 @@ private static void listFiles(final File directory, final List files, fina dirs.forEach(d -> listFiles(d, files, true, filter)); } } + return files; } /** * Lists files within a given directory (and optionally its subdirectories) * which match an array of extensions. * - * @param directory the directory to search in - * @param extensions an array of extensions, for example, {"java","xml"}. If this + * @param directory The directory to search. + * @param extensions an array of extensions, for example, {"java", "xml"}. If this * parameter is {@code null}, all files are returned. - * @param recursive if true all subdirectories are searched as well - * @return a collection of {@link File} with the matching files + * @param recursive if true all subdirectories are searched as well. + * @return a collection of {@link File} with the matching files. */ public static Collection listFiles(final File directory, final String[] extensions, final boolean recursive) { - // IO-856: Don't use NIO to path walk, allocate as little as possible while traversing. - final List files = new ArrayList<>(); - final FilenameFilter filter = extensions != null ? toSuffixFileFilter(extensions) : TrueFileFilter.INSTANCE; - listFiles(directory, files, recursive, filter); - return files; + return listFiles(directory, new ArrayList<>(), recursive, extensions != null ? toSuffixFileFilter(extensions) : TrueFileFilter.INSTANCE); } /** @@ -2405,12 +2424,12 @@ public static Collection listFiles(final File directory, final String[] ex * any subdirectories that match the directory filter. *

* - * @param directory the directory to search in + * @param directory The directory to search. * @param fileFilter filter to apply when finding files. * @param dirFilter optional filter to apply when finding subdirectories. * If this parameter is {@code null}, subdirectories will not be included in the * search. Use TrueFileFilter.INSTANCE to match all directories. - * @return a collection of {@link File} with the matching files + * @return a collection of {@link File} with the matching files. * @see org.apache.commons.io.FileUtils#listFiles * @see org.apache.commons.io.filefilter.FileFilterUtils * @see org.apache.commons.io.filefilter.NameFileFilter @@ -2454,7 +2473,7 @@ private static File mkdirs(final File directory) throws IOException { * @param srcDir the directory to be moved. * @param destDir the destination directory. * @throws NullPointerException if any of the given {@link File}s are {@code null}. - * @throws IllegalArgumentException if {@code srcDir} exists but is not a directory + * @throws IllegalArgumentException if {@code srcDir} exists but is not a directory. * @throws FileNotFoundException if the source does not exist. * @throws IOException if an error occurs or setting the last-modified time didn't succeed. * @since 1.4 @@ -2522,7 +2541,7 @@ public static void moveDirectoryToDirectory(final File source, final File destDi * @throws NullPointerException if any of the given {@link File}s are {@code null}. * @throws FileExistsException if the destination file exists. * @throws FileNotFoundException if the source file does not exist. - * @throws IllegalArgumentException if {@code srcFile} is a directory + * @throws IllegalArgumentException if {@code srcFile} is a directory. * @throws IOException if an error occurs. * @since 1.4 */ @@ -2542,7 +2561,7 @@ public static void moveFile(final File srcFile, final File destFile) throws IOEx * @throws NullPointerException if any of the given {@link File}s are {@code null}. * @throws FileExistsException if the destination file exists. * @throws FileNotFoundException if the source file does not exist. - * @throws IllegalArgumentException if {@code srcFile} is a directory + * @throws IllegalArgumentException if {@code srcFile} is a directory. * @throws IOException if an error occurs or setting the last-modified time didn't succeed. * @since 2.9.0 */ @@ -2568,7 +2587,7 @@ public static void moveFile(final File srcFile, final File destFile, final CopyO *

* * @param srcFile the file to be moved. - * @param destDir the directory to move the file into + * @param destDir the directory to move the file into. * @param createDestDir if {@code true} create the destination directory. If {@code false} throw an * IOException if the destination directory does not already exist. * @throws NullPointerException if any of the given {@link File}s are {@code null}. @@ -2578,7 +2597,7 @@ public static void moveFile(final File srcFile, final File destFile, final CopyO * @throws IOException if the directory was not created along with all its parent directories, if enabled. * @throws IOException if an error occurs or setting the last-modified time didn't succeed. * @throws SecurityException See {@link File#mkdirs()}. - * @throws IllegalArgumentException if {@code destDir} exists but is not a directory + * @throws IllegalArgumentException if {@code destDir} exists but is not a directory. * @since 1.4 */ public static void moveFileToDirectory(final File srcFile, final File destDir, final boolean createDestDir) throws IOException { @@ -2645,8 +2664,8 @@ public static OutputStream newOutputStream(final File file, final boolean append * directory. An exception is thrown if the file exists but cannot be read. *

* - * @param file the file to open for input, must not be {@code null} - * @return a new {@link FileInputStream} for the specified file + * @param file the file to open for input, must not be {@code null}. + * @return a new {@link FileInputStream} for the specified file. * @throws NullPointerException if file is {@code null}. * @throws FileNotFoundException if the file does not exist, is a directory rather than a regular file, or for some * other reason cannot be opened for reading. @@ -2673,7 +2692,7 @@ public static FileInputStream openInputStream(final File file) throws IOExceptio * An exception is thrown if the parent directory cannot be created. *

* - * @param file the file to open for output, must not be {@code null} + * @param file the file to open for output, must not be {@code null}. * @return a new {@link FileOutputStream} for the specified file * @throws NullPointerException if the file object is {@code null}. * @throws IllegalArgumentException if the file object is a directory @@ -2700,13 +2719,13 @@ public static FileOutputStream openOutputStream(final File file) throws IOExcept * An exception is thrown if the parent directory cannot be created. *

* - * @param file the file to open for output, must not be {@code null} + * @param file the file to open for output, must not be {@code null}. * @param append if {@code true}, then bytes will be added to the - * end of the file rather than overwriting - * @return a new {@link FileOutputStream} for the specified file + * end of the file rather than overwriting. + * @return a new {@link FileOutputStream} for the specified file. * @throws NullPointerException if the file object is {@code null}. - * @throws IllegalArgumentException if the file object is a directory - * @throws IOException if the directories could not be created, or the file is not writable + * @throws IllegalArgumentException if the file object is a directory. + * @throws IOException if the directories could not be created, or the file is not writable. * @since 2.1 */ public static FileOutputStream openOutputStream(final File file, final boolean append) throws IOException { @@ -2723,8 +2742,8 @@ public static FileOutputStream openOutputStream(final File file, final boolean a * Reads the contents of a file into a byte array. * The file is always closed. * - * @param file the file to read, must not be {@code null} - * @return the file contents, never {@code null} + * @param file the file to read, must not be {@code null}. + * @return the file contents, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a * regular file, or for some other reason why the file cannot be opened for reading. @@ -2736,16 +2755,16 @@ public static byte[] readFileToByteArray(final File file) throws IOException { } /** - * Reads the contents of a file into a String using the virtual machine's {@link Charset#defaultCharset() default charset}. The + * Reads the contents of a file into a String using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. The * file is always closed. * - * @param file the file to read, must not be {@code null} - * @return the file contents, never {@code null} + * @param file the file to read, must not be {@code null}. + * @return the file contents, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a regular file, or for some other * reason why the file cannot be opened for reading. * @since 1.3.1 - * @deprecated Use {@link #readFileToString(File, Charset)} instead (and specify the appropriate encoding) + * @deprecated Use {@link #readFileToString(File, Charset)} instead (and specify the appropriate encoding). */ @Deprecated public static String readFileToString(final File file) throws IOException { @@ -2756,9 +2775,9 @@ public static String readFileToString(final File file) throws IOException { * Reads the contents of a file into a String. * The file is always closed. * - * @param file the file to read, must not be {@code null} - * @param charsetName the name of the requested charset, {@code null} means platform default - * @return the file contents, never {@code null} + * @param file the file to read, must not be {@code null}. + * @param charsetName the name of the requested charset, {@code null} means platform default. + * @return the file contents, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a * regular file, or for some other reason why the file cannot be opened for reading. @@ -2771,9 +2790,9 @@ public static String readFileToString(final File file, final Charset charsetName /** * Reads the contents of a file into a String. The file is always closed. * - * @param file the file to read, must not be {@code null} - * @param charsetName the name of the requested charset, {@code null} means platform default - * @return the file contents, never {@code null} + * @param file the file to read, must not be {@code null}. + * @param charsetName the name of the requested charset, {@code null} means platform default. + * @return the file contents, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a * regular file, or for some other reason why the file cannot be opened for reading. @@ -2785,16 +2804,16 @@ public static String readFileToString(final File file, final String charsetName) } /** - * Reads the contents of a file line by line to a List of Strings using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Reads the contents of a file line by line to a List of Strings using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * The file is always closed. * - * @param file the file to read, must not be {@code null} - * @return the list of Strings representing each line in the file, never {@code null} + * @param file the file to read, must not be {@code null}. + * @return the list of Strings representing each line in the file, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a regular file, or for some other * reason why the file cannot be opened for reading. * @since 1.3 - * @deprecated Use {@link #readLines(File, Charset)} instead (and specify the appropriate encoding) + * @deprecated Use {@link #readLines(File, Charset)} instead (and specify the appropriate encoding). */ @Deprecated public static List readLines(final File file) throws IOException { @@ -2805,9 +2824,9 @@ public static List readLines(final File file) throws IOException { * Reads the contents of a file line by line to a List of Strings. * The file is always closed. * - * @param file the file to read, must not be {@code null} - * @param charset the charset to use, {@code null} means platform default - * @return the list of Strings representing each line in the file, never {@code null} + * @param file the file to read, must not be {@code null}. + * @param charset the charset to use, {@code null} means platform default. + * @return the list of Strings representing each line in the file, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a * regular file, or for some other reason why the file cannot be opened for reading. @@ -2820,9 +2839,9 @@ public static List readLines(final File file, final Charset charset) thr /** * Reads the contents of a file line by line to a List of Strings. The file is always closed. * - * @param file the file to read, must not be {@code null} - * @param charsetName the name of the requested charset, {@code null} means platform default - * @return the list of Strings representing each line in the file, never {@code null} + * @param file the file to read, must not be {@code null}. + * @param charsetName the name of the requested charset, {@code null} means platform default. + * @return the list of Strings representing each line in the file, never {@code null}. * @throws NullPointerException if file is {@code null}. * @throws IOException if an I/O error occurs, including when the file does not exist, is a directory rather than a * regular file, or for some other reason why the file cannot be opened for reading. @@ -2861,7 +2880,7 @@ private static void requireCanonicalPathsNotEquals(final File file1, final File * @param directory The {@link File} to check. * @param name The parameter name to use in the exception message in case of null input or if the file is not a directory. * @throws NullPointerException if the given {@link File} is {@code null}. - * @throws FileNotFoundException if the given {@link File} does not exist + * @throws FileNotFoundException if the given {@link File} does not exist. * @throws IllegalArgumentException if the given {@link File} exists but is not a directory. */ private static void requireDirectoryExists(final File directory, final String name) throws FileNotFoundException { @@ -2894,8 +2913,7 @@ private static void requireDirectoryIfExists(final File directory, final String * * @param sourceFile The source file to query. * @param targetFile The target file or directory to set. - * @return {@code true} if and only if the operation succeeded; - * {@code false} otherwise + * @return {@code true} if and only if the operation succeeded; {@code false} otherwise. * @throws NullPointerException if sourceFile is {@code null}. * @throws NullPointerException if targetFile is {@code null}. */ @@ -2977,7 +2995,7 @@ public static BigInteger sizeOfAsBigInteger(final File file) { * @param directory directory to inspect, must not be {@code null}. * @return size of directory in bytes, 0 if directory is security restricted, a negative number when the real total * is greater than {@link Long#MAX_VALUE}. - * @throws IllegalArgumentException if the given {@link File} exists but is not a directory + * @throws IllegalArgumentException if the given {@link File} exists but is not a directory. * @throws NullPointerException if the directory is {@code null}. * @throws UncheckedIOException if an IO error occurs. */ @@ -2995,7 +3013,7 @@ public static long sizeOfDirectory(final File directory) { * * @param directory directory to inspect, must not be {@code null}. * @return size of directory in bytes, 0 if directory is security restricted. - * @throws IllegalArgumentException if the given {@link File} exists but is not a directory + * @throws IllegalArgumentException if the given {@link File} exists but is not a directory. * @throws NullPointerException if the directory is {@code null}. * @throws UncheckedIOException if an IO error occurs. * @since 2.4 @@ -3017,9 +3035,9 @@ public static BigInteger sizeOfDirectoryAsBigInteger(final File directory) { * closed stream causes a {@link IllegalStateException}. *

* - * @param directory the directory to search in - * @param recursive if true all subdirectories are searched as well - * @param extensions an array of extensions, for example, {"java","xml"}. If this parameter is {@code null}, all files are returned. + * @param directory The directory to search. + * @param recursive if true all subdirectories are searched as well. + * @param extensions an array of extensions, for example, {"java", "xml"}. If this parameter is {@code null}, all files are returned. * @return a Stream of {@link File} for matching files. * @throws IOException if an I/O error is thrown when accessing the starting file. * @since 2.9.0 @@ -3036,16 +3054,12 @@ public static Stream streamFiles(final File directory, final boolean recur /** * Converts from a {@link URL} to a {@link File}. *

- * Syntax such as {@code file:///my%20docs/file.txt} will be - * correctly decoded to {@code /my docs/file.txt}. - * UTF-8 is used to decode percent-encoded octets to characters. - * Additionally, malformed percent-encoded octets are handled leniently by - * passing them through literally. + * Syntax such as {@code file:///my%20docs/file.txt} will be correctly decoded to {@code /my docs/file.txt}. UTF-8 is used to decode percent-encoded octets + * to characters. Additionally, malformed percent-encoded octets are handled leniently by passing them through literally. *

* - * @param url the file URL to convert, {@code null} returns {@code null} - * @return the equivalent {@link File} object, or {@code null} - * if the URL's protocol is not {@code file} + * @param url the file URL to convert, {@code null} returns {@code null}. + * @return the equivalent {@link File} object, or {@code null} if the URL's protocol is not {@code file}. */ public static File toFile(final URL url) { if (url == null || !isFileProtocol(url)) { @@ -3058,22 +3072,17 @@ public static File toFile(final URL url) { /** * Converts each of an array of {@link URL} to a {@link File}. *

- * Returns an array of the same size as the input. - * If the input is {@code null}, an empty array is returned. - * If the input contains {@code null}, the output array contains {@code null} at the same - * index. + * Returns an array of the same size as the input. If the input is {@code null}, an empty array is returned. If the input contains {@code null}, the output + * array contains {@code null} at the same index. *

*

- * This method will decode the URL. - * Syntax such as {@code file:///my%20docs/file.txt} will be - * correctly decoded to {@code /my docs/file.txt}. + * This method will decode the URL. Syntax such as {@code file:///my%20docs/file.txt} will be correctly decoded to {@code /my docs/file.txt}. *

* - * @param urls the file URLs to convert, {@code null} returns empty array - * @return a non-{@code null} array of Files matching the input, with a {@code null} item - * if there was a {@code null} at that index in the input array - * @throws IllegalArgumentException if any file is not a URL file - * @throws IllegalArgumentException if any file is incorrectly encoded + * @param urls the file URLs to convert, {@code null} returns empty array. + * @return a non-{@code null} array of Files matching the input, with a {@code null} item if there was a {@code null} at that index in the input array. + * @throws IllegalArgumentException if any file is not a URL file. + * @throws IllegalArgumentException if any file is incorrectly encoded. * @since 1.1 */ public static File[] toFiles(final URL... urls) { @@ -3109,8 +3118,8 @@ private static List toList(final Stream stream) { /** * Converts whether or not to recurse into a recursion max depth. * - * @param recursive whether or not to recurse - * @return the recursion depth + * @param recursive whether or not to recurse. + * @return the recursion depth. */ private static int toMaxDepth(final boolean recursive) { return recursive ? Integer.MAX_VALUE : 1; @@ -3119,9 +3128,9 @@ private static int toMaxDepth(final boolean recursive) { /** * Converts an array of file extensions to suffixes. * - * @param extensions an array of extensions. Format: {"java", "xml"} - * @return an array of suffixes. Format: {".java", ".xml"} - * @throws NullPointerException if the parameter is null + * @param extensions an array of extensions, for example: {@code ["java", "xml"]}. + * @return an array of suffixes, for example: {@code [".java", ".xml"]}. + * @throws NullPointerException if the parameter is null. */ private static String[] toSuffixes(final String... extensions) { return Stream.of(Objects.requireNonNull(extensions, "extensions")).map(s -> s.charAt(0) == '.' ? s : "." + s).toArray(String[]::new); @@ -3150,10 +3159,10 @@ public static void touch(final File file) throws IOException { * Returns an array of the same size as the input. *

* - * @param files the files to convert, must not be {@code null} - * @return an array of URLs matching the input - * @throws IOException if a file cannot be converted - * @throws NullPointerException if any argument is null + * @param files the files to convert, must not be {@code null}. + * @return an array of URLs matching the input. + * @throws IOException if a file cannot be converted. + * @throws NullPointerException if any argument is null. */ public static URL[] toURLs(final File... files) throws IOException { Objects.requireNonNull(files, "files"); @@ -3192,10 +3201,10 @@ private static void validateMoveParameters(final File source, final File destina * true up to the maximum time specified in seconds. *

* - * @param file the file to check, must not be {@code null} - * @param seconds the maximum time in seconds to wait - * @return true if file exists - * @throws NullPointerException if the file is {@code null} + * @param file the file to check, must not be {@code null}. + * @param seconds the maximum time in seconds to wait. + * @return true if file exists. + * @throws NullPointerException if the file is {@code null}. */ public static boolean waitFor(final File file, final int seconds) { Objects.requireNonNull(file, PROTOCOL_FILE); @@ -3203,13 +3212,13 @@ public static boolean waitFor(final File file, final int seconds) { } /** - * Writes a CharSequence to a file creating the file if it does not exist using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Writes a CharSequence to a file creating the file if it does not exist using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * - * @param file the file to write - * @param data the content to write to the file - * @throws IOException in case of an I/O error + * @param file the file to write. + * @param data the content to write to the file. + * @throws IOException in case of an I/O error. * @since 2.0 - * @deprecated Use {@link #write(File, CharSequence, Charset)} instead (and specify the appropriate encoding) + * @deprecated Use {@link #write(File, CharSequence, Charset)} instead (and specify the appropriate encoding). */ @Deprecated public static void write(final File file, final CharSequence data) throws IOException { @@ -3217,14 +3226,14 @@ public static void write(final File file, final CharSequence data) throws IOExce } /** - * Writes a CharSequence to a file creating the file if it does not exist using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Writes a CharSequence to a file creating the file if it does not exist using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * - * @param file the file to write - * @param data the content to write to the file - * @param append if {@code true}, then the data will be added to the end of the file rather than overwriting - * @throws IOException in case of an I/O error + * @param file the file to write. + * @param data the content to write to the file. + * @param append if {@code true}, then the data will be added to the end of the file rather than overwriting. + * @throws IOException in case of an I/O error. * @since 2.1 - * @deprecated Use {@link #write(File, CharSequence, Charset, boolean)} instead (and specify the appropriate encoding) + * @deprecated Use {@link #write(File, CharSequence, Charset, boolean)} instead (and specify the appropriate encoding). */ @Deprecated public static void write(final File file, final CharSequence data, final boolean append) throws IOException { @@ -3234,10 +3243,10 @@ public static void write(final File file, final CharSequence data, final boolean /** * Writes a CharSequence to a file creating the file if it does not exist. * - * @param file the file to write - * @param data the content to write to the file - * @param charset the name of the requested charset, {@code null} means platform default - * @throws IOException in case of an I/O error + * @param file the file to write. + * @param data the content to write to the file. + * @param charset the name of the requested charset, {@code null} means platform default. + * @throws IOException in case of an I/O error. * @since 2.3 */ public static void write(final File file, final CharSequence data, final Charset charset) throws IOException { @@ -3247,12 +3256,12 @@ public static void write(final File file, final CharSequence data, final Charset /** * Writes a CharSequence to a file creating the file if it does not exist. * - * @param file the file to write - * @param data the content to write to the file - * @param charset the charset to use, {@code null} means platform default - * @param append if {@code true}, then the data will be added to the - * end of the file rather than overwriting - * @throws IOException in case of an I/O error + * @param file the file to write. + * @param data the content to write to the file. + * @param charset the charset to use, {@code null} means platform default. + * @param append if {@code true}, then the data will be added to the. + * end of the file rather than overwriting. + * @throws IOException in case of an I/O error. * @since 2.3 */ public static void write(final File file, final CharSequence data, final Charset charset, final boolean append) throws IOException { @@ -3492,7 +3501,7 @@ public static void writeLines(final File file, final String charsetName, final C } /** - * Writes a String to a file creating the file if it does not exist using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Writes a String to a file creating the file if it does not exist using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * * @param file the file to write * @param data the content to write to the file @@ -3505,7 +3514,7 @@ public static void writeStringToFile(final File file, final String data) throws } /** - * Writes a String to a file creating the file if it does not exist using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Writes a String to a file creating the file if it does not exist using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * * @param file the file to write * @param data the content to write to the file diff --git a/src/main/java/org/apache/commons/io/HexDump.java b/src/main/java/org/apache/commons/io/HexDump.java index cb7efa7b2f2..41c026b2fb1 100644 --- a/src/main/java/org/apache/commons/io/HexDump.java +++ b/src/main/java/org/apache/commons/io/HexDump.java @@ -169,7 +169,7 @@ public static void dump(final byte[] data, final long offset, * data array are dumped. *

*

- * This method uses the virtual machine's {@link Charset#defaultCharset() default charset}. + * This method uses the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

* * @param data the byte array to be dumped diff --git a/src/main/java/org/apache/commons/io/IOUtils.java b/src/main/java/org/apache/commons/io/IOUtils.java index aa3752dc4f1..09a018b036d 100644 --- a/src/main/java/org/apache/commons/io/IOUtils.java +++ b/src/main/java/org/apache/commons/io/IOUtils.java @@ -65,6 +65,7 @@ import org.apache.commons.io.function.IOConsumer; import org.apache.commons.io.function.IOSupplier; import org.apache.commons.io.function.IOTriFunction; +import org.apache.commons.io.input.BoundedInputStream; import org.apache.commons.io.input.CharSequenceReader; import org.apache.commons.io.input.QueueInputStream; import org.apache.commons.io.output.AppendableWriter; @@ -72,7 +73,6 @@ import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.io.output.NullWriter; import org.apache.commons.io.output.StringBuilderWriter; -import org.apache.commons.io.output.ThresholdingOutputStream; import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; /** @@ -132,6 +132,144 @@ public class IOUtils { // Writer. Each method should take at least one of these as a parameter, // or return one of them. + /** + * Holder for per-thread internal scratch buffer. + * + *

Buffers are created lazily and reused within the same thread to reduce allocation overhead. In the rare case of reentrant access, a temporary buffer + * is allocated to avoid data corruption.

+ * + *

Typical usage:

+ * + *
{@code
+     * try (ScratchBytes scratch = ScratchBytes.get()) {
+     *     // use the buffer
+     *     byte[] bytes = scratch.array();
+     *     // ...
+     * }
+     * }
+ */ + static final class ScratchBytes implements AutoCloseable { + + /** + * Wraps an internal byte array. + * + * [0] boolean in use. + * [1] byte[] buffer. + */ + private static final ThreadLocal LOCAL = ThreadLocal.withInitial(() -> new Object[] { false, byteArray() }); + + private static final ScratchBytes INSTANCE = new ScratchBytes(null); + + /** + * Gets the internal byte array buffer. + * + * @return the internal byte array buffer. + */ + static ScratchBytes get() { + final Object[] holder = LOCAL.get(); + // If already in use, return a new array + if ((boolean) holder[0]) { + return new ScratchBytes(byteArray()); + } + holder[0] = true; + return INSTANCE; + } + + /** + * The buffer, or null if using the thread-local buffer. + */ + private final byte[] buffer; + + private ScratchBytes(final byte[] buffer) { + this.buffer = buffer; + } + + byte[] array() { + return buffer != null ? buffer : (byte[]) LOCAL.get()[1]; + } + + /** + * If the buffer is the internal array, clear and release it for reuse. + */ + @Override + public void close() { + if (buffer == null) { + final Object[] holder = LOCAL.get(); + Arrays.fill((byte[]) holder[1], (byte) 0); + holder[0] = false; + } + } + } + + /** + * Holder for per-thread internal scratch buffer. + * + *

Buffers are created lazily and reused within the same thread to reduce allocation overhead. In the rare case of reentrant access, a temporary buffer + * is allocated to avoid data corruption.

+ * + *

Typical usage:

+ * + *
{@code
+     * try (ScratchChars scratch = ScratchChars.get()) {
+     *     // use the buffer
+     *     char[] bytes = scratch.array();
+     *     // ...
+     * }
+     * }
+ */ + static final class ScratchChars implements AutoCloseable { + + /** + * Wraps an internal char array. + * + * [0] boolean in use. + * [1] char[] buffer. + */ + private static final ThreadLocal LOCAL = ThreadLocal.withInitial(() -> new Object[] { false, charArray() }); + + private static final ScratchChars INSTANCE = new ScratchChars(null); + + /** + * Gets the internal char array buffer. + * + * @return the internal char array buffer. + */ + static ScratchChars get() { + final Object[] holder = LOCAL.get(); + // If already in use, return a new array + if ((boolean) holder[0]) { + return new ScratchChars(charArray()); + } + holder[0] = true; + return INSTANCE; + } + + /** + * The buffer, or null if using the thread-local buffer. + */ + private final char[] buffer; + + private ScratchChars(final char[] buffer) { + this.buffer = buffer; + } + + char[] array() { + return buffer != null ? buffer : (char[]) LOCAL.get()[1]; + } + + /** + * If the buffer is the internal array, clear and release it for reuse. + */ + @Override + public void close() { + if (buffer == null) { + final Object[] holder = LOCAL.get(); + Arrays.fill((char[]) holder[1], (char) 0); + holder[0] = false; + } + } + } + /** * CR char '{@value}'. * @@ -202,32 +340,22 @@ public class IOUtils { public static final String LINE_SEPARATOR_WINDOWS = StandardLineSeparator.CRLF.getString(); /** - * Internal byte array buffer, intended for both reading and writing. - */ - private static final ThreadLocal SCRATCH_BYTE_BUFFER_RW = ThreadLocal.withInitial(IOUtils::byteArray); - - /** - * Internal byte array buffer, intended for write only operations. - */ - private static final byte[] SCRATCH_BYTE_BUFFER_WO = byteArray(); - - /** - * Internal char array buffer, intended for both reading and writing. - */ - private static final ThreadLocal SCRATCH_CHAR_BUFFER_RW = ThreadLocal.withInitial(IOUtils::charArray); - - /** - * Internal char array buffer, intended for write only operations. + * The maximum size of an array in many Java VMs. + *

+ * The constant is copied from OpenJDK's {@code jdk.internal.util.ArraysSupport#SOFT_MAX_ARRAY_LENGTH}. + *

+ * + * @since 2.21.0 */ - private static final char[] SCRATCH_CHAR_BUFFER_WO = charArray(); + public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8; /** * Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a * BufferedInputStream from the given InputStream. * - * @param inputStream the InputStream to wrap or return (not null) - * @return the given InputStream or a new {@link BufferedInputStream} for the given InputStream - * @throws NullPointerException if the input parameter is null + * @param inputStream the InputStream to wrap or return (not null). + * @return the given InputStream or a new {@link BufferedInputStream} for the given InputStream. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ @SuppressWarnings("resource") // parameter null check @@ -243,10 +371,10 @@ public static BufferedInputStream buffer(final InputStream inputStream) { * Returns the given InputStream if it is already a {@link BufferedInputStream}, otherwise creates a * BufferedInputStream from the given InputStream. * - * @param inputStream the InputStream to wrap or return (not null) + * @param inputStream the InputStream to wrap or return (not null). * @param size the buffer size, if a new BufferedInputStream is created. - * @return the given InputStream or a new {@link BufferedInputStream} for the given InputStream - * @throws NullPointerException if the input parameter is null + * @return the given InputStream or a new {@link BufferedInputStream} for the given InputStream. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ @SuppressWarnings("resource") // parameter null check @@ -262,9 +390,9 @@ public static BufferedInputStream buffer(final InputStream inputStream, final in * Returns the given OutputStream if it is already a {@link BufferedOutputStream}, otherwise creates a * BufferedOutputStream from the given OutputStream. * - * @param outputStream the OutputStream to wrap or return (not null) + * @param outputStream the OutputStream to wrap or return (not null). * @return the given OutputStream or a new {@link BufferedOutputStream} for the given OutputStream - * @throws NullPointerException if the input parameter is null + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ @SuppressWarnings("resource") // parameter null check @@ -280,10 +408,10 @@ public static BufferedOutputStream buffer(final OutputStream outputStream) { * Returns the given OutputStream if it is already a {@link BufferedOutputStream}, otherwise creates a * BufferedOutputStream from the given OutputStream. * - * @param outputStream the OutputStream to wrap or return (not null) + * @param outputStream the OutputStream to wrap or return (not null). * @param size the buffer size, if a new BufferedOutputStream is created. - * @return the given OutputStream or a new {@link BufferedOutputStream} for the given OutputStream - * @throws NullPointerException if the input parameter is null + * @return the given OutputStream or a new {@link BufferedOutputStream} for the given OutputStream. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ @SuppressWarnings("resource") // parameter null check @@ -299,9 +427,9 @@ public static BufferedOutputStream buffer(final OutputStream outputStream, final * Returns the given reader if it is already a {@link BufferedReader}, otherwise creates a BufferedReader from * the given reader. * - * @param reader the reader to wrap or return (not null) - * @return the given reader or a new {@link BufferedReader} for the given reader - * @throws NullPointerException if the input parameter is null + * @param reader the reader to wrap or return (not null). + * @return the given reader or a new {@link BufferedReader} for the given reader. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ public static BufferedReader buffer(final Reader reader) { @@ -312,10 +440,10 @@ public static BufferedReader buffer(final Reader reader) { * Returns the given reader if it is already a {@link BufferedReader}, otherwise creates a BufferedReader from the * given reader. * - * @param reader the reader to wrap or return (not null) + * @param reader the reader to wrap or return (not null). * @param size the buffer size, if a new BufferedReader is created. - * @return the given reader or a new {@link BufferedReader} for the given reader - * @throws NullPointerException if the input parameter is null + * @return the given reader or a new {@link BufferedReader} for the given reader. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ public static BufferedReader buffer(final Reader reader, final int size) { @@ -326,9 +454,9 @@ public static BufferedReader buffer(final Reader reader, final int size) { * Returns the given Writer if it is already a {@link BufferedWriter}, otherwise creates a BufferedWriter from the * given Writer. * - * @param writer the Writer to wrap or return (not null) - * @return the given Writer or a new {@link BufferedWriter} for the given Writer - * @throws NullPointerException if the input parameter is null + * @param writer the Writer to wrap or return (not null). + * @return the given Writer or a new {@link BufferedWriter} for the given Writer. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ public static BufferedWriter buffer(final Writer writer) { @@ -339,10 +467,10 @@ public static BufferedWriter buffer(final Writer writer) { * Returns the given Writer if it is already a {@link BufferedWriter}, otherwise creates a BufferedWriter from the * given Writer. * - * @param writer the Writer to wrap or return (not null) + * @param writer the Writer to wrap or return (not null). * @param size the buffer size, if a new BufferedWriter is created. - * @return the given Writer or a new {@link BufferedWriter} for the given Writer - * @throws NullPointerException if the input parameter is null + * @return the given Writer or a new {@link BufferedWriter} for the given Writer. + * @throws NullPointerException if the input parameter is null. * @since 2.5 */ public static BufferedWriter buffer(final Writer writer, final int size) { @@ -362,7 +490,7 @@ public static byte[] byteArray() { /** * Returns a new byte array of the given size. * - * TODO Consider guarding or warning against large allocations... + * TODO Consider guarding or warning against large allocations. * * @param size array size. * @return a new byte array of the given size. @@ -386,7 +514,7 @@ private static char[] charArray() { /** * Returns a new char array of the given size. * - * TODO Consider guarding or warning against large allocations... + * TODO Consider guarding or warning against large allocations. * * @param size array size. * @return a new char array of the given size. @@ -396,6 +524,183 @@ private static char[] charArray(final int size) { return new char[size]; } + /** + * Validates that the sub-range {@code [off, off + len)} is within the bounds of the given array. + * + *

The range is valid if all of the following hold:

+ *
    + *
  • {@code off >= 0}
  • + *
  • {@code len >= 0}
  • + *
  • {@code off + len <= array.length}
  • + *
+ * + *

If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.

+ * + *

Typical usage in {@link InputStream#read(byte[], int, int)} and {@link OutputStream#write(byte[], int, int)} implementations:

+ * + *

+     * public int read(byte[] b, int off, int len) throws IOException {
+     *     IOUtils.checkFromIndexSize(b, off, len);
+     *     if (len == 0) {
+     *         return 0;
+     *     }
+     *     ensureOpen();
+     *     // perform read...
+     * }
+     *
+     * public void write(byte[] b, int off, int len) throws IOException {
+     *     IOUtils.checkFromIndexSize(b, off, len);
+     *     if (len == 0) {
+     *         return;
+     *     }
+     *     ensureOpen();
+     *     // perform write...
+     * }
+     * 
+ * + * @param array the array against which the range is validated + * @param off the starting offset into the array (inclusive) + * @param len the number of elements to access + * @throws NullPointerException if {@code array} is {@code null} + * @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds for {@code array} + * @see InputStream#read(byte[], int, int) + * @see OutputStream#write(byte[], int, int) + * @since 2.21.0 + */ + public static void checkFromIndexSize(final byte[] array, final int off, final int len) { + checkFromIndexSize(off, len, Objects.requireNonNull(array, "byte array").length); + } + + /** + * Validates that the sub-range {@code [off, off + len)} is within the bounds of the given array. + * + *

The range is valid if all of the following hold:

+ *
    + *
  • {@code off >= 0}
  • + *
  • {@code len >= 0}
  • + *
  • {@code off + len <= array.length}
  • + *
+ * + *

If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.

+ * + *

Typical usage in {@link Reader#read(char[], int, int)} and {@link Writer#write(char[], int, int)} implementations:

+ * + *

+     * public int read(char[] cbuf, int off, int len) throws IOException {
+     *     ensureOpen();
+     *     IOUtils.checkFromIndexSize(cbuf, off, len);
+     *     if (len == 0) {
+     *         return 0;
+     *     }
+     *     // perform read...
+     * }
+     *
+     * public void write(char[] cbuf, int off, int len) throws IOException {
+     *     ensureOpen();
+     *     IOUtils.checkFromIndexSize(cbuf, off, len);
+     *     if (len == 0) {
+     *         return;
+     *     }
+     *     // perform write...
+     * }
+     * 
+ * + * @param array the array against which the range is validated + * @param off the starting offset into the array (inclusive) + * @param len the number of characters to access + * @throws NullPointerException if {@code array} is {@code null} + * @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds for {@code array} + * @see Reader#read(char[], int, int) + * @see Writer#write(char[], int, int) + * @since 2.21.0 + */ + public static void checkFromIndexSize(final char[] array, final int off, final int len) { + checkFromIndexSize(off, len, Objects.requireNonNull(array, "char array").length); + } + + static void checkFromIndexSize(final int off, final int len, final int arrayLength) { + if ((off | len | arrayLength) < 0 || arrayLength - len < off) { + throw new IndexOutOfBoundsException(String.format("Range [%s, %

The range is valid if all of the following hold:

+ *
    + *
  • {@code off >= 0}
  • + *
  • {@code len >= 0}
  • + *
  • {@code off + len <= str.length()}
  • + *
+ * + *

If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.

+ * + *

Typical usage in {@link Writer#write(String, int, int)} implementations:

+ * + *

+     * public void write(String str, int off, int len) throws IOException {
+     *     IOUtils.checkFromIndexSize(str, off, len);
+     *     if (len == 0) {
+     *         return;
+     *     }
+     *     // perform write...
+     * }
+     * 
+ * + * @param str the string against which the range is validated + * @param off the starting offset into the string (inclusive) + * @param len the number of characters to write + * @throws NullPointerException if {@code str} is {@code null} + * @throws IndexOutOfBoundsException if the range {@code [off, off + len)} is out of bounds for {@code str} + * @see Writer#write(String, int, int) + * @since 2.21.0 + */ + public static void checkFromIndexSize(final String str, final int off, final int len) { + checkFromIndexSize(off, len, Objects.requireNonNull(str, "str").length()); + } + + /** + * Validates that the sub-sequence {@code [fromIndex, toIndex)} is within the bounds of the given {@link CharSequence}. + * + *

The sub-sequence is valid if all of the following hold:

+ *
    + *
  • {@code fromIndex >= 0}
  • + *
  • {@code fromIndex <= toIndex}
  • + *
  • {@code toIndex <= seq.length()}
  • + *
+ * + *

If {@code seq} is {@code null}, it is treated as the literal string {@code "null"} (length {@code 4}).

+ * + *

If the range is invalid, throws {@link IndexOutOfBoundsException} with a descriptive message.

+ * + *

Typical usage in {@link Appendable#append(CharSequence, int, int)} implementations:

+ * + *

+     * public Appendable append(CharSequence csq, int start, int end) throws IOException {
+     *     IOUtils.checkFromToIndex(csq, start, end);
+     *     // perform append...
+     *     return this;
+     * }
+     * 
+ * + * @param seq the character sequence to validate (may be {@code null}, treated as {@code "null"}) + * @param fromIndex the starting index (inclusive) + * @param toIndex the ending index (exclusive) + * @throws IndexOutOfBoundsException if the range {@code [fromIndex, toIndex)} is out of bounds for {@code seq} + * @see Appendable#append(CharSequence, int, int) + * @since 2.21.0 + */ + public static void checkFromToIndex(final CharSequence seq, final int fromIndex, final int toIndex) { + checkFromToIndex(fromIndex, toIndex, seq != null ? seq.length() : 4); + } + + static void checkFromToIndex(final int fromIndex, final int toIndex, final int length) { + if (fromIndex < 0 || toIndex < fromIndex || length < toIndex) { + throw new IndexOutOfBoundsException(String.format("Range [%s, %s) out of bounds for length %s", fromIndex, toIndex, length)); + } + } + /** * Clears any state. *
    @@ -405,10 +710,8 @@ private static char[] charArray(final int size) { * @see IO#clear() */ static void clear() { - SCRATCH_BYTE_BUFFER_RW.remove(); - SCRATCH_CHAR_BUFFER_RW.remove(); - Arrays.fill(SCRATCH_BYTE_BUFFER_WO, (byte) 0); - Arrays.fill(SCRATCH_CHAR_BUFFER_WO, (char) 0); + ScratchBytes.LOCAL.remove(); + ScratchChars.LOCAL.remove(); } /** @@ -474,7 +777,7 @@ public static void close(final URLConnection conn) { /** * Avoids the need to type cast. * - * @param closeable the object to close, may be null + * @param closeable the object to close, may be null. */ private static void closeQ(final Closeable closeable) { closeQuietly(closeable, null); @@ -516,7 +819,7 @@ private static void closeQ(final Closeable closeable) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param closeable the objects to close, may be null or already closed + * @param closeable the objects to close, may be null or already closed. * @since 2.0 * @see Throwable#addSuppressed(Throwable) */ @@ -565,7 +868,7 @@ public static void closeQuietly(final Closeable closeable) { *

    * Also consider using a try-with-resources statement where appropriate. *

    - * @param closeables the objects to close, may be null or already closed + * @param closeables the objects to close, may be null or already closed. * @see #closeQuietly(Closeable) * @since 2.5 * @see Throwable#addSuppressed(Throwable) @@ -621,7 +924,7 @@ public static void closeQuietly(final Closeable closeable, final Consumer

    * - * @param input the InputStream to close, may be null or already closed + * @param input the InputStream to close, may be null or already closed. * @see Throwable#addSuppressed(Throwable) */ public static void closeQuietly(final InputStream input) { @@ -634,7 +937,7 @@ public static void closeQuietly(final InputStream input) { * Equivalent calling {@link Closeable#close()} on each element, except any exceptions will be ignored. *

    * - * @param closeables the objects to close, may be null or already closed + * @param closeables the objects to close, may be null or already closed. * @see #closeQuietly(Closeable) * @since 2.12.0 */ @@ -671,7 +974,7 @@ public static void closeQuietly(final Iterable closeables) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param output the OutputStream to close, may be null or already closed + * @param output the OutputStream to close, may be null or already closed. * @see Throwable#addSuppressed(Throwable) */ public static void closeQuietly(final OutputStream output) { @@ -704,7 +1007,7 @@ public static void closeQuietly(final OutputStream output) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param reader the Reader to close, may be null or already closed + * @param reader the Reader to close, may be null or already closed. * @see Throwable#addSuppressed(Throwable) */ public static void closeQuietly(final Reader reader) { @@ -736,7 +1039,7 @@ public static void closeQuietly(final Reader reader) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param selector the Selector to close, may be null or already closed + * @param selector the Selector to close, may be null or already closed. * @since 2.2 * @see Throwable#addSuppressed(Throwable) */ @@ -769,7 +1072,7 @@ public static void closeQuietly(final Selector selector) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param serverSocket the ServerSocket to close, may be null or already closed + * @param serverSocket the ServerSocket to close, may be null or already closed. * @since 2.2 * @see Throwable#addSuppressed(Throwable) */ @@ -802,7 +1105,7 @@ public static void closeQuietly(final ServerSocket serverSocket) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param socket the Socket to close, may be null or already closed + * @param socket the Socket to close, may be null or already closed. * @since 2.0 * @see Throwable#addSuppressed(Throwable) */ @@ -816,7 +1119,7 @@ public static void closeQuietly(final Socket socket) { * Equivalent calling {@link Closeable#close()} on each element, except any exceptions will be ignored. *

    * - * @param closeables the objects to close, may be null or already closed + * @param closeables the objects to close, may be null or already closed. * @see #closeQuietly(Closeable) * @since 2.12.0 */ @@ -851,7 +1154,7 @@ public static void closeQuietly(final Stream closeables) { * Also consider using a try-with-resources statement where appropriate. *

    * - * @param writer the Writer to close, may be null or already closed + * @param writer the Writer to close, may be null or already closed. * @see Throwable#addSuppressed(Throwable) */ public static void closeQuietly(final Writer writer) { @@ -898,11 +1201,11 @@ public static long consume(final Reader input) throws IOException { * {@link BufferedInputStream} if they are not already buffered. *

    * - * @param input1 the first stream - * @param input2 the second stream - * @return true if the content of the streams are equal or they both don't - * exist, false otherwise - * @throws IOException if an I/O error occurs + * @param input1 the first stream. + * @param input2 the second stream. + * @return true if the content of the streams are equal or they both don't. + * exist, false otherwise. + * @throws IOException if an I/O error occurs. */ @SuppressWarnings("resource") // Caller closes input streams public static boolean contentEquals(final InputStream input1, final InputStream input2) throws IOException { @@ -936,11 +1239,11 @@ private static boolean contentEquals(final Iterator iterator1, final Iterator * This method buffers the input internally using {@link BufferedReader} if they are not already buffered. *

    * - * @param input1 the first reader - * @param input2 the second reader - * @return true if the content of the readers are equal or they both don't exist, false otherwise - * @throws NullPointerException if either input is null - * @throws IOException if an I/O error occurs + * @param input1 the first reader. + * @param input2 the second reader. + * @return true if the content of the readers are equal or they both don't exist, false otherwise. + * @throws NullPointerException if either input is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ public static boolean contentEquals(final Reader input1, final Reader input2) throws IOException { @@ -952,37 +1255,39 @@ public static boolean contentEquals(final Reader input1, final Reader input2) th } // reuse one - final char[] array1 = getScratchCharArray(); - // but allocate another - final char[] array2 = charArray(); - int pos1; - int pos2; - int count1; - int count2; - while (true) { - pos1 = 0; - pos2 = 0; - for (int index = 0; index < DEFAULT_BUFFER_SIZE; index++) { - if (pos1 == index) { - do { - count1 = input1.read(array1, pos1, DEFAULT_BUFFER_SIZE - pos1); - } while (count1 == 0); - if (count1 == EOF) { - return pos2 == index && input2.read() == EOF; + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + final char[] array1 = scratch.array(); + // but allocate another + final char[] array2 = charArray(); + int pos1; + int pos2; + int count1; + int count2; + while (true) { + pos1 = 0; + pos2 = 0; + for (int index = 0; index < DEFAULT_BUFFER_SIZE; index++) { + if (pos1 == index) { + do { + count1 = input1.read(array1, pos1, DEFAULT_BUFFER_SIZE - pos1); + } while (count1 == 0); + if (count1 == EOF) { + return pos2 == index && input2.read() == EOF; + } + pos1 += count1; } - pos1 += count1; - } - if (pos2 == index) { - do { - count2 = input2.read(array2, pos2, DEFAULT_BUFFER_SIZE - pos2); - } while (count2 == 0); - if (count2 == EOF) { - return pos1 == index && input1.read() == EOF; + if (pos2 == index) { + do { + count2 = input2.read(array2, pos2, DEFAULT_BUFFER_SIZE - pos2); + } while (count2 == 0); + if (count2 == EOF) { + return pos1 == index && input1.read() == EOF; + } + pos2 += count2; + } + if (array1[index] != array2[index]) { + return false; } - pos2 += count2; - } - if (array1[index] != array2[index]) { - return false; } } } @@ -1018,11 +1323,11 @@ private static boolean contentEqualsIgnoreEOL(final BufferedReader reader1, fina * {@link BufferedReader} if they are not already buffered. *

    * - * @param reader1 the first reader - * @param reader2 the second reader - * @return true if the content of the readers are equal (ignoring EOL differences), false otherwise - * @throws NullPointerException if either input is null - * @throws UncheckedIOException if an I/O error occurs + * @param reader1 the first reader. + * @param reader2 the second reader. + * @return true if the content of the readers are equal (ignoring EOL differences), false otherwise. + * @throws NullPointerException if either input is null. + * @throws UncheckedIOException if an I/O error occurs. * @since 2.2 */ @SuppressWarnings("resource") @@ -1068,22 +1373,21 @@ public static int copy(final InputStream inputStream, final OutputStream outputS *

    * * @param inputStream the {@link InputStream} to read. - * @param outputStream the {@link OutputStream} to write to - * @param bufferSize the bufferSize used to copy from the input to the output + * @param outputStream the {@link OutputStream} to write to. + * @param bufferSize the bufferSize used to copy from the input to the output. * @return the number of bytes copied. * @throws NullPointerException if the InputStream is {@code null}. * @throws NullPointerException if the OutputStream is {@code null}. * @throws IOException if an I/O error occurs. * @since 2.5 */ - public static long copy(final InputStream inputStream, final OutputStream outputStream, final int bufferSize) - throws IOException { + public static long copy(final InputStream inputStream, final OutputStream outputStream, final int bufferSize) throws IOException { return copyLarge(inputStream, outputStream, byteArray(bufferSize)); } /** * Copies bytes from an {@link InputStream} to chars on a - * {@link Writer} using the virtual machine's {@link Charset#defaultCharset() default charset}. + * {@link Writer} using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. @@ -1092,16 +1396,15 @@ public static long copy(final InputStream inputStream, final OutputStream output * This method uses {@link InputStreamReader}. *

    * - * @param input the {@link InputStream} to read - * @param writer the {@link Writer} to write to - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param input the {@link InputStream} to read. + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 - * @deprecated Use {@link #copy(InputStream, Writer, Charset)} instead + * @deprecated Use {@link #copy(InputStream, Writer, Charset)} instead. */ @Deprecated - public static void copy(final InputStream input, final Writer writer) - throws IOException { + public static void copy(final InputStream input, final Writer writer) throws IOException { copy(input, writer, Charset.defaultCharset()); } @@ -1116,15 +1419,14 @@ public static void copy(final InputStream input, final Writer writer) * This method uses {@link InputStreamReader}. *

    * - * @param input the {@link InputStream} to read - * @param writer the {@link Writer} to write to - * @param inputCharset the charset to use for the input stream, null means platform default - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param input the {@link InputStream} to read. + * @param writer the {@link Writer} to write to. + * @param inputCharset the charset to use for the input stream, null means platform default. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ - public static void copy(final InputStream input, final Writer writer, final Charset inputCharset) - throws IOException { + public static void copy(final InputStream input, final Writer writer, final Charset inputCharset) throws IOException { copy(new InputStreamReader(input, Charsets.toCharset(inputCharset)), writer); } @@ -1145,14 +1447,13 @@ public static void copy(final InputStream input, final Writer writer, final Char * * @param input the {@link InputStream} to read * @param writer the {@link Writer} to write to - * @param inputCharsetName the name of the requested charset for the InputStream, null means platform default - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param inputCharsetName the name of the requested charset for the InputStream, null means platform default. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ - public static void copy(final InputStream input, final Writer writer, final String inputCharsetName) - throws IOException { + public static void copy(final InputStream input, final Writer writer, final String inputCharsetName) throws IOException { copy(input, writer, Charsets.toCharset(inputCharsetName)); } @@ -1200,11 +1501,11 @@ public static QueueInputStream copy(final java.io.ByteArrayOutputStream outputSt * use the {@link #copyLarge(Reader, Writer)} method. *

    * - * @param reader the {@link Reader} to read - * @param output the {@link Appendable} to write to - * @return the number of characters copied, or -1 if > Integer.MAX_VALUE - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param output the {@link Appendable} to write to. + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 2.7 */ public static long copy(final Reader reader, final Appendable output) throws IOException { @@ -1218,12 +1519,12 @@ public static long copy(final Reader reader, final Appendable output) throws IOE * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @param output the {@link Appendable} to write to - * @param buffer the buffer to be used for the copy - * @return the number of characters copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param output the {@link Appendable} to write to. + * @param buffer the buffer to be used for the copy. + * @return the number of characters copied. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 2.7 */ public static long copy(final Reader reader, final Appendable output, final CharBuffer buffer) throws IOException { @@ -1238,59 +1539,50 @@ public static long copy(final Reader reader, final Appendable output, final Char } /** - * Copies chars from a {@link Reader} to bytes on an - * {@link OutputStream} using the the virtual machine's {@link Charset#defaultCharset() default charset}, - * and calling flush. + * Copies chars from a {@link Reader} to bytes on an {@link OutputStream} using the the virtual machine's {@linkplain Charset#defaultCharset() default + * charset}, and calling flush. *

    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedReader}. + * This method buffers the input internally, so there is no need to use a {@link BufferedReader}. *

    *

    - * Due to the implementation of OutputStreamWriter, this method performs a - * flush. + * Due to the implementation of OutputStreamWriter, this method performs a flush. *

    *

    * This method uses {@link OutputStreamWriter}. *

    * - * @param reader the {@link Reader} to read - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 * @deprecated Use {@link #copy(Reader, OutputStream, Charset)} instead */ @Deprecated - public static void copy(final Reader reader, final OutputStream output) - throws IOException { + public static void copy(final Reader reader, final OutputStream output) throws IOException { copy(reader, output, Charset.defaultCharset()); } /** - * Copies chars from a {@link Reader} to bytes on an - * {@link OutputStream} using the specified character encoding, and - * calling flush. + * Copies chars from a {@link Reader} to bytes on an {@link OutputStream} using the specified character encoding, and calling flush. *

    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedReader}. + * This method buffers the input internally, so there is no need to use a {@link BufferedReader}. *

    *

    - * Due to the implementation of OutputStreamWriter, this method performs a - * flush. + * Due to the implementation of OutputStreamWriter, this method performs a flush. *

    *

    * This method uses {@link OutputStreamWriter}. *

    * - * @param reader the {@link Reader} to read - * @param output the {@link OutputStream} to write to - * @param outputCharset the charset to use for the OutputStream, null means platform default - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param output the {@link OutputStream} to write to. + * @param outputCharset the charset to use for the OutputStream, null means platform default. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ - public static void copy(final Reader reader, final OutputStream output, final Charset outputCharset) - throws IOException { + public static void copy(final Reader reader, final OutputStream output, final Charset outputCharset) throws IOException { final OutputStreamWriter writer = new OutputStreamWriter(output, Charsets.toCharset(outputCharset)); copy(reader, writer); // XXX Unless anyone is planning on rewriting OutputStreamWriter, @@ -1299,35 +1591,29 @@ public static void copy(final Reader reader, final OutputStream output, final Ch } /** - * Copies chars from a {@link Reader} to bytes on an - * {@link OutputStream} using the specified character encoding, and - * calling flush. + * Copies chars from a {@link Reader} to bytes on an {@link OutputStream} using the specified character encoding, and calling flush. *

    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedReader}. + * This method buffers the input internally, so there is no need to use a {@link BufferedReader}. *

    *

    - * Character encoding names can be found at - * IANA. + * Character encoding names can be found at IANA. *

    *

    - * Due to the implementation of OutputStreamWriter, this method performs a - * flush. + * Due to the implementation of OutputStreamWriter, this method performs a flush. *

    *

    * This method uses {@link OutputStreamWriter}. *

    * - * @param reader the {@link Reader} to read - * @param output the {@link OutputStream} to write to - * @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param reader the {@link Reader} to read. + * @param output the {@link OutputStream} to write to. + * @param outputCharsetName the name of the requested charset for the OutputStream, null means platform default. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ - public static void copy(final Reader reader, final OutputStream output, final String outputCharsetName) - throws IOException { + public static void copy(final Reader reader, final OutputStream output, final String outputCharsetName) throws IOException { copy(reader, output, Charsets.toCharset(outputCharsetName)); } @@ -1346,9 +1632,9 @@ public static void copy(final Reader reader, final OutputStream output, final St * * @param reader the {@link Reader} to read. * @param writer the {@link Writer} to write. - * @return the number of characters copied, or -1 if > Integer.MAX_VALUE - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @return the number of characters copied, or -1 if > Integer.MAX_VALUE. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ public static int copy(final Reader reader, final Writer writer) throws IOException { @@ -1483,9 +1769,10 @@ public static long copyLarge(final InputStream inputStream, final OutputStream o * @throws IOException if an I/O error occurs. * @since 2.2 */ - public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, - final long length) throws IOException { - return copyLarge(input, output, inputOffset, length, getScratchByteArray()); + public static long copyLarge(final InputStream input, final OutputStream output, final long inputOffset, final long length) throws IOException { + try (ScratchBytes scratch = ScratchBytes.get()) { + return copyLarge(input, output, inputOffset, length, scratch.array()); + } } /** @@ -1549,13 +1836,15 @@ public static long copyLarge(final InputStream input, final OutputStream output, * * @param reader the {@link Reader} to source. * @param writer the {@link Writer} to target. - * @return the number of characters copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @return the number of characters copied. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 1.3 */ public static long copyLarge(final Reader reader, final Writer writer) throws IOException { - return copyLarge(reader, writer, getScratchCharArray()); + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + return copyLarge(reader, writer, scratch.array()); + } } /** @@ -1584,53 +1873,46 @@ public static long copyLarge(final Reader reader, final Writer writer, final cha } /** - * Copies some or all chars from a large (over 2GB) {@link InputStream} to an - * {@link OutputStream}, optionally skipping input chars. + * Copies some or all chars from a large (over 2GB) {@link InputStream} to an {@link OutputStream}, optionally skipping input chars. *

    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedReader}. + * This method buffers the input internally, so there is no need to use a {@link BufferedReader}. *

    *

    * The buffer size is given by {@link #DEFAULT_BUFFER_SIZE}. *

    * - * @param reader the {@link Reader} to read - * @param writer the {@link Writer} to write to - * @param inputOffset number of chars to skip from input before copying - * -ve values are ignored - * @param length number of chars to copy. -ve means all - * @return the number of chars copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param writer the {@link Writer} to write to. + * @param inputOffset number of chars to skip from input before copying -ve values are ignored. + * @param length number of chars to copy. -ve means all. + * @return the number of chars copied. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 2.2 */ - public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length) - throws IOException { - return copyLarge(reader, writer, inputOffset, length, getScratchCharArray()); + public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length) throws IOException { + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + return copyLarge(reader, writer, inputOffset, length, scratch.array()); + } } /** - * Copies some or all chars from a large (over 2GB) {@link InputStream} to an - * {@link OutputStream}, optionally skipping input chars. + * Copies some or all chars from a large (over 2GB) {@link InputStream} to an {@link OutputStream}, optionally skipping input chars. *

    - * This method uses the provided buffer, so there is no need to use a - * {@link BufferedReader}. + * This method uses the provided buffer, so there is no need to use a {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @param writer the {@link Writer} to write to - * @param inputOffset number of chars to skip from input before copying - * -ve values are ignored - * @param length number of chars to copy. -ve means all - * @param buffer the buffer to be used for the copy - * @return the number of chars copied - * @throws NullPointerException if the input or output is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param writer the {@link Writer} to write to. + * @param inputOffset number of chars to skip from input before copying -ve values are ignored. + * @param length number of chars to copy. -ve means all. + * @param buffer the buffer to be used for the copy. + * @return the number of chars copied. + * @throws NullPointerException if the input or output is null. + * @throws IOException if an I/O error occurs. * @since 2.2 */ - public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length, - final char[] buffer) - throws IOException { + public static long copyLarge(final Reader reader, final Writer writer, final long inputOffset, final long length, final char[] buffer) throws IOException { if (inputOffset > 0) { skipFully(reader, inputOffset); } @@ -1655,67 +1937,33 @@ public static long copyLarge(final Reader reader, final Writer writer, final lon } /** - * Fills the given array with 0s. - * - * @param arr The non-null array to fill. - * @return The given array. - */ - private static byte[] fill0(final byte[] arr) { - Arrays.fill(arr, (byte) 0); - return arr; - } - - /** - * Fills the given array with 0s. - * - * @param arr The non-null array to fill. - * @return The given array. - */ - private static char[] fill0(final char[] arr) { - Arrays.fill(arr, (char) 0); - return arr; - } - - /** - * Gets the internal byte array buffer, intended for both reading and writing. - * - * @return the internal byte array buffer, intended for both reading and writing. - */ - static byte[] getScratchByteArray() { - return fill0(SCRATCH_BYTE_BUFFER_RW.get()); - } - - /** - * Gets the internal byte array intended for write only operations. - * - * @return the internal byte array intended for write only operations. - */ - static byte[] getScratchByteArrayWriteOnly() { - return fill0(SCRATCH_BYTE_BUFFER_WO); - } - - /** - * Gets the char byte array buffer, intended for both reading and writing. + * Copies up to {@code size} bytes from the given {@link InputStream} into a new {@link UnsynchronizedByteArrayOutputStream}. * - * @return the char byte array buffer, intended for both reading and writing. + * @param input The {@link InputStream} to read; must not be {@code null}. + * @param limit The maximum number of bytes to read; must be {@code >= 0}. + * The actual bytes read are validated to equal {@code size}. + * @param bufferSize The buffer size of the output stream; must be {@code > 0}. + * @return a ByteArrayOutputStream containing the read bytes. */ - static char[] getScratchCharArray() { - return fill0(SCRATCH_CHAR_BUFFER_RW.get()); - } - - /** - * Gets the internal char array intended for write only operations. - * - * @return the internal char array intended for write only operations. - */ - static char[] getScratchCharArrayWriteOnly() { - return fill0(SCRATCH_CHAR_BUFFER_WO); + static UnsynchronizedByteArrayOutputStream copyToOutputStream( + final InputStream input, final long limit, final int bufferSize) throws IOException { + try (UnsynchronizedByteArrayOutputStream output = UnsynchronizedByteArrayOutputStream.builder() + .setBufferSize(bufferSize) + .get(); + InputStream boundedInput = BoundedInputStream.builder() + .setMaxCount(limit) + .setPropagateClose(false) + .setInputStream(input) + .get()) { + output.write(boundedInput); + return output; + } } /** * Returns the length of the given array in a null-safe manner. * - * @param array an array or null + * @param array an array or null. * @return the array length, or 0 if the given array is null. * @since 2.7 */ @@ -1726,7 +1974,7 @@ public static int length(final byte[] array) { /** * Returns the length of the given array in a null-safe manner. * - * @param array an array or null + * @param array an array or null. * @return the array length, or 0 if the given array is null. * @since 2.7 */ @@ -1737,7 +1985,7 @@ public static int length(final char[] array) { /** * Returns the length of the given CharSequence in a null-safe manner. * - * @param csq a CharSequence or null + * @param csq a CharSequence or null. * @return the CharSequence length, or 0 if the given CharSequence is null. * @since 2.7 */ @@ -1748,7 +1996,7 @@ public static int length(final CharSequence csq) { /** * Returns the length of the given array in a null-safe manner. * - * @param array an array or null + * @param array an array or null. * @return the array length, or 0 if the given array is null. * @since 2.7 */ @@ -1781,10 +2029,10 @@ public static int length(final Object[] array) { * } * * - * @param input the {@link InputStream} to read, not null - * @param charset the charset to use, null means platform default - * @return an Iterator of the lines in the reader, never null - * @throws IllegalArgumentException if the input is null + * @param input the {@link InputStream} to read, not null. + * @param charset the charset to use, null means platform default. + * @return an Iterator of the lines in the reader, never null. + * @throws IllegalArgumentException if the input is null. * @since 2.3 */ public static LineIterator lineIterator(final InputStream input, final Charset charset) { @@ -1816,11 +2064,11 @@ public static LineIterator lineIterator(final InputStream input, final Charset c * } * * - * @param input the {@link InputStream} to read, not null - * @param charsetName the encoding to use, null means platform default - * @return an Iterator of the lines in the reader, never null - * @throws IllegalArgumentException if the input is null - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param input the {@link InputStream} to read, not null. + * @param charsetName the encoding to use, null means platform default. + * @return an Iterator of the lines in the reader, never null. + * @throws IllegalArgumentException if the input is null. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.2 */ public static LineIterator lineIterator(final InputStream input, final String charsetName) { @@ -1851,9 +2099,9 @@ public static LineIterator lineIterator(final InputStream input, final String ch * } * * - * @param reader the {@link Reader} to read, not null - * @return an Iterator of the lines in the reader, never null - * @throws NullPointerException if the reader is null + * @param reader the {@link Reader} to read, not null. + * @return an Iterator of the lines in the reader, never null. + * @throws NullPointerException if the reader is null. * @since 1.2 */ public static LineIterator lineIterator(final Reader reader) { @@ -1862,14 +2110,17 @@ public static LineIterator lineIterator(final Reader reader) { /** * Reads bytes from an input stream. + *

    * This implementation guarantees that it will read as many bytes * as possible before giving up; this may not always be the case for * subclasses of {@link InputStream}. + *

    * - * @param input where to read input from - * @param buffer destination - * @return actual length read; may be less than requested if EOF was reached - * @throws IOException if a read error occurs + * @param input where to read input from. + * @param buffer destination. + * @return actual length read; may be less than requested if EOF was reached. + * @throws NullPointerException if {@code input} or {@code buffer} is null. + * @throws IOException if a read error occurs. * @since 2.2 */ public static int read(final InputStream input, final byte[] buffer) throws IOException { @@ -1878,49 +2129,30 @@ public static int read(final InputStream input, final byte[] buffer) throws IOEx /** * Reads bytes from an input stream. + *

    * This implementation guarantees that it will read as many bytes * as possible before giving up; this may not always be the case for * subclasses of {@link InputStream}. + *

    * - * @param input where to read input - * @param buffer destination - * @param offset initial offset into buffer - * @param length length to read, must be >= 0 - * @return actual length read; may be less than requested if EOF was reached - * @throws IllegalArgumentException if length is negative - * @throws IOException if a read error occurs + * @param input where to read input. + * @param buffer destination. + * @param offset initial offset into buffer. + * @param length length to read, must be >= 0. + * @return actual length read; may be less than requested if EOF was reached. + * @throws NullPointerException if {@code input} or {@code buffer} is null. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} is negative, or if + * {@code offset + length} is greater than {@code buffer.length}. + * @throws IOException if a read error occurs. * @since 2.2 */ public static int read(final InputStream input, final byte[] buffer, final int offset, final int length) throws IOException { - if (length == 0) { - return 0; - } - return read(input::read, buffer, offset, length); - } - - /** - * Reads bytes from an input. This implementation guarantees that it will read as many bytes as possible before giving up; this may not always be the case - * for subclasses of {@link InputStream}. - * - * @param input How to read input - * @param buffer destination - * @param offset initial offset into buffer - * @param length length to read, must be >= 0 - * @return actual length read; may be less than requested if EOF was reached - * @throws IllegalArgumentException if length is negative - * @throws IOException if a read error occurs - * @since 2.2 - */ - static int read(final IOTriFunction input, final byte[] buffer, final int offset, final int length) - throws IOException { - if (length < 0) { - throw new IllegalArgumentException("Length must not be negative: " + length); - } + checkFromIndexSize(buffer, offset, length); int remaining = length; while (remaining > 0) { final int location = length - remaining; - final int count = input.apply(buffer, offset + location, remaining); + final int count = input.read(buffer, offset + location, remaining); if (EOF == count) { break; } @@ -1937,10 +2169,10 @@ static int read(final IOTriFunction input, fi * subclasses of {@link ReadableByteChannel}. *

    * - * @param input the byte channel to read - * @param buffer byte buffer destination - * @return the actual length read; may be less than requested if EOF was reached - * @throws IOException if a read error occurs + * @param input the byte channel to read. + * @param buffer byte buffer destination. + * @return the actual length read; may be less than requested if EOF was reached. + * @throws IOException if a read error occurs. * @since 2.5 */ public static int read(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException { @@ -1956,14 +2188,16 @@ public static int read(final ReadableByteChannel input, final ByteBuffer buffer) /** * Reads characters from an input character stream. + *

    * This implementation guarantees that it will read as many characters * as possible before giving up; this may not always be the case for * subclasses of {@link Reader}. + *

    * - * @param reader where to read input from - * @param buffer destination - * @return actual length read; may be less than requested if EOF was reached - * @throws IOException if a read error occurs + * @param reader where to read input from. + * @param buffer destination. + * @return actual length read; may be less than requested if EOF was reached. + * @throws IOException if a read error occurs. * @since 2.2 */ public static int read(final Reader reader, final char[] buffer) throws IOException { @@ -1972,24 +2206,26 @@ public static int read(final Reader reader, final char[] buffer) throws IOExcept /** * Reads characters from an input character stream. + *

    * This implementation guarantees that it will read as many characters * as possible before giving up; this may not always be the case for * subclasses of {@link Reader}. + *

    * - * @param reader where to read input from - * @param buffer destination - * @param offset initial offset into buffer - * @param length length to read, must be >= 0 - * @return actual length read; may be less than requested if EOF was reached - * @throws IllegalArgumentException if length is negative - * @throws IOException if a read error occurs + * @param reader where to read input from. + * @param buffer destination. + * @param offset initial offset into buffer. + * @param length length to read, must be >= 0. + * @return actual length read; may be less than requested if EOF was reached. + * @throws NullPointerException if {@code reader} or {@code buffer} is null. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} is negative, or if + * {@code offset + length} is greater than {@code buffer.length}. + * @throws IOException if a read error occurs. * @since 2.2 */ public static int read(final Reader reader, final char[] buffer, final int offset, final int length) throws IOException { - if (length < 0) { - throw new IllegalArgumentException("Length must not be negative: " + length); - } + checkFromIndexSize(buffer, offset, length); int remaining = length; while (remaining > 0) { final int location = length - remaining; @@ -2009,11 +2245,11 @@ public static int read(final Reader reader, final char[] buffer, final int offse * not read as many bytes as requested (most likely because of reaching EOF). *

    * - * @param input where to read input from - * @param buffer destination - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if length is negative - * @throws EOFException if the number of bytes read was incorrect + * @param input where to read input from. + * @param buffer destination. + * @throws NullPointerException if {@code input} or {@code buffer} is null. + * @throws EOFException if the number of bytes read was incorrect. + * @throws IOException if there is a problem reading the file. * @since 2.2 */ public static void readFully(final InputStream input, final byte[] buffer) throws IOException { @@ -2027,13 +2263,15 @@ public static void readFully(final InputStream input, final byte[] buffer) throw * not read as many bytes as requested (most likely because of reaching EOF). *

    * - * @param input where to read input from - * @param buffer destination - * @param offset initial offset into buffer - * @param length length to read, must be >= 0 - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if length is negative - * @throws EOFException if the number of bytes read was incorrect + * @param input where to read input from. + * @param buffer destination. + * @param offset initial offset into buffer. + * @param length length to read, must be >= 0. + * @throws NullPointerException if {@code input} or {@code buffer} is null. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} is negative, or if + * {@code offset + length} is greater than {@code buffer.length}. + * @throws EOFException if the number of bytes read was incorrect. + * @throws IOException if there is a problem reading the file. * @since 2.2 */ public static void readFully(final InputStream input, final byte[] buffer, final int offset, final int length) @@ -2051,18 +2289,18 @@ public static void readFully(final InputStream input, final byte[] buffer, final * not read as many bytes as requested (most likely because of reaching EOF). *

    * - * @param input where to read input from - * @param length length to read, must be >= 0 - * @return the bytes read from input - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if length is negative - * @throws EOFException if the number of bytes read was incorrect + * @param input where to read input from. + * @param length length to read, must be >= 0. + * @return the bytes read from input. + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if length is negative. + * @throws EOFException if the number of bytes read was incorrect. * @since 2.5 + * @deprecated Use {@link #toByteArray(InputStream, int)}. */ + @Deprecated public static byte[] readFully(final InputStream input, final int length) throws IOException { - final byte[] buffer = byteArray(length); - readFully(input, buffer, 0, buffer.length); - return buffer; + return toByteArray(input, length); } /** @@ -2072,10 +2310,10 @@ public static byte[] readFully(final InputStream input, final int length) throws * not read as many bytes as requested (most likely because of reaching EOF). *

    * - * @param input the byte channel to read - * @param buffer byte buffer destination - * @throws IOException if there is a problem reading the file - * @throws EOFException if the number of bytes read was incorrect + * @param input the byte channel to read. + * @param buffer byte buffer destination. + * @throws IOException if there is a problem reading the file. + * @throws EOFException if the number of bytes read was incorrect. * @since 2.5 */ public static void readFully(final ReadableByteChannel input, final ByteBuffer buffer) throws IOException { @@ -2093,11 +2331,11 @@ public static void readFully(final ReadableByteChannel input, final ByteBuffer b * not read as many characters as requested (most likely because of reaching EOF). *

    * - * @param reader where to read input from - * @param buffer destination - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if length is negative - * @throws EOFException if the number of characters read was incorrect + * @param reader where to read input from. + * @param buffer destination. + * @throws NullPointerException if {@code reader} or {@code buffer} is null. + * @throws EOFException if the number of characters read was incorrect. + * @throws IOException if there is a problem reading the file. * @since 2.2 */ public static void readFully(final Reader reader, final char[] buffer) throws IOException { @@ -2111,13 +2349,15 @@ public static void readFully(final Reader reader, final char[] buffer) throws IO * not read as many characters as requested (most likely because of reaching EOF). *

    * - * @param reader where to read input from - * @param buffer destination - * @param offset initial offset into buffer - * @param length length to read, must be >= 0 - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if length is negative - * @throws EOFException if the number of characters read was incorrect + * @param reader where to read input from. + * @param buffer destination. + * @param offset initial offset into buffer. + * @param length length to read, must be >= 0. + * @throws NullPointerException if {@code reader} or {@code buffer} is null. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} is negative, or if + * {@code offset + length} is greater than {@code buffer.length}. + * @throws EOFException if the number of characters read was incorrect. + * @throws IOException if there is a problem reading the file. * @since 2.2 */ public static void readFully(final Reader reader, final char[] buffer, final int offset, final int length) @@ -2131,9 +2371,9 @@ public static void readFully(final Reader reader, final char[] buffer, final int /** * Gets the contents of a {@link CharSequence} as a list of Strings, one entry per line. * - * @param csq the {@link CharSequence} to read, not null - * @return the list of Strings, never null - * @throws UncheckedIOException if an I/O error occurs + * @param csq the {@link CharSequence} to read, not null. + * @return the list of Strings, never null. + * @throws UncheckedIOException if an I/O error occurs. * @since 2.18.0 */ public static List readLines(final CharSequence csq) throws UncheckedIOException { @@ -2144,16 +2384,16 @@ public static List readLines(final CharSequence csq) throws UncheckedIOE /** * Gets the contents of an {@link InputStream} as a list of Strings, - * one entry per line, using the virtual machine's {@link Charset#defaultCharset() default charset}. + * one entry per line, using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. *

    * - * @param input the {@link InputStream} to read, not null - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws UncheckedIOException if an I/O error occurs + * @param input the {@link InputStream} to read, not null. + * @return the list of Strings, never null. + * @throws NullPointerException if the input is null. + * @throws UncheckedIOException if an I/O error occurs. * @since 1.1 * @deprecated Use {@link #readLines(InputStream, Charset)} instead */ @@ -2170,11 +2410,11 @@ public static List readLines(final InputStream input) throws UncheckedIO * {@link BufferedInputStream}. *

    * - * @param input the {@link InputStream} to read, not null - * @param charset the charset to use, null means platform default - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws UncheckedIOException if an I/O error occurs + * @param input the {@link InputStream} to read, not null. + * @param charset the charset to use, null means platform default. + * @return the list of Strings, never null. + * @throws NullPointerException if the input is null. + * @throws UncheckedIOException if an I/O error occurs. * @since 2.3 */ public static List readLines(final InputStream input, final Charset charset) throws UncheckedIOException { @@ -2186,19 +2426,19 @@ public static List readLines(final InputStream input, final Charset char * one entry per line, using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. *

    * - * @param input the {@link InputStream} to read, not null - * @param charsetName the name of the requested charset, null means platform default - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws UncheckedIOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param input the {@link InputStream} to read, not null. + * @param charsetName the name of the requested charset, null means platform default. + * @return the list of Strings, never null. + * @throws NullPointerException if the input is null. + * @throws UncheckedIOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static List readLines(final InputStream input, final String charsetName) throws UncheckedIOException { @@ -2213,10 +2453,10 @@ public static List readLines(final InputStream input, final String chars * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read, not null - * @return the list of Strings, never null - * @throws NullPointerException if the input is null - * @throws UncheckedIOException if an I/O error occurs + * @param reader the {@link Reader} to read, not null. + * @return the list of Strings, never null. + * @throws NullPointerException if the input is null. + * @throws UncheckedIOException if an I/O error occurs. * @since 1.1 */ @SuppressWarnings("resource") // reader wraps input and is the responsibility of the caller. @@ -2231,7 +2471,7 @@ public static List readLines(final Reader reader) throws UncheckedIOExce *

    * * @param name The resource name. - * @return the requested byte array + * @return the requested byte array. * @throws IOException if an I/O error occurs or the resource is not found. * @see #resourceToByteArray(String, ClassLoader) * @since 2.6 @@ -2247,8 +2487,8 @@ public static byte[] resourceToByteArray(final String name) throws IOException { *

    * * @param name The resource name. - * @param classLoader the class loader that the resolution of the resource is delegated to - * @return the requested byte array + * @param classLoader the class loader that the resolution of the resource is delegated to. + * @return the requested byte array. * @throws IOException if an I/O error occurs or the resource is not found. * @see #resourceToURL(String, ClassLoader) * @since 2.6 @@ -2264,8 +2504,8 @@ public static byte[] resourceToByteArray(final String name, final ClassLoader cl *

    * * @param name The resource name. - * @param charset the charset to use, null means platform default - * @return the requested String + * @param charset the charset to use, null means platform default. + * @return the requested String. * @throws IOException if an I/O error occurs or the resource is not found. * @see #resourceToString(String, Charset, ClassLoader) * @since 2.6 @@ -2281,9 +2521,9 @@ public static String resourceToString(final String name, final Charset charset) *

    * * @param name The resource name. - * @param charset the Charset to use, null means platform default - * @param classLoader the class loader that the resolution of the resource is delegated to - * @return the requested String + * @param charset the Charset to use, null means platform default. + * @param classLoader the class loader that the resolution of the resource is delegated to. + * @return the requested String. * @throws IOException if an I/O error occurs. * @see #resourceToURL(String, ClassLoader) * @since 2.6 @@ -2315,7 +2555,7 @@ public static URL resourceToURL(final String name) throws IOException { *

    * * @param name The resource name. - * @param classLoader Delegate to this class loader if not null + * @param classLoader Delegate to this class loader if not null. * @return A URL object for reading the resource. * @throws IOException if the resource is not found. * @since 2.6 @@ -2332,27 +2572,29 @@ public static URL resourceToURL(final String name, final ClassLoader classLoader /** * Skips bytes from an input byte stream. - * This implementation guarantees that it will read as many bytes - * as possible before giving up; this may not always be the case for - * skip() implementations in subclasses of {@link InputStream}. *

    - * Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather - * than delegating to {@link InputStream#skip(long)}. - * This means that the method may be considerably less efficient than using the actual skip implementation, - * this is done to guarantee that the correct number of bytes are skipped. + * This implementation guarantees that it will read as many bytes as possible before giving up; this may not always be the case for skip() implementations + * in subclasses of {@link InputStream}. + *

    + *

    + * Note that the implementation uses {@link InputStream#read(byte[], int, int)} rather than delegating to {@link InputStream#skip(long)}. This means that + * the method may be considerably less efficient than using the actual skip implementation, this is done to guarantee that the correct number of bytes are + * skipped. *

    * - * @param input byte stream to skip - * @param skip number of bytes to skip. + * @param input byte stream to skip. + * @param skip number of bytes to skip. * @return number of bytes actually skipped. - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if toSkip is negative + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if toSkip is negative. * @see InputStream#skip(long) * @see IO-203 - Add skipFully() method for InputStreams * @since 2.0 */ public static long skip(final InputStream input, final long skip) throws IOException { - return skip(input, skip, IOUtils::getScratchByteArrayWriteOnly); + try (ScratchBytes scratch = ScratchBytes.get()) { + return skip(input, skip, scratch::array); + } } /** @@ -2371,12 +2613,12 @@ public static long skip(final InputStream input, final long skip) throws IOExcep * skipped. *

    * - * @param input byte stream to skip + * @param input byte stream to skip. * @param skip number of bytes to skip. * @param skipBufferSupplier Supplies the buffer to use for reading. * @return number of bytes actually skipped. - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if toSkip is negative + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if toSkip is negative. * @see InputStream#skip(long) * @see IO-203 - Add skipFully() method for InputStreams * @since 2.14.0 @@ -2408,11 +2650,11 @@ public static long skip(final InputStream input, final long skip, final Supplier * This implementation guarantees that it will read as many bytes * as possible before giving up. * - * @param input ReadableByteChannel to skip + * @param input ReadableByteChannel to skip. * @param toSkip number of bytes to skip. * @return number of bytes actually skipped. - * @throws IOException if there is a problem reading the ReadableByteChannel - * @throws IllegalArgumentException if toSkip is negative + * @throws IOException if there is a problem reading the ReadableByteChannel. + * @throws IllegalArgumentException if toSkip is negative. * @since 2.5 */ public static long skip(final ReadableByteChannel input, final long toSkip) throws IOException { @@ -2445,11 +2687,11 @@ public static long skip(final ReadableByteChannel input, final long toSkip) thro * this is done to guarantee that the correct number of characters are skipped. *

    * - * @param reader character stream to skip + * @param reader character stream to skip. * @param toSkip number of characters to skip. * @return number of characters actually skipped. - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if toSkip is negative + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if toSkip is negative. * @see Reader#skip(long) * @see IO-203 - Add skipFully() method for InputStreams * @since 2.0 @@ -2459,14 +2701,16 @@ public static long skip(final Reader reader, final long toSkip) throws IOExcepti throw new IllegalArgumentException("Skip count must be non-negative, actual: " + toSkip); } long remain = toSkip; - while (remain > 0) { - // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() - final char[] charArray = getScratchCharArrayWriteOnly(); - final long n = reader.read(charArray, 0, (int) Math.min(remain, charArray.length)); - if (n < 0) { // EOF - break; + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + final char[] chars = scratch.array(); + while (remain > 0) { + // See https://issues.apache.org/jira/browse/IO-203 for why we use read() rather than delegating to skip() + final long n = reader.read(chars, 0, (int) Math.min(remain, chars.length)); + if (n < 0) { // EOF + break; + } + remain -= n; } - remain -= n; } return toSkip - remain; } @@ -2483,16 +2727,16 @@ public static long skip(final Reader reader, final long toSkip) throws IOExcepti * this is done to guarantee that the correct number of characters are skipped. *

    * - * @param input stream to skip - * @param toSkip the number of bytes to skip - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if toSkip is negative - * @throws EOFException if the number of bytes skipped was incorrect + * @param input stream to skip. + * @param toSkip the number of bytes to skip. + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if toSkip is negative. + * @throws EOFException if the number of bytes skipped was incorrect. * @see InputStream#skip(long) * @since 2.0 */ public static void skipFully(final InputStream input, final long toSkip) throws IOException { - final long skipped = skip(input, toSkip, IOUtils::getScratchByteArrayWriteOnly); + final long skipped = skip(input, toSkip); if (skipped != toSkip) { throw new EOFException("Bytes to skip: " + toSkip + " actual: " + skipped); } @@ -2512,12 +2756,12 @@ public static void skipFully(final InputStream input, final long toSkip) throws * skip implementation, this is done to guarantee that the correct number of characters are skipped. *

    * - * @param input stream to skip - * @param toSkip the number of bytes to skip + * @param input stream to skip. + * @param toSkip the number of bytes to skip. * @param skipBufferSupplier Supplies the buffer to use for reading. - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if toSkip is negative - * @throws EOFException if the number of bytes skipped was incorrect + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if toSkip is negative. + * @throws EOFException if the number of bytes skipped was incorrect. * @see InputStream#skip(long) * @since 2.14.0 */ @@ -2534,11 +2778,11 @@ public static void skipFully(final InputStream input, final long toSkip, final S /** * Skips the requested number of bytes or fail if there are not enough left. * - * @param input ReadableByteChannel to skip - * @param toSkip the number of bytes to skip - * @throws IOException if there is a problem reading the ReadableByteChannel - * @throws IllegalArgumentException if toSkip is negative - * @throws EOFException if the number of bytes skipped was incorrect + * @param input ReadableByteChannel to skip. + * @param toSkip the number of bytes to skip. + * @throws IOException if there is a problem reading the ReadableByteChannel. + * @throws IllegalArgumentException if toSkip is negative. + * @throws EOFException if the number of bytes skipped was incorrect. * @since 2.5 */ public static void skipFully(final ReadableByteChannel input, final long toSkip) throws IOException { @@ -2563,11 +2807,11 @@ public static void skipFully(final ReadableByteChannel input, final long toSkip) * this is done to guarantee that the correct number of characters are skipped. *

    * - * @param reader stream to skip - * @param toSkip the number of characters to skip - * @throws IOException if there is a problem reading the file - * @throws IllegalArgumentException if toSkip is negative - * @throws EOFException if the number of characters skipped was incorrect + * @param reader stream to skip. + * @param toSkip the number of characters to skip. + * @throws IOException if there is a problem reading the file. + * @throws IllegalArgumentException if toSkip is negative. + * @throws EOFException if the number of characters skipped was incorrect. * @see Reader#skip(long) * @since 2.0 */ @@ -2579,22 +2823,18 @@ public static void skipFully(final Reader reader, final long toSkip) throws IOEx } /** - * Fetches entire contents of an {@link InputStream} and represent - * same data as result InputStream. + * Fetches entire contents of an {@link InputStream} and represent same data as result InputStream. *

    * This method is useful where, *

    *
      *
    • Source InputStream is slow.
    • - *
    • It has network resources associated, so we cannot keep it open for - * long time.
    • + *
    • It has network resources associated, so we cannot keep it open for long time.
    • *
    • It has network timeout associated.
    • *
    *

    - * It can be used in favor of {@link #toByteArray(InputStream)}, since it - * avoids unnecessary allocation and copy of byte[].
    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedInputStream}. + * It can be used in favor of {@link #toByteArray(InputStream)}, since it avoids unnecessary allocation and copy of byte[].
    + * This method buffers the input internally, so there is no need to use a {@link BufferedInputStream}. *

    * * @param input Stream to be fully buffered. @@ -2607,26 +2847,22 @@ public static InputStream toBufferedInputStream(final InputStream input) throws } /** - * Fetches entire contents of an {@link InputStream} and represent - * same data as result InputStream. + * Fetches entire contents of an {@link InputStream} and represent same data as result InputStream. *

    * This method is useful where, *

    *
      *
    • Source InputStream is slow.
    • - *
    • It has network resources associated, so we cannot keep it open for - * long time.
    • + *
    • It has network resources associated, so we cannot keep it open for long time.
    • *
    • It has network timeout associated.
    • *
    *

    - * It can be used in favor of {@link #toByteArray(InputStream)}, since it - * avoids unnecessary allocation and copy of byte[].
    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedInputStream}. + * It can be used in favor of {@link #toByteArray(InputStream)}, since it avoids unnecessary allocation and copy of byte[].
    + * This method buffers the input internally, so there is no need to use a {@link BufferedInputStream}. *

    * * @param input Stream to be fully buffered. - * @param size the initial buffer size + * @param size the initial buffer size. * @return A fully buffered stream. * @throws IOException if an I/O error occurs. * @since 2.5 @@ -2639,9 +2875,9 @@ public static InputStream toBufferedInputStream(final InputStream input, final i * Returns the given reader if it is a {@link BufferedReader}, otherwise creates a BufferedReader from the given * reader. * - * @param reader the reader to wrap or return (not null) - * @return the given reader or a new {@link BufferedReader} for the given reader - * @throws NullPointerException if the input parameter is null + * @param reader the reader to wrap or return (not null). + * @return the given reader or a new {@link BufferedReader} for the given reader. + * @throws NullPointerException if the input parameter is null. * @see #buffer(Reader) * @since 2.2 */ @@ -2653,10 +2889,10 @@ public static BufferedReader toBufferedReader(final Reader reader) { * Returns the given reader if it is a {@link BufferedReader}, otherwise creates a BufferedReader from the given * reader. * - * @param reader the reader to wrap or return (not null) + * @param reader the reader to wrap or return (not null). * @param size the buffer size, if a new BufferedReader is created. - * @return the given reader or a new {@link BufferedReader} for the given reader - * @throws NullPointerException if the input parameter is null + * @return the given reader or a new {@link BufferedReader} for the given reader. + * @throws NullPointerException if the input parameter is null. * @see #buffer(Reader) * @since 2.5 */ @@ -2665,65 +2901,106 @@ public static BufferedReader toBufferedReader(final Reader reader, final int siz } /** - * Gets the contents of an {@link InputStream} as a {@code byte[]}. - *

    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedInputStream}. - *

    + * Reads all the bytes from an input stream in a byte array. * - * @param inputStream the {@link InputStream} to read. - * @return the requested byte array. - * @throws NullPointerException if the InputStream is {@code null}. - * @throws IOException if an I/O error occurs or reading more than {@link Integer#MAX_VALUE} occurs. + *

    The memory used by this method is proportional to the number + * of bytes read, which is only limited by {@link Integer#MAX_VALUE}. Only streams + * which fit into a single byte array with roughly 2 GiB limit can be processed + * with this method.

    + * + * @param inputStream The {@link InputStream} to read; must not be {@code null}. + * @return A new byte array. + * @throws IOException If an I/O error occurs while reading or if the maximum array size is exceeded. + * @throws NullPointerException If {@code inputStream} is {@code null}. */ public static byte[] toByteArray(final InputStream inputStream) throws IOException { - // We use a ThresholdingOutputStream to avoid reading AND writing more than Integer.MAX_VALUE. - try (UnsynchronizedByteArrayOutputStream ubaOutput = UnsynchronizedByteArrayOutputStream.builder().get(); - ThresholdingOutputStream thresholdOutput = new ThresholdingOutputStream(Integer.MAX_VALUE, os -> { - throw new IllegalArgumentException(String.format("Cannot read more than %,d into a byte array", Integer.MAX_VALUE)); - }, os -> ubaOutput)) { - copy(inputStream, thresholdOutput); - return ubaOutput.toByteArray(); + // Using SOFT_MAX_ARRAY_LENGTH guarantees that size() will not overflow + final UnsynchronizedByteArrayOutputStream output = copyToOutputStream(inputStream, SOFT_MAX_ARRAY_LENGTH + 1, DEFAULT_BUFFER_SIZE); + if (output.size() > SOFT_MAX_ARRAY_LENGTH) { + throw new IOException(String.format("Cannot read more than %,d into a byte array", SOFT_MAX_ARRAY_LENGTH)); } + return output.toByteArray(); } /** - * Gets the contents of an {@link InputStream} as a {@code byte[]}. Use this method instead of - * {@link #toByteArray(InputStream)} when {@link InputStream} size is known. + * Reads exactly {@code size} bytes from the given {@link InputStream} into a new {@code byte[]}. * - * @param input the {@link InputStream} to read. - * @param size the size of {@link InputStream} to read, where 0 < {@code size} <= length of input stream. - * @return byte [] of length {@code size}. - * @throws IOException if an I/O error occurs or {@link InputStream} length is smaller than parameter {@code size}. - * @throws IllegalArgumentException if {@code size} is less than zero. + *

    This variant always allocates the whole requested array size, + * for a dynamic growing variant use {@link #toByteArray(InputStream, int, int)}, + * which enforces stricter memory usage constraints.

    + * + * @param input the {@link InputStream} to read; must not be {@code null}. + * @param size the exact number of bytes to read; must be {@code >= 0}. + * @return a new byte array of length {@code size}. + * @throws IllegalArgumentException if {@code size} is negative. + * @throws EOFException if the stream ends before {@code size} bytes are read. + * @throws IOException if an I/O error occurs while reading. + * @throws NullPointerException if {@code input} is {@code null}. * @since 2.1 */ public static byte[] toByteArray(final InputStream input, final int size) throws IOException { - if (size == 0) { - return EMPTY_BYTE_ARRAY; - } return toByteArray(Objects.requireNonNull(input, "input")::read, size); } /** - * Gets contents of an {@link InputStream} as a {@code byte[]}. - * Use this method instead of {@link #toByteArray(InputStream)} - * when {@link InputStream} size is known. - * NOTE: the method checks that the length can safely be cast to an int without truncation - * before using {@link IOUtils#toByteArray(InputStream, int)} to read into the byte array. - * (Arrays can have no more than Integer.MAX_VALUE entries anyway) + * Reads exactly {@code size} bytes from the given {@link InputStream} into a new {@code byte[]}. * - * @param input the {@link InputStream} to read - * @param size the size of {@link InputStream} to read, where 0 < {@code size} <= min(Integer.MAX_VALUE, length of input stream). - * @return byte [] the requested byte array, of length {@code size} - * @throws IOException if an I/O error occurs or {@link InputStream} length is less than {@code size} - * @throws IllegalArgumentException if size is less than zero or size is greater than Integer.MAX_VALUE - * @see IOUtils#toByteArray(InputStream, int) + *

    The memory used by this method is proportional to the number + * of bytes read and limited by the specified {@code size}. This makes it suitable for + * processing large input streams, provided that sufficient heap space is + * available.

    + * + *

    This method processes the input stream in successive chunks of up to + * {@code chunkSize} bytes.

    + * + * @param input the {@link InputStream} to read; must not be {@code null}. + * @param size the exact number of bytes to read; must be {@code >= 0}. + * The actual bytes read are validated to equal {@code size}. + * @param chunkSize The chunk size for incremental reading; must be {@code > 0}. + * @return a new byte array of length {@code size}. + * @throws IllegalArgumentException if {@code size} is negative or {@code chunkSize <= 0}. + * @throws EOFException if the stream ends before {@code size} bytes are read. + * @throws IOException if an I/O error occurs while reading. + * @throws NullPointerException if {@code input} is {@code null}. + * @since 2.21.0 + */ + public static byte[] toByteArray(final InputStream input, final int size, final int chunkSize) throws IOException { + Objects.requireNonNull(input, "input"); + if (chunkSize <= 0) { + throw new IllegalArgumentException(String.format("chunkSize <= 0, chunkSize = %,d", chunkSize)); + } + if (size <= chunkSize) { + // throws if size < 0 + return toByteArray(input::read, size); + } + final UnsynchronizedByteArrayOutputStream output = copyToOutputStream(input, size, chunkSize); + final int outSize = output.size(); + if (outSize != size) { + throw new EOFException(String.format("Expected read size: %,d, actual: %,d", size, outSize)); + } + return output.toByteArray(); + } + + /** + * Reads exactly {@code size} bytes from the given {@link InputStream} into a new {@code byte[]}. + * + *

    This variant always allocates the whole requested array size, + * for a dynamic growing variant use {@link #toByteArray(InputStream, int, int)}, + * which enforces stricter memory usage constraints.

    + * + * @param input the {@link InputStream} to read; must not be {@code null}. + * @param size the exact number of bytes to read; must be {@code >= 0} and {@code <= Integer.MAX_VALUE}. + * @return a new byte array of length {@code size}. + * @throws IllegalArgumentException if {@code size} is negative or does not fit into an int. + * @throws EOFException if the stream ends before {@code size} bytes are read. + * @throws IOException if an I/O error occurs while reading. + * @throws NullPointerException if {@code input} is {@code null}. + * @see #toByteArray(InputStream, int, int) * @since 2.1 */ public static byte[] toByteArray(final InputStream input, final long size) throws IOException { if (size > Integer.MAX_VALUE) { - throw new IllegalArgumentException("Size cannot be greater than Integer max value: " + size); + throw new IllegalArgumentException(String.format("size > Integer.MAX_VALUE, size = %,d", size)); } return toByteArray(input, (int) size); } @@ -2731,50 +3008,45 @@ public static byte[] toByteArray(final InputStream input, final long size) throw /** * Gets the contents of an input as a {@code byte[]}. * - * @param input the input to read. + * @param input the input to read, not null. * @param size the size of the input to read, where 0 < {@code size} <= length of input. * @return byte [] of length {@code size}. + * @throws EOFException if the end of the input is reached before reading {@code size} bytes. * @throws IOException if an I/O error occurs or input length is smaller than parameter {@code size}. * @throws IllegalArgumentException if {@code size} is less than zero. */ static byte[] toByteArray(final IOTriFunction input, final int size) throws IOException { - if (size < 0) { - throw new IllegalArgumentException("Size must be equal or greater than zero: " + size); + throw new IllegalArgumentException(String.format("size < 0, size = %,d", size)); } - if (size == 0) { return EMPTY_BYTE_ARRAY; } - final byte[] data = byteArray(size); int offset = 0; int read; - while (offset < size && (read = input.apply(data, offset, size - offset)) != EOF) { offset += read; } - if (offset != size) { - throw new IOException("Unexpected read size, current: " + offset + ", expected: " + size); + throw new EOFException(String.format("Expected read size: %,d, actual: %,d", size, offset)); } - return data; } /** * Gets the contents of a {@link Reader} as a {@code byte[]} - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @deprecated Use {@link #toByteArray(Reader, Charset)} instead + * @param reader the {@link Reader} to read. + * @return the requested byte array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. + * @deprecated Use {@link #toByteArray(Reader, Charset)} instead. */ @Deprecated public static byte[] toByteArray(final Reader reader) throws IOException { @@ -2789,11 +3061,11 @@ public static byte[] toByteArray(final Reader reader) throws IOException { * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @param charset the charset to use, null means platform default - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @param charset the charset to use, null means platform default. + * @return the requested byte array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ public static byte[] toByteArray(final Reader reader, final Charset charset) throws IOException { @@ -2808,19 +3080,19 @@ public static byte[] toByteArray(final Reader reader, final Charset charset) thr * using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @param charsetName the name of the requested charset, null means platform default - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param reader the {@link Reader} to read. + * @param charsetName the name of the requested charset, null means platform default. + * @return the requested byte array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static byte[] toByteArray(final Reader reader, final String charsetName) throws IOException { @@ -2829,15 +3101,15 @@ public static byte[] toByteArray(final Reader reader, final String charsetName) /** * Gets the contents of a {@link String} as a {@code byte[]} - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This is the same as {@link String#getBytes()}. *

    * - * @param input the {@link String} to convert - * @return the requested byte array - * @throws NullPointerException if the input is null - * @deprecated Use {@link String#getBytes()} instead + * @param input the {@link String} to convert. + * @return the requested byte array. + * @throws NullPointerException if the input is null. + * @deprecated Use {@link String#getBytes()} instead. */ @Deprecated public static byte[] toByteArray(final String input) { @@ -2848,10 +3120,10 @@ public static byte[] toByteArray(final String input) { /** * Gets the contents of a {@link URI} as a {@code byte[]}. * - * @param uri the {@link URI} to read - * @return the requested byte array - * @throws NullPointerException if the uri is null - * @throws IOException if an I/O exception occurs + * @param uri the {@link URI} to read. + * @return the requested byte array. + * @throws NullPointerException if the uri is null. + * @throws IOException if an I/O exception occurs. * @since 2.4 */ public static byte[] toByteArray(final URI uri) throws IOException { @@ -2861,10 +3133,10 @@ public static byte[] toByteArray(final URI uri) throws IOException { /** * Gets the contents of a {@link URL} as a {@code byte[]}. * - * @param url the {@link URL} to read - * @return the requested byte array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O exception occurs + * @param url the {@link URL} to read. + * @return the requested byte array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O exception occurs. * @since 2.4 */ public static byte[] toByteArray(final URL url) throws IOException { @@ -2890,16 +3162,16 @@ public static byte[] toByteArray(final URLConnection urlConnection) throws IOExc /** * Gets the contents of an {@link InputStream} as a character array - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. *

    * - * @param inputStream the {@link InputStream} to read - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param inputStream the {@link InputStream} to read. + * @return the requested character array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 1.1 * @deprecated Use {@link #toCharArray(InputStream, Charset)} instead */ @@ -2916,11 +3188,11 @@ public static char[] toCharArray(final InputStream inputStream) throws IOExcepti * {@link BufferedInputStream}. *

    * - * @param inputStream the {@link InputStream} to read - * @param charset the charset to use, null means platform default - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param inputStream the {@link InputStream} to read. + * @param charset the charset to use, null means platform default. + * @return the requested character array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ public static char[] toCharArray(final InputStream inputStream, final Charset charset) @@ -2935,19 +3207,19 @@ public static char[] toCharArray(final InputStream inputStream, final Charset ch * using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. *

    * - * @param inputStream the {@link InputStream} to read - * @param charsetName the name of the requested charset, null means platform default - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param inputStream the {@link InputStream} to read. + * @param charsetName the name of the requested charset, null means platform default. + * @return the requested character array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static char[] toCharArray(final InputStream inputStream, final String charsetName) throws IOException { @@ -2961,10 +3233,10 @@ public static char[] toCharArray(final InputStream inputStream, final String cha * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @return the requested character array - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @return the requested character array. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ public static char[] toCharArray(final Reader reader) throws IOException { @@ -2975,12 +3247,12 @@ public static char[] toCharArray(final Reader reader) throws IOException { /** * Converts the specified CharSequence to an input stream, encoded as bytes - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * - * @param input the CharSequence to convert - * @return an input stream + * @param input the CharSequence to convert. + * @return an input stream. * @since 2.0 - * @deprecated Use {@link #toInputStream(CharSequence, Charset)} instead + * @deprecated Use {@link #toInputStream(CharSequence, Charset)} instead. */ @Deprecated public static InputStream toInputStream(final CharSequence input) { @@ -2991,9 +3263,9 @@ public static InputStream toInputStream(final CharSequence input) { * Converts the specified CharSequence to an input stream, encoded as bytes * using the specified character encoding. * - * @param input the CharSequence to convert - * @param charset the charset to use, null means platform default - * @return an input stream + * @param input the CharSequence to convert. + * @param charset the charset to use, null means platform default. + * @return an input stream. * @since 2.3 */ public static InputStream toInputStream(final CharSequence input, final Charset charset) { @@ -3005,13 +3277,13 @@ public static InputStream toInputStream(final CharSequence input, final Charset * using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    * - * @param input the CharSequence to convert - * @param charsetName the name of the requested charset, null means platform default - * @return an input stream - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param input the CharSequence to convert. + * @param charsetName the name of the requested charset, null means platform default. + * @return an input stream. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 2.0 */ public static InputStream toInputStream(final CharSequence input, final String charsetName) { @@ -3020,12 +3292,12 @@ public static InputStream toInputStream(final CharSequence input, final String c /** * Converts the specified string to an input stream, encoded as bytes - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * - * @param input the string to convert - * @return an input stream + * @param input the string to convert. + * @return an input stream. * @since 1.1 - * @deprecated Use {@link #toInputStream(String, Charset)} instead + * @deprecated Use {@link #toInputStream(String, Charset)} instead. */ @Deprecated public static InputStream toInputStream(final String input) { @@ -3036,9 +3308,9 @@ public static InputStream toInputStream(final String input) { * Converts the specified string to an input stream, encoded as bytes * using the specified character encoding. * - * @param input the string to convert - * @param charset the charset to use, null means platform default - * @return an input stream + * @param input the string to convert. + * @param charset the charset to use, null means platform default. + * @return an input stream. * @since 2.3 */ public static InputStream toInputStream(final String input, final Charset charset) { @@ -3050,13 +3322,13 @@ public static InputStream toInputStream(final String input, final Charset charse * using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    * - * @param input the string to convert - * @param charsetName the name of the requested charset, null means platform default - * @return an input stream - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param input the string to convert. + * @param charsetName the name of the requested charset, null means platform default. + * @return an input stream. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static InputStream toInputStream(final String input, final String charsetName) { @@ -3065,12 +3337,12 @@ public static InputStream toInputStream(final String input, final String charset /** * Gets the contents of a {@code byte[]} as a String - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * - * @param input the byte array to read - * @return the requested String - * @throws NullPointerException if the input is null - * @deprecated Use {@link String#String(byte[])} instead + * @param input the byte array to read. + * @return the requested String. + * @throws NullPointerException if the input is null. + * @deprecated Use {@link String#String(byte[])} instead. */ @Deprecated public static String toString(final byte[] input) { @@ -3083,13 +3355,13 @@ public static String toString(final byte[] input) { * using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    * - * @param input the byte array to read - * @param charsetName the name of the requested charset, null means platform default - * @return the requested String - * @throws NullPointerException if the input is null + * @param input the byte array to read. + * @param charsetName the name of the requested charset, null means platform default. + * @return the requested String. + * @throws NullPointerException if the input is null. */ public static String toString(final byte[] input, final String charsetName) { return new String(input, Charsets.toCharset(charsetName)); @@ -3097,17 +3369,17 @@ public static String toString(final byte[] input, final String charsetName) { /** * Gets the contents of an {@link InputStream} as a String - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. *

    * - * @param input the {@link InputStream} to read - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @deprecated Use {@link #toString(InputStream, Charset)} instead + * @param input the {@link InputStream} to read. + * @return the requested String. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. + * @deprecated Use {@link #toString(InputStream, Charset)} instead. */ @Deprecated public static String toString(final InputStream input) throws IOException { @@ -3122,11 +3394,11 @@ public static String toString(final InputStream input) throws IOException { * {@link BufferedInputStream}. *

    * - * @param input the {@link InputStream} to read - * @param charset the charset to use, null means platform default - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param input the {@link InputStream} to read. + * @param charset the charset to use, null means platform default. + * @return the requested String. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ public static String toString(final InputStream input, final Charset charset) throws IOException { @@ -3141,19 +3413,19 @@ public static String toString(final InputStream input, final Charset charset) th * using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    *

    * This method buffers the input internally, so there is no need to use a * {@link BufferedInputStream}. *

    * - * @param input the {@link InputStream} to read - * @param charsetName the name of the requested charset, null means platform default - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param input the {@link InputStream} to read. + * @param charsetName the name of the requested charset, null means platform default. + * @return the requested String. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. */ public static String toString(final InputStream input, final String charsetName) throws IOException { @@ -3168,11 +3440,11 @@ public static String toString(final InputStream input, final String charsetName) * {@link BufferedInputStream}. *

    * - * @param input supplies the {@link InputStream} to read - * @param charset the charset to use, null means platform default - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param input supplies the {@link InputStream} to read. + * @param charset the charset to use, null means platform default. + * @return the requested String. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 2.12.0 */ public static String toString(final IOSupplier input, final Charset charset) throws IOException { @@ -3189,12 +3461,12 @@ public static String toString(final IOSupplier input, final Charset * {@link BufferedInputStream}. *

    * - * @param input supplies the {@link InputStream} to read - * @param charset the charset to use, null means platform default + * @param input supplies the {@link InputStream} to read. + * @param charset the charset to use, null means platform default. * @param defaultString the default return value if the supplier or its value is null. - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @return the requested String. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 2.12.0 */ public static String toString(final IOSupplier input, final Charset charset, final IOSupplier defaultString) throws IOException { @@ -3213,10 +3485,10 @@ public static String toString(final IOSupplier input, final Charset * {@link BufferedReader}. *

    * - * @param reader the {@link Reader} to read - * @return the requested String - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param reader the {@link Reader} to read. + * @return the requested String. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. */ public static String toString(final Reader reader) throws IOException { try (StringBuilderWriter sw = new StringBuilderWriter()) { @@ -3226,13 +3498,13 @@ public static String toString(final Reader reader) throws IOException { } /** - * Gets the contents at the given URI using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Gets the contents at the given URI using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * * @param uri The URI source. * @return The contents of the URL as a String. * @throws IOException if an I/O exception occurs. * @since 2.1 - * @deprecated Use {@link #toString(URI, Charset)} instead + * @deprecated Use {@link #toString(URI, Charset)} instead. */ @Deprecated public static String toString(final URI uri) throws IOException { @@ -3259,7 +3531,7 @@ public static String toString(final URI uri, final Charset encoding) throws IOEx * @param charsetName The encoding name for the URL contents. * @return The contents of the URL as a String. * @throws IOException if an I/O exception occurs. - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 2.1 */ public static String toString(final URI uri, final String charsetName) throws IOException { @@ -3267,13 +3539,13 @@ public static String toString(final URI uri, final String charsetName) throws IO } /** - * Gets the contents at the given URL using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Gets the contents at the given URL using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * * @param url The URL source. * @return The contents of the URL as a String. * @throws IOException if an I/O exception occurs. * @since 2.1 - * @deprecated Use {@link #toString(URL, Charset)} instead + * @deprecated Use {@link #toString(URL, Charset)} instead. */ @Deprecated public static String toString(final URL url) throws IOException { @@ -3300,7 +3572,7 @@ public static String toString(final URL url, final Charset encoding) throws IOEx * @param charsetName The encoding name for the URL contents. * @return The contents of the URL as a String. * @throws IOException if an I/O exception occurs. - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 2.1 */ public static String toString(final URL url, final String charsetName) throws IOException { @@ -3310,11 +3582,10 @@ public static String toString(final URL url, final String charsetName) throws IO /** * Writes bytes from a {@code byte[]} to an {@link OutputStream}. * - * @param data the byte array to write, do not modify during output, - * null ignored - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the byte array to write, do not modify during output, null ignored. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ public static void write(final byte[] data, final OutputStream output) @@ -3326,18 +3597,18 @@ public static void write(final byte[] data, final OutputStream output) /** * Writes bytes from a {@code byte[]} to chars on a {@link Writer} - * using the virtual machine's {@link Charset#defaultCharset() default charset}. + * using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method uses {@link String#String(byte[])}. *

    * * @param data the byte array to write, do not modify during output, * null ignored - * @param writer the {@link Writer} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 - * @deprecated Use {@link #write(byte[], Writer, Charset)} instead + * @deprecated Use {@link #write(byte[], Writer, Charset)} instead. */ @Deprecated public static void write(final byte[] data, final Writer writer) throws IOException { @@ -3353,10 +3624,10 @@ public static void write(final byte[] data, final Writer writer) throws IOExcept * * @param data the byte array to write, do not modify during output, * null ignored - * @param writer the {@link Writer} to write to - * @param charset the charset to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param writer the {@link Writer} to write to. + * @param charset the charset to use, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ public static void write(final byte[] data, final Writer writer, final Charset charset) throws IOException { @@ -3366,23 +3637,20 @@ public static void write(final byte[] data, final Writer writer, final Charset c } /** - * Writes bytes from a {@code byte[]} to chars on a {@link Writer} - * using the specified character encoding. + * Writes bytes from a {@code byte[]} to chars on a {@link Writer} using the specified character encoding. *

    - * Character encoding names can be found at - * IANA. + * Character encoding names can be found at IANA. *

    *

    * This method uses {@link String#String(byte[], String)}. *

    * - * @param data the byte array to write, do not modify during output, - * null ignored - * @param writer the {@link Writer} to write to - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param data the byte array to write, do not modify during output, null ignored. + * @param writer the {@link Writer} to write to. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static void write(final byte[] data, final Writer writer, final String charsetName) throws IOException { @@ -3390,19 +3658,17 @@ public static void write(final byte[] data, final Writer writer, final String ch } /** - * Writes chars from a {@code char[]} to bytes on an - * {@link OutputStream}. + * Writes chars from a {@code char[]} to bytes on an {@link OutputStream}. *

    - * This method uses the virtual machine's {@link Charset#defaultCharset() default charset}. + * This method uses the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * - * @param data the char array to write, do not modify during output, - * null ignored - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the char array to write, do not modify during output, null ignored. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 - * @deprecated Use {@link #write(char[], OutputStream, Charset)} instead + * @deprecated Use {@link #write(char[], OutputStream, Charset)} instead. */ @Deprecated public static void write(final char[] data, final OutputStream output) @@ -3411,19 +3677,16 @@ public static void write(final char[] data, final OutputStream output) } /** - * Writes chars from a {@code char[]} to bytes on an - * {@link OutputStream} using the specified character encoding. + * Writes chars from a {@code char[]} to bytes on an {@link OutputStream} using the specified character encoding. *

    - * This method uses {@link String#String(char[])} and - * {@link String#getBytes(String)}. + * This method uses {@link String#String(char[])} and {@link String#getBytes(String)}. *

    * - * @param data the char array to write, do not modify during output, - * null ignored - * @param output the {@link OutputStream} to write to - * @param charset the charset to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the char array to write, do not modify during output, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charset the charset to use, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ public static void write(final char[] data, final OutputStream output, final Charset charset) throws IOException { @@ -3433,24 +3696,20 @@ public static void write(final char[] data, final OutputStream output, final Cha } /** - * Writes chars from a {@code char[]} to bytes on an - * {@link OutputStream} using the specified character encoding. + * Writes chars from a {@code char[]} to bytes on an {@link OutputStream} using the specified character encoding. *

    - * Character encoding names can be found at - * IANA. + * Character encoding names can be found at IANA. *

    *

    - * This method uses {@link String#String(char[])} and - * {@link String#getBytes(String)}. + * This method uses {@link String#String(char[])} and {@link String#getBytes(String)}. *

    * - * @param data the char array to write, do not modify during output, - * null ignored - * @param output the {@link OutputStream} to write to - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param data the char array to write, do not modify during output, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static void write(final char[] data, final OutputStream output, final String charsetName) @@ -3461,11 +3720,10 @@ public static void write(final char[] data, final OutputStream output, final Str /** * Writes chars from a {@code char[]} to a {@link Writer} * - * @param data the char array to write, do not modify during output, - * null ignored - * @param writer the {@link Writer} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the char array to write, do not modify during output, null ignored. + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ public static void write(final char[] data, final Writer writer) throws IOException { @@ -3475,18 +3733,18 @@ public static void write(final char[] data, final Writer writer) throws IOExcept } /** - * Writes chars from a {@link CharSequence} to bytes on an - * {@link OutputStream} using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Writes chars from a {@link CharSequence} to bytes on an {@link OutputStream} using the virtual machine's {@link Charset#defaultCharset() default + * charset}. *

    * This method uses {@link String#getBytes()}. *

    * - * @param data the {@link CharSequence} to write, null ignored - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link CharSequence} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.0 - * @deprecated Use {@link #write(CharSequence, OutputStream, Charset)} instead + * @deprecated Use {@link #write(CharSequence, OutputStream, Charset)} instead. */ @Deprecated public static void write(final CharSequence data, final OutputStream output) @@ -3495,17 +3753,16 @@ public static void write(final CharSequence data, final OutputStream output) } /** - * Writes chars from a {@link CharSequence} to bytes on an - * {@link OutputStream} using the specified character encoding. + * Writes chars from a {@link CharSequence} to bytes on an {@link OutputStream} using the specified character encoding. *

    * This method uses {@link String#getBytes(String)}. *

    * - * @param data the {@link CharSequence} to write, null ignored - * @param output the {@link OutputStream} to write to - * @param charset the charset to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link CharSequence} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charset the charset to use, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ public static void write(final CharSequence data, final OutputStream output, final Charset charset) @@ -3516,22 +3773,20 @@ public static void write(final CharSequence data, final OutputStream output, fin } /** - * Writes chars from a {@link CharSequence} to bytes on an - * {@link OutputStream} using the specified character encoding. + * Writes chars from a {@link CharSequence} to bytes on an {@link OutputStream} using the specified character encoding. *

    - * Character encoding names can be found at - * IANA. + * Character encoding names can be found at IANA. *

    *

    * This method uses {@link String#getBytes(String)}. *

    * - * @param data the {@link CharSequence} to write, null ignored - * @param output the {@link OutputStream} to write to - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param data the {@link CharSequence} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 2.0 */ public static void write(final CharSequence data, final OutputStream output, final String charsetName) @@ -3542,10 +3797,10 @@ public static void write(final CharSequence data, final OutputStream output, fin /** * Writes chars from a {@link CharSequence} to a {@link Writer}. * - * @param data the {@link CharSequence} to write, null ignored - * @param writer the {@link Writer} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link CharSequence} to write, null ignored. + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.0 */ public static void write(final CharSequence data, final Writer writer) throws IOException { @@ -3556,15 +3811,15 @@ public static void write(final CharSequence data, final Writer writer) throws IO /** * Writes chars from a {@link String} to bytes on an - * {@link OutputStream} using the virtual machine's {@link Charset#defaultCharset() default charset}. + * {@link OutputStream} using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * This method uses {@link String#getBytes()}. *

    * - * @param data the {@link String} to write, null ignored - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link String} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 * @deprecated Use {@link #write(String, OutputStream, Charset)} instead */ @@ -3581,11 +3836,11 @@ public static void write(final String data, final OutputStream output) * This method uses {@link String#getBytes(String)}. *

    * - * @param data the {@link String} to write, null ignored - * @param output the {@link OutputStream} to write to - * @param charset the charset to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link String} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charset the charset to use, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ @SuppressWarnings("resource") @@ -3603,18 +3858,18 @@ public static void write(final String data, final OutputStream output, final Cha * {@link OutputStream} using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    *

    * This method uses {@link String#getBytes(String)}. *

    * - * @param data the {@link String} to write, null ignored - * @param output the {@link OutputStream} to write to - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param data the {@link String} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ public static void write(final String data, final OutputStream output, final String charsetName) @@ -3625,10 +3880,10 @@ public static void write(final String data, final OutputStream output, final Str /** * Writes chars from a {@link String} to a {@link Writer}. * - * @param data the {@link String} to write, null ignored - * @param writer the {@link Writer} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link String} to write, null ignored. + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ public static void write(final String data, final Writer writer) throws IOException { @@ -3645,12 +3900,12 @@ public static void write(final String data, final Writer writer) throws IOExcept * This method uses {@link String#getBytes()}. *

    * - * @param data the {@link StringBuffer} to write, null ignored - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if output is null + * @param data the {@link StringBuffer} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if output is null. * @throws IOException if an I/O error occurs * @since 1.1 - * @deprecated Use {@link #write(CharSequence, OutputStream)} + * @deprecated Use {@link #write(CharSequence, OutputStream)}. */ @Deprecated public static void write(final StringBuffer data, final OutputStream output) //NOSONAR @@ -3663,18 +3918,18 @@ public static void write(final StringBuffer data, final OutputStream output) //N * {@link OutputStream} using the specified character encoding. *

    * Character encoding names can be found at - * IANA. + * IANA. *

    *

    * This method uses {@link String#getBytes(String)}. *

    * - * @param data the {@link StringBuffer} to write, null ignored - * @param output the {@link OutputStream} to write to - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param data the {@link StringBuffer} to write, null ignored. + * @param output the {@link OutputStream} to write to. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 * @deprecated Use {@link #write(CharSequence, OutputStream, String)}. */ @@ -3689,12 +3944,12 @@ public static void write(final StringBuffer data, final OutputStream output, fin /** * Writes chars from a {@link StringBuffer} to a {@link Writer}. * - * @param data the {@link StringBuffer} to write, null ignored - * @param writer the {@link Writer} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the {@link StringBuffer} to write, null ignored. + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 - * @deprecated Use {@link #write(CharSequence, Writer)} + * @deprecated Use {@link #write(CharSequence, Writer)}. */ @Deprecated public static void write(final StringBuffer data, final Writer writer) //NOSONAR @@ -3710,10 +3965,10 @@ public static void write(final StringBuffer data, final Writer writer) //NOSONAR * memory usage if the native code has to allocate a copy. * * @param data the byte array to write, do not modify during output, - * null ignored - * @param output the {@link OutputStream} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * null ignored. + * @param output the {@link OutputStream} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.5 */ public static void writeChunked(final byte[] data, final OutputStream output) @@ -3731,15 +3986,13 @@ public static void writeChunked(final byte[] data, final OutputStream output) } /** - * Writes chars from a {@code char[]} to a {@link Writer} using chunked writes. - * This is intended for writing very large byte arrays which might otherwise cause excessive - * memory usage if the native code has to allocate a copy. + * Writes chars from a {@code char[]} to a {@link Writer} using chunked writes. This is intended for writing very large byte arrays which might otherwise + * cause excessive memory usage if the native code has to allocate a copy. * - * @param data the char array to write, do not modify during output, - * null ignored - * @param writer the {@link Writer} to write to - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param data the char array to write, do not modify during output, null ignored. + * @param writer the {@link Writer} to write to. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.5 */ public static void writeChunked(final char[] data, final Writer writer) throws IOException { @@ -3756,44 +4009,38 @@ public static void writeChunked(final char[] data, final Writer writer) throws I } /** - * Writes the {@link #toString()} value of each item in a collection to - * an {@link OutputStream} line by line, using the virtual machine's {@link Charset#defaultCharset() default charset} - * and the specified line ending. + * Writes the {@link #toString()} value of each item in a collection to an {@link OutputStream} line by line, using the virtual machine's + * {@linkplain Charset#defaultCharset() default charset} and the specified line ending. * - * @param lines the lines to write, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param output the {@link OutputStream} to write to, not null, not closed - * @throws NullPointerException if the output is null - * @throws IOException if an I/O error occurs + * @param lines the lines to write, null entries produce blank lines. + * @param lineEnding the line separator to use, null is system default. + * @param output the {@link OutputStream} to write to, not null, not closed. + * @throws NullPointerException if the output is null. + * @throws IOException if an I/O error occurs. * @since 1.1 * @deprecated Use {@link #writeLines(Collection, String, OutputStream, Charset)} instead */ @Deprecated - public static void writeLines(final Collection lines, final String lineEnding, - final OutputStream output) throws IOException { + public static void writeLines(final Collection lines, final String lineEnding, final OutputStream output) throws IOException { writeLines(lines, lineEnding, output, Charset.defaultCharset()); } /** - * Writes the {@link #toString()} value of each item in a collection to - * an {@link OutputStream} line by line, using the specified character - * encoding and the specified line ending. + * Writes the {@link #toString()} value of each item in a collection to an {@link OutputStream} line by line, using the specified character encoding and the + * specified line ending. *

    - * UTF-16 is written big-endian with no byte order mark. - * For little-endian, use UTF-16LE. For a BOM, write it to the stream - * before calling this method. + * UTF-16 is written big-endian with no byte order mark. For little-endian, use UTF-16LE. For a BOM, write it to the stream before calling this method. *

    * - * @param lines the lines to write, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param output the {@link OutputStream} to write to, not null, not closed - * @param charset the charset to use, null means platform default - * @throws NullPointerException if output is null - * @throws IOException if an I/O error occurs + * @param lines the lines to write, null entries produce blank lines. + * @param lineEnding the line separator to use, null is system default. + * @param output the {@link OutputStream} to write to, not null, not closed. + * @param charset the charset to use, null means platform default. + * @throws NullPointerException if output is null. + * @throws IOException if an I/O error occurs. * @since 2.3 */ - public static void writeLines(final Collection lines, String lineEnding, final OutputStream output, - Charset charset) throws IOException { + public static void writeLines(final Collection lines, String lineEnding, final OutputStream output, Charset charset) throws IOException { if (lines == null) { return; } @@ -3814,41 +4061,36 @@ public static void writeLines(final Collection lines, String lineEnding, fina } /** - * Writes the {@link #toString()} value of each item in a collection to - * an {@link OutputStream} line by line, using the specified character - * encoding and the specified line ending. + * Writes the {@link #toString()} value of each item in a collection to an {@link OutputStream} line by line, using the specified character encoding and the + * specified line ending. *

    - * Character encoding names can be found at - * IANA. + * Character encoding names can be found at IANA. *

    * - * @param lines the lines to write, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param output the {@link OutputStream} to write to, not null, not closed - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if the output is null - * @throws IOException if an I/O error occurs - * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported + * @param lines the lines to write, null entries produce blank lines. + * @param lineEnding the line separator to use, null is system default. + * @param output the {@link OutputStream} to write to, not null, not closed. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if the output is null. + * @throws IOException if an I/O error occurs. + * @throws java.nio.charset.UnsupportedCharsetException if the encoding is not supported. * @since 1.1 */ - public static void writeLines(final Collection lines, final String lineEnding, - final OutputStream output, final String charsetName) throws IOException { + public static void writeLines(final Collection lines, final String lineEnding, final OutputStream output, final String charsetName) throws IOException { writeLines(lines, lineEnding, output, Charsets.toCharset(charsetName)); } /** - * Writes the {@link #toString()} value of each item in a collection to - * a {@link Writer} line by line, using the specified line ending. + * Writes the {@link #toString()} value of each item in a collection to a {@link Writer} line by line, using the specified line ending. * - * @param lines the lines to write, null entries produce blank lines - * @param lineEnding the line separator to use, null is system default - * @param writer the {@link Writer} to write to, not null, not closed - * @throws NullPointerException if the input is null - * @throws IOException if an I/O error occurs + * @param lines the lines to write, null entries produce blank lines. + * @param lineEnding the line separator to use, null is system default. + * @param writer the {@link Writer} to write to, not null, not closed. + * @throws NullPointerException if the input is null. + * @throws IOException if an I/O error occurs. * @since 1.1 */ - public static void writeLines(final Collection lines, String lineEnding, - final Writer writer) throws IOException { + public static void writeLines(final Collection lines, String lineEnding, final Writer writer) throws IOException { if (lines == null) { return; } @@ -3867,9 +4109,9 @@ public static void writeLines(final Collection lines, String lineEnding, * Returns the given Appendable if it is already a {@link Writer}, otherwise creates a Writer wrapper around the * given Appendable. * - * @param appendable the Appendable to wrap or return (not null) - * @return the given Appendable or a Writer wrapper around the given Appendable - * @throws NullPointerException if the input parameter is null + * @param appendable the Appendable to wrap or return (not null). + * @return the given Appendable or a Writer wrapper around the given Appendable. + * @throws NullPointerException if the input parameter is null. * @since 2.7 */ public static Writer writer(final Appendable appendable) { diff --git a/src/main/java/org/apache/commons/io/build/AbstractOrigin.java b/src/main/java/org/apache/commons/io/build/AbstractOrigin.java index 3c81bfd38bf..d8edc15ea46 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractOrigin.java +++ b/src/main/java/org/apache/commons/io/build/AbstractOrigin.java @@ -18,7 +18,9 @@ package org.apache.commons.io.build; import java.io.ByteArrayInputStream; +import java.io.Closeable; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -29,13 +31,18 @@ import java.io.Reader; import java.io.Writer; import java.net.URI; +import java.nio.channels.Channel; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import java.nio.file.spi.FileSystemProvider; import java.util.Arrays; import java.util.Objects; @@ -44,7 +51,7 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.io.RandomAccessFileMode; import org.apache.commons.io.RandomAccessFiles; -import org.apache.commons.io.file.spi.FileSystemProviders; +import org.apache.commons.io.channels.ByteArraySeekableByteChannel; import org.apache.commons.io.input.BufferedFileChannelInputStream; import org.apache.commons.io.input.CharSequenceInputStream; import org.apache.commons.io.input.CharSequenceReader; @@ -53,15 +60,288 @@ import org.apache.commons.io.output.WriterOutputStream; /** - * Abstracts the origin of data for builders like a {@link File}, {@link Path}, {@link Reader}, {@link Writer}, {@link InputStream}, {@link OutputStream}, and - * {@link URI}. + * Abstracts and wraps an origin for builders, where an origin is a {@code byte[]}, {@link Channel}, {@link CharSequence}, {@link File}, + * {@link InputStream}, {@link IORandomAccessFile}, {@link OutputStream}, {@link Path}, {@link RandomAccessFile}, {@link Reader}, {@link URI}, + * or {@link Writer}. *

    - * Some methods may throw {@link UnsupportedOperationException} if that method is not implemented in a concrete subclass, see {@link #getFile()} and - * {@link #getPath()}. + * An origin represents where bytes/characters come from or go to. Concrete subclasses + * expose only the operations that make sense for the underlying source or sink; invoking an unsupported operation + * results in {@link UnsupportedOperationException} (see, for example, {@link #getFile()} and {@link #getPath()}). *

    + *

    + * An instance doesn't own its origin, it holds on to it to allow conversions. There are two use cases related to resource management for a Builder: + *

    + *
      + *
    • + * A client allocates a {@linkplain Closeable} (or {@linkplain AutoCloseable}) resource, creates a Builder, and gives the Builder that resource by calling a + * setter method. No matter what happens next, the client is responsible for releasing the resource ({@code Closeable.close()}). In this case, the origin + * wraps but doesn't own the closeable resource. There is no transfer of ownership. + *
    • + *
    • + * A client creates a Builder and gives it a non-Closeable object, like a File or a Path. The client then calls the Builder's factory method + * (like {@linkplain #get()}), and that call returns a Closeable or a resource that requires releasing in some other way. No matter what happens next, the + * client is responsible for releasing that resource. In this case, the origin doesn't wrap a closeable resource. + *
    • + *
    + *

    + * In both cases, the client causes the allocation and is responsible for releasing the resource. + *

    + *

    + * The table below summarizes which views and conversions are supported for each origin type. + * Column headers show the target view; cells indicate whether that view is available from the origin in that row. + *

    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    Supported Conversions
    Origin Typebyte[]CSFilePathRAFISReaderRBCOSWriterWBCChannel type2
    byte[]SBC
    {@link CharSequence} (CS)11SBC
    {@link File}FC
    {@link Path}FC
    {@link IORandomAccessFile}FC
    {@link RandomAccessFile} (RAF)FC
    {@link InputStream} (IS)RBC
    {@link Reader}11RBC
    {@link ReadableByteChannel} (RBC)RBC
    {@link OutputStream} (OS)WBC
    {@link Writer}11WBC
    {@link WritableByteChannel} (WBC)WBC
    {@link URI} (FileSystem)FC
    {@link URI} (http/https)RBC
    + * + *

    Legend

    + *
      + *
    • ✔ = Supported
    • + *
    • ✖ = Not supported (throws {@link UnsupportedOperationException})
    • + *
    • 1 = Characters are converted to bytes using the default {@link Charset}.
    • + *
    • 2 Minimum channel type provided by the origin: + *
        + *
      • RBC = {@linkplain ReadableByteChannel}
      • + *
      • WBC = {@linkplain WritableByteChannel}
      • + *
      • SBC = {@linkplain SeekableByteChannel}
      • + *
      • FC = {@linkplain FileChannel}
      • + *
      + * The exact channel type may be a subtype of the minimum shown. + *
    • + *
    * - * @param the type of instances to build. - * @param the type of builder subclass. + * @param the type produced by the builder. + * @param the concrete builder subclass type. * @since 2.12.0 */ public abstract class AbstractOrigin> extends AbstractSupplier { @@ -86,6 +366,7 @@ public abstract static class AbstractRandomAccessFileOrigin

    * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ public AbstractRandomAccessFileOrigin(final T origin) { super(origin); @@ -105,6 +386,11 @@ public byte[] getByteArray(final long position, final int length) throws IOExcep return RandomAccessFiles.read(origin, position, length); } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + return getRandomAccessFile(options).getChannel(); + } + @Override public CharSequence getCharSequence(final Charset charset) throws IOException { return new String(getByteArray(), charset); @@ -152,17 +438,35 @@ public static class ByteArrayOrigin extends AbstractOrigin

    + * No conversion should occur when calling this method. + *

    + */ @Override public byte[] getByteArray() { // No conversion return get(); } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + for (final OpenOption option : options) { + if (option == StandardOpenOption.WRITE) { + throw new UnsupportedOperationException("Only READ is supported for byte[] origins: " + Arrays.toString(options)); + } + } + return ByteArraySeekableByteChannel.wrap(getByteArray()); + } + /** * {@inheritDoc} *

    @@ -186,6 +490,73 @@ public long size() throws IOException { } + /** + * A {@link Channel} origin. + * + * @since 2.21.0 + */ + public static class ChannelOrigin extends AbstractOrigin { + + /** + * Constructs a new instance for the given origin. + * + * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. + */ + public ChannelOrigin(final Channel origin) { + super(origin); + } + + @Override + public byte[] getByteArray() throws IOException { + return IOUtils.toByteArray(getInputStream()); + } + + /** + * {@inheritDoc} + * + *

    + * No conversion should occur when calling this method. + *

    + */ + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + // No conversion + return get(); + } + + @Override + public InputStream getInputStream(final OpenOption... options) throws IOException { + return Channels.newInputStream(getChannel(ReadableByteChannel.class, options)); + } + + @Override + public OutputStream getOutputStream(final OpenOption... options) throws IOException { + return Channels.newOutputStream(getChannel(WritableByteChannel.class, options)); + } + + @Override + public Reader getReader(final Charset charset) throws IOException { + return Channels.newReader( + getChannel(ReadableByteChannel.class), + Charsets.toCharset(charset).newDecoder(), + -1); + } + + @Override + public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException { + return Channels.newWriter(getChannel(WritableByteChannel.class, options), Charsets.toCharset(charset).newEncoder(), -1); + } + + @Override + public long size() throws IOException { + if (origin instanceof SeekableByteChannel) { + return ((SeekableByteChannel) origin).size(); + } + throw unsupportedOperation("size"); + } + } + /** * A {@link CharSequence} origin. */ @@ -195,6 +566,7 @@ public static class CharSequenceOrigin extends AbstractOrigin

    * The {@code charset} parameter is ignored since a {@link CharSequence} does not need a {@link Charset} to be read. *

    + *

    + * No conversion should occur when calling this method. + *

    */ @Override public CharSequence getCharSequence(final Charset charset) { @@ -260,6 +645,7 @@ public static class FileOrigin extends AbstractOrigin { * Constructs a new instance for the given origin. * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ public FileOrigin(final File origin) { super(origin); @@ -272,6 +658,18 @@ public byte[] getByteArray(final long position, final int length) throws IOExcep } } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); + } + + /** + * {@inheritDoc} + * + *

    + * No conversion should occur when calling this method. + *

    + */ @Override public File getFile() { // No conversion @@ -282,7 +680,6 @@ public File getFile() { public Path getPath() { return get().toPath(); } - } /** @@ -297,6 +694,7 @@ public static class InputStreamOrigin extends AbstractOrigin

    * The {@code options} parameter is ignored since a {@link InputStream} does not need an {@link OpenOption} to be read. *

    + *

    + * No conversion should occur when calling this method. + *

    */ @Override public InputStream getInputStream(final OpenOption... options) { @@ -324,10 +730,17 @@ public Reader getReader(final Charset charset) throws IOException { return new InputStreamReader(getInputStream(), Charsets.toCharset(charset)); } + @Override + public long size() throws IOException { + if (origin instanceof FileInputStream) { + return ((FileInputStream) origin).getChannel().size(); + } + throw unsupportedOperation("size"); + } } /** - * A {@link IORandomAccessFile} origin. + * An {@link IORandomAccessFile} origin. * * @since 2.18.0 */ @@ -367,16 +780,25 @@ public static class OutputStreamOrigin extends AbstractOrigin

    * The {@code options} parameter is ignored since a {@link OutputStream} does not need an {@link OpenOption} to be written. *

    + *

    + * No conversion should occur when calling this method. + *

    */ @Override public OutputStream getOutputStream(final OpenOption... options) { @@ -408,6 +830,7 @@ public static class PathOrigin extends AbstractOrigin { * Constructs a new instance for the given origin. * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ public PathOrigin(final Path origin) { super(origin); @@ -418,17 +841,28 @@ public byte[] getByteArray(final long position, final int length) throws IOExcep return RandomAccessFileMode.READ_ONLY.apply(origin, raf -> RandomAccessFiles.read(raf, position, length)); } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + return Files.newByteChannel(getPath(), options); + } + @Override public File getFile() { return get().toFile(); } + /** + * {@inheritDoc} + * + *

    + * No conversion should occur when calling this method. + *

    + */ @Override public Path getPath() { // No conversion return get(); } - } /** @@ -466,6 +900,7 @@ public static class ReaderOrigin extends AbstractOrigin { * Constructs a new instance for the given origin. * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ public ReaderOrigin(final Reader origin) { super(origin); @@ -477,6 +912,11 @@ public byte[] getByteArray() throws IOException { return IOUtils.toByteArray(origin, Charset.defaultCharset()); } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + return Channels.newChannel(getInputStream()); + } + /** * {@inheritDoc} *

    @@ -505,6 +945,9 @@ public InputStream getInputStream(final OpenOption... options) throws IOExceptio *

    * The {@code charset} parameter is ignored since a {@link Reader} does not need a {@link Charset} to be read. *

    + *

    + * No conversion should occur when calling this method. + *

    */ @Override public Reader getReader(final Charset charset) throws IOException { @@ -525,11 +968,22 @@ public static class URIOrigin extends AbstractOrigin { * Constructs a new instance for the given origin. * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ public URIOrigin(final URI origin) { super(origin); } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + final URI uri = get(); + final String scheme = uri.getScheme(); + if (SCHEME_HTTP.equalsIgnoreCase(scheme) || SCHEME_HTTPS.equalsIgnoreCase(scheme)) { + return Channels.newChannel(uri.toURL().openStream()); + } + return Files.newByteChannel(getPath(), options); + } + @Override public File getFile() { return getPath().toFile(); @@ -539,10 +993,6 @@ public File getFile() { public InputStream getInputStream(final OpenOption... options) throws IOException { final URI uri = get(); final String scheme = uri.getScheme(); - final FileSystemProvider fileSystemProvider = FileSystemProviders.installed().getFileSystemProvider(scheme); - if (fileSystemProvider != null) { - return Files.newInputStream(fileSystemProvider.getPath(uri), options); - } if (SCHEME_HTTP.equalsIgnoreCase(scheme) || SCHEME_HTTPS.equalsIgnoreCase(scheme)) { return uri.toURL().openStream(); } @@ -567,11 +1017,17 @@ public static class WriterOrigin extends AbstractOrigin { * Constructs a new instance for the given origin. * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ public WriterOrigin(final Writer origin) { super(origin); } + @Override + protected Channel getChannel(final OpenOption... options) throws IOException { + return Channels.newChannel(getOutputStream()); + } + /** * {@inheritDoc} *

    @@ -592,6 +1048,9 @@ public OutputStream getOutputStream(final OpenOption... options) throws IOExcept *

    * The {@code options} parameter is ignored since a {@link Writer} does not need an {@link OpenOption} to be written. *

    + *

    + * No conversion should occur when calling this method. + *

    */ @Override public Writer getWriter(final Charset charset, final OpenOption... options) throws IOException { @@ -609,15 +1068,16 @@ public Writer getWriter(final Charset charset, final OpenOption... options) thro * Constructs a new instance for subclasses. * * @param origin The origin, not null. + * @throws NullPointerException if {@code origin} is {@code null}. */ protected AbstractOrigin(final T origin) { this.origin = Objects.requireNonNull(origin, "origin"); } /** - * Gets the origin. + * Gets the origin, never null. * - * @return the origin. + * @return the origin, never null. */ @Override public T get() { @@ -656,6 +1116,41 @@ public byte[] getByteArray(final long position, final int length) throws IOExcep return Arrays.copyOfRange(bytes, start, start + length); } + /** + * Gets this origin as a Channel of the given type, if possible. + * + * @param channelType The type of channel to return. + * @param options Options specifying how a file-based origin is opened, ignored otherwise. + * @return A new Channel on the origin of the given type. + * @param The type of channel to return. + * @throws IOException If an I/O error occurs. + * @throws UnsupportedOperationException If this origin cannot be converted to a channel of the given type. + * @see #getChannel(OpenOption...) + * @since 2.21.0 + */ + public final C getChannel(final Class channelType, final OpenOption... options) throws IOException { + Objects.requireNonNull(channelType, "channelType"); + final Channel channel = getChannel(options); + if (channelType.isInstance(channel)) { + return channelType.cast(channel); + } + throw unsupportedChannelType(channelType); + } + + /** + * Gets this origin as a Channel, if possible. + * + * @param options Options specifying how a file-based origin is opened, ignored otherwise. + * @return A new Channel on the origin. + * @throws IOException If an I/O error occurs. + * @throws UnsupportedOperationException If this origin cannot be converted to a channel. + * @see #getChannel(Class, OpenOption...) + * @since 2.21.0 + */ + protected Channel getChannel(final OpenOption... options) throws IOException { + throw unsupportedOperation("getChannel"); + } + /** * Gets this origin as a byte array, if possible. * @@ -669,14 +1164,13 @@ public CharSequence getCharSequence(final Charset charset) throws IOException { } /** - * Gets this origin as a Path, if possible. + * Gets this origin as a File, if possible. * - * @return this origin as a Path, if possible. + * @return this origin as a File, if possible. * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass. */ public File getFile() { - throw new UnsupportedOperationException( - String.format("%s#getFile() for %s origin %s", getSimpleClassName(), origin.getClass().getSimpleName(), origin)); + throw unsupportedOperation("getFile"); } /** @@ -710,8 +1204,7 @@ public OutputStream getOutputStream(final OpenOption... options) throws IOExcept * @throws UnsupportedOperationException if this method is not implemented in a concrete subclass. */ public Path getPath() { - throw new UnsupportedOperationException( - String.format("%s#getPath() for %s origin %s", getSimpleClassName(), origin.getClass().getSimpleName(), origin)); + throw unsupportedOperation("getPath"); } /** @@ -738,6 +1231,11 @@ public Reader getReader(final Charset charset) throws IOException { return Files.newBufferedReader(getPath(), Charsets.toCharset(charset)); } + /** + * Gets simple name of the underlying class. + * + * @return The simple name of the underlying class. + */ private String getSimpleClassName() { return getClass().getSimpleName(); } @@ -770,4 +1268,19 @@ public long size() throws IOException { public String toString() { return getSimpleClassName() + "[" + origin.toString() + "]"; } + + UnsupportedOperationException unsupportedChannelType(final Class channelType) { + return new UnsupportedOperationException(String.format( + "%s#getChannel(%s) for %s origin %s", + getSimpleClassName(), + channelType.getSimpleName(), + origin.getClass().getSimpleName(), + origin)); + } + + UnsupportedOperationException unsupportedOperation(final String method) { + return new UnsupportedOperationException(String.format( + "%s#%s() for %s origin %s", + getSimpleClassName(), method, origin.getClass().getSimpleName(), origin)); + } } diff --git a/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java b/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java index 8f2354d95bf..ba8e6b393ba 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java +++ b/src/main/java/org/apache/commons/io/build/AbstractOriginSupplier.java @@ -24,11 +24,13 @@ import java.io.Reader; import java.io.Writer; import java.net.URI; +import java.nio.channels.Channel; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.commons.io.IORandomAccessFile; import org.apache.commons.io.build.AbstractOrigin.ByteArrayOrigin; +import org.apache.commons.io.build.AbstractOrigin.ChannelOrigin; import org.apache.commons.io.build.AbstractOrigin.CharSequenceOrigin; import org.apache.commons.io.build.AbstractOrigin.FileOrigin; import org.apache.commons.io.build.AbstractOrigin.IORandomAccessFileOrigin; @@ -41,7 +43,7 @@ import org.apache.commons.io.build.AbstractOrigin.WriterOrigin; /** - * Abstracts building an instance of {@code T}. + * Abstracts building an instance of type {@code T} where {@code T} is unbounded from a wrapped {@linkplain AbstractOrigin origin}. * * @param the type of instances to build. * @param the type of builder subclass. @@ -59,6 +61,17 @@ protected static ByteArrayOrigin newByteArrayOrigin(final byte[] origin) { return new ByteArrayOrigin(origin); } + /** + * Constructs a new channel origin for a channel. + * + * @param origin the channel. + * @return a new channel origin. + * @since 2.21.0 + */ + protected static ChannelOrigin newChannelOrigin(final Channel origin) { + return new ChannelOrigin(origin); + } + /** * Constructs a new CharSequence origin for a CharSequence. * @@ -235,6 +248,17 @@ public B setByteArray(final byte[] origin) { return setOrigin(newByteArrayOrigin(origin)); } + /** + * Sets a new origin. + * + * @param origin the new origin. + * @return {@code this} instance. + * @since 2.21.0 + */ + public B setChannel(final Channel origin) { + return setOrigin(newChannelOrigin(origin)); + } + /** * Sets a new origin. * diff --git a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java index 3feac4a6533..541548c7295 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java +++ b/src/main/java/org/apache/commons/io/build/AbstractStreamBuilder.java @@ -24,6 +24,8 @@ import java.io.RandomAccessFile; import java.io.Reader; import java.io.Writer; +import java.nio.channels.Channel; +import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -34,7 +36,9 @@ import org.apache.commons.io.file.PathUtils; /** - * Abstracts building a typed instance of {@code T}. + * Abstracts building a typed instance of type {@code T} where {@code T} is unbounded. This class contains various properties like a buffer size, + * buffer size checker, a buffer size default, buffer size maximum, Charset, Charset default, default size checker, and open options. A subclass may use all, + * some, or none of these properties in building instances of {@code T}. * * @param the type of instances to build. * @param the type of builder subclass. @@ -118,6 +122,23 @@ public int getBufferSizeDefault() { return bufferSizeDefault; } + /** + * Gets a Channel from the origin with OpenOption[]. + * + * @param channelType The channel type, not null. + * @return A channel of the specified type. + * @param The channel type. + * @throws IllegalStateException if the {@code origin} is {@code null}. + * @throws UnsupportedOperationException if the origin cannot be converted to a {@link ReadableByteChannel}. + * @throws IOException if an I/O error occurs. + * @see AbstractOrigin#getChannel + * @see #getOpenOptions() + * @since 2.21.0 + */ + public C getChannel(final Class channelType) throws IOException { + return checkOrigin().getChannel(channelType, getOpenOptions()); + } + /** * Gets a CharSequence from the origin with a Charset. * diff --git a/src/main/java/org/apache/commons/io/build/AbstractSupplier.java b/src/main/java/org/apache/commons/io/build/AbstractSupplier.java index 96de75cb9eb..6715ae61074 100644 --- a/src/main/java/org/apache/commons/io/build/AbstractSupplier.java +++ b/src/main/java/org/apache/commons/io/build/AbstractSupplier.java @@ -20,7 +20,7 @@ import org.apache.commons.io.function.IOSupplier; /** - * Abstracts supplying an instance of {@code T}. + * Abstracts supplying an instance of type {@code T} where {@code T} is unbounded. This class carries no state. *

    * Extend this class to implement the builder pattern. *

    @@ -120,6 +120,7 @@ public void test() { * * @param the type of instances to build. * @param the type of builder subclass. + * @see IOSupplier * @since 2.12.0 */ public abstract class AbstractSupplier> implements IOSupplier { @@ -140,7 +141,7 @@ public AbstractSupplier() { * (B) this * * - * @return this instance typed as the subclass type {@code B}. + * @return {@code this} instance typed as the subclass type {@code B}. */ @SuppressWarnings("unchecked") protected B asThis() { diff --git a/src/main/java/org/apache/commons/io/build/package-info.java b/src/main/java/org/apache/commons/io/build/package-info.java index 70a47261afd..bda177c354c 100644 --- a/src/main/java/org/apache/commons/io/build/package-info.java +++ b/src/main/java/org/apache/commons/io/build/package-info.java @@ -14,10 +14,40 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - /** - * Provides classes to implement IO builders. + * Provides classes to implement the builder pattern for IO classes. + * + *

    + * The main classes in this package are (indentation reflects hierarchy): + *

    + *
      + *
    • The root class {@linkplain org.apache.commons.io.build.AbstractSupplier AbstractSupplier} abstracts supplying an instance of type {@code T} + * where {@code T} is unbounded. This class carries no state. + *
        + * + *
      • {@linkplain org.apache.commons.io.build.AbstractOrigin AbstractOrigin} extends {@linkplain org.apache.commons.io.build.AbstractSupplier AbstractSupplier} + * to abstract and wrap an origin for builders, where an origin is a {@code byte[]}, {@linkplain java.nio.channels.Channel Channel}, + * {@linkplain java.lang.CharSequence CharSequence}, {@linkplain java.io.File File}, {@linkplain java.io.InputStream InputStream}, + * {@linkplain org.apache.commons.io.IORandomAccessFile IORandomAccessFile}, {@linkplain java.io.OutputStream OutputStream}, {@linkplain java.nio.file.Path + * Path}, {@linkplain java.io.RandomAccessFile RandomAccessFile}, {@linkplain java.io.Reader Reader}, {@linkplain java.net.URI URI}, or + * {@linkplain java.io.Writer Writer}.
      • + * + *
      • {@linkplain org.apache.commons.io.build.AbstractOriginSupplier AbstractOriginSupplier} extends {@linkplain org.apache.commons.io.build.AbstractSupplier + * AbstractSupplier} to abstract building an instance of type {@code T} where {@code T} is unbounded from a wrapped + * {@linkplain org.apache.commons.io.build.AbstractOrigin origin}. + * + *
          + *
        • {@linkplain org.apache.commons.io.build.AbstractStreamBuilder AbstractStreamBuilder} extends + * {@linkplain org.apache.commons.io.build.AbstractOriginSupplier AbstractOriginSupplier} to abstract building a typed instance of type {@code T} where + * {@code T} is unbounded. This class contains various properties like a buffer size, buffer size checker, a buffer size default, buffer size maximum, Charset, + * Charset default, default size checker, and open options. A subclass may use all, some, or none of these properties in building instances of {@code T}.
        • + *
        + *
      • + *
      + *
    • + *
    * * @since 2.12.0 */ + package org.apache.commons.io.build; diff --git a/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java b/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java new file mode 100644 index 00000000000..4135a815e83 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/ByteArraySeekableByteChannel.java @@ -0,0 +1,280 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.commons.io.channels; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; + +import org.apache.commons.io.IOUtils; + +/** + * A {@link SeekableByteChannel} implementation backed by a byte array. + *

    + * When used for writing, the internal buffer grows to accommodate incoming data. The natural size limit is the value of {@link IOUtils#SOFT_MAX_ARRAY_LENGTH} + * and it's not possible to {@link #position(long) set the position} or {@link #truncate(long) truncate} to a value bigger than that. The raw internal buffer is + * accessed via {@link ByteArraySeekableByteChannel#array()}. + *

    + * + * @since 2.21.0 + */ +public class ByteArraySeekableByteChannel implements SeekableByteChannel { + + private static final int RESIZE_LIMIT = Integer.MAX_VALUE >> 1; + + /** + * Constructs a new channel backed directly by the given byte array. + * + *

    The channel initially contains the full contents of the array, with its + * size set to {@code bytes.length} and its position set to {@code 0}.

    + * + *

    Reads and writes operate on the shared array. + * If a write operation extends beyond the current capacity, the channel will + * automatically allocate a larger backing array and copy the existing contents.

    + * + * @param bytes The byte array to wrap, must not be {@code null} + * @return A new channel that uses the given array as its initial backing store + * @throws NullPointerException If {@code bytes} is {@code null} + * @see #array() + * @see ByteArrayInputStream#ByteArrayInputStream(byte[]) + */ + public static ByteArraySeekableByteChannel wrap(final byte[] bytes) { + Objects.requireNonNull(bytes, "bytes"); + return new ByteArraySeekableByteChannel(bytes); + } + + private byte[] data; + private volatile boolean closed; + private int position; + private int size; + private final ReentrantLock lock = new ReentrantLock(); + + /** + * Constructs a new instance, with a default internal buffer capacity. + *

    + * The initial size and position of the channel are 0. + *

    + * + * @see ByteArrayOutputStream#ByteArrayOutputStream() + */ + public ByteArraySeekableByteChannel() { + this(IOUtils.DEFAULT_BUFFER_SIZE); + } + + private ByteArraySeekableByteChannel(final byte[] data) { + this.data = data; + this.position = 0; + this.size = data.length; + } + + /** + * Constructs a new instance, with an internal buffer of the given capacity, in bytes. + *

    + * The initial size and position of the channel are 0. + *

    + * + * @param size Capacity of the internal buffer to allocate, in bytes. + * @see ByteArrayOutputStream#ByteArrayOutputStream(int) + */ + public ByteArraySeekableByteChannel(final int size) { + if (size < 0) { + throw new IllegalArgumentException("Size must be non-negative"); + } + this.data = new byte[size]; + this.position = 0; + this.size = 0; + } + + /** + * Gets the raw byte array backing this channel, this is not a copy. + *

    + * NOTE: The returned buffer is not aligned with containing data, use {@link #size()} to obtain the size of data stored in the buffer. + *

    + * + * @return internal byte array. + */ + public byte[] array() { + return data; + } + + private void checkOpen() throws ClosedChannelException { + if (!isOpen()) { + throw new ClosedChannelException(); + } + } + + private int checkRange(final long newSize, final String method) { + if (newSize < 0L || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) { + throw new IllegalArgumentException(String.format("%s must be in range [0..%,d]: %,d", method, IOUtils.SOFT_MAX_ARRAY_LENGTH, newSize)); + } + return (int) newSize; + } + + @Override + public void close() { + closed = true; + } + + /** + * Like {@link #size()} but never throws {@link ClosedChannelException}. + * + * @return See {@link #size()}. + */ + public long getSize() { + return size; + } + + @Override + public boolean isOpen() { + return !closed; + } + + @Override + public long position() throws ClosedChannelException { + checkOpen(); + lock.lock(); + try { + return position; + } finally { + lock.unlock(); + } + } + + @Override + public SeekableByteChannel position(final long newPosition) throws IOException { + checkOpen(); + final int intPos = checkRange(newPosition, "position()"); + lock.lock(); + try { + position = intPos; + } finally { + lock.unlock(); + } + return this; + } + + @Override + public int read(final ByteBuffer buf) throws IOException { + checkOpen(); + lock.lock(); + try { + int wanted = buf.remaining(); + final int possible = size - position; + if (possible <= 0) { + return IOUtils.EOF; + } + if (wanted > possible) { + wanted = possible; + } + buf.put(data, position, wanted); + position += wanted; + return wanted; + } finally { + lock.unlock(); + } + } + + private void resize(final int newLength) { + int len = data.length; + if (len == 0) { + len = 1; + } + if (newLength < RESIZE_LIMIT) { + while (len < newLength) { + len <<= 1; + } + } else { // avoid overflow + len = newLength; + } + data = Arrays.copyOf(data, len); + } + + @Override + public long size() throws ClosedChannelException { + checkOpen(); + lock.lock(); + try { + return size; + } finally { + lock.unlock(); + } + } + + /** + * Gets a copy of the data stored in this channel. + *

    + * The returned array is a copy of the internal buffer, sized to the actual data stored in this channel. + *

    + * + * @return a new byte array containing the data stored in this channel. + */ + public byte[] toByteArray() { + return Arrays.copyOf(data, size); + } + + @Override + public SeekableByteChannel truncate(final long newSize) throws ClosedChannelException { + checkOpen(); + final int intSize = checkRange(newSize, "truncate()"); + lock.lock(); + try { + if (size > intSize) { + size = intSize; + } + if (position > intSize) { + position = intSize; + } + } finally { + lock.unlock(); + } + return this; + } + + @Override + public int write(final ByteBuffer b) throws IOException { + checkOpen(); + lock.lock(); + try { + final int wanted = b.remaining(); + final int possibleWithoutResize = Math.max(0, size - position); + if (wanted > possibleWithoutResize) { + final int newSize = position + wanted; + if (newSize < 0 || newSize > IOUtils.SOFT_MAX_ARRAY_LENGTH) { // overflow + throw new OutOfMemoryError("required array size " + Integer.toUnsignedString(newSize) + " too large"); + } + resize(newSize); + } + b.get(data, position, wanted); + position += wanted; + if (size < position) { + size = position; + } + return wanted; + } finally { + lock.unlock(); + } + } +} diff --git a/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java b/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java new file mode 100644 index 00000000000..ba890d8a6a7 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/CloseShieldChannel.java @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.channels; + +import java.io.Closeable; +import java.lang.reflect.Proxy; +import java.nio.channels.AsynchronousChannel; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channel; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.InterruptibleChannel; +import java.nio.channels.NetworkChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Creates a close-shielding proxy for a {@link Channel}. + * + *

    The returned proxy implements all {@link Channel} sub-interfaces that are both supported by this implementation and actually implemented by the given + * delegate.

    + * + *

    The following interfaces are supported:

    + * + *
      + *
    • {@link AsynchronousChannel}
    • + *
    • {@link ByteChannel}
    • + *
    • {@link Channel}
    • + *
    • {@link GatheringByteChannel}
    • + *
    • {@link InterruptibleChannel}
    • + *
    • {@link NetworkChannel}
    • + *
    • {@link ReadableByteChannel}
    • + *
    • {@link ScatteringByteChannel}
    • + *
    • {@link SeekableByteChannel}
    • + *
    • {@link WritableByteChannel}
    • + *
    + * + * @see Channel + * @see Closeable + * @since 2.21.0 + */ +public final class CloseShieldChannel { + + private static final Class[] EMPTY = {}; + + private static Set> collectChannelInterfaces(final Class type, final Set> out) { + Class currentType = type; + // Visit interfaces + while (currentType != null) { + for (final Class iface : currentType.getInterfaces()) { + if (CloseShieldChannelHandler.isSupported(iface) && out.add(iface)) { + collectChannelInterfaces(iface, out); + } + } + currentType = currentType.getSuperclass(); + } + return out; + } + + /** + * Wraps a channel to shield it from being closed. + * + * @param channel The underlying channel to shield, not {@code null}. + * @param A supported channel type. + * @return A proxy that shields {@code close()} and enforces closed semantics on other calls. + * @throws ClassCastException if {@code T} is not a supported channel type. + * @throws NullPointerException if {@code channel} is {@code null}. + */ + @SuppressWarnings({ "unchecked", "resource" }) // caller closes + public static T wrap(final T channel) { + Objects.requireNonNull(channel, "channel"); + // Fast path: already our shield + if (Proxy.isProxyClass(channel.getClass()) && Proxy.getInvocationHandler(channel) instanceof CloseShieldChannelHandler) { + return channel; + } + // Collect only Channel sub-interfaces. + final Set> set = collectChannelInterfaces(channel.getClass(), new LinkedHashSet<>()); + // fallback to root surface + return (T) Proxy.newProxyInstance(channel.getClass().getClassLoader(), // use delegate's loader + set.isEmpty() ? new Class[] { Channel.class } : set.toArray(EMPTY), new CloseShieldChannelHandler(channel)); + } + + private CloseShieldChannel() { + // no instance + } +} diff --git a/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java b/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java new file mode 100644 index 00000000000..fdafd544f07 --- /dev/null +++ b/src/main/java/org/apache/commons/io/channels/CloseShieldChannelHandler.java @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.channels; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.nio.channels.AsynchronousChannel; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.InterruptibleChannel; +import java.nio.channels.NetworkChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +final class CloseShieldChannelHandler implements InvocationHandler { + + private static final Set> SUPPORTED_INTERFACES; + + static { + final Set> interfaces = new HashSet<>(); + interfaces.add(AsynchronousChannel.class); + interfaces.add(ByteChannel.class); + interfaces.add(Channel.class); + interfaces.add(GatheringByteChannel.class); + interfaces.add(InterruptibleChannel.class); + interfaces.add(NetworkChannel.class); + interfaces.add(ReadableByteChannel.class); + interfaces.add(ScatteringByteChannel.class); + interfaces.add(SeekableByteChannel.class); + interfaces.add(WritableByteChannel.class); + SUPPORTED_INTERFACES = Collections.unmodifiableSet(interfaces); + } + + /** + * Tests whether the given method is allowed to be called after the shield is closed. + * + * @param declaringClass The class declaring the method. + * @param name The method name. + * @param parameterCount The number of parameters. + * @return {@code true} if the method is allowed after {@code close()}, {@code false} otherwise. + */ + private static boolean isAllowedAfterClose(final Class declaringClass, final String name, final int parameterCount) { + // JDK explicitly allows NetworkChannel.supportedOptions() post-close + return parameterCount == 0 && name.equals("supportedOptions") && NetworkChannel.class.equals(declaringClass); + } + + static boolean isSupported(final Class interfaceClass) { + return SUPPORTED_INTERFACES.contains(interfaceClass); + } + + /** + * Tests whether the given method returns 'this' (the channel) as per JDK spec. + * + * @param declaringClass The class declaring the method. + * @param name The method name. + * @param parameterCount The number of parameters. + * @return {@code true} if the method returns 'this', {@code false} otherwise. + */ + private static boolean returnsThis(final Class declaringClass, final String name, final int parameterCount) { + if (SeekableByteChannel.class.equals(declaringClass)) { + // SeekableByteChannel.position(long) and truncate(long) return 'this' + return parameterCount == 1 && (name.equals("position") || name.equals("truncate")); + } + if (NetworkChannel.class.equals(declaringClass)) { + // NetworkChannel.bind and NetworkChannel.setOption returns 'this' + return parameterCount == 1 && name.equals("bind") || parameterCount == 2 && name.equals("setOption"); + } + return false; + } + + private final Channel delegate; + private volatile boolean closed; + + CloseShieldChannelHandler(final Channel delegate) { + this.delegate = Objects.requireNonNull(delegate, "delegate"); + } + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { + final Class declaringClass = method.getDeclaringClass(); + final String name = method.getName(); + final int parameterCount = method.getParameterCount(); + // 1) java.lang.Object methods + if (declaringClass == Object.class) { + return invokeObjectMethod(proxy, method, args); + } + // 2) Channel.close(): mark shield closed, do NOT close the delegate + if (parameterCount == 0 && name.equals("close")) { + closed = true; + return null; + } + // 3) Channel.isOpen(): reflect shield state only + if (parameterCount == 0 && name.equals("isOpen")) { + return !closed && delegate.isOpen(); + } + // 4) After the shield is closed, only allow a tiny allowlist of safe queries + if (closed && !isAllowedAfterClose(declaringClass, name, parameterCount)) { + throw new ClosedChannelException(); + } + // 5) Delegate to the underlying channel and unwrap target exceptions + try { + final Object result = method.invoke(delegate, args); + return returnsThis(declaringClass, name, parameterCount) ? proxy : result; + } catch (final InvocationTargetException e) { + throw e.getCause(); + } + } + + private Object invokeObjectMethod(final Object proxy, final Method method, final Object[] args) { + switch (method.getName()) { + case "toString": + return "CloseShieldChannel(" + delegate + ")"; + case "hashCode": + return Objects.hashCode(delegate); + case "equals": { + final Object other = args[0]; + if (other == null) { + return false; + } + if (proxy == other) { + return true; + } + if (Proxy.isProxyClass(other.getClass())) { + final InvocationHandler h = Proxy.getInvocationHandler(other); + if (h instanceof CloseShieldChannelHandler) { + return Objects.equals(((CloseShieldChannelHandler) h).delegate, this.delegate); + } + } + return false; + } + default: + // Not possible, all non-final Object methods are handled above + return null; + } + } +} diff --git a/src/main/java/org/apache/commons/io/charset/CharsetDecoders.java b/src/main/java/org/apache/commons/io/charset/CharsetDecoders.java index 8595be79e8a..a8aa9a3d50b 100644 --- a/src/main/java/org/apache/commons/io/charset/CharsetDecoders.java +++ b/src/main/java/org/apache/commons/io/charset/CharsetDecoders.java @@ -30,7 +30,7 @@ public final class CharsetDecoders { /** * Returns the given non-null CharsetDecoder or a new default CharsetDecoder. *

    - * Null input maps to the virtual machine's {@link Charset#defaultCharset() default charset} decoder. + * Null input maps to the virtual machine's {@linkplain Charset#defaultCharset() default charset} decoder. *

    * * @param charsetDecoder The CharsetDecoder to test. diff --git a/src/main/java/org/apache/commons/io/charset/CharsetEncoders.java b/src/main/java/org/apache/commons/io/charset/CharsetEncoders.java index d0f9cd04be1..a1fff5cf1f3 100644 --- a/src/main/java/org/apache/commons/io/charset/CharsetEncoders.java +++ b/src/main/java/org/apache/commons/io/charset/CharsetEncoders.java @@ -31,7 +31,7 @@ public final class CharsetEncoders { /** * Returns the given non-null CharsetEncoder or a new default CharsetEncoder. *

    - * Null input maps to the virtual machine's {@link Charset#defaultCharset() default charset} decoder. + * Null input maps to the virtual machine's {@linkplain Charset#defaultCharset() default charset} decoder. *

    * * @param charsetEncoder The CharsetEncoder to test. diff --git a/src/main/java/org/apache/commons/io/comparator/package-info.java b/src/main/java/org/apache/commons/io/comparator/package-info.java index 08ae0e9d840..e48cd30c041 100644 --- a/src/main/java/org/apache/commons/io/comparator/package-info.java +++ b/src/main/java/org/apache/commons/io/comparator/package-info.java @@ -20,8 +20,8 @@ * for {@link java.io.File}s and {@link java.nio.file.Path}. *

    Sorting

    *

    - * All the comparators include convenience utility sort(File...) and - * sort(List) methods. + * All the comparators include convenience utility {@code sort(File...)} and + * {@code sort(List)} methods. *

    *

    * For example, to sort the files in a directory by name: @@ -62,42 +62,42 @@ *

  • DefaultFileComparator - default file compare: *
      *
    • DEFAULT_COMPARATOR - * - Compare using File.compareTo(File) method. + * - Compare using {@code File.compareTo(File)} method. *
    • *
    • DEFAULT_REVERSE - * - Reverse compare of File.compareTo(File) method. + * - Reverse compare of {@code File.compareTo(File)} method. *
    • *
    *
  • *
  • DirectoryFileComparator - compare by type (directory or file): *
      *
    • DIRECTORY_COMPARATOR - * - Compare using File.isDirectory() method (directories < files). + * - Compare using {@code File.isDirectory()} method (directories < files). *
    • *
    • DIRECTORY_REVERSE - * - Reverse compare of File.isDirectory() method (directories >files). + * - Reverse compare of {@code File.isDirectory()} method (directories >files). *
    • *
    *
  • *
  • ExtensionFileComparator - compare file extensions: *
      *
    • EXTENSION_COMPARATOR - * - Compare using FilenameUtils.getExtension(String) method. + * - Compare using {@code FilenameUtils.getExtension(String)} method. *
    • *
    • EXTENSION_REVERSE - * - Reverse compare of FilenameUtils.getExtension(String) method. + * - Reverse compare of {@code FilenameUtils.getExtension(String)} method. *
    • *
    • EXTENSION_INSENSITIVE_COMPARATOR - * - Case-insensitive compare using FilenameUtils.getExtension(String) method. + * - Case-insensitive compare using {@code FilenameUtils.getExtension(String)} method. *
    • *
    • EXTENSION_INSENSITIVE_REVERSE - * - Reverse case-insensitive compare of FilenameUtils.getExtension(String) method. + * - Reverse case-insensitive compare of {@code FilenameUtils.getExtension(String)} method. *
    • *
    • EXTENSION_SYSTEM_COMPARATOR - * - System sensitive compare using FilenameUtils.getExtension(String) method. + * - System sensitive compare using {@code FilenameUtils.getExtension(String)} method. *
    • *
    • EXTENSION_SYSTEM_REVERSE - * - Reverse system sensitive compare of FilenameUtils.getExtension(String) method. + * - Reverse system sensitive compare of {@code FilenameUtils.getExtension(String)} method. *
    • *
    *
  • @@ -105,71 +105,71 @@ * - compare the file's last modified date/time: *
      *
    • LASTMODIFIED_COMPARATOR - * - Compare using File.lastModified() method. + * - Compare using {@code File.lastModified()} method. *
    • *
    • LASTMODIFIED_REVERSE - * - Reverse compare of File.lastModified() method. + * - Reverse compare of {@code File.lastModified()} method. *
    • *
    * *
  • NameFileComparator - compare file names: *
      *
    • NAME_COMPARATOR - * - Compare using File.getName() method. + * - Compare using {@code File.getName()} method. *
    • *
    • NAME_REVERSE - * - Reverse compare of File.getName() method. + * - Reverse compare of {@code File.getName()} method. *
    • *
    • NAME_INSENSITIVE_COMPARATOR - * - Case-insensitive compare using File.getName() method. + * - Case-insensitive compare using {@code File.getName()} method. *
    • *
    • NAME_INSENSITIVE_REVERSE - * - Reverse case-insensitive compare of File.getName() method. + * - Reverse case-insensitive compare of {@code File.getName()} method. *
    • *
    • NAME_SYSTEM_COMPARATOR - * - System sensitive compare using File.getName() method. + * - System sensitive compare using {@code File.getName()} method. *
    • *
    • NAME_SYSTEM_REVERSE - * - Reverse system sensitive compare of File.getName() method. + * - Reverse system sensitive compare of {@code File.getName()} method. *
    • *
    *
  • *
  • PathFileComparator - compare file paths: *
      *
    • PATH_COMPARATOR - * - Compare using File.getPath() method. + * - Compare using {@code File.getPath()} method. *
    • *
    • PATH_REVERSE - * - Reverse compare of File.getPath() method. + * - Reverse compare of {@code File.getPath()} method. *
    • *
    • PATH_INSENSITIVE_COMPARATOR - * - Case-insensitive compare using File.getPath() method. + * - Case-insensitive compare using {@code File.getPath()} method. *
    • *
    • PATH_INSENSITIVE_REVERSE - * - Reverse case-insensitive compare of File.getPath() method. + * - Reverse case-insensitive compare of {@code File.getPath()} method. *
    • *
    • PATH_SYSTEM_COMPARATOR - * - System sensitive compare using File.getPath() method. + * - System sensitive compare using {@code File.getPath()} method. *
    • *
    • PATH_SYSTEM_REVERSE - * - Reverse system sensitive compare of File.getPath() method. + * - Reverse system sensitive compare of {@code File.getPath()} method. *
    • *
    *
  • *
  • SizeFileComparator - compare the file's size: *
      *
    • SIZE_COMPARATOR - * - Compare using File.length() method (directories treated as zero length). + * - Compare using {@code File.length()} method (directories treated as zero length). *
    • *
    • LASTMODIFIED_REVERSE - * - Reverse compare of File.length() method (directories treated as zero length). + * - Reverse compare of {@code File.length()} method (directories treated as zero length). *
    • *
    • SIZE_SUMDIR_COMPARATOR - * - Compare using FileUtils.sizeOfDirectory(File) method + * - Compare using {@code FileUtils.sizeOfDirectory(File)} method * (sums the size of a directory's contents). *
    • *
    • SIZE_SUMDIR_REVERSE - * - Reverse compare of FileUtils.sizeOfDirectory(File) method + * - Reverse compare of {@code FileUtils.sizeOfDirectory(File)} method * (sums the size of a directory's contents). *
    • *
    diff --git a/src/main/java/org/apache/commons/io/doc-files/leaf.svg b/src/main/java/org/apache/commons/io/doc-files/leaf.svg new file mode 100644 index 00000000000..71de588c648 --- /dev/null +++ b/src/main/java/org/apache/commons/io/doc-files/leaf.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/org/apache/commons/io/doc-files/logo.png b/src/main/java/org/apache/commons/io/doc-files/logo.png new file mode 100644 index 00000000000..02a758f0ed8 Binary files /dev/null and b/src/main/java/org/apache/commons/io/doc-files/logo.png differ diff --git a/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java b/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java index 16f1e26756b..ab02ad073e2 100644 --- a/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java +++ b/src/main/java/org/apache/commons/io/file/CountingPathVisitor.java @@ -80,7 +80,7 @@ PathCounters getPathCounters() { * Sets how to filter directories. * * @param directoryFilter how to filter files. - * @return this instance. + * @return {@code this} instance. */ public B setDirectoryFilter(final PathFilter directoryFilter) { this.directoryFilter = directoryFilter != null ? directoryFilter : defaultDirectoryFilter(); @@ -91,7 +91,7 @@ public B setDirectoryFilter(final PathFilter directoryFilter) { * Sets how to transform directories, defaults to {@link UnaryOperator#identity()}. * * @param directoryTransformer how to filter files. - * @return this instance. + * @return {@code this} instance. */ public B setDirectoryPostTransformer(final UnaryOperator directoryTransformer) { this.directoryPostTransformer = directoryTransformer != null ? directoryTransformer : defaultDirectoryTransformer(); @@ -102,7 +102,7 @@ public B setDirectoryPostTransformer(final UnaryOperator directoryTransfor * Sets how to filter files. * * @param fileFilter how to filter files. - * @return this instance. + * @return {@code this} instance. */ public B setFileFilter(final PathFilter fileFilter) { this.fileFilter = fileFilter != null ? fileFilter : defaultFileFilter(); @@ -113,7 +113,7 @@ public B setFileFilter(final PathFilter fileFilter) { * Sets how to count path visits. * * @param pathCounters How to count path visits. - * @return this instance. + * @return {@code this} instance. */ public B setPathCounters(final PathCounters pathCounters) { this.pathCounters = pathCounters != null ? pathCounters : defaultPathCounters(); diff --git a/src/main/java/org/apache/commons/io/file/PathUtils.java b/src/main/java/org/apache/commons/io/file/PathUtils.java index 5708a92a7c8..055bbe06a0b 100644 --- a/src/main/java/org/apache/commons/io/file/PathUtils.java +++ b/src/main/java/org/apache/commons/io/file/PathUtils.java @@ -1096,6 +1096,19 @@ private static Path getParent(final Path path) { return path == null ? null : path.getParent(); } + /** + * Gets the system property with the specified name as a Path, or the default value as a Path if there is no property with that key. + * + * @param key the name of the system property. + * @param defaultPath a default path, may be null. + * @return the resulting {@code Path}, or the default value as a Path if there is no property with that key. + * @since 2.21.0 + */ + public static Path getPath(final String key, final String defaultPath) { + final String property = key != null && !key.isEmpty() ? System.getProperty(key, defaultPath) : defaultPath; + return property != null ? Paths.get(property) : null; + } + /** * Shorthand for {@code Files.getFileAttributeView(path, PosixFileAttributeView.class)}. * @@ -1956,8 +1969,8 @@ public static boolean waitFor(final Path file, final Duration timeout, final Lin @SuppressWarnings("resource") // Caller closes public static Stream walk(final Path start, final PathFilter pathFilter, final int maxDepth, final boolean readAttributes, final FileVisitOption... options) throws IOException { - return Files.walk(start, maxDepth, options) - .filter(path -> pathFilter.accept(path, readAttributes ? readBasicFileAttributesUnchecked(path) : null) == FileVisitResult.CONTINUE); + return Files.walk(start, maxDepth, options).filter( + path -> pathFilter.accept(path, readAttributes ? readBasicFileAttributes(path, EMPTY_LINK_OPTION_ARRAY) : null) == FileVisitResult.CONTINUE); } private static R withPosixFileAttributes(final Path path, final LinkOption[] linkOptions, final boolean overrideReadOnly, diff --git a/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java b/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java index 85007c90265..3b32cb09c88 100644 --- a/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java +++ b/src/main/java/org/apache/commons/io/file/SimplePathVisitor.java @@ -62,7 +62,7 @@ IOBiFunction getVisitFileFailedFunction() { *

    * * @param visitFileFailedFunction the function to call on {@link #visitFileFailed(Path, IOException)}. - * @return this instance. + * @return {@code this} instance. */ public B setVisitFileFailedFunction(final IOBiFunction visitFileFailedFunction) { this.visitFileFailedFunction = visitFileFailedFunction; diff --git a/src/main/java/org/apache/commons/io/filefilter/FileFilterUtils.java b/src/main/java/org/apache/commons/io/filefilter/FileFilterUtils.java index 5e37676ac9c..937895c822c 100644 --- a/src/main/java/org/apache/commons/io/filefilter/FileFilterUtils.java +++ b/src/main/java/org/apache/commons/io/filefilter/FileFilterUtils.java @@ -153,7 +153,7 @@ public static IOFileFilter and(final IOFileFilter... filters) { * @return a filter that ANDs the two specified filters * @see #and(IOFileFilter...) * @see AndFileFilter - * @deprecated use {@link #and(IOFileFilter...)} + * @deprecated Use {@link #and(IOFileFilter...)} */ @Deprecated public static IOFileFilter andFileFilter(final IOFileFilter filter1, final IOFileFilter filter2) { @@ -603,7 +603,7 @@ public static IOFileFilter or(final IOFileFilter... filters) { * @return a filter that ORs the two specified filters * @see #or(IOFileFilter...) * @see OrFileFilter - * @deprecated use {@link #or(IOFileFilter...)} + * @deprecated Use {@link #or(IOFileFilter...)} */ @Deprecated public static IOFileFilter orFileFilter(final IOFileFilter filter1, final IOFileFilter filter2) { diff --git a/src/main/java/org/apache/commons/io/filefilter/MagicNumberFileFilter.java b/src/main/java/org/apache/commons/io/filefilter/MagicNumberFileFilter.java index 79aa55d68c1..5868af39a32 100644 --- a/src/main/java/org/apache/commons/io/filefilter/MagicNumberFileFilter.java +++ b/src/main/java/org/apache/commons/io/filefilter/MagicNumberFileFilter.java @@ -218,7 +218,7 @@ public MagicNumberFileFilter(final String magicNumber) { * MagicNumberFileFilter tarFileFilter = MagicNumberFileFilter("ustar", 257); * *

    - * This method uses the virtual machine's {@link Charset#defaultCharset() default charset}. + * This method uses the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * * @param magicNumber the magic number to look for in the file. diff --git a/src/main/java/org/apache/commons/io/filefilter/package-info.java b/src/main/java/org/apache/commons/io/filefilter/package-info.java index 99cadae0468..b82f04a445d 100644 --- a/src/main/java/org/apache/commons/io/filefilter/package-info.java +++ b/src/main/java/org/apache/commons/io/filefilter/package-info.java @@ -148,7 +148,7 @@ * } * *

    Using NIO

    - *

    You can combine Java file tree walking by using java.nio.file.Files.walk() APIs with filters:

    + *

    You can combine Java file tree walking by using {@code java.nio.file.Files.walk()} APIs with filters:

    *
      * final Path dir = Paths.get("");
      * // We are interested in files older than one day
    diff --git a/src/main/java/org/apache/commons/io/function/IOIterable.java b/src/main/java/org/apache/commons/io/function/IOIterable.java
    index 2f35f81ea29..238040d1d65 100644
    --- a/src/main/java/org/apache/commons/io/function/IOIterable.java
    +++ b/src/main/java/org/apache/commons/io/function/IOIterable.java
    @@ -18,6 +18,7 @@
     package org.apache.commons.io.function;
     
     import java.io.IOException;
    +import java.io.UncheckedIOException;
     import java.util.Objects;
     
     /**
    @@ -28,6 +29,17 @@
      */
     public interface IOIterable {
     
    +    /**
    +     * Creates an {@link Iterable} for this instance that throws {@link UncheckedIOException} instead of
    +     * {@link IOException}.
    +     *
    +     * @return an {@link UncheckedIOException} {@link Iterable}.
    +     * @since 2.21.0
    +     */
    +    default Iterable asIterable() {
    +        return new UncheckedIOIterable<>(this);
    +    }
    +
         /**
          * Like {@link Iterable#iterator()}.
          *
    diff --git a/src/main/java/org/apache/commons/io/input/BOMInputStream.java b/src/main/java/org/apache/commons/io/input/BOMInputStream.java
    index c5872d7cbc9..acfe8d9ddbb 100644
    --- a/src/main/java/org/apache/commons/io/input/BOMInputStream.java
    +++ b/src/main/java/org/apache/commons/io/input/BOMInputStream.java
    @@ -419,8 +419,10 @@ public int read() throws IOException {
          * Invokes the delegate's {@code read(byte[])} method, detecting and optionally skipping BOM.
          *
          * @param buf
    -     *            the buffer to read the bytes into
    +     *            the buffer to read the bytes into, never {@code null}
          * @return the number of bytes read (excluding BOM) or -1 if the end of stream
    +     * @throws NullPointerException
    +     *             if the buffer is {@code null}
          * @throws IOException
          *             if an I/O error occurs
          */
    @@ -439,11 +441,19 @@ public int read(final byte[] buf) throws IOException {
          * @param len
          *            The number of bytes to read (excluding BOM)
          * @return the number of bytes read or -1 if the end of stream
    +     * @throws NullPointerException
    +     *             if the buffer is {@code null}
    +     * @throws IndexOutOfBoundsException
    +     *             if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code buf.length}
          * @throws IOException
          *             if an I/O error occurs
          */
         @Override
         public int read(final byte[] buf, int off, int len) throws IOException {
    +        IOUtils.checkFromIndexSize(buf, off, len);
    +        if (len == 0) {
    +            return 0;
    +        }
             int firstCount = 0;
             int b = 0;
             while (len > 0 && b >= 0) {
    diff --git a/src/main/java/org/apache/commons/io/input/BoundedInputStream.java b/src/main/java/org/apache/commons/io/input/BoundedInputStream.java
    index c950d0493b1..e3e2d86bf0f 100644
    --- a/src/main/java/org/apache/commons/io/input/BoundedInputStream.java
    +++ b/src/main/java/org/apache/commons/io/input/BoundedInputStream.java
    @@ -73,12 +73,12 @@
      *   .get();
      * }
      * 
    - *

    Listening for the max count reached

    + *

    Listening for the maximum count reached

    *
    {@code
      * BoundedInputStream s = BoundedInputStream.builder()
      *   .setPath(Paths.get("MyFile.xml"))
      *   .setMaxCount(1024)
    - *   .setOnMaxCount((max, count) -> System.out.printf("Max count %,d reached with a last read count of %,d%n", max, count))
    + *   .setOnMaxCount((max, count) -> System.out.printf("Maximum count %,d reached with a last read count of %,d%n", max, count))
      *   .get();
      * }
      * 
    @@ -98,7 +98,7 @@ abstract static class AbstractBuilder> extends Prox /** The current count of bytes counted. */ private long count; - /** The max count of bytes to read. */ + /** The maximum count of bytes to read. */ private long maxCount = EOF; private IOBiConsumer onMaxCount = IOBiConsumer.noop(); @@ -156,7 +156,8 @@ public T setMaxCount(final long maxCount) { /** * Sets the default {@link BoundedInputStream#onMaxLength(long, long)} behavior, {@code null} resets to a NOOP. *

    - * The first Long is the max count of bytes to read. The second Long is the count of bytes read. + * The first Long is the number of bytes remaining to read before the maximum is reached count of bytes to read. The second Long is the count of bytes + * read. *

    *

    * This does not override a {@code BoundedInputStream} subclass' implementation of the {@link BoundedInputStream#onMaxLength(long, long)} @@ -164,7 +165,7 @@ public T setMaxCount(final long maxCount) { *

    * * @param onMaxCount the {@link ProxyInputStream#afterRead(int)} behavior. - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public T setOnMaxCount(final IOBiConsumer onMaxCount) { @@ -263,10 +264,10 @@ public Builder() { *
      *
    • {@link #getInputStream()} gets the target aspect.
    • *
    • {@link #getAfterRead()}
    • - *
    • {@link #getCount()}
    • - *
    • {@link #getMaxCount()}
    • - *
    • {@link #getOnMaxCount()}
    • - *
    • {@link #isPropagateClose()}
    • + *
    • {@code #getCount()}
    • + *
    • {@code #getMaxCount()}
    • + *
    • {@code #getOnMaxCount()}
    • + *
    • {@code #isPropagateClose()}
    • *
    * * @return a new instance. @@ -299,7 +300,7 @@ public static Builder builder() { /** The current mark. */ private long mark; - /** The max count of bytes to read. */ + /** The maximum count of bytes to read. */ private final long maxCount; private final IOBiConsumer onMaxCount; @@ -311,7 +312,7 @@ public static Builder builder() { */ private boolean propagateClose = true; - BoundedInputStream(final Builder builder) throws IOException { + private BoundedInputStream(final Builder builder) throws IOException { super(builder); this.count = builder.getCount(); this.maxCount = builder.getMaxCount(); @@ -333,7 +334,7 @@ public BoundedInputStream(final InputStream in) { this(in, EOF); } - BoundedInputStream(final InputStream inputStream, final Builder builder) { + private BoundedInputStream(final InputStream inputStream, final Builder builder) { super(inputStream, builder); this.count = builder.getCount(); this.maxCount = builder.getMaxCount(); @@ -350,7 +351,7 @@ public BoundedInputStream(final InputStream in) { */ @Deprecated public BoundedInputStream(final InputStream inputStream, final long maxCount) { - // Some badly designed methods - e.g. the Servlet API - overload length + // Some badly designed methods, for example the Servlet API, overload length // such that "-1" means stream finished this(inputStream, builder().setMaxCount(maxCount)); } @@ -358,7 +359,7 @@ public BoundedInputStream(final InputStream inputStream, final long maxCount) { /** * Adds the number of read bytes to the count. * - * @param n number of bytes read, or -1 if no more bytes are available + * @param n number of bytes read, or -1 if no more bytes are available. * @throws IOException Not thrown here but subclasses may throw. * @since 2.0 */ @@ -370,16 +371,11 @@ protected synchronized void afterRead(final int n) throws IOException { super.afterRead(n); } - /** - * {@inheritDoc} - */ @Override public int available() throws IOException { - if (isMaxCount()) { - onMaxLength(maxCount, getCount()); - return 0; - } - return in.available(); + // Safe cast: value is between 0 and Integer.MAX_VALUE + final int remaining = (int) Math.min(getRemaining(), Integer.MAX_VALUE); + return Math.min(super.available(), remaining); } /** @@ -405,9 +401,9 @@ public synchronized long getCount() { } /** - * Gets the max count of bytes to read. + * Gets the maximum number of bytes to read. * - * @return The max count of bytes to read. + * @return The maximum number of bytes to read, or {@value IOUtils#EOF} if unbounded. * @since 2.16.0 */ public long getMaxCount() { @@ -415,9 +411,9 @@ public long getMaxCount() { } /** - * Gets the max count of bytes to read. + * Gets the maximum count of bytes to read. * - * @return The max count of bytes to read. + * @return The maximum count of bytes to read. * @since 2.12.0 * @deprecated Use {@link #getMaxCount()}. */ @@ -427,13 +423,21 @@ public long getMaxLength() { } /** - * Gets how many bytes remain to read. + * Gets the number of bytes remaining to read before the maximum is reached. + * + *

    + * This method does not report the bytes available in the + * underlying stream; it only reflects the remaining allowance imposed by this + * {@code BoundedInputStream}. + *

    * - * @return bytes how many bytes remain to read. + * @return The number of bytes remaining to read before the maximum is reached, + * or {@link Long#MAX_VALUE} if no bound is set. * @since 2.16.0 */ public long getRemaining() { - return Math.max(0, getMaxCount() - getCount()); + final long maxCount = getMaxCount(); + return maxCount == EOF ? Long.MAX_VALUE : Math.max(0, maxCount - getCount()); } private boolean isMaxCount() { @@ -452,7 +456,7 @@ public boolean isPropagateClose() { /** * Invokes the delegate's {@link InputStream#mark(int)} method. * - * @param readLimit read ahead limit + * @param readLimit read ahead limit. */ @Override public synchronized void mark(final int readLimit) { @@ -463,7 +467,7 @@ public synchronized void mark(final int readLimit) { /** * Invokes the delegate's {@link InputStream#markSupported()} method. * - * @return true if mark is supported, otherwise false + * @return true if mark is supported, otherwise false. */ @Override public boolean markSupported() { @@ -476,7 +480,7 @@ public boolean markSupported() { * Delegates to the consumer set in {@link Builder#setOnMaxCount(IOBiConsumer)}. *

    * - * @param max The max count of bytes to read. + * @param max The maximum count of bytes to read. * @param count The count of bytes read. * @throws IOException Subclasses may throw. * @since 2.12.0 @@ -505,7 +509,7 @@ public int read() throws IOException { /** * Invokes the delegate's {@link InputStream#read(byte[])} method. * - * @param b the buffer to read the bytes into + * @param b the buffer to read the bytes into. * @return the number of bytes read or -1 if the end of stream or the limit has been reached. * @throws IOException if an I/O error occurs. */ @@ -517,9 +521,9 @@ public int read(final byte[] b) throws IOException { /** * Invokes the delegate's {@link InputStream#read(byte[], int, int)} method. * - * @param b the buffer to read the bytes into - * @param off The start offset - * @param len The number of bytes to read + * @param b the buffer to read the bytes into. + * @param off The start offset. + * @param len The number of bytes to read. * @return the number of bytes read or -1 if the end of stream or the limit has been reached. * @throws IOException if an I/O error occurs. */ @@ -558,8 +562,8 @@ public synchronized void setPropagateClose(final boolean propagateClose) { /** * Invokes the delegate's {@link InputStream#skip(long)} method. * - * @param n the number of bytes to skip - * @return the actual number of bytes skipped + * @param n the number of bytes to skip. + * @return the actual number of bytes skipped. * @throws IOException if an I/O error occurs. */ @Override @@ -569,6 +573,15 @@ public synchronized long skip(final long n) throws IOException { return skip; } + /** + * Converts a request to read {@code len} bytes to a lower count if reading would put us over the limit. + *

    + * If a {@code maxCount} is not set, then return max{@code maxCount}. + *

    + * + * @param len The requested byte count. + * @return How many bytes to actually attempt to read. + */ private long toReadLen(final long len) { return maxCount >= 0 ? Math.min(len, maxCount - getCount()) : len; } @@ -576,7 +589,7 @@ private long toReadLen(final long len) { /** * Invokes the delegate's {@link InputStream#toString()} method. * - * @return the delegate's {@link InputStream#toString()} + * @return the delegate's {@link InputStream#toString()}. */ @Override public String toString() { diff --git a/src/main/java/org/apache/commons/io/input/BoundedReader.java b/src/main/java/org/apache/commons/io/input/BoundedReader.java index b6bf15f658f..5c1c255c0fe 100644 --- a/src/main/java/org/apache/commons/io/input/BoundedReader.java +++ b/src/main/java/org/apache/commons/io/input/BoundedReader.java @@ -23,6 +23,8 @@ import java.io.IOException; import java.io.Reader; +import org.apache.commons.io.IOUtils; + /** * A reader that imposes a limit to the number of characters that can be read from an underlying reader, returning EOF * when this limit is reached, regardless of state of underlying reader. @@ -77,7 +79,7 @@ public void close() throws IOException { * is no way to pass past maxCharsFromTargetReader, even if this value is greater. * * @throws IOException If an I/O error occurs while calling the underlying reader's mark method - * @see java.io.Reader#mark(int) + * @see Reader#mark(int) */ @Override public void mark(final int readAheadLimit) throws IOException { @@ -93,7 +95,7 @@ public void mark(final int readAheadLimit) throws IOException { * * @return -1 on EOF or the character read * @throws IOException If an I/O error occurs while calling the underlying reader's read method - * @see java.io.Reader#read() + * @see Reader#read() */ @Override public int read() throws IOException { @@ -116,11 +118,14 @@ public int read() throws IOException { * @param off The offset * @param len The number of chars to read * @return the number of chars read + * @throws NullPointerException if the buffer is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code cbuf.length}. * @throws IOException If an I/O error occurs while calling the underlying reader's read method - * @see java.io.Reader#read(char[], int, int) + * @see Reader#read(char[], int, int) */ @Override public int read(final char[] cbuf, final int off, final int len) throws IOException { + IOUtils.checkFromIndexSize(cbuf, off, len); int c; for (int i = 0; i < len; i++) { c = read(); @@ -136,7 +141,7 @@ public int read(final char[] cbuf, final int off, final int len) throws IOExcept * Resets the target to the latest mark, * * @throws IOException If an I/O error occurs while calling the underlying reader's reset method - * @see java.io.Reader#reset() + * @see Reader#reset() */ @Override public void reset() throws IOException { diff --git a/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java b/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java index 220b21d614f..8e81b34508a 100644 --- a/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java +++ b/src/main/java/org/apache/commons/io/input/BufferedFileChannelInputStream.java @@ -115,7 +115,7 @@ public BufferedFileChannelInputStream get() throws IOException { *

    * * @param fileChannel the file channel. - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder setFileChannel(final FileChannel fileChannel) { @@ -254,8 +254,9 @@ public synchronized int read() throws IOException { @Override public synchronized int read(final byte[] b, final int offset, int len) throws IOException { - if (offset < 0 || len < 0 || offset + len < 0 || offset + len > b.length) { - throw new IndexOutOfBoundsException(); + IOUtils.checkFromIndexSize(b, offset, len); + if (len == 0) { + return 0; } if (!refill()) { return EOF; diff --git a/src/main/java/org/apache/commons/io/input/CharSequenceInputStream.java b/src/main/java/org/apache/commons/io/input/CharSequenceInputStream.java index 8666105491a..874dcf1cb39 100644 --- a/src/main/java/org/apache/commons/io/input/CharSequenceInputStream.java +++ b/src/main/java/org/apache/commons/io/input/CharSequenceInputStream.java @@ -28,7 +28,6 @@ import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; -import java.util.Objects; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; @@ -319,10 +318,7 @@ public int read(final byte[] b) throws IOException { @Override public int read(final byte[] array, int off, int len) throws IOException { - Objects.requireNonNull(array, "array"); - if (len < 0 || off + len > array.length) { - throw new IndexOutOfBoundsException("Array Size=" + array.length + ", offset=" + off + ", length=" + len); - } + IOUtils.checkFromIndexSize(array, off, len); if (len == 0) { return 0; // must return 0 for zero length read } diff --git a/src/main/java/org/apache/commons/io/input/CharSequenceReader.java b/src/main/java/org/apache/commons/io/input/CharSequenceReader.java index 8fee366b2da..bb2be3692c9 100644 --- a/src/main/java/org/apache/commons/io/input/CharSequenceReader.java +++ b/src/main/java/org/apache/commons/io/input/CharSequenceReader.java @@ -20,7 +20,8 @@ import java.io.Reader; import java.io.Serializable; -import java.util.Objects; + +import org.apache.commons.io.IOUtils; /** * {@link Reader} implementation that can read from String, StringBuffer, @@ -204,19 +205,19 @@ public int read() { * @param array The array to store the characters in * @param offset The starting position in the array to store * @param length The maximum number of characters to read - * @return The number of characters read or -1 if there are - * no more + * @return The number of characters read or -1 if there are no more + * @throws NullPointerException if the array is {@code null}. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if {@code offset + length} is greater than {@code array.length}. */ @Override public int read(final char[] array, final int offset, final int length) { + IOUtils.checkFromIndexSize(array, offset, length); + if (length == 0) { + return 0; + } if (idx >= end()) { return EOF; } - Objects.requireNonNull(array, "array"); - if (length < 0 || offset < 0 || offset + length > array.length) { - throw new IndexOutOfBoundsException("Array Size=" + array.length + - ", offset=" + offset + ", length=" + length); - } if (charSequence instanceof String) { final int count = Math.min(length, end() - idx); diff --git a/src/main/java/org/apache/commons/io/input/ClassLoaderObjectInputStream.java b/src/main/java/org/apache/commons/io/input/ClassLoaderObjectInputStream.java index c9515b1d1a8..fef0d6cf17b 100644 --- a/src/main/java/org/apache/commons/io/input/ClassLoaderObjectInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ClassLoaderObjectInputStream.java @@ -80,7 +80,7 @@ protected Class resolveClass(final ObjectStreamClass objectStreamClass) * @return a proxy class implementing the interfaces * @throws IOException in case of an I/O error * @throws ClassNotFoundException if the Class cannot be found - * @see java.io.ObjectInputStream#resolveProxyClass(String[]) + * @see ObjectInputStream#resolveProxyClass(String[]) * @since 2.1 */ @Override diff --git a/src/main/java/org/apache/commons/io/input/ClosedInputStream.java b/src/main/java/org/apache/commons/io/input/ClosedInputStream.java index ceab0176090..11bc7be6e19 100644 --- a/src/main/java/org/apache/commons/io/input/ClosedInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ClosedInputStream.java @@ -79,13 +79,19 @@ public int read() { /** * Returns {@code -1} to indicate that the stream is closed. * - * @param b ignored. - * @param off ignored. - * @param len ignored. - * @return always -1. + * @param b The buffer to read bytes into. + * @param off The start offset. + * @param len The number of bytes to read. + * @return If len is zero, then {@code 0}; otherwise {@code -1}. + * @throws NullPointerException if the byte array is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code b.length}. */ @Override public int read(final byte[] b, final int off, final int len) throws IOException { + IOUtils.checkFromIndexSize(b, off, len); + if (len == 0) { + return 0; + } return EOF; } diff --git a/src/main/java/org/apache/commons/io/input/ClosedReader.java b/src/main/java/org/apache/commons/io/input/ClosedReader.java index 93c99e3c550..b50721c120d 100644 --- a/src/main/java/org/apache/commons/io/input/ClosedReader.java +++ b/src/main/java/org/apache/commons/io/input/ClosedReader.java @@ -62,15 +62,26 @@ public void close() throws IOException { } /** - * Returns -1 to indicate that the stream is closed. + * A no-op read method that always indicates end-of-stream. * - * @param cbuf ignored - * @param off ignored - * @param len ignored - * @return always -1 + *

    Behavior:

    + *
      + *
    • If {@code len == 0}, returns {@code 0} immediately (no characters are read).
    • + *
    • Otherwise, always returns {@value IOUtils#EOF} to signal that the stream is closed or at end-of-stream.
    • + *
    + * + * @param cbuf The destination buffer. + * @param off The offset at which to start storing characters. + * @param len The maximum number of characters to read. + * @return {@code 0} if {@code len == 0}; otherwise always {@value IOUtils#EOF}. + * @throws IndexOutOfBoundsException If {@code off < 0}, {@code len < 0}, or {@code off + len > cbuf.length}. */ @Override public int read(final char[] cbuf, final int off, final int len) { + IOUtils.checkFromIndexSize(cbuf, off, len); + if (len == 0) { + return 0; + } return EOF; } diff --git a/src/main/java/org/apache/commons/io/input/CountingInputStream.java b/src/main/java/org/apache/commons/io/input/CountingInputStream.java index 716f9ed5fcb..f14e7130507 100644 --- a/src/main/java/org/apache/commons/io/input/CountingInputStream.java +++ b/src/main/java/org/apache/commons/io/input/CountingInputStream.java @@ -28,6 +28,7 @@ * A typical use case would be during debugging, to ensure that data is being * read as expected. *

    + * * @deprecated Use {@link BoundedInputStream} (unbounded by default). */ @Deprecated @@ -149,7 +150,7 @@ public int resetCount() { * @param length the number of bytes to skip * @return the actual number of bytes skipped * @throws IOException if an I/O error occurs. - * @see java.io.InputStream#skip(long) + * @see InputStream#skip(long) */ @Override public synchronized long skip(final long length) throws IOException { diff --git a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java index 8fdd7aec8b9..22fae70ca75 100644 --- a/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java +++ b/src/main/java/org/apache/commons/io/input/MemoryMappedFileInputStream.java @@ -27,6 +27,7 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractStreamBuilder; /** @@ -216,6 +217,10 @@ public int read() throws IOException { @Override public int read(final byte[] b, final int off, final int len) throws IOException { + IOUtils.checkFromIndexSize(b, off, len); + if (len == 0) { + return 0; + } checkOpen(); if (!buffer.hasRemaining()) { nextBuffer(); diff --git a/src/main/java/org/apache/commons/io/input/NullInputStream.java b/src/main/java/org/apache/commons/io/input/NullInputStream.java index 2b0f751cba9..5fa9b817b5c 100644 --- a/src/main/java/org/apache/commons/io/input/NullInputStream.java +++ b/src/main/java/org/apache/commons/io/input/NullInputStream.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.InputStream; +import org.apache.commons.io.IOUtils; + /** * A lightweight {@link InputStream} that emulates a stream of a specified size. *

    @@ -186,7 +188,7 @@ private int handleEof() throws IOException { /** * Initializes or re-initializes this instance for reuse. * - * @return this instance. + * @return {@code this} instance. * @since 2.17.0 */ public NullInputStream init() { @@ -241,9 +243,9 @@ protected int processByte() { * This implementation leaves the byte array unchanged. *

    * - * @param bytes The byte array - * @param offset The offset to start at. - * @param length The number of bytes. + * @param bytes The byte array, never {@code null}. + * @param offset The offset to start at, always non-negative. + * @param length The number of bytes to process, always non-negative and at most {@code bytes.length - offset}. */ protected void processBytes(final byte[] bytes, final int offset, final int length) { // do nothing - overridable by subclass @@ -272,6 +274,7 @@ public int read() throws IOException { * * @param bytes The byte array to read into * @return The number of bytes read or {@code -1} if the end of file has been reached and {@code throwEofException} is set to {@code false}. + * @throws NullPointerException if the byte array is {@code null}. * @throws EOFException if the end of file is reached and {@code throwEofException} is set to {@code true}. * @throws IOException if trying to read past the end of file. */ @@ -287,12 +290,15 @@ public int read(final byte[] bytes) throws IOException { * @param offset The offset to start reading bytes into. * @param length The number of bytes to read. * @return The number of bytes read or {@code -1} if the end of file has been reached and {@code throwEofException} is set to {@code false}. + * @throws NullPointerException if the byte array is {@code null}. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if {@code offset + length} is greater than {@code bytes.length}. * @throws EOFException if the end of file is reached and {@code throwEofException} is set to {@code true}. * @throws IOException if trying to read past the end of file. */ @Override public int read(final byte[] bytes, final int offset, final int length) throws IOException { - if (bytes.length == 0 || length == 0) { + IOUtils.checkFromIndexSize(bytes, offset, length); + if (length == 0) { return 0; } checkOpen(); diff --git a/src/main/java/org/apache/commons/io/input/NullReader.java b/src/main/java/org/apache/commons/io/input/NullReader.java index c5ae530a170..28f3d37eb21 100644 --- a/src/main/java/org/apache/commons/io/input/NullReader.java +++ b/src/main/java/org/apache/commons/io/input/NullReader.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.io.Reader; +import org.apache.commons.io.IOUtils; + /** * A functional, lightweight {@link Reader} that emulates * a reader of a specified size. @@ -270,12 +272,18 @@ public int read(final char[] chars) throws IOException { * @return The number of characters read or {@code -1} * if the end of file has been reached and * {@code throwEofException} is set to {@code false}. + * @throws NullPointerException if the array is {@code null}. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if {@code offset + length} is greater than {@code chars.length}. * @throws EOFException if the end of file is reached and * {@code throwEofException} is set to {@code true}. * @throws IOException if trying to read past the end of file. */ @Override public int read(final char[] chars, final int offset, final int length) throws IOException { + IOUtils.checkFromIndexSize(chars, offset, length); + if (length == 0) { + return 0; + } if (eof) { throw new IOException("Read after end of file"); } diff --git a/src/main/java/org/apache/commons/io/input/ProxyInputStream.java b/src/main/java/org/apache/commons/io/input/ProxyInputStream.java index a2e38271973..b2e04c334b5 100644 --- a/src/main/java/org/apache/commons/io/input/ProxyInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ProxyInputStream.java @@ -24,8 +24,6 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractStreamBuilder; -import org.apache.commons.io.function.Erase; -import org.apache.commons.io.function.IOConsumer; import org.apache.commons.io.function.IOIntConsumer; /** @@ -88,7 +86,7 @@ public IOIntConsumer getAfterRead() { *

    * * @param afterRead the {@link ProxyInputStream#afterRead(int)} behavior. - * @return this instance. + * @return {@code this} instance. */ public B setAfterRead(final IOIntConsumer afterRead) { this.afterRead = afterRead; @@ -102,11 +100,6 @@ public B setAfterRead(final IOIntConsumer afterRead) { */ private volatile boolean closed; - /** - * Handles exceptions. - */ - private final IOConsumer exceptionHandler; - private final IOIntConsumer afterRead; /** @@ -130,7 +123,6 @@ protected ProxyInputStream(final AbstractBuilder builder) throws IOExcepti public ProxyInputStream(final InputStream proxy) { // the delegate is stored in a protected superclass variable named 'in'. super(proxy); - this.exceptionHandler = Erase::rethrow; this.afterRead = IOIntConsumer.NOOP; } @@ -144,7 +136,6 @@ public ProxyInputStream(final InputStream proxy) { protected ProxyInputStream(final InputStream proxy, final AbstractBuilder builder) { // the delegate is stored in a protected superclass instance variable named 'in'. super(proxy); - this.exceptionHandler = Erase::rethrow; this.afterRead = builder.getAfterRead() != null ? builder.getAfterRead() : IOIntConsumer.NOOP; } @@ -245,7 +236,7 @@ public void close() throws IOException { * @since 2.0 */ protected void handleIOException(final IOException e) throws IOException { - exceptionHandler.accept(e); + throw e; } /** @@ -370,7 +361,7 @@ public synchronized void reset() throws IOException { * Sets the underlying input stream. * * @param in The input stream to set in {@link java.io.FilterInputStream#in}. - * @return this instance. + * @return {@code this} instance. * @since 2.19.0 */ public ProxyInputStream setReference(final InputStream in) { diff --git a/src/main/java/org/apache/commons/io/input/QueueInputStream.java b/src/main/java/org/apache/commons/io/input/QueueInputStream.java index 1515eb94036..5f701945cb3 100644 --- a/src/main/java/org/apache/commons/io/input/QueueInputStream.java +++ b/src/main/java/org/apache/commons/io/input/QueueInputStream.java @@ -30,6 +30,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractStreamBuilder; import org.apache.commons.io.output.QueueOutputStream; @@ -244,13 +245,7 @@ public int read() { */ @Override public int read(final byte[] b, final int offset, final int length) { - if (b == null) { - throw new NullPointerException(); - } - if (offset < 0 || length < 0 || length > b.length - offset) { - throw new IndexOutOfBoundsException( - String.format("Range [%d, %<= 0) { - throw new IllegalArgumentException("bufferSizeInBytes should be greater than 0, but the value is " + bufferSizeInBytes); + throw new IllegalArgumentException(String.format("bufferSizeInBytes <= 0, bufferSizeInBytes = %,d", bufferSizeInBytes)); } this.executorService = Objects.requireNonNull(executorService, "executorService"); this.shutdownExecutorService = shutdownExecutorService; @@ -341,9 +342,7 @@ public int read() throws IOException { @Override public int read(final byte[] b, final int offset, int len) throws IOException { - if (offset < 0 || len < 0 || len > b.length - offset) { - throw new IndexOutOfBoundsException(); - } + IOUtils.checkFromIndexSize(b, offset, len); if (len == 0) { return 0; } diff --git a/src/main/java/org/apache/commons/io/input/ReaderInputStream.java b/src/main/java/org/apache/commons/io/input/ReaderInputStream.java index bcd03abd565..09d7bbaa1a8 100644 --- a/src/main/java/org/apache/commons/io/input/ReaderInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ReaderInputStream.java @@ -30,7 +30,6 @@ import java.nio.charset.CharsetEncoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; -import java.util.Objects; import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; @@ -226,8 +225,8 @@ private ReaderInputStream(final Builder builder) throws IOException { } /** - * Constructs a new {@link ReaderInputStream} that uses the virtual machine's {@link Charset#defaultCharset() default charset} with a default input buffer - * size of {@value IOUtils#DEFAULT_BUFFER_SIZE} characters. + * Constructs a new {@link ReaderInputStream} that uses the virtual machine's {@linkplain Charset#defaultCharset() default charset} with a default input + * buffer size of {@value IOUtils#DEFAULT_BUFFER_SIZE} characters. * * @param reader the target {@link Reader} * @deprecated Use {@link ReaderInputStream#builder()} instead @@ -436,8 +435,9 @@ public int read() throws IOException { /** * Reads the specified number of bytes into an array. * - * @param b the byte array to read into + * @param b the byte array to read into, must not be {@code null} * @return the number of bytes read or {@code -1} if the end of the stream has been reached + * @throws NullPointerException if the byte array is {@code null}. * @throws IOException if an I/O error occurs. */ @Override @@ -452,18 +452,17 @@ public int read(final byte[] b) throws IOException { * @param off the offset to start reading bytes into * @param len the number of bytes to read * @return the number of bytes read or {@code -1} if the end of the stream has been reached + * @throws NullPointerException if the byte array is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code array.length}. * @throws IOException if an I/O error occurs. */ @Override public int read(final byte[] array, int off, int len) throws IOException { - Objects.requireNonNull(array, "array"); - if (len < 0 || off < 0 || off + len > array.length) { - throw new IndexOutOfBoundsException("Array size=" + array.length + ", offset=" + off + ", length=" + len); - } - int read = 0; + IOUtils.checkFromIndexSize(array, off, len); if (len == 0) { return 0; // Always return 0 if len == 0 } + int read = 0; while (len > 0) { if (encoderOut.hasRemaining()) { // Data from the last read not fully copied final int c = Math.min(encoderOut.remaining(), len); diff --git a/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java b/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java index 949ff8cbd8e..e88606d9e76 100644 --- a/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java +++ b/src/main/java/org/apache/commons/io/input/ReversedLinesFileReader.java @@ -25,7 +25,6 @@ import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.ArrayList; @@ -93,6 +92,7 @@ public static class Builder extends AbstractStreamBuilder 0) { - this.totalBlockCount = this.totalByteLength / blockSize + 1; + this.totalBlockCount = totalByteLength / blockSize + 1; } else { - this.totalBlockCount = this.totalByteLength / blockSize; - if (this.totalByteLength > 0) { + this.totalBlockCount = totalByteLength / blockSize; + if (totalByteLength > 0) { lastBlockLength = blockSize; } } @@ -354,7 +354,7 @@ private ReversedLinesFileReader(final Builder builder) throws IOException { } /** - * Constructs a ReversedLinesFileReader with default block size of 4KB and the virtual machine's {@link Charset#defaultCharset() default charset}. + * Constructs a ReversedLinesFileReader with default block size of 4KB and the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * * @param file the file to be read * @throws IOException if an I/O error occurs. diff --git a/src/main/java/org/apache/commons/io/input/SequenceReader.java b/src/main/java/org/apache/commons/io/input/SequenceReader.java index 0fbb68f5e4a..daeb71faa92 100644 --- a/src/main/java/org/apache/commons/io/input/SequenceReader.java +++ b/src/main/java/org/apache/commons/io/input/SequenceReader.java @@ -25,6 +25,7 @@ import java.util.Iterator; import java.util.Objects; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.function.Uncheck; /** @@ -62,7 +63,7 @@ public SequenceReader(final Reader... readers) { /* * (non-Javadoc) * - * @see java.io.Reader#close() + * @see Reader#close() */ @Override public void close() throws IOException { @@ -92,7 +93,7 @@ private Reader nextReader() throws IOException { /* * (non-Javadoc) * - * @see java.io.Reader#read(char[], int, int) + * @see Reader#read(char[], int, int) */ @Override public int read() throws IOException { @@ -107,16 +108,11 @@ public int read() throws IOException { return c; } - /* - * (non-Javadoc) - * - * @see java.io.Reader#read() - */ @Override public int read(final char[] cbuf, int off, int len) throws IOException { - Objects.requireNonNull(cbuf, "cbuf"); - if (len < 0 || off < 0 || off + len > cbuf.length) { - throw new IndexOutOfBoundsException("Array Size=" + cbuf.length + ", offset=" + off + ", length=" + len); + IOUtils.checkFromIndexSize(cbuf, off, len); + if (len == 0) { + return 0; } int count = 0; while (reader != null) { diff --git a/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java b/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java index 0070eafd5cb..a901ec6bc74 100644 --- a/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java +++ b/src/main/java/org/apache/commons/io/input/SwappedDataInputStream.java @@ -44,7 +44,7 @@ public SwappedDataInputStream(final InputStream input) { } /** - * Return {@link #readByte()} != 0 + * Return {@code {@link #readByte()} != 0} * * @return false if the byte read is zero, otherwise true * @throws IOException if an I/O error occurs. @@ -68,7 +68,7 @@ public byte readByte() throws IOException, EOFException { } /** - * Reads a 2 byte, unsigned, little endian UTF-16 code point. + * Reads a 2 byte, unsigned, little-endian UTF-16 code point. * * @return the UTF-16 code point read or -1 if the end of stream * @throws IOException if an I/O error occurs. diff --git a/src/main/java/org/apache/commons/io/input/Tailer.java b/src/main/java/org/apache/commons/io/input/Tailer.java index 4d9fe9c4751..620ba6febcd 100644 --- a/src/main/java/org/apache/commons/io/input/Tailer.java +++ b/src/main/java/org/apache/commons/io/input/Tailer.java @@ -495,7 +495,7 @@ public String toString() { private static final String RAF_READ_ONLY_MODE = "r"; /** - * The the virtual machine's {@link Charset#defaultCharset() default charset} used for reading files. + * The the virtual machine's {@linkplain Charset#defaultCharset() default charset} used for reading files. */ private static final Charset DEFAULT_CHARSET = Charset.defaultCharset(); diff --git a/src/main/java/org/apache/commons/io/input/ThrottledInputStream.java b/src/main/java/org/apache/commons/io/input/ThrottledInputStream.java index 9f4d9f01c04..b081db3c6e5 100644 --- a/src/main/java/org/apache/commons/io/input/ThrottledInputStream.java +++ b/src/main/java/org/apache/commons/io/input/ThrottledInputStream.java @@ -130,7 +130,7 @@ public ThrottledInputStream get() throws IOException { * * @param value the maximum bytes * @param chronoUnit a duration scale goal. - * @return this instance. + * @return {@code this} instance. * @throws IllegalArgumentException Thrown if maxBytesPerSecond <= 0. * @since 2.19.0 */ @@ -153,7 +153,7 @@ public Builder setMaxBytes(final long value, final ChronoUnit chronoUnit) { * * @param value the maximum bytes * @param duration a duration goal. - * @return this instance. + * @return {@code this} instance. * @throws IllegalArgumentException Thrown if maxBytesPerSecond <= 0. */ // Consider making public in the future @@ -166,7 +166,7 @@ Builder setMaxBytes(final long value, final Duration duration) { * Sets the maximum bytes per second. * * @param maxBytesPerSecond the maximum bytes per second. - * @return this instance. + * @return {@code this} instance. * @throws IllegalArgumentException Thrown if maxBytesPerSecond <= 0. */ private Builder setMaxBytesPerSecond(final double maxBytesPerSecond) { diff --git a/src/main/java/org/apache/commons/io/input/UnixLineEndingInputStream.java b/src/main/java/org/apache/commons/io/input/UnixLineEndingInputStream.java index 88328df1fa1..3d0400fca71 100644 --- a/src/main/java/org/apache/commons/io/input/UnixLineEndingInputStream.java +++ b/src/main/java/org/apache/commons/io/input/UnixLineEndingInputStream.java @@ -53,7 +53,7 @@ public UnixLineEndingInputStream(final InputStream inputStream, final boolean en /** * Closes the stream. Also closes the underlying stream. - * @throws IOException upon error + * @throws IOException If an I/O error occurs. */ @Override public void close() throws IOException { @@ -113,7 +113,7 @@ public synchronized int read() throws IOException { /** * Reads the next item from the target, updating internal flags in the process * @return the next int read from the target stream - * @throws IOException upon error + * @throws IOException If an I/O error occurs. */ private int readWithUpdate() throws IOException { final int target = this.in.read(); diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java index 8106a065631..8d60f6cedb8 100644 --- a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedInputStream.java @@ -306,19 +306,16 @@ public int read() throws IOException { */ @Override public int read(final byte[] dest, int offset, final int length) throws IOException { + IOUtils.checkFromIndexSize(dest, offset, length); + if (length == 0) { + return 0; + } // Use local ref since buf may be invalidated by an unsynchronized // close() byte[] localBuf = buffer; if (localBuf == null) { throw new IOException("Stream is closed"); } - // avoid int overflow - if (offset > dest.length - length || offset < 0 || length < 0) { - throw new IndexOutOfBoundsException(); - } - if (length == 0) { - return 0; - } final InputStream localIn = inputStream; if (localIn == null) { throw new IOException("Stream is closed"); diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java index 51b48c07af3..bde55873f24 100644 --- a/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedBufferedReader.java @@ -271,10 +271,17 @@ public int read() throws IOException { */ @Override public int read(final char[] buffer, int offset, final int length) throws IOException { + /* + * First throw on a closed reader, then check the parameters. + * + * This behavior is not specified in the Javadoc, but is followed by most readers in java.io. + */ checkOpen(); - if (offset < 0 || offset > buffer.length - length || length < 0) { - throw new IndexOutOfBoundsException(); + IOUtils.checkFromIndexSize(buffer, offset, length); + if (length == 0) { + return 0; } + int outstanding = length; while (outstanding > 0) { diff --git a/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java b/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java index cb2f5d3ddc5..a56dec7206c 100644 --- a/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java +++ b/src/main/java/org/apache/commons/io/input/UnsynchronizedByteArrayInputStream.java @@ -21,6 +21,7 @@ import java.io.InputStream; import java.util.Objects; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractOrigin; import org.apache.commons.io.build.AbstractStreamBuilder; @@ -288,9 +289,9 @@ public int read(final byte[] dest) { @Override public int read(final byte[] dest, final int off, final int len) { - Objects.requireNonNull(dest, "dest"); - if (off < 0 || len < 0 || off + len > dest.length) { - throw new IndexOutOfBoundsException(); + IOUtils.checkFromIndexSize(dest, off, len); + if (len == 0) { + return 0; } if (offset >= eod) { diff --git a/src/main/java/org/apache/commons/io/input/WindowsLineEndingInputStream.java b/src/main/java/org/apache/commons/io/input/WindowsLineEndingInputStream.java index 2567d5048c9..b09deec0d3e 100644 --- a/src/main/java/org/apache/commons/io/input/WindowsLineEndingInputStream.java +++ b/src/main/java/org/apache/commons/io/input/WindowsLineEndingInputStream.java @@ -56,7 +56,7 @@ public WindowsLineEndingInputStream(final InputStream in, final boolean lineFeed /** * Closes the stream. Also closes the underlying stream. * - * @throws IOException upon error + * @throws IOException If an I/O error occurs. */ @Override public void close() throws IOException { diff --git a/src/main/java/org/apache/commons/io/input/XmlStreamReader.java b/src/main/java/org/apache/commons/io/input/XmlStreamReader.java index 7f5102e8218..e438c940487 100644 --- a/src/main/java/org/apache/commons/io/input/XmlStreamReader.java +++ b/src/main/java/org/apache/commons/io/input/XmlStreamReader.java @@ -42,7 +42,6 @@ import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractStreamBuilder; import org.apache.commons.io.function.IOConsumer; -import org.apache.commons.io.output.XmlStreamWriter; /** * Character stream that handles all the necessary Voodoo to figure out the charset encoding of the XML document within the stream. @@ -76,7 +75,7 @@ public class XmlStreamReader extends Reader { // @formatter:off /** - * Builds a new {@link XmlStreamWriter}. + * Builds a new {@link XmlStreamReader}. * * Constructs a Reader using an InputStream and the associated content-type header. This constructor is lenient regarding the encoding detection. *

    @@ -131,7 +130,7 @@ public Builder() { } /** - * Builds a new {@link XmlStreamWriter}. + * Builds a new {@link XmlStreamReader}. *

    * You must set an aspect that supports {@link #getInputStream()}, otherwise, this method throws an exception. *

    diff --git a/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java b/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java index 8be305ddd04..b92163478d0 100644 --- a/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java +++ b/src/main/java/org/apache/commons/io/input/buffer/CircularBufferInputStream.java @@ -125,12 +125,9 @@ public int read() throws IOException { @Override public int read(final byte[] targetBuffer, final int offset, final int length) throws IOException { - Objects.requireNonNull(targetBuffer, "targetBuffer"); - if (offset < 0) { - throw new IllegalArgumentException("Offset must not be negative"); - } - if (length < 0) { - throw new IllegalArgumentException("Length must not be negative"); + IOUtils.checkFromIndexSize(targetBuffer, offset, length); + if (length == 0) { + return 0; } if (!haveBytes(length)) { return EOF; diff --git a/src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java b/src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java index 071aaf40839..dc2de64a4a2 100644 --- a/src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/AbstractByteArrayOutputStream.java @@ -72,9 +72,9 @@ protected interface InputStreamConstructor { /** * Constructs an InputStream subclass. * - * @param buffer the buffer - * @param offset the offset into the buffer - * @param length the length of the buffer + * @param buffer the buffer. + * @param offset the offset into the buffer. + * @param length the length of the buffer. * @return the InputStream subclass. */ T construct(byte[] buffer, int offset, int length); @@ -110,7 +110,7 @@ public AbstractByteArrayOutputStream() { /** * Returns this instance typed to {@code T}. * - * @return this instance + * @return {@code this} instance */ @SuppressWarnings("unchecked") protected T asThis() { @@ -123,7 +123,7 @@ protected T asThis() { * The methods in this class can be called after the stream has been closed without generating an {@link IOException}. * * @throws IOException never (this method should not declare this exception but it has to now due to backwards - * compatibility) + * compatibility). */ @Override public void close() throws IOException { @@ -134,7 +134,7 @@ public void close() throws IOException { * Makes a new buffer available either by allocating * a new one or re-cycling an existing one. * - * @param newCount the size of the buffer if one is created + * @param newCount the size of the buffer if one is created. */ protected void needNewBuffer(final int newCount) { if (currentBufferIndex < buffers.size() - 1) { @@ -190,7 +190,7 @@ protected void resetImpl() { /** * Returns the current size of the byte array. * - * @return the current size of the byte array + * @return the current size of the byte array. */ public abstract int size(); @@ -198,7 +198,7 @@ protected void resetImpl() { * Gets the current contents of this byte stream as a byte array. * The result is independent of this stream. * - * @return the current contents of this output stream, as a byte array + * @return the current contents of this output stream, as a byte array. * @see java.io.ByteArrayOutputStream#toByteArray() */ public abstract byte[] toByteArray(); @@ -207,7 +207,7 @@ protected void resetImpl() { * Gets the current contents of this byte stream as a byte array. * The result is independent of this stream. * - * @return the current contents of this output stream, as a byte array + * @return the current contents of this output stream, as a byte array. * @see java.io.ByteArrayOutputStream#toByteArray() */ protected byte[] toByteArrayImpl() { @@ -276,12 +276,12 @@ protected InputStream toInputStream(final InputStreamCon } /** - * Gets the current contents of this byte stream as a string using the virtual machine's {@link Charset#defaultCharset() default charset}. + * Gets the current contents of this byte stream as a string using the virtual machine's {@linkplain Charset#defaultCharset() default charset}. * - * @return the contents of the byte array as a String + * @return the contents of the byte array as a String. * @see java.io.ByteArrayOutputStream#toString() * @see Charset#defaultCharset() - * @deprecated Use {@link #toString(String)} instead + * @deprecated Use {@link #toString(String)} instead. */ @Override @Deprecated @@ -294,8 +294,8 @@ public String toString() { * Gets the current contents of this byte stream as a string * using the specified encoding. * - * @param charset the character encoding - * @return the string converted from the byte array + * @param charset the character encoding. + * @return the string converted from the byte array. * @see java.io.ByteArrayOutputStream#toString(String) * @since 2.5 */ @@ -307,9 +307,9 @@ public String toString(final Charset charset) { * Gets the current contents of this byte stream as a string * using the specified encoding. * - * @param enc the name of the character encoding - * @return the string converted from the byte array - * @throws UnsupportedEncodingException if the encoding is not supported + * @param enc the name of the character encoding. + * @return the string converted from the byte array. + * @throws UnsupportedEncodingException if the encoding is not supported. * @see java.io.ByteArrayOutputStream#toString(String) */ public String toString(final String enc) throws UnsupportedEncodingException { @@ -336,7 +336,7 @@ public void write(final byte b[]) { * * @param data The String to convert to bytes. not null. * @param charset The {@link Charset} o encode the {@code String}, null means the default encoding. - * @return this instance. + * @return {@code this} instance. * @since 2.19.0 */ public T write(final CharSequence data, final Charset charset) { @@ -345,14 +345,12 @@ public T write(final CharSequence data, final Charset charset) { } /** - * Writes the entire contents of the specified input stream to this - * byte stream. Bytes from the input stream are read directly into the - * internal buffer of this stream. + * Writes the entire contents of the specified input stream to this byte stream. Bytes from the input stream are read directly into the internal buffer of + * this stream. * - * @param in the input stream to read from - * @return total number of bytes read from the input stream - * (and written to this stream) - * @throws IOException if an I/O error occurs while reading the input stream + * @param in the input stream to read from. + * @return total number of bytes read from the input stream (and written to this stream) + * @throws IOException if an I/O error occurs while reading the input stream. * @since 1.4 */ public abstract int write(InputStream in) throws IOException; @@ -362,9 +360,10 @@ public T write(final CharSequence data, final Charset charset) { /** * Writes the bytes to the byte array. - * @param b the bytes to write - * @param off The start offset - * @param len The number of bytes to write + * + * @param b the bytes to write. + * @param off The start offset. + * @param len The number of bytes to write. */ protected void writeImpl(final byte[] b, final int off, final int len) { final int newCount = count + len; @@ -383,14 +382,12 @@ protected void writeImpl(final byte[] b, final int off, final int len) { } /** - * Writes the entire contents of the specified input stream to this - * byte stream. Bytes from the input stream are read directly into the - * internal buffer of this stream. + * Writes the entire contents of the specified input stream to this byte stream. Bytes from the input stream are read directly into the internal buffer of + * this stream. * - * @param in the input stream to read from - * @return total number of bytes read from the input stream - * (and written to this stream) - * @throws IOException if an I/O error occurs while reading the input stream + * @param in the input stream to read from. + * @return total number of bytes read from the input stream (and written to this stream). + * @throws IOException if an I/O error occurs while reading the input stream. * @since 2.7 */ protected int writeImpl(final InputStream in) throws IOException { @@ -412,7 +409,8 @@ protected int writeImpl(final InputStream in) throws IOException { /** * Writes a byte to byte array. - * @param b the byte to write + * + * @param b the byte to write. */ protected void writeImpl(final int b) { int inBufferPos = count - filledBufferSum; @@ -425,21 +423,19 @@ protected void writeImpl(final int b) { } /** - * Writes the entire contents of this byte stream to the - * specified output stream. + * Writes the entire contents of this byte stream to the specified output stream. * - * @param out the output stream to write to - * @throws IOException if an I/O error occurs, such as if the stream is closed + * @param out the output stream to write to. + * @throws IOException if an I/O error occurs, such as if the stream is closed. * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) */ public abstract void writeTo(OutputStream out) throws IOException; /** - * Writes the entire contents of this byte stream to the - * specified output stream. + * Writes the entire contents of this byte stream to the specified output stream. * - * @param out the output stream to write to - * @throws IOException if an I/O error occurs, such as if the stream is closed + * @param out the output stream to write to. + * @throws IOException if an I/O error occurs, such as if the stream is closed. * @see java.io.ByteArrayOutputStream#writeTo(OutputStream) */ protected void writeToImpl(final OutputStream out) throws IOException { diff --git a/src/main/java/org/apache/commons/io/output/AppendableOutputStream.java b/src/main/java/org/apache/commons/io/output/AppendableOutputStream.java index e9c00bd7530..e8320d99d64 100644 --- a/src/main/java/org/apache/commons/io/output/AppendableOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/AppendableOutputStream.java @@ -58,7 +58,7 @@ public T getAppendable() { * Writes a character to the underlying appendable. * * @param b the character to write - * @throws IOException upon error + * @throws IOException If an I/O error occurs. */ @Override public void write(final int b) throws IOException { diff --git a/src/main/java/org/apache/commons/io/output/AppendableWriter.java b/src/main/java/org/apache/commons/io/output/AppendableWriter.java index a4e422e6468..b1ace0b908c 100644 --- a/src/main/java/org/apache/commons/io/output/AppendableWriter.java +++ b/src/main/java/org/apache/commons/io/output/AppendableWriter.java @@ -14,32 +14,33 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.IOException; import java.io.Writer; import java.util.Objects; +import org.apache.commons.io.IOUtils; + /** - * Writer implementation that writes the data to an {@link Appendable} - * Object. + * Writer implementation that writes the data to an {@link Appendable} Object. *

    - * For example, can be used with a {@link StringBuilder} - * or {@link StringBuffer}. + * For example, can be used with a {@link StringBuilder} or {@link StringBuffer}. *

    * * @since 2.7 * @see Appendable * @param The type of the {@link Appendable} wrapped by this AppendableWriter. */ -public class AppendableWriter extends Writer { +public class AppendableWriter extends Writer { private final T appendable; /** * Constructs a new instance with the specified appendable. * - * @param appendable the appendable to write to + * @param appendable the appendable to write to. */ public AppendableWriter(final T appendable) { this.appendable = appendable; @@ -48,9 +49,9 @@ public AppendableWriter(final T appendable) { /** * Appends the specified character to the underlying appendable. * - * @param c the character to append - * @return this writer - * @throws IOException upon error + * @param c the character to append. + * @return this writer. + * @throws IOException If an I/O error occurs. */ @Override public Writer append(final char c) throws IOException { @@ -61,9 +62,9 @@ public Writer append(final char c) throws IOException { /** * Appends the specified character sequence to the underlying appendable. * - * @param csq the character sequence to append - * @return this writer - * @throws IOException upon error + * @param csq the character sequence to append. + * @return this writer. + * @throws IOException If an I/O error occurs. */ @Override public Writer append(final CharSequence csq) throws IOException { @@ -74,11 +75,13 @@ public Writer append(final CharSequence csq) throws IOException { /** * Appends a subsequence of the specified character sequence to the underlying appendable. * - * @param csq the character sequence from which a subsequence will be appended + * @param csq the character sequence from which a subsequence will be appended * @param start the index of the first character in the subsequence - * @param end the index of the character following the last character in the subsequence + * @param end the index of the character following the last character in the subsequence * @return this writer - * @throws IOException upon error + * @throws IndexOutOfBoundsException If {@code start} or {@code end} are negative, {@code start} is greater than + * {@code end}, or {@code end} is greater than {@code csq.length()}. + * @throws IOException If an I/O error occurs. */ @Override public Writer append(final CharSequence csq, final int start, final int end) throws IOException { @@ -89,7 +92,7 @@ public Writer append(final CharSequence csq, final int start, final int end) thr /** * Closes the stream. This implementation does nothing. * - * @throws IOException upon error + * @throws IOException Thrown by a subclass. */ @Override public void close() throws IOException { @@ -99,7 +102,7 @@ public void close() throws IOException { /** * Flushes the stream. This implementation does nothing. * - * @throws IOException upon error + * @throws IOException Thrown by a subclass. */ @Override public void flush() throws IOException { @@ -109,7 +112,7 @@ public void flush() throws IOException { /** * Gets the target appendable. * - * @return the target appendable + * @return the target appendable. */ public T getAppendable() { return appendable; @@ -118,18 +121,16 @@ public T getAppendable() { /** * Writes a portion of an array of characters to the underlying appendable. * - * @param cbuf an array with the characters to write - * @param off offset from which to start writing characters - * @param len number of characters to write - * @throws IOException upon error + * @param cbuf an array with the characters to write. + * @param off offset from which to start writing characters. + * @param len number of characters to write. + * @throws NullPointerException if the array is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code cbuf.length}. + * @throws IOException If an I/O error occurs. */ @Override public void write(final char[] cbuf, final int off, final int len) throws IOException { - Objects.requireNonNull(cbuf, "cbuf"); - if (len < 0 || off + len > cbuf.length) { - throw new IndexOutOfBoundsException("Array Size=" + cbuf.length + - ", offset=" + off + ", length=" + len); - } + IOUtils.checkFromIndexSize(cbuf, off, len); for (int i = 0; i < len; i++) { appendable.append(cbuf[off + i]); } @@ -138,8 +139,8 @@ public void write(final char[] cbuf, final int off, final int len) throws IOExce /** * Writes a character to the underlying appendable. * - * @param c the character to write - * @throws IOException upon error + * @param c the character to write. + * @throws IOException If an I/O error occurs. */ @Override public void write(final int c) throws IOException { @@ -149,10 +150,12 @@ public void write(final int c) throws IOException { /** * Writes a portion of a String to the underlying appendable. * - * @param str a string - * @param off offset from which to start writing characters - * @param len number of characters to write - * @throws IOException upon error + * @param str a string. + * @param off offset from which to start writing characters. + * @param len number of characters to write. + * @throws NullPointerException if the string is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code str.length()}. + * @throws IOException If an I/O error occurs. */ @Override public void write(final String str, final int off, final int len) throws IOException { @@ -160,5 +163,4 @@ public void write(final String str, final int off, final int len) throws IOExcep Objects.requireNonNull(str, "str"); appendable.append(str, off, off + len); } - } diff --git a/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java b/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java index 043594968f7..48fb793669b 100644 --- a/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ByteArrayOutputStream.java @@ -21,6 +21,8 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; + /** * Implements a ThreadSafe version of {@link AbstractByteArrayOutputStream} using instance synchronization. */ @@ -28,21 +30,19 @@ public class ByteArrayOutputStream extends AbstractByteArrayOutputStream { /** - * Fetches entire contents of an {@link InputStream} and represent - * same data as result InputStream. + * Fetches entire contents of an {@link InputStream} and represent same data as result InputStream. *

    * This method is useful where, *

    *
      *
    • Source InputStream is slow.
    • - *
    • It has network resources associated, so we cannot keep it open for - * long time.
    • + *
    • It has network resources associated, so we cannot keep it open for long time.
    • *
    • It has network timeout associated.
    • *
    - * It can be used in favor of {@link #toByteArray()}, since it - * avoids unnecessary allocation and copy of byte[].
    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedInputStream}. + *

    + * It can be used in favor of {@link #toByteArray()}, since it avoids unnecessary allocation and copy of byte[].
    + * This method buffers the input internally, so there is no need to use a {@link BufferedInputStream}. + *

    * * @param input Stream to be fully buffered. * @return A fully buffered stream. @@ -55,24 +55,22 @@ public static InputStream toBufferedInputStream(final InputStream input) } /** - * Fetches entire contents of an {@link InputStream} and represent - * same data as result InputStream. + * Fetches entire contents of an {@link InputStream} and represent same data as result InputStream. *

    * This method is useful where, *

    *
      *
    • Source InputStream is slow.
    • - *
    • It has network resources associated, so we cannot keep it open for - * long time.
    • + *
    • It has network resources associated, so we cannot keep it open for long time.
    • *
    • It has network timeout associated.
    • *
    - * It can be used in favor of {@link #toByteArray()}, since it - * avoids unnecessary allocation and copy of byte[].
    - * This method buffers the input internally, so there is no need to use a - * {@link BufferedInputStream}. + *

    + * It can be used in favor of {@link #toByteArray()}, since it avoids unnecessary allocation and copy of byte[].
    + * This method buffers the input internally, so there is no need to use a {@link BufferedInputStream}. + *

    * * @param input Stream to be fully buffered. - * @param size the initial buffer size + * @param size the initial buffer size. * @return A fully buffered stream. * @throws IOException if an I/O error occurs. * @since 2.5 @@ -134,13 +132,7 @@ public synchronized InputStream toInputStream() { @Override public void write(final byte[] b, final int off, final int len) { - if (off < 0 - || off > b.length - || len < 0 - || off + len > b.length - || off + len < 0) { - throw new IndexOutOfBoundsException(); - } + IOUtils.checkFromIndexSize(b, off, len); if (len == 0) { return; } diff --git a/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java b/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java index 7338ad8020f..2588264e172 100644 --- a/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ChunkedOutputStream.java @@ -109,7 +109,7 @@ public static Builder builder() { } /** - * The maximum chunk size to us when writing data arrays + * The maximum chunk size to us when writing data arrays. */ private final int chunkSize; @@ -132,8 +132,8 @@ private ChunkedOutputStream(final Builder builder) throws IOException { /** * Constructs a new stream that uses a chunk size of {@link IOUtils#DEFAULT_BUFFER_SIZE}. * - * @param stream the stream to wrap - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param stream the stream to wrap. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public ChunkedOutputStream(final OutputStream stream) { @@ -145,8 +145,8 @@ public ChunkedOutputStream(final OutputStream stream) { * * @param stream the stream to wrap * @param chunkSize the chunk size to use; must be a positive number. - * @throws IllegalArgumentException if the chunk size is <= 0 - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @throws IllegalArgumentException if the chunk size is <= 0. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public ChunkedOutputStream(final OutputStream stream, final int chunkSize) { @@ -165,13 +165,17 @@ int getChunkSize() { /** * Writes the data buffer in chunks to the underlying stream * - * @param data the data to write - * @param srcOffset the offset - * @param length the length of data to write + * @param data the data to write. + * @param srcOffset the offset. + * @param length the length of data to write. + * @throws NullPointerException if the data is {@code null}. + * @throws IndexOutOfBoundsException if {@code srcOffset} or {@code length} are negative, + * or if {@code srcOffset + length} is greater than {@code data.length}. * @throws IOException if an I/O error occurs. */ @Override public void write(final byte[] data, final int srcOffset, final int length) throws IOException { + IOUtils.checkFromIndexSize(data, srcOffset, length); int bytes = length; int dstOffset = srcOffset; while (bytes > 0) { diff --git a/src/main/java/org/apache/commons/io/output/ChunkedWriter.java b/src/main/java/org/apache/commons/io/output/ChunkedWriter.java index 05a71b88086..ed22625b667 100644 --- a/src/main/java/org/apache/commons/io/output/ChunkedWriter.java +++ b/src/main/java/org/apache/commons/io/output/ChunkedWriter.java @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.FilterWriter; @@ -23,9 +24,7 @@ import org.apache.commons.io.IOUtils; /** - * Writer which breaks larger output blocks into chunks. - * Native code may need to copy the input array; if the write buffer - * is very large this can cause OOME. + * Writer which breaks larger output blocks into chunks. Native code may need to copy the input array; if the write buffer is very large this can cause OOME. * * @since 2.5 */ @@ -37,13 +36,14 @@ public class ChunkedWriter extends FilterWriter { private static final int DEFAULT_CHUNK_SIZE = IOUtils.DEFAULT_BUFFER_SIZE; /** - * The maximum chunk size to us when writing data arrays + * The maximum chunk size to us when writing data arrays. */ private final int chunkSize; /** * Constructs a new writer that uses a chunk size of {@link #DEFAULT_CHUNK_SIZE} - * @param writer the writer to wrap + * + * @param writer the writer to wrap. */ public ChunkedWriter(final Writer writer) { this(writer, DEFAULT_CHUNK_SIZE); @@ -52,28 +52,32 @@ public ChunkedWriter(final Writer writer) { /** * Constructs a new writer that uses the specified chunk size. * - * @param writer the writer to wrap + * @param writer the writer to wrap. * @param chunkSize the chunk size to use; must be a positive number. - * @throws IllegalArgumentException if the chunk size is <= 0 + * @throws IllegalArgumentException if the chunk size is <= 0. */ public ChunkedWriter(final Writer writer, final int chunkSize) { - super(writer); - if (chunkSize <= 0) { - throw new IllegalArgumentException(); - } - this.chunkSize = chunkSize; + super(writer); + if (chunkSize <= 0) { + throw new IllegalArgumentException(); + } + this.chunkSize = chunkSize; } /** * Writes the data buffer in chunks to the underlying writer. * - * @param data The data - * @param srcOffset the offset - * @param length the number of bytes to write - * @throws IOException upon error + * @param data The data. + * @param srcOffset the offset. + * @param length the number of bytes to write. + * @throws NullPointerException if the data is {@code null}. + * @throws IndexOutOfBoundsException if {@code srcOffset} or {@code length} are negative, + * or if {@code srcOffset + length} is greater than {@code data.length}. + * @throws IOException If an I/O error occurs. */ @Override public void write(final char[] data, final int srcOffset, final int length) throws IOException { + IOUtils.checkFromIndexSize(data, srcOffset, length); int bytes = length; int dstOffset = srcOffset; while (bytes > 0) { @@ -83,5 +87,4 @@ public void write(final char[] data, final int srcOffset, final int length) thro dstOffset += chunk; } } - } diff --git a/src/main/java/org/apache/commons/io/output/CloseShieldOutputStream.java b/src/main/java/org/apache/commons/io/output/CloseShieldOutputStream.java index eada01549b2..cefa71ad41f 100644 --- a/src/main/java/org/apache/commons/io/output/CloseShieldOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/CloseShieldOutputStream.java @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.OutputStream; @@ -21,8 +22,7 @@ /** * Proxy stream that prevents the underlying output stream from being closed. *

    - * This class is typically used in cases where an output stream needs to be - * passed to a component that wants to explicitly close the stream even if other + * This class is typically used in cases where an output stream needs to be passed to a component that wants to explicitly close the stream even if other * components would still use the stream for output. *

    * @@ -33,7 +33,7 @@ public class CloseShieldOutputStream extends ProxyOutputStream { /** * Constructs a proxy that shields the given output stream from being closed. * - * @param outputStream the output stream to wrap + * @param outputStream the output stream to wrap. * @return the created proxy * @since 2.9.0 */ @@ -44,10 +44,8 @@ public static CloseShieldOutputStream wrap(final OutputStream outputStream) { /** * Constructs a proxy that shields the given output stream from being closed. * - * @param outputStream underlying output stream - * @deprecated Using this constructor prevents IDEs from warning if the - * underlying output stream is never closed. Use - * {@link #wrap(OutputStream)} instead. + * @param outputStream underlying output stream. + * @deprecated Using this constructor prevents IDEs from warning if the underlying output stream is never closed. Use {@link #wrap(OutputStream)} instead. */ @Deprecated public CloseShieldOutputStream(final OutputStream outputStream) { @@ -55,13 +53,11 @@ public CloseShieldOutputStream(final OutputStream outputStream) { } /** - * Replaces the underlying output stream with a {@link ClosedOutputStream} - * sentinel. The original output stream will remain open, but this proxy will - * appear closed. + * Replaces the underlying output stream with a {@link ClosedOutputStream} sentinel. The original output stream will remain open, but this proxy will appear + * closed. */ @Override public void close() { out = ClosedOutputStream.INSTANCE; } - } diff --git a/src/main/java/org/apache/commons/io/output/CloseShieldWriter.java b/src/main/java/org/apache/commons/io/output/CloseShieldWriter.java index badd6ce8cb2..b6e2f4abb44 100644 --- a/src/main/java/org/apache/commons/io/output/CloseShieldWriter.java +++ b/src/main/java/org/apache/commons/io/output/CloseShieldWriter.java @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.Writer; @@ -21,8 +22,7 @@ /** * Proxy writer that prevents the underlying writer from being closed. *

    - * This class is typically used in cases where a writer needs to be passed to a - * component that wants to explicitly close the writer even if other components + * This class is typically used in cases where a writer needs to be passed to a component that wants to explicitly close the writer even if other components * would still use the writer for output. *

    * @@ -33,8 +33,8 @@ public class CloseShieldWriter extends ProxyWriter { /** * Constructs a proxy that shields the given writer from being closed. * - * @param writer the writer to wrap - * @return the created proxy + * @param writer the writer to wrap. + * @return the created proxy. * @since 2.9.0 */ public static CloseShieldWriter wrap(final Writer writer) { @@ -44,10 +44,8 @@ public static CloseShieldWriter wrap(final Writer writer) { /** * Constructs a proxy that shields the given writer from being closed. * - * @param writer underlying writer - * @deprecated Using this constructor prevents IDEs from warning if the - * underlying writer is never closed. Use {@link #wrap(Writer)} - * instead. + * @param writer underlying writer. + * @deprecated Using this constructor prevents IDEs from warning if the underlying writer is never closed. Use {@link #wrap(Writer)} instead. */ @Deprecated public CloseShieldWriter(final Writer writer) { @@ -55,12 +53,10 @@ public CloseShieldWriter(final Writer writer) { } /** - * Replaces the underlying writer with a {@link ClosedWriter} sentinel. The - * original writer will remain open, but this proxy will appear closed. + * Replaces the underlying writer with a {@link ClosedWriter} sentinel. The original writer will remain open, but this proxy will appear closed. */ @Override public void close() { out = ClosedWriter.INSTANCE; } - } diff --git a/src/main/java/org/apache/commons/io/output/ClosedOutputStream.java b/src/main/java/org/apache/commons/io/output/ClosedOutputStream.java index 6e72e1075ec..c7216ce6f90 100644 --- a/src/main/java/org/apache/commons/io/output/ClosedOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ClosedOutputStream.java @@ -14,11 +14,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.IOException; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; + /** * Throws an IOException on all attempts to write to the stream. *

    @@ -65,21 +68,24 @@ public void flush() throws IOException { /** * Throws an {@link IOException} to indicate that the stream is closed. * - * @param b ignored - * @param off ignored - * @param len ignored - * @throws IOException always thrown + * @param b Byte array, never {@code null}. + * @param off The start offset in the byte array. + * @param len The number of bytes to write. + * @throws NullPointerException if the byte array is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code b.length}. + * @throws IOException always thrown. */ @Override public void write(final byte b[], final int off, final int len) throws IOException { + IOUtils.checkFromIndexSize(b, off, len); throw new IOException("write(byte[], int, int) failed: stream is closed"); } /** * Throws an {@link IOException} to indicate that the stream is closed. * - * @param b ignored - * @throws IOException always thrown + * @param b ignored. + * @throws IOException always thrown. */ @Override public void write(final int b) throws IOException { diff --git a/src/main/java/org/apache/commons/io/output/ClosedWriter.java b/src/main/java/org/apache/commons/io/output/ClosedWriter.java index e741e906dbb..c4e1d93b93c 100644 --- a/src/main/java/org/apache/commons/io/output/ClosedWriter.java +++ b/src/main/java/org/apache/commons/io/output/ClosedWriter.java @@ -14,16 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.IOException; import java.io.Writer; +import java.util.Arrays; /** * Throws an IOException on all attempts to write with {@link #close()} implemented as a noop. *

    - * Typically uses of this class include testing for corner cases in methods that accept a writer and acting as a - * sentinel value instead of a {@code null} writer. + * Typically uses of this class include testing for corner cases in methods that accept a writer and acting as a sentinel value instead of a {@code null} + * writer. *

    * * @since 2.7 @@ -36,7 +38,6 @@ public class ClosedWriter extends Writer { * @since 2.12.0 */ public static final ClosedWriter INSTANCE = new ClosedWriter(); - /** * The singleton instance. * @@ -70,13 +71,13 @@ public void flush() throws IOException { /** * Throws an {@link IOException} to indicate that the writer is closed. * - * @param cbuf ignored - * @param off ignored - * @param len ignored - * @throws IOException always thrown + * @param cbuf ignored. + * @param off ignored. + * @param len ignored. + * @throws IOException always thrown. */ @Override public void write(final char[] cbuf, final int off, final int len) throws IOException { - throw new IOException("write(" + new String(cbuf) + ", " + off + ", " + len + ") failed: stream is closed"); + throw new IOException(String.format("write(%s, %d, %d) failed: stream is closed", Arrays.toString(cbuf), off, len)); } } diff --git a/src/main/java/org/apache/commons/io/output/CountingOutputStream.java b/src/main/java/org/apache/commons/io/output/CountingOutputStream.java index 2d8fec003a1..02f4a6c10b6 100644 --- a/src/main/java/org/apache/commons/io/output/CountingOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/CountingOutputStream.java @@ -34,7 +34,7 @@ public class CountingOutputStream extends ProxyOutputStream { /** * Constructs a new CountingOutputStream. * - * @param out the OutputStream to write to + * @param out the OutputStream to write to. */ public CountingOutputStream(final OutputStream out) { super(out); @@ -43,7 +43,7 @@ public CountingOutputStream(final OutputStream out) { /** * Updates the count with the number of bytes that are being written. * - * @param n number of bytes to be written to the stream + * @param n number of bytes to be written to the stream. * @since 2.0 */ @Override @@ -59,7 +59,7 @@ protected synchronized void beforeWrite(final int n) { * result in incorrect count for files over 2GB. *

    * - * @return the number of bytes accumulated + * @return the number of bytes accumulated. * @since 1.3 */ public synchronized long getByteCount() { @@ -74,8 +74,8 @@ public synchronized long getByteCount() { * See {@link #getByteCount()} for a method using a {@code long}. *

    * - * @return the number of bytes accumulated - * @throws ArithmeticException if the byte count is too large + * @return the number of bytes accumulated. + * @throws ArithmeticException if the byte count is too large. */ public int getCount() { final long result = getByteCount(); @@ -93,7 +93,7 @@ public int getCount() { * result in incorrect count for files over 2GB. *

    * - * @return the count previous to resetting + * @return the count previous to resetting. * @since 1.3 */ public synchronized long resetByteCount() { @@ -110,8 +110,8 @@ public synchronized long resetByteCount() { * See {@link #resetByteCount()} for a method using a {@code long}. *

    * - * @return the count previous to resetting - * @throws ArithmeticException if the byte count is too large + * @return the count previous to resetting. + * @throws ArithmeticException if the byte count is too large. */ public int resetCount() { final long result = resetByteCount(); diff --git a/src/main/java/org/apache/commons/io/output/DemuxOutputStream.java b/src/main/java/org/apache/commons/io/output/DemuxOutputStream.java index ea3f86a90e7..22770a0ba6b 100644 --- a/src/main/java/org/apache/commons/io/output/DemuxOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/DemuxOutputStream.java @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.IOException; @@ -38,9 +39,8 @@ public DemuxOutputStream() { /** * Binds the specified stream to the current thread. * - * @param output - * the stream to bind - * @return the OutputStream that was previously active + * @param output the stream to bind. + * @return the OutputStream that was previously active. */ public OutputStream bindStream(final OutputStream output) { final OutputStream stream = outputStreamThreadLocal.get(); @@ -51,8 +51,7 @@ public OutputStream bindStream(final OutputStream output) { /** * Closes stream associated with current thread. * - * @throws IOException - * if an error occurs + * @throws IOException if an error occurs. */ @SuppressWarnings("resource") // we actually close the stream here @Override @@ -63,8 +62,7 @@ public void close() throws IOException { /** * Flushes stream associated with current thread. * - * @throws IOException - * if an error occurs + * @throws IOException if an error occurs. */ @Override public void flush() throws IOException { @@ -78,10 +76,8 @@ public void flush() throws IOException { /** * Writes byte to stream associated with current thread. * - * @param ch - * the byte to write to stream - * @throws IOException - * if an error occurs + * @param ch the byte to write to stream. + * @throws IOException if an error occurs. */ @Override public void write(final int ch) throws IOException { diff --git a/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java b/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java index c20f8456a58..42d378cc0a5 100644 --- a/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java +++ b/src/main/java/org/apache/commons/io/output/FileWriterWithEncoding.java @@ -128,8 +128,7 @@ private Object getEncoder() { if (charsetEncoder != null && getCharset() != null && !charsetEncoder.charset().equals(getCharset())) { throw new IllegalStateException(String.format("Mismatched Charset(%s) and CharsetEncoder(%s)", getCharset(), charsetEncoder.charset())); } - final Object encoder = charsetEncoder != null ? charsetEncoder : getCharset(); - return encoder; + return charsetEncoder != null ? charsetEncoder : getCharset(); } /** @@ -209,11 +208,11 @@ private FileWriterWithEncoding(final Builder builder) throws IOException { /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param file the file to write to, not null - * @param charset the encoding to use, not null - * @throws NullPointerException if the file or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param file the file to write to, not null. + * @param charset the encoding to use, not null. + * @throws NullPointerException if the file or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final File file, final Charset charset) throws IOException { @@ -228,7 +227,7 @@ public FileWriterWithEncoding(final File file, final Charset charset) throws IOE * @param append true if content should be appended, false to overwrite. * @throws NullPointerException if the file is null. * @throws IOException in case of an I/O error. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated @SuppressWarnings("resource") // Call site is responsible for closing a new instance. @@ -239,11 +238,11 @@ public FileWriterWithEncoding(final File file, final Charset encoding, final boo /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param file the file to write to, not null - * @param charsetEncoder the encoding to use, not null - * @throws NullPointerException if the file or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param file the file to write to, not null. + * @param charsetEncoder the encoding to use, not null. + * @throws NullPointerException if the file or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncoder) throws IOException { @@ -258,7 +257,7 @@ public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncod * @param append true if content should be appended, false to overwrite. * @throws NullPointerException if the file is null. * @throws IOException in case of an I/O error. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated @SuppressWarnings("resource") // Call site is responsible for closing a new instance. @@ -269,11 +268,11 @@ public FileWriterWithEncoding(final File file, final CharsetEncoder charsetEncod /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param file the file to write to, not null - * @param charsetName the name of the requested charset, not null - * @throws NullPointerException if the file or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param file the file to write to, not null. + * @param charsetName the name of the requested charset, not null. + * @throws NullPointerException if the file or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final File file, final String charsetName) throws IOException { @@ -288,7 +287,7 @@ public FileWriterWithEncoding(final File file, final String charsetName) throws * @param append true if content should be appended, false to overwrite. * @throws NullPointerException if the file is null. * @throws IOException in case of an I/O error. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated @SuppressWarnings("resource") // Call site is responsible for closing a new instance. @@ -303,11 +302,11 @@ private FileWriterWithEncoding(final OutputStreamWriter outputStreamWriter) { /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param fileName the name of the file to write to, not null - * @param charset the charset to use, not null - * @throws NullPointerException if the file name or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the name of the file to write to, not null. + * @param charset the charset to use, not null. + * @throws NullPointerException if the file name or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final String fileName, final Charset charset) throws IOException { @@ -317,12 +316,12 @@ public FileWriterWithEncoding(final String fileName, final Charset charset) thro /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param fileName the name of the file to write to, not null - * @param charset the encoding to use, not null - * @param append true if content should be appended, false to overwrite - * @throws NullPointerException if the file name or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the name of the file to write to, not null. + * @param charset the encoding to use, not null. + * @param append true if content should be appended, false to overwrite. + * @throws NullPointerException if the file name or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final String fileName, final Charset charset, final boolean append) throws IOException { @@ -332,11 +331,11 @@ public FileWriterWithEncoding(final String fileName, final Charset charset, fina /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param fileName the name of the file to write to, not null - * @param encoding the encoding to use, not null - * @throws NullPointerException if the file name or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the name of the file to write to, not null. + * @param encoding the encoding to use, not null. + * @throws NullPointerException if the file name or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final String fileName, final CharsetEncoder encoding) throws IOException { @@ -346,12 +345,12 @@ public FileWriterWithEncoding(final String fileName, final CharsetEncoder encodi /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param fileName the name of the file to write to, not null - * @param charsetEncoder the encoding to use, not null - * @param append true if content should be appended, false to overwrite - * @throws NullPointerException if the file name or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the name of the file to write to, not null. + * @param charsetEncoder the encoding to use, not null. + * @param append true if content should be appended, false to overwrite. + * @throws NullPointerException if the file name or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final String fileName, final CharsetEncoder charsetEncoder, final boolean append) throws IOException { @@ -361,11 +360,11 @@ public FileWriterWithEncoding(final String fileName, final CharsetEncoder charse /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param fileName the name of the file to write to, not null - * @param charsetName the name of the requested charset, not null - * @throws NullPointerException if the file name or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the name of the file to write to, not null. + * @param charsetName the name of the requested charset, not null. + * @throws NullPointerException if the file name or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final String fileName, final String charsetName) throws IOException { @@ -375,12 +374,12 @@ public FileWriterWithEncoding(final String fileName, final String charsetName) t /** * Constructs a FileWriterWithEncoding with a file encoding. * - * @param fileName the name of the file to write to, not null - * @param charsetName the name of the requested charset, not null - * @param append true if content should be appended, false to overwrite - * @throws NullPointerException if the file name or encoding is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the name of the file to write to, not null. + * @param charsetName the name of the requested charset, not null. + * @param append true if content should be appended, false to overwrite. + * @throws NullPointerException if the file name or encoding is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public FileWriterWithEncoding(final String fileName, final String charsetName, final boolean append) throws IOException { diff --git a/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java b/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java index ddab5eefca7..d78f049e635 100644 --- a/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java +++ b/src/main/java/org/apache/commons/io/output/FilterCollectionWriter.java @@ -98,7 +98,7 @@ public void close() throws IOException { /** * Flushes the stream. * - * @throws IOException If an I/O error occurs + * @throws IOException If an I/O error occurs. */ @SuppressWarnings("resource") // no allocation @Override @@ -120,10 +120,10 @@ public void write(final char[] cbuf) throws IOException { /** * Writes a portion of an array of characters. * - * @param cbuf Buffer of characters to be written - * @param off Offset from which to start reading characters - * @param len Number of characters to be written - * @throws IOException If an I/O error occurs + * @param cbuf Buffer of characters to be written. + * @param off Offset from which to start reading characters. + * @param len Number of characters to be written. + * @throws IOException If an I/O error occurs. */ @SuppressWarnings("resource") // no allocation @Override @@ -134,7 +134,7 @@ public void write(final char[] cbuf, final int off, final int len) throws IOExce /** * Writes a single character. * - * @throws IOException If an I/O error occurs + * @throws IOException If an I/O error occurs. */ @SuppressWarnings("resource") // no allocation @Override @@ -151,10 +151,10 @@ public void write(final String str) throws IOException { /** * Writes a portion of a string. * - * @param str String to be written - * @param off Offset from which to start reading characters - * @param len Number of characters to be written - * @throws IOException If an I/O error occurs + * @param str String to be written. + * @param off Offset from which to start reading characters. + * @param len Number of characters to be written. + * @throws IOException If an I/O error occurs. */ @SuppressWarnings("resource") // no allocation @Override diff --git a/src/main/java/org/apache/commons/io/output/LockableFileWriter.java b/src/main/java/org/apache/commons/io/output/LockableFileWriter.java index 7a0489179d5..1ee9391af8f 100644 --- a/src/main/java/org/apache/commons/io/output/LockableFileWriter.java +++ b/src/main/java/org/apache/commons/io/output/LockableFileWriter.java @@ -177,10 +177,10 @@ private LockableFileWriter(final Builder builder) throws IOException { /** * Constructs a LockableFileWriter. If the file exists, it is overwritten. * - * @param file the file to write to, not null - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param file the file to write to, not null. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final File file) throws IOException { @@ -190,11 +190,11 @@ public LockableFileWriter(final File file) throws IOException { /** * Constructs a LockableFileWriter. * - * @param file the file to write to, not null - * @param append true if content should be appended, false to overwrite - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param file the file to write to, not null. + * @param append true if content should be appended, false to overwrite. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final File file, final boolean append) throws IOException { @@ -204,15 +204,15 @@ public LockableFileWriter(final File file, final boolean append) throws IOExcept /** * Constructs a LockableFileWriter. *

    - * The new instance uses the virtual machine's {@link Charset#defaultCharset() default charset}. + * The new instance uses the virtual machine's {@linkplain Charset#defaultCharset() default charset}. *

    * - * @param file the file to write to, not null - * @param append true if content should be appended, false to overwrite - * @param lockDir the directory in which the lock file should be held - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #LockableFileWriter(File, Charset, boolean, String)} instead + * @param file the file to write to, not null. + * @param append true if content should be appended, false to overwrite. + * @param lockDir the directory in which the lock file should be held. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #LockableFileWriter(File, Charset, boolean, String)} instead. */ @Deprecated public LockableFileWriter(final File file, final boolean append, final String lockDir) throws IOException { @@ -222,12 +222,12 @@ public LockableFileWriter(final File file, final boolean append, final String lo /** * Constructs a LockableFileWriter with a file encoding. * - * @param file the file to write to, not null - * @param charset the charset to use, null means platform default - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error + * @param file the file to write to, not null. + * @param charset the charset to use, null means platform default. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. * @since 2.3 - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final File file, final Charset charset) throws IOException { @@ -237,14 +237,14 @@ public LockableFileWriter(final File file, final Charset charset) throws IOExcep /** * Constructs a LockableFileWriter with a file encoding. * - * @param file the file to write to, not null - * @param charset the name of the requested charset, null means platform default - * @param append true if content should be appended, false to overwrite - * @param lockDir the directory in which the lock file should be held - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error + * @param file the file to write to, not null. + * @param charset the name of the requested charset, null means platform default. + * @param append true if content should be appended, false to overwrite. + * @param lockDir the directory in which the lock file should be held. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. * @since 2.3 - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final File file, final Charset charset, final boolean append, final String lockDir) throws IOException { @@ -270,13 +270,13 @@ public LockableFileWriter(final File file, final Charset charset, final boolean /** * Constructs a LockableFileWriter with a file encoding. * - * @param file the file to write to, not null - * @param charsetName the name of the requested charset, null means platform default - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error + * @param file the file to write to, not null. + * @param charsetName the name of the requested charset, null means platform default. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not * supported. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final File file, final String charsetName) throws IOException { @@ -286,15 +286,15 @@ public LockableFileWriter(final File file, final String charsetName) throws IOEx /** * Constructs a LockableFileWriter with a file encoding. * - * @param file the file to write to, not null - * @param charsetName the encoding to use, null means platform default - * @param append true if content should be appended, false to overwrite - * @param lockDir the directory in which the lock file should be held - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error + * @param file the file to write to, not null. + * @param charsetName the encoding to use, null means platform default. + * @param append true if content should be appended, false to overwrite. + * @param lockDir the directory in which the lock file should be held. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. * @throws java.nio.charset.UnsupportedCharsetException thrown instead of {@link UnsupportedEncodingException} in version 2.2 if the encoding is not * supported. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final File file, final String charsetName, final boolean append, final String lockDir) throws IOException { @@ -304,10 +304,10 @@ public LockableFileWriter(final File file, final String charsetName, final boole /** * Constructs a LockableFileWriter. If the file exists, it is overwritten. * - * @param fileName the file to write to, not null - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the file to write to, not null. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final String fileName) throws IOException { @@ -317,11 +317,11 @@ public LockableFileWriter(final String fileName) throws IOException { /** * Constructs a LockableFileWriter. * - * @param fileName file to write to, not null - * @param append true if content should be appended, false to overwrite - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName file to write to, not null. + * @param append true if content should be appended, false to overwrite. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final String fileName, final boolean append) throws IOException { @@ -331,12 +331,12 @@ public LockableFileWriter(final String fileName, final boolean append) throws IO /** * Constructs a LockableFileWriter. * - * @param fileName the file to write to, not null - * @param append true if content should be appended, false to overwrite - * @param lockDir the directory in which the lock file should be held - * @throws NullPointerException if the file is null - * @throws IOException in case of an I/O error - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param fileName the file to write to, not null. + * @param append true if content should be appended, false to overwrite. + * @param lockDir the directory in which the lock file should be held. + * @throws NullPointerException if the file is null. + * @throws IOException in case of an I/O error. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public LockableFileWriter(final String fileName, final boolean append, final String lockDir) throws IOException { @@ -360,7 +360,7 @@ public void close() throws IOException { /** * Creates the lock file. * - * @throws IOException if we cannot create the file + * @throws IOException if we cannot create the file. */ private void createLock() throws IOException { synchronized (LockableFileWriter.class) { @@ -384,11 +384,11 @@ public void flush() throws IOException { /** * Initializes the wrapped file writer. Ensure that a cleanup occurs if the writer creation fails. * - * @param file the file to be accessed - * @param charset the charset to use - * @param append true to append - * @return The initialized writer - * @throws IOException if an error occurs + * @param file the file to be accessed. + * @param charset the charset to use. + * @param append true to append. + * @return The initialized writer. + * @throws IOException if an error occurs. */ private Writer initWriter(final File file, final Charset charset, final boolean append) throws IOException { final boolean fileExistedAlready = file.exists(); @@ -406,9 +406,9 @@ private Writer initWriter(final File file, final Charset charset, final boolean /** * Tests that we can write to the lock directory. * - * @param lockDir the File representing the lock directory - * @throws IOException if we cannot write to the lock directory - * @throws IOException if we cannot find the lock file + * @param lockDir the File representing the lock directory. + * @throws IOException if we cannot write to the lock directory. + * @throws IOException if we cannot find the lock file. */ private void testLockDir(final File lockDir) throws IOException { if (!lockDir.exists()) { @@ -422,7 +422,7 @@ private void testLockDir(final File lockDir) throws IOException { /** * Writes the characters from an array. * - * @param cbuf the characters to write + * @param cbuf the characters to write. * @throws IOException if an I/O error occurs. */ @Override @@ -433,9 +433,9 @@ public void write(final char[] cbuf) throws IOException { /** * Writes the specified characters from an array. * - * @param cbuf the characters to write - * @param off The start offset - * @param len The number of characters to write + * @param cbuf the characters to write. + * @param off The start offset. + * @param len The number of characters to write. * @throws IOException if an I/O error occurs. */ @Override @@ -446,7 +446,7 @@ public void write(final char[] cbuf, final int off, final int len) throws IOExce /** * Writes a character. * - * @param c the character to write + * @param c the character to write. * @throws IOException if an I/O error occurs. */ @Override @@ -457,7 +457,7 @@ public void write(final int c) throws IOException { /** * Writes the characters from a string. * - * @param str the string to write + * @param str the string to write. * @throws IOException if an I/O error occurs. */ @Override @@ -468,9 +468,9 @@ public void write(final String str) throws IOException { /** * Writes the specified characters from a string. * - * @param str the string to write - * @param off The start offset - * @param len The number of characters to write + * @param str the string to write. + * @param off The start offset. + * @param len The number of characters to write. * @throws IOException if an I/O error occurs. */ @Override diff --git a/src/main/java/org/apache/commons/io/output/NullAppendable.java b/src/main/java/org/apache/commons/io/output/NullAppendable.java index 520daa3a64b..c5ea2cfcbeb 100644 --- a/src/main/java/org/apache/commons/io/output/NullAppendable.java +++ b/src/main/java/org/apache/commons/io/output/NullAppendable.java @@ -19,6 +19,8 @@ import java.io.IOException; +import org.apache.commons.io.IOUtils; + /** * Appends all data to the famous /dev/null. *

    @@ -49,8 +51,24 @@ public Appendable append(final CharSequence csq) throws IOException { return this; } + /** + * Does nothing except argument validation, like writing to {@code /dev/null}. + * + * @param csq The character sequence from which a subsequence will be + * appended. + * If {@code csq} is {@code null}, it is treated as if it were + * {@code "null"}. + * @param start The index of the first character in the subsequence. + * @param end The index of the character following the last character in the + * subsequence. + * @return {@code this} instance. + * @throws IndexOutOfBoundsException If {@code start} or {@code end} are negative, {@code end} is + * greater than {@code csq.length()}, or {@code start} is greater + * than {@code end}. + */ @Override public Appendable append(final CharSequence csq, final int start, final int end) throws IOException { + IOUtils.checkFromToIndex(csq, start, end); return this; } diff --git a/src/main/java/org/apache/commons/io/output/NullOutputStream.java b/src/main/java/org/apache/commons/io/output/NullOutputStream.java index 5275aea9c75..0334a4ea4b8 100644 --- a/src/main/java/org/apache/commons/io/output/NullOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/NullOutputStream.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; + /** * Never writes data. Calls never go beyond this class. *

    @@ -67,15 +69,20 @@ public void write(final byte[] b) throws IOException { } /** - * Does nothing. + * No-op operation. * - * @param b This method ignores this parameter. - * @param off This method ignores this parameter. - * @param len This method ignores this parameter. + *

    Validates the arguments but does not write the data.

    + * + * @param b The byte array to write from, not {@code null}. + * @param off The offset to start at. + * @param len The number of bytes to write. + * @throws NullPointerException If {@code b} is {@code null}. + * @throws IndexOutOfBoundsException If {@code off} or {@code len} are negative, {@code off + len} is greater than + * {@code b.length}. */ @Override public void write(final byte[] b, final int off, final int len) { - // noop + IOUtils.checkFromIndexSize(b, off, len); } /** diff --git a/src/main/java/org/apache/commons/io/output/NullWriter.java b/src/main/java/org/apache/commons/io/output/NullWriter.java index 0a58a7d4c85..635ffb044a6 100644 --- a/src/main/java/org/apache/commons/io/output/NullWriter.java +++ b/src/main/java/org/apache/commons/io/output/NullWriter.java @@ -18,6 +18,8 @@ import java.io.Writer; +import org.apache.commons.io.IOUtils; + /** * Never writes data. Calls never go beyond this class. *

    @@ -51,9 +53,10 @@ public NullWriter() { } /** - * Does nothing - output to {@code /dev/null}. - * @param c The character to write - * @return this writer + * Does nothing, like writing to {@code /dev/null}. + * + * @param c The character to write. + * @return this writer. * @since 2.0 */ @Override @@ -63,8 +66,9 @@ public Writer append(final char c) { } /** - * Does nothing - output to {@code /dev/null}. - * @param csq The character sequence to write + * Does nothing, like writing to {@code /dev/null}. + * + * @param csq The character sequence to write. * @return this writer * @since 2.0 */ @@ -75,53 +79,70 @@ public Writer append(final CharSequence csq) { } /** - * Does nothing - output to {@code /dev/null}. - * @param csq The character sequence to write - * @param start The index of the first character to write - * @param end The index of the first character to write (exclusive) - * @return this writer + * Does nothing except argument validation, like writing to {@code /dev/null}. + * + * @param csq The character sequence from which a subsequence will be + * appended. + * If {@code csq} is {@code null}, it is treated as if it were + * {@code "null"}. + * @param start The index of the first character in the subsequence. + * @param end The index of the character following the last character in the + * subsequence. + * @return {@code this} instance. + * @throws IndexOutOfBoundsException If {@code start} or {@code end} are negative, {@code end} is + * greater than {@code csq.length()}, or {@code start} is greater + * than {@code end}. * @since 2.0 */ @Override public Writer append(final CharSequence csq, final int start, final int end) { + IOUtils.checkFromToIndex(csq, start, end); //to /dev/null return this; } - /** @see java.io.Writer#close() */ + /** @see Writer#close() */ @Override public void close() { //to /dev/null } - /** @see java.io.Writer#flush() */ + /** @see Writer#flush() */ @Override public void flush() { //to /dev/null } /** - * Does nothing - output to {@code /dev/null}. - * @param chr The characters to write + * Does nothing except argument validation, like writing to {@code /dev/null}. + * + * @param chr The characters to write, not {@code null}. + * @throws NullPointerException if {@code chr} is {@code null}. */ @Override public void write(final char[] chr) { + write(chr, 0, chr.length); //to /dev/null } /** - * Does nothing - output to {@code /dev/null}. - * @param chr The characters to write - * @param st The start offset - * @param end The number of characters to write + * Does nothing except argument validation, like writing to {@code /dev/null}. + * + * @param cbuf The characters to write, not {@code null}. + * @param off The start offset. + * @param len The number of characters to write. + * @throws NullPointerException if {@code chr} is {@code null}. + * @throws IndexOutOfBoundsException If ({@code off} or {@code len} are negative, or {@code off + len} is greater than {@code cbuf.length}. */ @Override - public void write(final char[] chr, final int st, final int end) { + public void write(final char[] cbuf, final int off, final int len) { + IOUtils.checkFromIndexSize(cbuf, off, len); //to /dev/null } /** - * Does nothing - output to {@code /dev/null}. + * Does nothing, like writing to {@code /dev/null}. + * * @param b The character to write. */ @Override @@ -130,22 +151,29 @@ public void write(final int b) { } /** - * Does nothing - output to {@code /dev/null}. - * @param str The string to write + * Does nothing except argument validation, like writing to {@code /dev/null}. + * + * @param str The string to write, not {@code null}. + * @throws NullPointerException if {@code str} is {@code null}. */ @Override public void write(final String str) { + write(str, 0, str.length()); //to /dev/null } /** - * Does nothing - output to {@code /dev/null}. - * @param str The string to write - * @param st The start offset - * @param end The number of characters to write + * Does nothing except argument validation, like writing to {@code /dev/null}. + * + * @param str The string to write, not {@code null}. + * @param off The start offset. + * @param len The number of characters to write. + * @throws NullPointerException If {@code str} is {@code null}. + * @throws IndexOutOfBoundsException If ({@code off} or {@code len} are negative, or {@code off + len} is greater than {@code str.length()}. */ @Override - public void write(final String str, final int st, final int end) { + public void write(final String str, final int off, final int len) { + IOUtils.checkFromIndexSize(str, off, len); //to /dev/null } diff --git a/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java b/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java index 3459a931e17..d14dd39aba1 100644 --- a/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java +++ b/src/main/java/org/apache/commons/io/output/ProxyCollectionWriter.java @@ -61,8 +61,8 @@ public ProxyCollectionWriter(final Writer... writers) { * the write methods. The default implementation does nothing. *

    * - * @param n number of chars written - * @throws IOException if the post-processing fails + * @param n number of chars written. + * @throws IOException if the post-processing fails. */ @SuppressWarnings("unused") // Possibly thrown from subclasses. protected void afterWrite(final int n) throws IOException { @@ -72,8 +72,8 @@ protected void afterWrite(final int n) throws IOException { /** * Invokes the delegates' {@code append(char)} methods. * - * @param c The character to write - * @return this writer + * @param c The character to write. + * @return this writer. * @throws IOException if an I/O error occurs. * @since 2.0 */ @@ -92,8 +92,8 @@ public Writer append(final char c) throws IOException { /** * Invokes the delegates' {@code append(CharSequence)} methods. * - * @param csq The character sequence to write - * @return this writer + * @param csq The character sequence to write. + * @return this writer. * @throws IOException if an I/O error occurs. */ @Override @@ -112,10 +112,10 @@ public Writer append(final CharSequence csq) throws IOException { /** * Invokes the delegates' {@code append(CharSequence, int, int)} methods. * - * @param csq The character sequence to write - * @param start The index of the first character to write - * @param end The index of the first character to write (exclusive) - * @return this writer + * @param csq The character sequence to write. + * @param start The index of the first character to write. + * @param end The index of the first character to write (exclusive). + * @return this writer. * @throws IOException if an I/O error occurs. */ @Override @@ -138,8 +138,8 @@ public Writer append(final CharSequence csq, final int start, final int end) thr * write methods. The default implementation does nothing. *

    * - * @param n number of chars to be written - * @throws IOException if the pre-processing fails + * @param n number of chars to be written. + * @throws IOException if the pre-processing fails. */ @SuppressWarnings("unused") // Possibly thrown from subclasses. protected void beforeWrite(final int n) throws IOException { @@ -181,7 +181,7 @@ public void flush() throws IOException { * exception. *

    * - * @param e The IOException thrown + * @param e The IOException thrown. * @throws IOException if an I/O error occurs. */ protected void handleIOException(final IOException e) throws IOException { @@ -191,7 +191,7 @@ protected void handleIOException(final IOException e) throws IOException { /** * Invokes the delegate's {@code write(char[])} method. * - * @param cbuf the characters to write + * @param cbuf the characters to write. * @throws IOException if an I/O error occurs. */ @Override @@ -209,9 +209,9 @@ public void write(final char[] cbuf) throws IOException { /** * Invokes the delegate's {@code write(char[], int, int)} method. * - * @param cbuf the characters to write - * @param off The start offset - * @param len The number of characters to write + * @param cbuf the characters to write. + * @param off The start offset. + * @param len The number of characters to write. * @throws IOException if an I/O error occurs. */ @Override @@ -228,7 +228,7 @@ public void write(final char[] cbuf, final int off, final int len) throws IOExce /** * Invokes the delegate's {@code write(int)} method. * - * @param c the character to write + * @param c the character to write. * @throws IOException if an I/O error occurs. */ @Override @@ -245,7 +245,7 @@ public void write(final int c) throws IOException { /** * Invokes the delegate's {@code write(String)} method. * - * @param str the string to write + * @param str the string to write. * @throws IOException if an I/O error occurs. */ @Override @@ -263,9 +263,9 @@ public void write(final String str) throws IOException { /** * Invokes the delegate's {@code write(String)} method. * - * @param str the string to write - * @param off The start offset - * @param len The number of characters to write + * @param str the string to write. + * @param off The start offset. + * @param len The number of characters to write. * @throws IOException if an I/O error occurs. */ @Override diff --git a/src/main/java/org/apache/commons/io/output/ProxyOutputStream.java b/src/main/java/org/apache/commons/io/output/ProxyOutputStream.java index 6d4edb7fa3b..090f2d059a7 100644 --- a/src/main/java/org/apache/commons/io/output/ProxyOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ProxyOutputStream.java @@ -14,6 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.commons.io.output; import java.io.FilterOutputStream; @@ -24,13 +25,10 @@ import org.apache.commons.io.build.AbstractStreamBuilder; /** - * A Proxy stream which acts as expected, that is it passes the method - * calls on to the proxied stream and doesn't change which methods are - * being called. It is an alternative base class to FilterOutputStream - * to increase reusability. + * A Proxy stream which acts as expected, that is it passes the method calls on to the proxied stream and doesn't change which methods are being called. It is + * an alternative base class to FilterOutputStream to increase reusability. *

    - * See the protected methods for ways in which a subclass can easily decorate - * a stream with custom pre-, post- or error processing functionality. + * See the protected methods for ways in which a subclass can easily decorate a stream with custom pre-, post- or error processing functionality. *

    */ public class ProxyOutputStream extends FilterOutputStream { @@ -72,7 +70,6 @@ public Builder() { public ProxyOutputStream get() throws IOException { return new ProxyOutputStream(this); } - } @SuppressWarnings("resource") // caller closes @@ -84,7 +81,7 @@ public ProxyOutputStream get() throws IOException { /** * Constructs a new ProxyOutputStream. * - * @param delegate the OutputStream to delegate to + * @param delegate the OutputStream to delegate to. */ public ProxyOutputStream(final OutputStream delegate) { // the delegate is stored in a protected superclass variable named 'out' @@ -92,17 +89,15 @@ public ProxyOutputStream(final OutputStream delegate) { } /** - * Invoked by the write methods after the proxied call has returned - * successfully. The number of bytes written (1 for the - * {@link #write(int)} method, buffer length for {@link #write(byte[])}, - * etc.) is given as an argument. + * Invoked by the write methods after the proxied call has returned successfully. The number of bytes written (1 for the {@link #write(int)} method, buffer + * length for {@link #write(byte[])}, etc.) is given as an argument. *

    - * Subclasses can override this method to add common post-processing - * functionality without having to override all the write methods. - * The default implementation does nothing. + * Subclasses can override this method to add common post-processing functionality without having to override all the write methods. The default + * implementation does nothing. + *

    * - * @param n number of bytes written - * @throws IOException if the post-processing fails + * @param n number of bytes written. + * @throws IOException if the post-processing fails. * @since 2.0 */ @SuppressWarnings("unused") // Possibly thrown from subclasses. @@ -111,16 +106,15 @@ protected void afterWrite(final int n) throws IOException { } /** - * Invoked by the write methods before the call is proxied. The number - * of bytes to be written (1 for the {@link #write(int)} method, buffer - * length for {@link #write(byte[])}, etc.) is given as an argument. + * Invoked by the write methods before the call is proxied. The number of bytes to be written (1 for the {@link #write(int)} method, buffer length for + * {@link #write(byte[])}, etc.) is given as an argument. *

    - * Subclasses can override this method to add common pre-processing - * functionality without having to override all the write methods. - * The default implementation does nothing. + * Subclasses can override this method to add common pre-processing functionality without having to override all the write methods. The default + * implementation does nothing. + *

    * - * @param n number of bytes to be written - * @throws IOException if the pre-processing fails + * @param n number of bytes to be written. + * @throws IOException if the pre-processing fails. * @since 2.0 */ @SuppressWarnings("unused") // Possibly thrown from subclasses. @@ -130,6 +124,7 @@ protected void beforeWrite(final int n) throws IOException { /** * Invokes the delegate's {@code close()} method. + * * @throws IOException if an I/O error occurs. */ @Override @@ -139,6 +134,7 @@ public void close() throws IOException { /** * Invokes the delegate's {@code flush()} method. + * * @throws IOException if an I/O error occurs. */ @Override @@ -153,9 +149,10 @@ public void flush() throws IOException { /** * Handle any IOExceptions thrown. *

    - * This method provides a point to implement custom exception - * handling. The default behavior is to re-throw the exception. - * @param e The IOException thrown + * This method provides a point to implement custom exception. handling. The default behavior is to re-throw the exception. + *

    + * + * @param e The IOException thrown. * @throws IOException if an I/O error occurs. * @since 2.0 */ @@ -167,7 +164,7 @@ protected void handleIOException(final IOException e) throws IOException { * Sets the underlying output stream. * * @param out the underlying output stream. - * @return this instance. + * @return {@code this} instance. * @since 2.19.0 */ public ProxyOutputStream setReference(final OutputStream out) { @@ -189,15 +186,16 @@ OutputStream unwrap() { /** * Invokes the delegate's {@code write(byte[])} method. - * @param bts the bytes to write + * + * @param b the bytes to write. * @throws IOException if an I/O error occurs. */ @Override - public void write(final byte[] bts) throws IOException { + public void write(final byte[] b) throws IOException { try { - final int len = IOUtils.length(bts); + final int len = IOUtils.length(b); beforeWrite(len); - out.write(bts); + out.write(b); afterWrite(len); } catch (final IOException e) { handleIOException(e); @@ -206,17 +204,18 @@ public void write(final byte[] bts) throws IOException { /** * Invokes the delegate's {@code write(byte[])} method. - * @param bts the bytes to write - * @param st The start offset - * @param end The number of bytes to write + * + * @param b the bytes to write. + * @param off The start offset. + * @param len The number of bytes to write. * @throws IOException if an I/O error occurs. */ @Override - public void write(final byte[] bts, final int st, final int end) throws IOException { + public void write(final byte[] b, final int off, final int len) throws IOException { try { - beforeWrite(end); - out.write(bts, st, end); - afterWrite(end); + beforeWrite(len); + out.write(b, off, len); + afterWrite(len); } catch (final IOException e) { handleIOException(e); } @@ -224,7 +223,8 @@ public void write(final byte[] bts, final int st, final int end) throws IOExcept /** * Invokes the delegate's {@code write(int)} method. - * @param b the byte to write + * + * @param b the byte to write. * @throws IOException if an I/O error occurs. */ @Override @@ -238,4 +238,50 @@ public void write(final int b) throws IOException { } } + /** + * Invokes the delegate's {@code write(byte[])} method for the {@code repeat} count. + * + * @param b the bytes to write. + * @param off The start offset. + * @param len The number of bytes to write. + * @param repeat How many times to write the bytes in {@code b}. + * @throws IOException if an I/O error occurs. + * @since 2.21.0 + */ + public void writeRepeat(final byte[] b, final int off, final int len, final long repeat) throws IOException { + long remains = repeat; + while (remains-- > 0) { + write(b, off, len); + } + } + + /** + * Invokes the delegate's {@code write(byte[])} method for the {@code repeat} count. + * + * @param b the bytes to write. + * @param repeat How many times to write the bytes in {@code b}. + * @throws IOException if an I/O error occurs. + * @since 2.21.0 + */ + public void writeRepeat(final byte[] b, final long repeat) throws IOException { + long remains = repeat; + while (remains-- > 0) { + write(b); + } + } + + /** + * Invokes the delegate's {@code write(int)} method. + * + * @param b the byte to write. + * @param repeat How many times to write the byte in {@code b}. + * @throws IOException if an I/O error occurs. + * @since 2.21.0 + */ + public void writeRepeat(final int b, final long repeat) throws IOException { + long remains = repeat; + while (remains-- > 0) { + write(b); + } + } } diff --git a/src/main/java/org/apache/commons/io/output/ProxyWriter.java b/src/main/java/org/apache/commons/io/output/ProxyWriter.java index 9c571cfd140..ac88f4c6943 100644 --- a/src/main/java/org/apache/commons/io/output/ProxyWriter.java +++ b/src/main/java/org/apache/commons/io/output/ProxyWriter.java @@ -33,7 +33,7 @@ public class ProxyWriter extends FilterWriter { /** * Constructs a new ProxyWriter. * - * @param delegate the Writer to delegate to + * @param delegate the Writer to delegate to. */ public ProxyWriter(final Writer delegate) { // the delegate is stored in a protected superclass variable named 'out' @@ -62,8 +62,9 @@ protected void afterWrite(final int n) throws IOException { /** * Invokes the delegate's {@code append(char)} method. - * @param c The character to write - * @return this writer + * + * @param c The character to write. + * @return this writer. * @throws IOException if an I/O error occurs. * @since 2.0 */ @@ -81,8 +82,9 @@ public Writer append(final char c) throws IOException { /** * Invokes the delegate's {@code append(CharSequence)} method. - * @param csq The character sequence to write - * @return this writer + * + * @param csq The character sequence to write. + * @return this writer. * @throws IOException if an I/O error occurs. * @since 2.0 */ @@ -101,10 +103,11 @@ public Writer append(final CharSequence csq) throws IOException { /** * Invokes the delegate's {@code append(CharSequence, int, int)} method. - * @param csq The character sequence to write - * @param start The index of the first character to write - * @param end The index of the first character to write (exclusive) - * @return this writer + * + * @param csq The character sequence to write. + * @param start The index of the first character to write. + * @param end The index of the first character to write (exclusive). + * @return this writer. * @throws IOException if an I/O error occurs. * @since 2.0 */ @@ -130,8 +133,8 @@ public Writer append(final CharSequence csq, final int start, final int end) thr * The default implementation does nothing. *

    * - * @param n number of chars to be written - * @throws IOException if the pre-processing fails + * @param n number of chars to be written. + * @throws IOException if the pre-processing fails. * @since 2.0 */ @SuppressWarnings("unused") // Possibly thrown from subclasses. @@ -168,7 +171,7 @@ public void flush() throws IOException { * handling. The default behavior is to re-throw the exception. *

    * - * @param e The IOException thrown + * @param e The IOException thrown. * @throws IOException if an I/O error occurs. * @since 2.0 */ @@ -178,7 +181,8 @@ protected void handleIOException(final IOException e) throws IOException { /** * Invokes the delegate's {@code write(char[])} method. - * @param cbuf the characters to write + * + * @param cbuf the characters to write. * @throws IOException if an I/O error occurs. */ @Override @@ -195,9 +199,10 @@ public void write(final char[] cbuf) throws IOException { /** * Invokes the delegate's {@code write(char[], int, int)} method. - * @param cbuf the characters to write - * @param off The start offset - * @param len The number of characters to write + * + * @param cbuf the characters to write. + * @param off The start offset. + * @param len The number of characters to write. * @throws IOException if an I/O error occurs. */ @Override @@ -213,7 +218,8 @@ public void write(final char[] cbuf, final int off, final int len) throws IOExce /** * Invokes the delegate's {@code write(int)} method. - * @param c the character to write + * + * @param c the character to write. * @throws IOException if an I/O error occurs. */ @Override @@ -229,7 +235,8 @@ public void write(final int c) throws IOException { /** * Invokes the delegate's {@code write(String)} method. - * @param str the string to write + * + * @param str the string to write. * @throws IOException if an I/O error occurs. */ @Override @@ -246,9 +253,10 @@ public void write(final String str) throws IOException { /** * Invokes the delegate's {@code write(String)} method. - * @param str the string to write - * @param off The start offset - * @param len The number of characters to write + * + * @param str the string to write. + * @param off The start offset. + * @param len The number of characters to write. * @throws IOException if an I/O error occurs. */ @Override diff --git a/src/main/java/org/apache/commons/io/output/QueueOutputStream.java b/src/main/java/org/apache/commons/io/output/QueueOutputStream.java index 50c93bd5feb..99e334f88f6 100644 --- a/src/main/java/org/apache/commons/io/output/QueueOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/QueueOutputStream.java @@ -67,7 +67,7 @@ public QueueOutputStream() { /** * Constructs a new instance with given buffer. * - * @param blockingQueue backing queue for the stream + * @param blockingQueue backing queue for the stream. */ public QueueOutputStream(final BlockingQueue blockingQueue) { this.blockingQueue = Objects.requireNonNull(blockingQueue, "blockingQueue"); @@ -77,7 +77,7 @@ public QueueOutputStream(final BlockingQueue blockingQueue) { * Constructs a new QueueInputStream instance connected to this. Writes to this output stream will be visible to the * input stream. * - * @return QueueInputStream connected to this stream + * @return QueueInputStream connected to this stream. */ public QueueInputStream newQueueInputStream() { return QueueInputStream.builder().setBlockingQueue(blockingQueue).get(); diff --git a/src/main/java/org/apache/commons/io/output/StringBuilderWriter.java b/src/main/java/org/apache/commons/io/output/StringBuilderWriter.java index db8e9d41a8e..bdb757cf684 100644 --- a/src/main/java/org/apache/commons/io/output/StringBuilderWriter.java +++ b/src/main/java/org/apache/commons/io/output/StringBuilderWriter.java @@ -20,6 +20,8 @@ import java.io.StringWriter; import java.io.Writer; +import org.apache.commons.io.IOUtils; + /** * {@link Writer} implementation that outputs to a {@link StringBuilder}. *

    @@ -70,8 +72,8 @@ public StringBuilderWriter(final StringBuilder builder) { /** * Appends a single character to this Writer. * - * @param value The character to append - * @return This writer instance + * @param value The character to append. + * @return This writer instance. */ @Override public Writer append(final char value) { @@ -82,8 +84,8 @@ public Writer append(final char value) { /** * Appends a character sequence to this Writer. * - * @param value The character to append - * @return This writer instance + * @param value The character to append. + * @return This writer instance. */ @Override public Writer append(final CharSequence value) { @@ -94,10 +96,10 @@ public Writer append(final CharSequence value) { /** * Appends a portion of a character sequence to the {@link StringBuilder}. * - * @param value The character to append - * @param start The index of the first character - * @param end The index of the last character + 1 - * @return This writer instance + * @param value The character to append. + * @param start The index of the first character. + * @param end The index of the last character + 1. + * @return This writer instance. */ @Override public Writer append(final CharSequence value, final int start, final int end) { @@ -124,7 +126,7 @@ public void flush() { /** * Gets the underlying builder. * - * @return The underlying builder + * @return The underlying builder. */ public StringBuilder getBuilder() { return builder; @@ -143,13 +145,15 @@ public String toString() { /** * Writes a portion of a character array to the {@link StringBuilder}. * - * @param value The value to write - * @param offset The index of the first character - * @param length The number of characters to write + * @param value The value to write. + * @param offset The index of the first character. + * @param length The number of characters to write. + * @throws IndexOutOfBoundsException if {@code offset} or {@code length} are negative, or if {@code offset + length} is greater than {@code value.length}. */ @Override public void write(final char[] value, final int offset, final int length) { if (value != null) { + IOUtils.checkFromIndexSize(value, offset, length); builder.append(value, offset, length); } } @@ -157,7 +161,7 @@ public void write(final char[] value, final int offset, final int length) { /** * Writes a String to the {@link StringBuilder}. * - * @param value The value to write + * @param value The value to write. */ @Override public void write(final String value) { diff --git a/src/main/java/org/apache/commons/io/output/TaggedOutputStream.java b/src/main/java/org/apache/commons/io/output/TaggedOutputStream.java index 26284584a8c..fa92b9c3d2a 100644 --- a/src/main/java/org/apache/commons/io/output/TaggedOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/TaggedOutputStream.java @@ -72,7 +72,7 @@ public class TaggedOutputStream extends ProxyOutputStream { /** * Constructs a tagging decorator for the given output stream. * - * @param proxy output stream to be decorated + * @param proxy output stream to be decorated. */ public TaggedOutputStream(final OutputStream proxy) { super(proxy); @@ -81,7 +81,7 @@ public TaggedOutputStream(final OutputStream proxy) { /** * Tags any IOExceptions thrown, wrapping and re-throwing. * - * @param e The IOException thrown + * @param e The IOException thrown. * @throws IOException if an I/O error occurs. */ @Override @@ -92,9 +92,9 @@ protected void handleIOException(final IOException e) throws IOException { /** * Tests if the given exception was caused by this stream. * - * @param exception an exception + * @param exception an exception. * @return {@code true} if the exception was thrown by this stream, - * {@code false} otherwise + * {@code false} otherwise. */ public boolean isCauseOf(final Exception exception) { return TaggedIOException.isTaggedWith(exception, tag); @@ -107,8 +107,8 @@ public boolean isCauseOf(final Exception exception) { * original wrapped exception. Returns normally if the exception was * not thrown by this stream. * - * @param exception an exception - * @throws IOException original exception, if any, thrown by this stream + * @param exception an exception. + * @throws IOException original exception, if any, thrown by this stream. */ public void throwIfCauseOf(final Exception exception) throws IOException { TaggedIOException.throwCauseIfTaggedWith(exception, tag); diff --git a/src/main/java/org/apache/commons/io/output/TaggedWriter.java b/src/main/java/org/apache/commons/io/output/TaggedWriter.java index 04263df9032..cae40750519 100644 --- a/src/main/java/org/apache/commons/io/output/TaggedWriter.java +++ b/src/main/java/org/apache/commons/io/output/TaggedWriter.java @@ -72,7 +72,7 @@ public class TaggedWriter extends ProxyWriter { /** * Constructs a tagging decorator for the given writer. * - * @param proxy writer to be decorated + * @param proxy writer to be decorated. */ public TaggedWriter(final Writer proxy) { super(proxy); @@ -81,7 +81,7 @@ public TaggedWriter(final Writer proxy) { /** * Tags any IOExceptions thrown, wrapping and re-throwing. * - * @param e The IOException thrown + * @param e The IOException thrown. * @throws IOException if an I/O error occurs. */ @Override @@ -92,9 +92,9 @@ protected void handleIOException(final IOException e) throws IOException { /** * Tests if the given exception was caused by this writer. * - * @param exception an exception + * @param exception an exception. * @return {@code true} if the exception was thrown by this writer, - * {@code false} otherwise + * {@code false} otherwise. */ public boolean isCauseOf(final Exception exception) { return TaggedIOException.isTaggedWith(exception, tag); @@ -107,8 +107,8 @@ public boolean isCauseOf(final Exception exception) { * original wrapped exception. Returns normally if the exception was * not thrown by this writer. * - * @param exception an exception - * @throws IOException original exception, if any, thrown by this writer + * @param exception an exception. + * @throws IOException original exception, if any, thrown by this writer. */ public void throwIfCauseOf(final Exception exception) throws IOException { TaggedIOException.throwCauseIfTaggedWith(exception, tag); diff --git a/src/main/java/org/apache/commons/io/output/TeeOutputStream.java b/src/main/java/org/apache/commons/io/output/TeeOutputStream.java index 8c3aff11826..72a1eb9e4c6 100644 --- a/src/main/java/org/apache/commons/io/output/TeeOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/TeeOutputStream.java @@ -79,7 +79,7 @@ public void flush() throws IOException { /** * Writes the bytes to both streams. * - * @param b the bytes to write + * @param b the bytes to write. * @throws IOException if an I/O error occurs. */ @Override @@ -91,9 +91,9 @@ public synchronized void write(final byte[] b) throws IOException { /** * Writes the specified bytes to both streams. * - * @param b the bytes to write - * @param off The start offset - * @param len The number of bytes to write + * @param b the bytes to write. + * @param off The start offset. + * @param len The number of bytes to write. * @throws IOException if an I/O error occurs. */ @Override @@ -105,7 +105,7 @@ public synchronized void write(final byte[] b, final int off, final int len) thr /** * Writes a byte to both streams. * - * @param b the byte to write + * @param b the byte to write. * @throws IOException if an I/O error occurs. */ @Override diff --git a/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java b/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java index d596df9f174..742572b2ebc 100644 --- a/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/ThresholdingOutputStream.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.function.IOConsumer; import org.apache.commons.io.function.IOFunction; @@ -206,7 +207,7 @@ protected void resetByteCount() { /** * Sets the byteCount to count. Useful for re-opening an output stream that has previously been written to. * - * @param count The number of bytes that have already been written to the output stream + * @param count The number of bytes that have already been written to the output stream. * @since 2.5 */ protected void setByteCount(final long count) { @@ -227,6 +228,7 @@ protected void thresholdReached() throws IOException { * Writes {@code b.length} bytes from the specified byte array to this output stream. * * @param b The array of bytes to be written. + * @throws NullPointerException if the byte array is {@code null}. * @throws IOException if an error occurs. */ @SuppressWarnings("resource") // the underlying stream is managed by a subclass. @@ -244,11 +246,14 @@ public void write(final byte[] b) throws IOException { * @param b The byte array from which the data will be written. * @param off The start offset in the byte array. * @param len The number of bytes to write. + * @throws NullPointerException if the byte array is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code b.length}. * @throws IOException if an error occurs. */ @SuppressWarnings("resource") // the underlying stream is managed by a subclass. @Override public void write(final byte[] b, final int off, final int len) throws IOException { + IOUtils.checkFromIndexSize(b, off, len); // TODO we could write the sub-array up the threshold, fire the event, // and then write the rest so the event is always fired at the precise point. checkThreshold(len); diff --git a/src/main/java/org/apache/commons/io/output/UncheckedFilterWriter.java b/src/main/java/org/apache/commons/io/output/UncheckedFilterWriter.java index 97da43f7cc5..f04a4df653f 100644 --- a/src/main/java/org/apache/commons/io/output/UncheckedFilterWriter.java +++ b/src/main/java/org/apache/commons/io/output/UncheckedFilterWriter.java @@ -110,7 +110,6 @@ public static Builder builder() { * Constructs a new filtered writer. * * @param builder a Writer object providing the underlying stream. - * @throws IOException * @throws NullPointerException if {@code builder} the its {@code Writer} is {@code null}. * @throws IOException if an I/O error occurs converting to an {@link Writer} using {@link #getWriter()}. */ diff --git a/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java b/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java index fe477b24c4c..a47a3b2aa2a 100644 --- a/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/UnsynchronizedByteArrayOutputStream.java @@ -21,6 +21,7 @@ import java.io.InputStream; import java.io.OutputStream; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractOrigin; import org.apache.commons.io.build.AbstractStreamBuilder; import org.apache.commons.io.function.Uncheck; @@ -111,8 +112,10 @@ public static Builder builder() { *

  • It has network resources associated, so we cannot keep it open for long time.
  • *
  • It has network timeout associated.
  • *
+ *

* It can be used in favor of {@link #toByteArray()}, since it avoids unnecessary allocation and copy of byte[].
* This method buffers the input internally, so there is no need to use a {@link BufferedInputStream}. + *

* * @param input Stream to be fully buffered. * @return A fully buffered stream. @@ -132,11 +135,13 @@ public static InputStream toBufferedInputStream(final InputStream input) throws *
  • It has network resources associated, so we cannot keep it open for long time.
  • *
  • It has network timeout associated.
  • * + *

    * It can be used in favor of {@link #toByteArray()}, since it avoids unnecessary allocation and copy of byte[].
    * This method buffers the input internally, so there is no need to use a {@link BufferedInputStream}. + *

    * * @param input Stream to be fully buffered. - * @param size the initial buffer size + * @param size the initial buffer size. * @return A fully buffered stream. * @throws IOException if an I/O error occurs. */ @@ -149,7 +154,7 @@ public static InputStream toBufferedInputStream(final InputStream input, final i } /** - * Constructs a new byte array output stream. The buffer capacity is initially + * Constructs a new byte array output stream. The buffer capacity is initially. * * {@value AbstractByteArrayOutputStream#DEFAULT_SIZE} bytes, though its size increases if necessary. * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. @@ -166,8 +171,8 @@ private UnsynchronizedByteArrayOutputStream(final Builder builder) { /** * Constructs a new byte array output stream, with a buffer capacity of the specified size, in bytes. * - * @param size the initial size - * @throws IllegalArgumentException if size is negative + * @param size the initial size. + * @throws IllegalArgumentException if size is negative. * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. Will be private in 3.0.0. */ @Deprecated @@ -210,9 +215,7 @@ public InputStream toInputStream() { @Override public void write(final byte[] b, final int off, final int len) { - if (off < 0 || off > b.length || len < 0 || off + len > b.length || off + len < 0) { - throw new IndexOutOfBoundsException(String.format("offset=%,d, length=%,d", off, len)); - } + IOUtils.checkFromIndexSize(b, off, len); if (len == 0) { return; } diff --git a/src/main/java/org/apache/commons/io/output/WriterOutputStream.java b/src/main/java/org/apache/commons/io/output/WriterOutputStream.java index cea2dfb86b5..9292aaf70e8 100644 --- a/src/main/java/org/apache/commons/io/output/WriterOutputStream.java +++ b/src/main/java/org/apache/commons/io/output/WriterOutputStream.java @@ -251,12 +251,12 @@ private WriterOutputStream(final Builder builder) throws IOException { } /** - * Constructs a new {@link WriterOutputStream} that uses the virtual machine's {@link Charset#defaultCharset() default charset} and with a default output - * buffer size of {@value #BUFFER_SIZE} characters. The output buffer will only be flushed when it overflows or when {@link #flush()} or {@link #close()} is - * called. + * Constructs a new {@link WriterOutputStream} that uses the virtual machine's {@linkplain Charset#defaultCharset() default charset} and with a default + * output buffer size of {@value #BUFFER_SIZE} characters. The output buffer will only be flushed when it overflows or when {@link #flush()} or + * {@link #close()} is called. * - * @param writer the target {@link Writer} - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param writer the target {@link Writer}. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer) { @@ -267,9 +267,9 @@ public WriterOutputStream(final Writer writer) { * Constructs a new {@link WriterOutputStream} with a default output buffer size of {@value #BUFFER_SIZE} characters. The output buffer will only be flushed * when it overflows or when {@link #flush()} or {@link #close()} is called. * - * @param writer the target {@link Writer} - * @param charset the charset encoding - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param writer the target {@link Writer}. + * @param charset the charset encoding. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer, final Charset charset) { @@ -279,13 +279,13 @@ public WriterOutputStream(final Writer writer, final Charset charset) { /** * Constructs a new {@link WriterOutputStream}. * - * @param writer the target {@link Writer} - * @param charset the charset encoding - * @param bufferSize the size of the output buffer in number of characters + * @param writer the target {@link Writer}. + * @param charset the charset encoding. + * @param bufferSize the size of the output buffer in number of characters. * @param writeImmediately If {@code true} the output buffer will be flushed after each write operation, meaning all available data will be written to the * underlying {@link Writer} immediately. If {@code false}, the output buffer will only be flushed when it overflows or when * {@link #flush()} or {@link #close()} is called. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer, final Charset charset, final int bufferSize, final boolean writeImmediately) { @@ -304,10 +304,10 @@ public WriterOutputStream(final Writer writer, final Charset charset, final int * Constructs a new {@link WriterOutputStream} with a default output buffer size of {@value #BUFFER_SIZE} characters. The output buffer will only be flushed * when it overflows or when {@link #flush()} or {@link #close()} is called. * - * @param writer the target {@link Writer} - * @param decoder the charset decoder + * @param writer the target {@link Writer}. + * @param decoder the charset decoder. * @since 2.1 - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer, final CharsetDecoder decoder) { @@ -317,14 +317,14 @@ public WriterOutputStream(final Writer writer, final CharsetDecoder decoder) { /** * Constructs a new {@link WriterOutputStream}. * - * @param writer the target {@link Writer} - * @param decoder the charset decoder - * @param bufferSize the size of the output buffer in number of characters + * @param writer the target {@link Writer}. + * @param decoder the charset decoder. + * @param bufferSize the size of the output buffer in number of characters. * @param writeImmediately If {@code true} the output buffer will be flushed after each write operation, meaning all available data will be written to the * underlying {@link Writer} immediately. If {@code false}, the output buffer will only be flushed when it overflows or when * {@link #flush()} or {@link #close()} is called. * @since 2.1 - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer, final CharsetDecoder decoder, final int bufferSize, final boolean writeImmediately) { @@ -339,9 +339,9 @@ public WriterOutputStream(final Writer writer, final CharsetDecoder decoder, fin * Constructs a new {@link WriterOutputStream} with a default output buffer size of {@value #BUFFER_SIZE} characters. The output buffer will only be flushed * when it overflows or when {@link #flush()} or {@link #close()} is called. * - * @param writer the target {@link Writer} - * @param charsetName the name of the charset encoding - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param writer the target {@link Writer}. + * @param charsetName the name of the charset encoding. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer, final String charsetName) { @@ -351,13 +351,13 @@ public WriterOutputStream(final Writer writer, final String charsetName) { /** * Constructs a new {@link WriterOutputStream}. * - * @param writer the target {@link Writer} - * @param charsetName the name of the charset encoding - * @param bufferSize the size of the output buffer in number of characters + * @param writer the target {@link Writer}. + * @param charsetName the name of the charset encoding. + * @param bufferSize the size of the output buffer in number of characters. * @param writeImmediately If {@code true} the output buffer will be flushed after each write operation, meaning all available data will be written to the * underlying {@link Writer} immediately. If {@code false}, the output buffer will only be flushed when it overflows or when * {@link #flush()} or {@link #close()} is called. - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public WriterOutputStream(final Writer writer, final String charsetName, final int bufferSize, final boolean writeImmediately) { @@ -404,7 +404,7 @@ private void flushOutput() throws IOException { /** * Decode the contents of the input ByteBuffer into a CharBuffer. * - * @param endOfInput indicates end of input + * @param endOfInput indicates end of input. * @throws IOException if an I/O error occurs. */ private void processInput(final boolean endOfInput) throws IOException { @@ -430,7 +430,8 @@ private void processInput(final boolean endOfInput) throws IOException { /** * Writes bytes from the specified byte array to the stream. * - * @param b the byte array containing the bytes to write + * @param b the byte array containing the bytes to write. + * @throws NullPointerException if the byte array is {@code null}. * @throws IOException if an I/O error occurs. */ @Override @@ -441,13 +442,16 @@ public void write(final byte[] b) throws IOException { /** * Writes bytes from the specified byte array to the stream. * - * @param b the byte array containing the bytes to write - * @param off the start offset in the byte array - * @param len the number of bytes to write + * @param b the byte array containing the bytes to write. + * @param off the start offset in the byte array. + * @param len the number of bytes to write. + * @throws NullPointerException if the byte array is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, or if {@code off + len} is greater than {@code b.length}. * @throws IOException if an I/O error occurs. */ @Override public void write(final byte[] b, int off, int len) throws IOException { + IOUtils.checkFromIndexSize(b, off, len); while (len > 0) { final int c = Math.min(len, decoderIn.remaining()); decoderIn.put(b, off, c); @@ -463,7 +467,7 @@ public void write(final byte[] b, int off, int len) throws IOException { /** * Writes a single byte to the stream. * - * @param b the byte to write + * @param b the byte to write. * @throws IOException if an I/O error occurs. */ @Override diff --git a/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java b/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java index cc205a42364..873df0238d8 100644 --- a/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java +++ b/src/main/java/org/apache/commons/io/output/XmlStreamWriter.java @@ -133,10 +133,10 @@ private XmlStreamWriter(final Builder builder) throws IOException { * Constructs a new XML stream writer for the specified file * with a default encoding of UTF-8. * - * @param file The file to write to + * @param file The file to write to. * @throws FileNotFoundException if there is an error creating or - * opening the file - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * opening the file. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public XmlStreamWriter(final File file) throws FileNotFoundException { @@ -147,11 +147,11 @@ public XmlStreamWriter(final File file) throws FileNotFoundException { * Constructs a new XML stream writer for the specified file * with the specified default encoding. * - * @param file The file to write to - * @param defaultEncoding The default encoding if not encoding could be detected + * @param file The file to write to. + * @param defaultEncoding The default encoding if not encoding could be detected. * @throws FileNotFoundException if there is an error creating or - * opening the file - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * opening the file. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated @SuppressWarnings("resource") @@ -163,8 +163,8 @@ public XmlStreamWriter(final File file, final String defaultEncoding) throws Fil * Constructs a new XML stream writer for the specified output stream * with a default encoding of UTF-8. * - * @param out The output stream - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param out The output stream. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public XmlStreamWriter(final OutputStream out) { @@ -175,8 +175,8 @@ public XmlStreamWriter(final OutputStream out) { * Constructs a new XML stream writer for the specified output stream * with the specified default encoding. * - * @param out The output stream - * @param defaultEncoding The default encoding if not encoding could be detected + * @param out The output stream. + * @param defaultEncoding The default encoding if not encoding could be detected. */ private XmlStreamWriter(final OutputStream out, final Charset defaultEncoding) { this.out = out; @@ -187,9 +187,9 @@ private XmlStreamWriter(final OutputStream out, final Charset defaultEncoding) { * Constructs a new XML stream writer for the specified output stream * with the specified default encoding. * - * @param out The output stream - * @param defaultEncoding The default encoding if not encoding could be detected - * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()} + * @param out The output stream. + * @param defaultEncoding The default encoding if not encoding could be detected. + * @deprecated Use {@link #builder()}, {@link Builder}, and {@link Builder#get()}. */ @Deprecated public XmlStreamWriter(final OutputStream out, final String defaultEncoding) { @@ -199,7 +199,7 @@ public XmlStreamWriter(final OutputStream out, final String defaultEncoding) { /** * Closes the underlying writer. * - * @throws IOException if an error occurs closing the underlying writer + * @throws IOException if an error occurs closing the underlying writer. */ @Override public void close() throws IOException { @@ -214,10 +214,10 @@ public void close() throws IOException { /** * Detects the encoding. * - * @param cbuf the buffer to write the characters from - * @param off The start offset - * @param len The number of characters to write - * @throws IOException if an error occurs detecting the encoding + * @param cbuf the buffer to write the characters from. + * @param off The start offset. + * @param len The number of characters to write. + * @throws IOException if an error occurs detecting the encoding. */ private void detectEncoding(final char[] cbuf, final int off, final int len) throws IOException { @@ -269,7 +269,7 @@ private void detectEncoding(final char[] cbuf, final int off, final int len) /** * Flushes the underlying writer. * - * @throws IOException if an error occurs flushing the underlying writer + * @throws IOException if an error occurs flushing the underlying writer. */ @Override public void flush() throws IOException { @@ -281,7 +281,7 @@ public void flush() throws IOException { /** * Returns the default encoding. * - * @return the default encoding + * @return the default encoding. */ public String getDefaultEncoding() { return defaultCharset.name(); @@ -290,7 +290,7 @@ public String getDefaultEncoding() { /** * Returns the detected encoding. * - * @return the detected encoding + * @return the detected encoding. */ public String getEncoding() { return charset.name(); @@ -299,13 +299,17 @@ public String getEncoding() { /** * Writes the characters to the underlying writer, detecting encoding. * - * @param cbuf the buffer to write the characters from - * @param off The start offset - * @param len The number of characters to write - * @throws IOException if an error occurs detecting the encoding + * @param cbuf the buffer to write the characters from. + * @param off The start offset. + * @param len The number of characters to write. + * @throws NullPointerException if the buffer is {@code null}. + * @throws IndexOutOfBoundsException if {@code off} or {@code len} are negative, + * or if {@code off + len} is greater than {@code cbuf.length}. + * @throws IOException if an error occurs detecting the encoding. */ @Override public void write(final char[] cbuf, final int off, final int len) throws IOException { + IOUtils.checkFromIndexSize(cbuf, off, len); if (prologWriter != null) { detectEncoding(cbuf, off, len); } else { diff --git a/src/main/java/org/apache/commons/io/serialization/ObjectStreamClassPredicate.java b/src/main/java/org/apache/commons/io/serialization/ObjectStreamClassPredicate.java index 1f91b0b0e95..5a3375cd72e 100644 --- a/src/main/java/org/apache/commons/io/serialization/ObjectStreamClassPredicate.java +++ b/src/main/java/org/apache/commons/io/serialization/ObjectStreamClassPredicate.java @@ -70,7 +70,7 @@ public ObjectStreamClassPredicate accept(final Class... classes) { *

    * * @param matcher a class name matcher to accept objects. - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate accept(final ClassNameMatcher matcher) { acceptMatchers.add(matcher); @@ -84,7 +84,7 @@ public ObjectStreamClassPredicate accept(final ClassNameMatcher matcher) { *

    * * @param pattern a Pattern for compiled regular expression. - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate accept(final Pattern pattern) { acceptMatchers.add(new RegexpClassNameMatcher(pattern)); @@ -99,7 +99,7 @@ public ObjectStreamClassPredicate accept(final Pattern pattern) { * * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate accept(final String... patterns) { Stream.of(patterns).map(WildcardClassNameMatcher::new).forEach(acceptMatchers::add); @@ -113,7 +113,7 @@ public ObjectStreamClassPredicate accept(final String... patterns) { *

    * * @param classes Classes to reject - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate reject(final Class... classes) { Stream.of(classes).map(c -> new FullClassNameMatcher(c.getName())).forEach(rejectMatchers::add); @@ -127,7 +127,7 @@ public ObjectStreamClassPredicate reject(final Class... classes) { *

    * * @param m the matcher to use - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate reject(final ClassNameMatcher m) { rejectMatchers.add(m); @@ -141,7 +141,7 @@ public ObjectStreamClassPredicate reject(final ClassNameMatcher m) { *

    * * @param pattern standard Java regexp - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate reject(final Pattern pattern) { rejectMatchers.add(new RegexpClassNameMatcher(pattern)); @@ -156,7 +156,7 @@ public ObjectStreamClassPredicate reject(final Pattern pattern) { * * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} - * @return this instance. + * @return {@code this} instance. */ public ObjectStreamClassPredicate reject(final String... patterns) { Stream.of(patterns).map(WildcardClassNameMatcher::new).forEach(rejectMatchers::add); diff --git a/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java b/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java index 1ee48ed4d88..d2e51425b24 100644 --- a/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java +++ b/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java @@ -74,7 +74,7 @@ * } * } *

    - * Design inspired by a IBM DeveloperWorks Article. + * Design inspired by a IBM DeveloperWorks Article. *

    * * @since 2.5 @@ -132,7 +132,7 @@ public Builder accept(final Class... classes) { * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected. * * @param matcher a class name matcher to accept objects. - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder accept(final ClassNameMatcher matcher) { @@ -144,7 +144,7 @@ public Builder accept(final ClassNameMatcher matcher) { * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected. * * @param pattern a Pattern for compiled regular expression. - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder accept(final Pattern pattern) { @@ -157,7 +157,7 @@ public Builder accept(final Pattern pattern) { * * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder accept(final String... patterns) { @@ -205,7 +205,7 @@ public ObjectStreamClassPredicate getPredicate() { * Rejects the specified classes for deserialization, even if they are otherwise accepted. * * @param classes Classes to reject - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder reject(final Class... classes) { @@ -217,7 +217,7 @@ public Builder reject(final Class... classes) { * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted. * * @param matcher the matcher to use - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder reject(final ClassNameMatcher matcher) { @@ -229,7 +229,7 @@ public Builder reject(final ClassNameMatcher matcher) { * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted. * * @param pattern standard Java regexp - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder reject(final Pattern pattern) { @@ -242,7 +242,7 @@ public Builder reject(final Pattern pattern) { * * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder reject(final String... patterns) { @@ -254,7 +254,7 @@ public Builder reject(final String... patterns) { * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate. * * @param predicate the predicate. - * @return this instance. + * @return {@code this} instance. * @since 2.18.0 */ public Builder setPredicate(final ObjectStreamClassPredicate predicate) { @@ -314,7 +314,7 @@ private ValidatingObjectInputStream(final InputStream input, final ObjectStreamC *

    * * @param classes Classes to accept - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream accept(final Class... classes) { predicate.accept(classes); @@ -328,7 +328,7 @@ public ValidatingObjectInputStream accept(final Class... classes) { *

    * * @param matcher a class name matcher to accept objects. - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) { predicate.accept(matcher); @@ -342,7 +342,7 @@ public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) { *

    * * @param pattern a Pattern for compiled regular expression. - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream accept(final Pattern pattern) { predicate.accept(pattern); @@ -357,7 +357,7 @@ public ValidatingObjectInputStream accept(final Pattern pattern) { * * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch}. - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream accept(final String... patterns) { predicate.accept(patterns); @@ -412,7 +412,7 @@ public T readObjectCast() throws ClassNotFoundException, IOException { *

    * * @param classes Classes to reject. - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream reject(final Class... classes) { predicate.reject(classes); @@ -426,7 +426,7 @@ public ValidatingObjectInputStream reject(final Class... classes) { *

    * * @param matcher a class name matcher to reject objects. - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) { predicate.reject(matcher); @@ -440,7 +440,7 @@ public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) { *

    * * @param pattern a Pattern for compiled regular expression. - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream reject(final Pattern pattern) { predicate.reject(pattern); @@ -455,16 +455,36 @@ public ValidatingObjectInputStream reject(final Pattern pattern) { * * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} - * @return this instance. + * @return {@code this} instance. */ public ValidatingObjectInputStream reject(final String... patterns) { predicate.reject(patterns); return this; } + /** + * Checks that the given object's class name conforms to requirements and if so delegates to the superclass. + *

    + * The reject list takes precedence over the accept list. + *

    + */ @Override protected Class resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException { checkClassName(osc.getName()); return super.resolveClass(osc); } + + /** + * Checks that the given names conform to requirements and if so delegates to the superclass. + *

    + * The reject list takes precedence over the accept list. + *

    + */ + @Override + protected Class resolveProxyClass(final String[] interfaces) throws IOException, ClassNotFoundException { + for (final String interfaceName : interfaces) { + checkClassName(interfaceName); + } + return super.resolveProxyClass(interfaces); + } } diff --git a/src/main/javadoc/overview.html b/src/main/javadoc/overview.html index 7fa4bd1f700..a2204feeade 100644 --- a/src/main/javadoc/overview.html +++ b/src/main/javadoc/overview.html @@ -17,16 +17,18 @@ --> -

    -The Apache Commons IO component contains utility classes, -filters, streams, readers and writers. -

    -

    -These classes aim to add to the standard JDK IO classes. -The utilities provide convenience wrappers around the JDK, simplifying -various operations into pre-tested units of code. -The filters and streams provide useful implementations that perhaps should -be in the JDK itself. -

    + Apache Commons IO + +

    + leafIntroduction +

    +

    Apache Commons IO is a library of utilities to assist with developing IO functionality as the package list below describes.

    +

    + leafRequirements +

    +
      +
    • Java 8 or above.
    • +
    • If using OSGi, R7 or above.
    • +
    diff --git a/src/media/commons-logo-component-100.xcf b/src/media/commons-logo-component-100.xcf new file mode 100644 index 00000000000..d767311a12d Binary files /dev/null and b/src/media/commons-logo-component-100.xcf differ diff --git a/src/media/commons-logo-component.xcf b/src/media/commons-logo-component.xcf new file mode 100644 index 00000000000..a7dcdd43c1e Binary files /dev/null and b/src/media/commons-logo-component.xcf differ diff --git a/src/media/io-logo-white.xcf b/src/media/io-logo-white.xcf deleted file mode 100644 index a3c899b544a..00000000000 Binary files a/src/media/io-logo-white.xcf and /dev/null differ diff --git a/src/media/logo.gif b/src/media/logo.gif deleted file mode 100644 index 314441b415c..00000000000 Binary files a/src/media/logo.gif and /dev/null differ diff --git a/src/media/logo.png b/src/media/logo.png new file mode 100644 index 00000000000..02a758f0ed8 Binary files /dev/null and b/src/media/logo.png differ diff --git a/src/site/resources/images/leaf.svg b/src/site/resources/images/leaf.svg new file mode 100644 index 00000000000..71de588c648 --- /dev/null +++ b/src/site/resources/images/leaf.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/site/resources/images/logo.png b/src/site/resources/images/logo.png index 75be7af462d..02a758f0ed8 100644 Binary files a/src/site/resources/images/logo.png and b/src/site/resources/images/logo.png differ diff --git a/src/site/xdoc/building.xml b/src/site/xdoc/building.xml index be5de6b1a1d..bef84849f24 100644 --- a/src/site/xdoc/building.xml +++ b/src/site/xdoc/building.xml @@ -25,7 +25,7 @@ limitations under the License.

    - Commons IO uses Maven its build system. + Commons IO uses Maven its build system.

    Commons IO requires a minimum of JDK 8 to build. @@ -46,7 +46,7 @@ limitations under the License.

    - The following Maven commands can be used to build io: + The following Maven commands can be used to build io:

    • mvn - runs the default Maven goal which performs all build checks
    • diff --git a/src/site/xdoc/description.xml b/src/site/xdoc/description.xml index ac5ab0a77ed..230a43a8410 100644 --- a/src/site/xdoc/description.xml +++ b/src/site/xdoc/description.xml @@ -157,7 +157,7 @@ limitations under the License.

    For more information, see - http://www.cs.umass.edu/~verts/cs32/endian.html + https://www.cs.umass.edu/~verts/cs32/endian.html

    diff --git a/src/site/xdoc/download_io.xml b/src/site/xdoc/download_io.xml index 567d2653265..6a0ba144cd9 100644 --- a/src/site/xdoc/download_io.xml +++ b/src/site/xdoc/download_io.xml @@ -115,32 +115,32 @@ limitations under the License.

    -
    +
    - - - + + + - - - + + +
    commons-io-2.20.0-bin.tar.gzsha512pgpcommons-io-2.21.0-bin.tar.gzsha512pgp
    commons-io-2.20.0-bin.zipsha512pgpcommons-io-2.21.0-bin.zipsha512pgp
    - - - + + + - - - + + +
    commons-io-2.20.0-src.tar.gzsha512pgpcommons-io-2.21.0-src.tar.gzsha512pgp
    commons-io-2.20.0-src.zipsha512pgpcommons-io-2.21.0-src.zipsha512pgp
    diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml index 54022e401ae..403c225db03 100644 --- a/src/site/xdoc/index.xml +++ b/src/site/xdoc/index.xml @@ -28,7 +28,7 @@ limitations under the License. Apache Commons IO is a library of utilities to assist with developing IO functionality.

    - Main areas include: + The Commons IO packages include:

    • diff --git a/src/test/java/org/apache/commons/io/EndianUtilsTest.java b/src/test/java/org/apache/commons/io/EndianUtilsTest.java index 3015c606f62..b7cd538e4b3 100644 --- a/src/test/java/org/apache/commons/io/EndianUtilsTest.java +++ b/src/test/java/org/apache/commons/io/EndianUtilsTest.java @@ -44,7 +44,7 @@ void testEOFException() { } @Test - void testInvalidOffset() throws IOException { + void testInvalidOffset() { final byte[] bytes = {}; assertThrows(IllegalArgumentException.class, () -> EndianUtils.readSwappedInteger(bytes, 0)); diff --git a/src/test/java/org/apache/commons/io/FileSystemTest.java b/src/test/java/org/apache/commons/io/FileSystemTest.java index d061a618e01..c90c95d6c72 100644 --- a/src/test/java/org/apache/commons/io/FileSystemTest.java +++ b/src/test/java/org/apache/commons/io/FileSystemTest.java @@ -17,21 +17,269 @@ package org.apache.commons.io; +import static java.nio.charset.StandardCharsets.ISO_8859_1; +import static java.nio.charset.StandardCharsets.US_ASCII; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.io.FileSystem.NameLengthStrategy.BYTES; +import static org.apache.commons.io.FileSystem.NameLengthStrategy.UTF16_CODE_UNITS; +import static org.apache.commons.lang3.StringUtils.repeat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; +import java.util.stream.Stream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.io.FileSystem.NameLengthStrategy; +import org.apache.commons.lang3.JavaVersion; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemProperties; import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledOnOs; -import org.junit.jupiter.api.condition.OS; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; /** * Tests {@link FileSystem}. */ class FileSystemTest { + /** A single ASCII character that encodes to 1 UTF-8 byte. */ + private static final String CHAR_UTF8_1B = "a"; + + /** A single Unicode character that encodes to 2 UTF-8 bytes. */ + private static final String CHAR_UTF8_2B = "é"; + + /** A single Unicode character that encodes to 3 UTF-8 bytes. */ + private static final String CHAR_UTF8_3B = "★"; + + /** A single Unicode code point that encodes to 2 UTF-16 code units and 4 UTF-8 bytes. */ + private static final String CHAR_UTF8_4B = "😀"; + + /** + * A grapheme cluster that encodes to 69 UTF-8 bytes and 31 UTF-16 code units: 👩🏻‍🦰‍👨🏿‍🦲‍👧🏽‍🦱‍👦🏼‍🦳 + *

      + * This should be treated as a single character in JDK 20+ for truncation purposes, + * even if it contains parts that have a meaning on their own. + *

      + *
        + *
      • {@code 👩}: 4 UTF-8 bytes and 2 UTF-16 code points.
      • + *
      • {@code 👩🏻‍🦰}: 15 UTF-8 bytes and 7 UTF-16 code points.
      • + *
      + */ + private static final String CHAR_UTF8_69B = + // woman + light skin + ZWJ + red hair = 15 bytes + "\uD83D\uDC69\uD83C\uDFFB\u200D\uD83E\uDDB0" + // ZWJ = 3 bytes + + "\u200D" + // man + dark skin + ZWJ + bald = 15 bytes + + "\uD83D\uDC68\uD83C\uDFFF\u200D\uD83E\uDDB2" + // ZWJ = 3 bytes + + "\u200D" + // girl + medium skin + ZWJ + curly hair = 15 bytes + + "\uD83D\uDC67\uD83C\uDFFD\u200D\uD83E\uDDB1" + // ZWJ = 3 bytes + + "\u200D" + // boy + medium-light skin + ZWJ + white hair = 15 bytes + + "\uD83D\uDC66\uD83C\uDFFC\u200D\uD83E\uDDB3"; + + /** File name of 255 bytes and 255 UTF-16 code units. */ + private static final String FILE_NAME_255_BYTES_UTF8_1B = repeat(CHAR_UTF8_1B, 255); + + /** File name of 255 bytes and 128 UTF-16 code units. */ + private static final String FILE_NAME_255_BYTES_UTF8_2B = repeat(CHAR_UTF8_2B, 127) + CHAR_UTF8_1B; + + /** File name of 255 bytes and 85 UTF-16 code units. */ + private static final String FILE_NAME_255_BYTES_UTF8_3B = repeat(CHAR_UTF8_3B, 85); + + /** File name of 255 bytes and 64 UTF-16 code units. */ + private static final String FILE_NAME_255_BYTES_UTF8_4B = repeat(CHAR_UTF8_4B, 63) + CHAR_UTF8_3B; + + /** File name of 255 bytes and 255 UTF-16 code units. */ + private static final String FILE_NAME_255_CHARS_UTF8_1B = FILE_NAME_255_BYTES_UTF8_1B; + + /** File name of 510 bytes and 255 UTF-16 code units. */ + private static final String FILE_NAME_255_CHARS_UTF8_2B = repeat(CHAR_UTF8_2B, 255); + + /** File name of 765 bytes and 255 UTF-16 code units. */ + private static final String FILE_NAME_255_CHARS_UTF8_3B = repeat(CHAR_UTF8_3B, 255); + + /** File name of 511 bytes and 255 UTF-16 code units. */ + private static final String FILE_NAME_255_CHARS_UTF8_4B = repeat(CHAR_UTF8_4B, 127) + CHAR_UTF8_3B; + + private static void createAndDelete(final Path tempDir, final String fileName) throws IOException { + final Path filePath = tempDir.resolve(fileName); + Files.createFile(filePath); + try (Stream files = Files.list(tempDir)) { + final boolean found = files.anyMatch(filePath::equals); + if (!found) { + throw new FileNotFoundException(fileName + " not found in " + tempDir); + } + } + Files.delete(filePath); + } + + static Stream testIsLegalName_Length() { + return Stream.of( + Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_1B, 4), UTF_8), + Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_2B, 4), UTF_8), + Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_3B, 4), UTF_8), + Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_4B, 4), UTF_8), + Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_1B, UTF_8), + Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_2B, UTF_8), + Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_3B, UTF_8), + Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_4B, UTF_8), + Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_1B, UTF_8), + Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_2B, UTF_8), + Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_3B, UTF_8), + Arguments.of(FileSystem.MAC_OSX, FILE_NAME_255_BYTES_UTF8_4B, UTF_8), + Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_1B, UTF_8), + Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_2B, UTF_8), + Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_3B, UTF_8), + Arguments.of(FileSystem.WINDOWS, FILE_NAME_255_CHARS_UTF8_4B, UTF_8), + // Repeat some tests with other encodings for GENERIC and LINUX + Arguments.of(FileSystem.GENERIC, repeat(FILE_NAME_255_BYTES_UTF8_1B, 4), US_ASCII), + Arguments.of(FileSystem.GENERIC, repeat(CHAR_UTF8_2B, 1020), ISO_8859_1), + Arguments.of(FileSystem.LINUX, FILE_NAME_255_BYTES_UTF8_1B, US_ASCII), + Arguments.of(FileSystem.LINUX, repeat(CHAR_UTF8_2B, 255), ISO_8859_1)); + } + + static Stream testNameLengthStrategyTruncate_Succeeds() { + // The grapheme cluster CHAR_UTF8_69B is treated as a single character in JDK 20+, + final String woman; + final String redHeadWoman; + if (SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_19)) { + woman = CHAR_UTF8_69B.substring(0, 2); // 👩 + redHeadWoman = CHAR_UTF8_69B.substring(0, 7); // 👩🏻‍🦰 + } else { + woman = ""; + redHeadWoman = ""; + } + return Stream.of( + // Truncation by bytes + // ------------------- + // + // Empty + Arguments.of(BYTES, 0, "", ""), + // Simple name without truncation + Arguments.of(BYTES, 10, "simple.txt", "simple.txt"), + // Name starting with dot + Arguments.of(BYTES, 10, "." + repeat(CHAR_UTF8_1B, 10), "." + repeat(CHAR_UTF8_1B, 9)), + Arguments.of(BYTES, 20, "." + repeat(CHAR_UTF8_2B, 10), "." + repeat(CHAR_UTF8_2B, 9)), + Arguments.of(BYTES, 30, "." + repeat(CHAR_UTF8_3B, 10), "." + repeat(CHAR_UTF8_3B, 9)), + Arguments.of(BYTES, 40, "." + repeat(CHAR_UTF8_4B, 10), "." + repeat(CHAR_UTF8_4B, 9)), + // Names with extensions + Arguments.of(BYTES, 13, repeat(CHAR_UTF8_1B, 10) + ".txt", repeat(CHAR_UTF8_1B, 9) + ".txt"), + Arguments.of(BYTES, 23, repeat(CHAR_UTF8_2B, 10) + ".txt", repeat(CHAR_UTF8_2B, 9) + ".txt"), + Arguments.of(BYTES, 33, repeat(CHAR_UTF8_3B, 10) + ".txt", repeat(CHAR_UTF8_3B, 9) + ".txt"), + Arguments.of(BYTES, 43, repeat(CHAR_UTF8_4B, 10) + ".txt", repeat(CHAR_UTF8_4B, 9) + ".txt"), + // Names without extensions + Arguments.of(BYTES, 1, CHAR_UTF8_1B, CHAR_UTF8_1B), + Arguments.of(BYTES, 2, CHAR_UTF8_2B, CHAR_UTF8_2B), + Arguments.of(BYTES, 3, CHAR_UTF8_3B, CHAR_UTF8_3B), + Arguments.of(BYTES, 4, CHAR_UTF8_4B, CHAR_UTF8_4B), + Arguments.of(BYTES, 9, repeat(CHAR_UTF8_1B, 10), repeat(CHAR_UTF8_1B, 9)), + Arguments.of(BYTES, 19, repeat(CHAR_UTF8_2B, 10), repeat(CHAR_UTF8_2B, 9)), + Arguments.of(BYTES, 29, repeat(CHAR_UTF8_3B, 10), repeat(CHAR_UTF8_3B, 9)), + Arguments.of(BYTES, 39, repeat(CHAR_UTF8_4B, 10), repeat(CHAR_UTF8_4B, 9)), + // Grapheme cluster + Arguments.of(BYTES, 69, CHAR_UTF8_69B, CHAR_UTF8_69B), + // Will not cut 4 or 15 bytes of the grapheme cluster + Arguments.of(BYTES, 69 + 4, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + woman), + Arguments.of(BYTES, 69 + 15, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + redHeadWoman), + // Truncation by UTF-16 code units + // ------------------------------- + // Empty + Arguments.of(UTF16_CODE_UNITS, 0, "", ""), + // Simple name without truncation + Arguments.of(UTF16_CODE_UNITS, 10, "simple.txt", "simple.txt"), + // Name starting with dot + Arguments.of(UTF16_CODE_UNITS, 10, "." + repeat(CHAR_UTF8_1B, 10), "." + repeat(CHAR_UTF8_1B, 9)), + Arguments.of(UTF16_CODE_UNITS, 10, "." + repeat(CHAR_UTF8_2B, 10), "." + repeat(CHAR_UTF8_2B, 9)), + Arguments.of(UTF16_CODE_UNITS, 10, "." + repeat(CHAR_UTF8_3B, 10), "." + repeat(CHAR_UTF8_3B, 9)), + Arguments.of(UTF16_CODE_UNITS, 20, "." + repeat(CHAR_UTF8_4B, 10), "." + repeat(CHAR_UTF8_4B, 9)), + // Names with extensions + Arguments.of(UTF16_CODE_UNITS, 13, repeat(CHAR_UTF8_1B, 10) + ".txt", repeat(CHAR_UTF8_1B, 9) + ".txt"), + Arguments.of(UTF16_CODE_UNITS, 13, repeat(CHAR_UTF8_2B, 10) + ".txt", repeat(CHAR_UTF8_2B, 9) + ".txt"), + Arguments.of(UTF16_CODE_UNITS, 13, repeat(CHAR_UTF8_3B, 10) + ".txt", repeat(CHAR_UTF8_3B, 9) + ".txt"), + Arguments.of(UTF16_CODE_UNITS, 23, repeat(CHAR_UTF8_4B, 10) + ".txt", repeat(CHAR_UTF8_4B, 9) + ".txt"), + // Names without extensions + Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_1B, CHAR_UTF8_1B), + Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_2B, CHAR_UTF8_2B), + Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_3B, CHAR_UTF8_3B), + Arguments.of(UTF16_CODE_UNITS, 2, CHAR_UTF8_4B, CHAR_UTF8_4B), + Arguments.of(UTF16_CODE_UNITS, 9, repeat(CHAR_UTF8_1B, 10), repeat(CHAR_UTF8_1B, 9)), + Arguments.of(UTF16_CODE_UNITS, 9, repeat(CHAR_UTF8_2B, 10), repeat(CHAR_UTF8_2B, 9)), + Arguments.of(UTF16_CODE_UNITS, 9, repeat(CHAR_UTF8_3B, 10), repeat(CHAR_UTF8_3B, 9)), + Arguments.of(UTF16_CODE_UNITS, 19, repeat(CHAR_UTF8_4B, 10), repeat(CHAR_UTF8_4B, 9)), + // Grapheme cluster + Arguments.of(UTF16_CODE_UNITS, 31, CHAR_UTF8_69B, CHAR_UTF8_69B), + // Will not cut 2 or 7 UTF-16 code units of the grapheme cluster + Arguments.of(UTF16_CODE_UNITS, 31 + 2, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + woman), + Arguments.of(UTF16_CODE_UNITS, 31 + 7, repeat(CHAR_UTF8_69B, 2), CHAR_UTF8_69B + redHeadWoman)); + } + + static Stream testNameLengthStrategyTruncate_Throws() { + final Stream common = Stream.of( + // Encoding issues + Arguments.of(BYTES, 10, "café", US_ASCII, "US-ASCII"), + Arguments.of(UTF16_CODE_UNITS, 10, "\uD800.txt", UTF_8, "UTF-16"), + Arguments.of(UTF16_CODE_UNITS, 10, "\uDC00.txt", UTF_8, "UTF-16"), + // Extension too long + Arguments.of(BYTES, 4, "a.txt", UTF_8, "extension"), + Arguments.of(UTF16_CODE_UNITS, 4, "a.txt", UTF_8, "extension"), + // Limit too small + Arguments.of(BYTES, 3, CHAR_UTF8_4B, UTF_8, "truncated to 1 character"), + Arguments.of(UTF16_CODE_UNITS, 1, CHAR_UTF8_4B, UTF_8, "truncated to 1 character")); + return SystemUtils.isJavaVersionAtMost(JavaVersion.JAVA_19) + ? common + : Stream.concat( + common, + // In JDK 20+ the grapheme cluster CHAR_UTF8_69B is treated as a single character, + // so cannot be truncated to 2 or 7 code units + Stream.of( + Arguments.of(BYTES, 68, CHAR_UTF8_69B, UTF_8, "truncated to 29 characters"), + Arguments.of(UTF16_CODE_UNITS, 30, CHAR_UTF8_69B, UTF_8, "truncated to 30 characters"))); + } + + private String parseXmlRootValue(final Path xmlPath, final Charset charset) throws SAXException, IOException, ParserConfigurationException { + final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + try (BufferedReader reader = Files.newBufferedReader(xmlPath, charset)) { + final Document document = builder.parse(new InputSource(reader)); + return document.getDocumentElement().getTextContent(); + } + } + + private String parseXmlRootValue(final String xmlString) throws SAXException, IOException, ParserConfigurationException { + final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + final Document document = builder.parse(new InputSource(new StringReader(xmlString))); + return document.getDocumentElement().getTextContent(); + } + @Test void testGetBlockSize() { assertTrue(FileSystem.getCurrent().getBlockSize() >= 0); @@ -57,19 +305,38 @@ void testGetIllegalFileNameChars() { } @Test - void testIsLegalName() { - for (final FileSystem fs : FileSystem.values()) { - assertFalse(fs.isLegalFileName(""), fs.name()); // Empty is always illegal - assertFalse(fs.isLegalFileName(null), fs.name()); // null is always illegal - assertFalse(fs.isLegalFileName("\0"), fs.name()); // Assume NUL is always illegal - assertTrue(fs.isLegalFileName("0"), fs.name()); // Assume simple name always legal - for (final String candidate : fs.getReservedFileNames()) { - // Reserved file names are not legal - assertFalse(fs.isLegalFileName(candidate), candidate); - } + void testGetNameSeparator() { + final FileSystem current = FileSystem.getCurrent(); + assertEquals(SystemProperties.getFileSeparator(), Character.toString(current.getNameSeparator())); + } + + @ParameterizedTest + @EnumSource(FileSystem.class) + void testIsLegalName(final FileSystem fs) { + assertFalse(fs.isLegalFileName(""), fs.name()); // Empty is always illegal + assertFalse(fs.isLegalFileName(null), fs.name()); // null is always illegal + assertFalse(fs.isLegalFileName("\0"), fs.name()); // Assume NUL is always illegal + assertTrue(fs.isLegalFileName("0"), fs.name()); // Assume simple name always legal + for (final String candidate : fs.getReservedFileNames()) { + // Reserved file names are not legal + assertFalse(fs.isLegalFileName(candidate), candidate); } } + @Test + void testIsLegalName_Encoding() { + assertFalse(FileSystem.GENERIC.isLegalFileName(FILE_NAME_255_BYTES_UTF8_3B, US_ASCII), "US-ASCII cannot represent all chars"); + assertTrue(FileSystem.GENERIC.isLegalFileName(FILE_NAME_255_BYTES_UTF8_3B, UTF_8), "UTF-8 can represent all chars"); + } + + @ParameterizedTest(name = "{index}: {0} with charset {2}") + @MethodSource + void testIsLegalName_Length(final FileSystem fs, final String nameAtLimit, final Charset charset) { + assertTrue(fs.isLegalFileName(nameAtLimit, charset), fs.name() + " length at limit"); + final String nameOverLimit = nameAtLimit + "a"; + assertFalse(fs.isLegalFileName(nameOverLimit, charset), fs.name() + " length over limit"); + } + @Test void testIsReservedFileName() { for (final FileSystem fs : FileSystem.values()) { @@ -80,7 +347,6 @@ void testIsReservedFileName() { } @Test - @EnabledOnOs(OS.WINDOWS) void testIsReservedFileNameOnWindows() { final FileSystem fs = FileSystem.WINDOWS; for (final String candidate : fs.getReservedFileNames()) { @@ -107,6 +373,92 @@ void testIsReservedFileNameOnWindows() { // } } + @Test + void testMaxNameLength_MatchesRealSystem(@TempDir final Path tempDir) { + final FileSystem fs = FileSystem.getCurrent(); + final String[] validNames; + switch (fs) { + case MAC_OSX: + case LINUX: + // Names with 255 UTF-8 bytes are legal + // @formatter:off + validNames = new String[] { + FILE_NAME_255_BYTES_UTF8_1B, + FILE_NAME_255_BYTES_UTF8_2B, + FILE_NAME_255_BYTES_UTF8_3B, + FILE_NAME_255_BYTES_UTF8_4B + }; + // @formatter:on + break; + case WINDOWS: + // Names with 255 UTF-16 code units are legal + // @formatter:off + validNames = new String[] { + FILE_NAME_255_CHARS_UTF8_1B, + FILE_NAME_255_CHARS_UTF8_2B, + FILE_NAME_255_CHARS_UTF8_3B, + FILE_NAME_255_CHARS_UTF8_4B + }; + // @formatter:on + break; + default: + throw new IllegalStateException("Unexpected value: " + fs); + } + int failures = 0; + for (final String fileName : validNames) { + // 1) OS should accept names at the documented limit. + assertDoesNotThrow(() -> createAndDelete(tempDir, fileName), "OS should accept max-length name: " + fileName); + // 2) Library should consider them legal. + assertTrue(fs.isLegalFileName(fileName, UTF_8), "Commons IO should accept max-length name: " + fileName); + // 3) For “one over” the limit: Commons IO must reject; OS may or may not enforce strictly. + final String tooLongName = fileName + "a"; + // Library contract: must be illegal. + assertFalse(fs.isLegalFileName(tooLongName, UTF_8), "Commons IO should reject too-long name: " + tooLongName); + // OS behavior: may or may not reject. + try { + createAndDelete(tempDir, tooLongName); + } catch (final Throwable e) { + failures++; + assertInstanceOf(IOException.class, e, "OS rejects too-long name"); + } + } + // On Linux and Windows the API and the filesystem measure name length + // in the same unit as the underlying limit (255 bytes on Linux/most POSIX, + // 255 UTF-16 code units on Windows). + // So all “too-long” variants should fail. + // + // macOS is trickier because the API and filesystem limits don’t always match: + // + // - POSIX API layer (getdirentries/readdir): 1023 bytes per component since macOS 10.5. + // https://man.freebsd.org/cgi/man.cgi?query=dir&sektion=5&apropos=0&manpath=macOS+15.6 + // - HFS+: enforces 255 UTF-16 code units per component. + // - APFS: enforces 255 UTF-8 bytes per component. + // + // Because of this mismatch, depending on which filesystem is mounted, + // either all or only FILE_NAME_255BYTES_UTF8_1B + "a" will be rejected. + if (SystemUtils.IS_OS_MAC_OSX) { + assertTrue(failures >= 1, "Expected at least one too-long name rejected, got " + failures); + } else { + assertEquals(4, failures, "All too-long names were rejected"); + } + } + + @ParameterizedTest(name = "{index}: {0} truncates {1} to {2}") + @MethodSource + void testNameLengthStrategyTruncate_Succeeds(final NameLengthStrategy strategy, final int limit, final String input, final String expected) { + final CharSequence out = strategy.truncate(input, limit, UTF_8); + assertEquals(expected, out.toString(), strategy.name() + " truncates to limit"); + } + + @ParameterizedTest(name = "{index}: {0} truncates {2} with limit {1} throws") + @MethodSource + void testNameLengthStrategyTruncate_Throws(final NameLengthStrategy strategy, final int limit, final String input, final Charset charset, + final String message) { + final IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> strategy.truncate(input, limit, charset)); + final String exMessage = ex.getMessage(); + assertTrue(exMessage.contains(message), "ex message contains " + message + ": " + exMessage); + } + @Test void testReplacementWithNUL() { for (final FileSystem fs : FileSystem.values()) { @@ -156,5 +508,73 @@ void testToLegalFileNameWindows() { for (char i = '0'; i < '9'; i++) { assertEquals(i, fs.toLegalFileName(String.valueOf(i), replacement).charAt(0)); } + // Null and empty + assertThrows(NullPointerException.class, () -> fs.toLegalFileName(null, '_')); + assertThrows(IllegalArgumentException.class, () -> fs.toLegalFileName("", '_')); + // Illegal replacement + assertThrows(IllegalArgumentException.class, () -> fs.toLegalFileName("test", '\0')); + assertThrows(IllegalArgumentException.class, () -> fs.toLegalFileName("test", ':')); + } + + @ParameterizedTest + @EnumSource(FileSystem.class) + void testXmlRoundtrip(final FileSystem fs, @TempDir final Path tempDir) throws Exception { + if (SystemUtils.IS_OS_WINDOWS) { + // TODO + // Window failures with Charset issues on Java 8, 11, and 17 as seen on GH CI, 21 and 24 are OK. + assumeTrue(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_21)); + } + final Charset charset = StandardCharsets.UTF_8; + assertEquals("a", fs.toLegalFileName("a", '_', charset)); + assertEquals("abcdefghijklmno", fs.toLegalFileName("abcdefghijklmno", '_', charset)); + assertEquals("\u4F60\u597D\u55CE", fs.toLegalFileName("\u4F60\u597D\u55CE", '_', charset)); + assertEquals("\u2713\u2714", fs.toLegalFileName("\u2713\u2714", '_', charset)); + assertEquals("\uD83D\uDE80\u2728\uD83C\uDF89", fs.toLegalFileName("\uD83D\uDE80\u2728\uD83C\uDF89", '_', charset)); + assertEquals("\uD83D\uDE03", fs.toLegalFileName("\uD83D\uDE03", '_', charset)); + assertEquals("\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03", + fs.toLegalFileName("\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03\uD83D\uDE03", '_', charset)); + for (int i = 1; i <= 10; i++) { + final String name1 = fs.toLegalFileName(StringUtils.repeat("🦊", i), '_', charset); + assertNotNull(name1); + final byte[] name1Bytes = name1.getBytes(); + final String xmlString1 = toXmlString(name1, charset); + final Path path = tempDir.resolve(name1); + Files.write(path, xmlString1.getBytes(charset)); + final String xmlFromPath = parseXmlRootValue(path, charset); + assertEquals(name1, xmlFromPath, "i = " + i); + final String name2 = new String(name1Bytes, charset); + assertEquals(name1, name2); + final String xmlString2 = toXmlString(name2, charset); + assertEquals(xmlString1, xmlString2); + final String parsedValue = Objects.toString(parseXmlRootValue(xmlString2), ""); + assertEquals(name1, parsedValue, "i = " + i); + assertEquals(name2, parsedValue, "i = " + i); + } +// Fails on some OS' on GH CI +// for (int i = 1; i <= 100; i++) { +// final String name1 = fs.toLegalFileName(fs.getNameLengthStrategy().truncate( +// "👩🏻‍👨🏻‍👦🏻‍👦🏻👩🏼‍👨🏼‍👦🏼‍👦🏼👩🏽‍👨🏽‍👦🏽‍👦🏽👩🏾‍👨🏾‍👦🏾‍👦🏾👩🏿‍👨🏿‍👦🏿‍👦🏿👩🏻‍👨🏻‍👦🏻‍👦🏻👩🏼‍👨🏼‍👦🏼‍👦🏼👩🏽‍👨🏽‍👦🏽‍👦🏽👩🏾‍👨🏾‍👦🏾‍👦🏾👩🏿‍👨🏿‍👦🏿‍👦🏿", +// // TODO hack 100: truncate blows up when it can't. +// 100 + i, charset), '_', charset); +// assertNotNull(name1); +// final byte[] name1Bytes = name1.getBytes(); +// final String xmlString1 = toXmlString(name1, charset); +// final Path path = tempDir.resolve(name1); +// Files.write(path, xmlString1.getBytes(charset)); +// final String xmlFromPath = parseXmlRootValue(path, charset); +// assertEquals(name1, xmlFromPath, "i = " + i); +// final String name2 = new String(name1Bytes, charset); +// assertEquals(name1, name2); +// final String xmlString2 = toXmlString(name2, charset); +// assertEquals(xmlString1, xmlString2); +// final String parsedValue = Objects.toString(parseXmlRootValue(xmlString2), ""); +// assertEquals(name1, parsedValue, "i = " + i); +// assertEquals(name2, parsedValue, "i = " + i); +// } + } + + private String toXmlString(final String s, final Charset charset) { + return String.format("%s", charset.name(), s); } + } diff --git a/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java b/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java index 0ac00880d90..80f33c6777e 100644 --- a/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java +++ b/src/test/java/org/apache/commons/io/FileUtilsListFilesTest.java @@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeFalse; import java.io.File; import java.io.IOException; @@ -41,21 +42,21 @@ import org.apache.commons.io.file.PathUtils; import org.apache.commons.io.filefilter.FileFilterUtils; import org.apache.commons.io.filefilter.IOFileFilter; -import org.apache.commons.io.function.Uncheck; +import org.apache.commons.lang3.JavaVersion; +import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.function.Consumers; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; /** - * Tests FileUtils.listFiles() methods. + * Tests {@link FileUtils#listFiles(File, IOFileFilter, IOFileFilter)} and friends. */ class FileUtilsListFilesTest { @TempDir public File temporaryFolder; - @SuppressWarnings("ResultOfMethodCallIgnored") @BeforeEach public void setUp() throws Exception { File dir = temporaryFolder; @@ -217,24 +218,6 @@ void testListFilesMissing() { assertTrue(FileUtils.listFiles(new File(temporaryFolder, "dir/does/not/exist/at/all"), null, false).isEmpty()); } - @Test - void testListFilesWithDeletion() throws IOException { - final String[] extensions = {"xml", "txt"}; - final List list; - final File xFile = new File(temporaryFolder, "x.xml"); - if (!xFile.createNewFile()) { - fail("could not create test file: " + xFile); - } - final Collection files = FileUtils.listFiles(temporaryFolder, extensions, true); - assertEquals(5, files.size()); - try (Stream stream = Uncheck.get(() -> FileUtils.streamFiles(temporaryFolder, true, extensions))) { - assertTrue(xFile.delete()); - list = stream.collect(Collectors.toList()); - assertFalse(list.contains(xFile), list::toString); - } - assertEquals(4, list.size()); - } - /** * Tests IO-856 ListFiles should not fail on vanishing files. */ @@ -290,6 +273,86 @@ void testListFilesWithDeletionThreaded() throws ExecutionException, InterruptedE c2.get(); } + @Test + void testStreamFilesWithDeletionCollect() throws IOException { + final String[] extensions = {"xml", "txt"}; + final File xFile = new File(temporaryFolder, "x.xml"); + if (!xFile.createNewFile()) { + fail("could not create test file: " + xFile); + } + final Collection files = FileUtils.listFiles(temporaryFolder, extensions, true); + assertEquals(5, files.size()); + final List list; + try (Stream stream = FileUtils.streamFiles(temporaryFolder, true, extensions)) { + assertTrue(xFile.delete()); + // TODO? Should we create a custom stream to ignore missing files for Java 24 and up? + // collect() will fail on Java 24 and up here + // GitHub CI: + // Fails on Java 24 macOS, but OK on Windows and Ubuntu + // Fails on Java 25-EA Windows and macOS, but OK on Ubuntu + // forEach() will fail on Java 24 and up here + assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_24)); + list = stream.collect(Collectors.toList()); + assertFalse(list.contains(xFile), list::toString); + } + assertEquals(4, list.size()); + } + + @Test + void testStreamFilesWithDeletionForEach() throws IOException { + final String[] extensions = {"xml", "txt"}; + final File xFile = new File(temporaryFolder, "x.xml"); + if (!xFile.createNewFile()) { + fail("could not create test file: " + xFile); + } + final Collection files = FileUtils.listFiles(temporaryFolder, extensions, true); + assertEquals(5, files.size()); + final List list; + try (Stream stream = FileUtils.streamFiles(temporaryFolder, true, extensions)) { + assertTrue(xFile.delete()); + list = new ArrayList<>(); + // TODO? Should we create a custom stream to ignore missing files for Java 24 and up? + // forEach() will fail on Java 24 and up here + // GitHub CI: + // Fails on Java 24 macOS, but OK on Windows and Ubuntu + // Fails on Java 25-EA Windows and macOS, but OK on Ubuntu + // forEach() will fail on Java 24 and up here + assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_24)); + stream.forEach(list::add); + assertFalse(list.contains(xFile), list::toString); + } + assertEquals(4, list.size()); + } + + @Test + void testStreamFilesWithDeletionIterator() throws IOException { + final String[] extensions = {"xml", "txt"}; + final File xFile = new File(temporaryFolder, "x.xml"); + if (!xFile.createNewFile()) { + fail("could not create test file: " + xFile); + } + final Collection files = FileUtils.listFiles(temporaryFolder, extensions, true); + assertEquals(5, files.size()); + final List list; + try (Stream stream = FileUtils.streamFiles(temporaryFolder, true, extensions)) { + assertTrue(xFile.delete()); + list = new ArrayList<>(); + final Iterator iterator = stream.iterator(); + // TODO? Should we create a custom stream to ignore missing files for Java 24 and up? + // hasNext() will fail on Java 24 and up here + // GitHub CI: + // Fails on Java 24 macOS, but OK on Windows and Ubuntu + // Fails on Java 25-EA Windows and macOS, but OK on Ubuntu + // forEach() will fail on Java 24 and up here + assumeFalse(SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_24)); + while (iterator.hasNext()) { + list.add(iterator.next()); + } + assertFalse(list.contains(xFile), list::toString); + } + assertEquals(4, list.size()); + } + private Collection toFileNames(final Collection files) { return files.stream().map(File::getName).collect(Collectors.toList()); } diff --git a/src/test/java/org/apache/commons/io/FileUtilsTest.java b/src/test/java/org/apache/commons/io/FileUtilsTest.java index ece78bcd853..91d351efeaa 100644 --- a/src/test/java/org/apache/commons/io/FileUtilsTest.java +++ b/src/test/java/org/apache/commons/io/FileUtilsTest.java @@ -52,6 +52,7 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.nio.file.attribute.AclFileAttributeView; +import java.nio.file.attribute.DosFileAttributeView; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.PosixFilePermission; import java.nio.file.attribute.PosixFilePermissions; @@ -169,9 +170,15 @@ List list(final File startDirectory) throws IOException { */ private static final ListDirectoryWalker LIST_WALKER = new ListDirectoryWalker(); + private static void setDosReadOnly(final Path p, final boolean readOnly) throws IOException { + if (Files.getFileStore(p).supportsFileAttributeView(DosFileAttributeView.class)) { + Files.setAttribute(p, "dos:readonly", readOnly, LinkOption.NOFOLLOW_LINKS); + } + } private File testFile1; private File testFile2; private long testFile1Size; + private long testFile2Size; private void assertContentMatchesAfterCopyURLToFileFor(final String resourceName, final File destination) throws IOException { @@ -370,6 +377,10 @@ void testByteCountToDisplaySizeBigInteger() { final BigInteger TB1 = GB1.multiply(KB1); final BigInteger PB1 = TB1.multiply(KB1); final BigInteger EB1 = PB1.multiply(KB1); + final BigInteger ZB1 = EB1.multiply(KB1); + final BigInteger YB1 = ZB1.multiply(KB1); + final BigInteger RB1 = YB1.multiply(KB1); + final BigInteger QB1 = RB1.multiply(KB1); assertEquals("0 bytes", FileUtils.byteCountToDisplaySize(BigInteger.ZERO)); assertEquals("1 bytes", FileUtils.byteCountToDisplaySize(BigInteger.ONE)); assertEquals("1023 bytes", FileUtils.byteCountToDisplaySize(b1023)); @@ -386,6 +397,10 @@ void testByteCountToDisplaySizeBigInteger() { assertEquals("1 TB", FileUtils.byteCountToDisplaySize(TB1)); assertEquals("1 PB", FileUtils.byteCountToDisplaySize(PB1)); assertEquals("1 EB", FileUtils.byteCountToDisplaySize(EB1)); + assertEquals("1 ZB", FileUtils.byteCountToDisplaySize(ZB1)); + assertEquals("1 YB", FileUtils.byteCountToDisplaySize(YB1)); + assertEquals("1 RB", FileUtils.byteCountToDisplaySize(RB1)); + assertEquals("1 QB", FileUtils.byteCountToDisplaySize(QB1)); assertEquals("7 EB", FileUtils.byteCountToDisplaySize(Long.MAX_VALUE)); // Other MAX_VALUEs assertEquals("63 KB", FileUtils.byteCountToDisplaySize(BigInteger.valueOf(Character.MAX_VALUE))); @@ -413,6 +428,44 @@ void testByteCountToDisplaySizeLong() { assertEquals("1 PB", FileUtils.byteCountToDisplaySize(1024L * 1024 * 1024 * 1024 * 1024)); assertEquals("1 EB", FileUtils.byteCountToDisplaySize(1024L * 1024 * 1024 * 1024 * 1024 * 1024)); assertEquals("7 EB", FileUtils.byteCountToDisplaySize(Long.MAX_VALUE)); + // Constants and round down. + assertEquals("1 EB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_EB)); + assertEquals("1 EB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_EB + 1)); + assertEquals("1 EB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_EB_BI)); + assertEquals("1 EB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_EB_BI.add(BigInteger.ONE))); + assertEquals("1 GB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_GB)); + assertEquals("1 GB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_GB + 1)); + assertEquals("1 GB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_GB_BI)); + assertEquals("1 GB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_GB_BI.add(BigInteger.ONE))); + assertEquals("1 KB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_KB)); + assertEquals("1 KB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_KB + 1)); + assertEquals("1 KB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_KB_BI)); + assertEquals("1 KB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_KB_BI.add(BigInteger.ONE))); + assertEquals("1 MB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_MB)); + assertEquals("1 MB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_MB + 1)); + assertEquals("1 MB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_MB_BI)); + assertEquals("1 MB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_MB_BI.add(BigInteger.ONE))); + assertEquals("1 PB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_PB)); + assertEquals("1 PB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_PB + 1)); + assertEquals("1 PB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_PB_BI)); + assertEquals("1 PB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_PB_BI.add(BigInteger.ONE))); + assertEquals("1 TB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_TB)); + assertEquals("1 TB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_TB + 1)); + assertEquals("1 TB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_TB_BI)); + assertEquals("1 TB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_TB_BI.add(BigInteger.ONE))); + // Constants and round down. + assertEquals("1023 PB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_EB - 1)); + assertEquals("1023 PB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_EB_BI.subtract(BigInteger.ONE))); + assertEquals("1023 MB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_GB - 1)); + assertEquals("1023 MB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_GB_BI.subtract(BigInteger.ONE))); + assertEquals("1023 bytes", FileUtils.byteCountToDisplaySize(FileUtils.ONE_KB - 1)); + assertEquals("1023 bytes", FileUtils.byteCountToDisplaySize(FileUtils.ONE_KB_BI.subtract(BigInteger.ONE))); + assertEquals("1023 KB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_MB - 1)); + assertEquals("1023 KB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_MB_BI.subtract(BigInteger.ONE))); + assertEquals("1023 TB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_PB - 1)); + assertEquals("1023 TB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_PB_BI.subtract(BigInteger.ONE))); + assertEquals("1023 GB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_TB - 1)); + assertEquals("1023 GB", FileUtils.byteCountToDisplaySize(FileUtils.ONE_TB_BI.subtract(BigInteger.ONE))); // Other MAX_VALUEs assertEquals("63 KB", FileUtils.byteCountToDisplaySize(Character.MAX_VALUE)); assertEquals("31 KB", FileUtils.byteCountToDisplaySize(Short.MAX_VALUE)); @@ -613,17 +666,17 @@ void testContentEqualsIgnoreEOL() throws Exception { assertFalse(FileUtils.contentEqualsIgnoreEOL(tfile1, tfile3, null)); assertFalse(FileUtils.contentEqualsIgnoreEOL(tfile2, tfile3, null)); - final URL urlCR = getClass().getResource("FileUtilsTestDataCR.dat"); + final URL urlCR = getClass().getResource("FileUtilsTestDataCR.bin"); assertNotNull(urlCR); final File cr = new File(urlCR.toURI()); assertTrue(cr.exists()); - final URL urlCRLF = getClass().getResource("FileUtilsTestDataCRLF.dat"); + final URL urlCRLF = getClass().getResource("FileUtilsTestDataCRLF.bin"); assertNotNull(urlCRLF); final File crlf = new File(urlCRLF.toURI()); assertTrue(crlf.exists()); - final URL urlLF = getClass().getResource("FileUtilsTestDataLF.dat"); + final URL urlLF = getClass().getResource("FileUtilsTestDataLF.bin"); assertNotNull(urlLF); final File lf = new File(urlLF.toURI()); assertTrue(lf.exists()); @@ -1689,21 +1742,25 @@ void testForceDeleteReadOnlyDirectory() throws Exception { void testForceDeleteReadOnlyFile() throws Exception { try (TempFile destination = TempFile.create("test-", ".txt")) { final File file = destination.toFile(); - assertTrue(file.setReadOnly()); - assertTrue(file.canRead()); - assertFalse(file.canWrite()); - // sanity check that File.delete() deletes read-only files. - assertTrue(file.delete()); + assertTrue(file.setReadOnly(), "Setting file read-only successful"); + assertTrue(file.canRead(), "File must be readable"); + assertFalse(file.canWrite(), "File must not be writable"); + assertTrue(file.exists(), "File doesn't exist to delete"); + // Since JDK 25 on Windows, File.delete() refuses to remove files + // with the DOS readonly bit set (JDK-8355954). + // We clear the bit here for consistency across JDK versions. + setDosReadOnly(file.toPath(), false); + assertTrue(file.delete(), "File.delete() must delete read-only file"); } try (TempFile destination = TempFile.create("test-", ".txt")) { final File file = destination.toFile(); // real test - assertTrue(file.setReadOnly()); - assertTrue(file.canRead()); - assertFalse(file.canWrite()); + assertTrue(file.setReadOnly(), "Setting file read-only successful"); + assertTrue(file.canRead(), "File must be readable"); + assertFalse(file.canWrite(), "File must not be writable"); assertTrue(file.exists(), "File doesn't exist to delete"); FileUtils.forceDelete(file); - assertFalse(file.exists(), "Check deletion"); + assertFalse(file.exists(), "FileUtils.forceDelete() must delete read-only file"); } } @@ -1777,23 +1834,27 @@ void testForceDeleteUnwritableDirectory() throws Exception { void testForceDeleteUnwritableFile() throws Exception { try (TempFile destination = TempFile.create("test-", ".txt")) { final File file = destination.toFile(); - assertTrue(file.canWrite()); - assertTrue(file.setWritable(false)); - assertFalse(file.canWrite()); - assertTrue(file.canRead()); - // sanity check that File.delete() deletes unwritable files. - assertTrue(file.delete()); + assertTrue(file.canWrite(), "File must be writable"); + assertTrue(file.setWritable(false), "Setting file unwritable successful"); + assertFalse(file.canWrite(), "File must not be writable"); + assertTrue(file.canRead(), "File must be readable"); + assertTrue(file.exists(), "File must exist to delete"); + // Since JDK 25 on Windows, File.delete() refuses to remove files + // with the DOS readonly bit set (JDK-8355954). + // We clear the bit here for consistency across JDK versions. + setDosReadOnly(file.toPath(), false); + assertTrue(file.delete(), "File.delete() must delete unwritable file"); } try (TempFile destination = TempFile.create("test-", ".txt")) { final File file = destination.toFile(); // real test - assertTrue(file.canWrite()); - assertTrue(file.setWritable(false)); - assertFalse(file.canWrite()); - assertTrue(file.canRead()); - assertTrue(file.exists(), "File doesn't exist to delete"); + assertTrue(file.canWrite(), "File must be writable"); + assertTrue(file.setWritable(false), "Setting file unwritable successful"); + assertFalse(file.canWrite(), "File must not be writable"); + assertTrue(file.canRead(), "File must be readable"); + assertTrue(file.exists(), "File must exist to delete"); FileUtils.forceDelete(file); - assertFalse(file.exists(), "Check deletion"); + assertFalse(file.exists(), "FileUtils.forceDelete() must delete unwritable file"); } } @@ -3213,7 +3274,7 @@ void testWriteByteArrayToFile_WithOffsetAndLength_WithAppendOptionTrue_ShouldNot FileUtils.writeStringToFile(file, "This line was there before you..."); final byte[] data = "SKIP_THIS_this is brand new data_AND_SKIP_THIS".getBytes(StandardCharsets.UTF_8); FileUtils.writeByteArrayToFile(file, data, 10, 22, true); - final String expected = "This line was there before you..." + "this is brand new data"; + final String expected = "This line was there before you...this is brand new data"; final String actual = FileUtils.readFileToString(file, StandardCharsets.UTF_8); assertEquals(expected, actual); } diff --git a/src/test/java/org/apache/commons/io/FilenameUtilsTest.java b/src/test/java/org/apache/commons/io/FilenameUtilsTest.java index f94b1b762f3..0441c6ac2e6 100644 --- a/src/test/java/org/apache/commons/io/FilenameUtilsTest.java +++ b/src/test/java/org/apache/commons/io/FilenameUtilsTest.java @@ -42,6 +42,8 @@ */ class FilenameUtilsTest { + private static final String DRIVE_C = "C:"; + private static final String SEP = "" + File.separatorChar; private static final boolean WINDOWS = File.separatorChar == '\\'; @@ -128,7 +130,7 @@ void testConcat() { assertEquals(SEP + "c" + SEP + "d", FilenameUtils.concat("a/b/", "/c/d")); assertEquals("C:c" + SEP + "d", FilenameUtils.concat("a/b/", "C:c/d")); - assertEquals("C:" + SEP + "c" + SEP + "d", FilenameUtils.concat("a/b/", "C:/c/d")); + assertEquals(DRIVE_C + SEP + "c" + SEP + "d", FilenameUtils.concat("a/b/", "C:/c/d")); assertEquals("~" + SEP + "c" + SEP + "d", FilenameUtils.concat("a/b/", "~/c/d")); assertEquals("~user" + SEP + "c" + SEP + "d", FilenameUtils.concat("a/b/", "~user/c/d")); assertEquals("~" + SEP, FilenameUtils.concat("a/b/", "~")); @@ -277,10 +279,10 @@ void testGetFullPath() { assertEquals("", FilenameUtils.getFullPath("")); if (SystemUtils.IS_OS_WINDOWS) { - assertEquals("C:", FilenameUtils.getFullPath("C:")); + assertEquals(DRIVE_C, FilenameUtils.getFullPath(DRIVE_C)); } if (SystemUtils.IS_OS_LINUX) { - assertEquals("", FilenameUtils.getFullPath("C:")); + assertEquals("", FilenameUtils.getFullPath(DRIVE_C)); } assertEquals("C:/", FilenameUtils.getFullPath("C:/")); @@ -292,7 +294,7 @@ void testGetFullPath() { assertEquals("a/b/", FilenameUtils.getFullPath("a/b/c.txt")); assertEquals("/a/b/", FilenameUtils.getFullPath("/a/b/c.txt")); - assertEquals("C:", FilenameUtils.getFullPath("C:a")); + assertEquals(DRIVE_C, FilenameUtils.getFullPath("C:a")); assertEquals("C:a/b/", FilenameUtils.getFullPath("C:a/b/c.txt")); assertEquals("C:/a/b/", FilenameUtils.getFullPath("C:/a/b/c.txt")); assertEquals("//server/a/b/", FilenameUtils.getFullPath("//server/a/b/c.txt")); @@ -319,10 +321,10 @@ void testGetFullPathNoEndSeparator() { assertEquals("", FilenameUtils.getFullPathNoEndSeparator("")); if (SystemUtils.IS_OS_WINDOWS) { - assertEquals("C:", FilenameUtils.getFullPathNoEndSeparator("C:")); + assertEquals(DRIVE_C, FilenameUtils.getFullPathNoEndSeparator(DRIVE_C)); } if (SystemUtils.IS_OS_LINUX) { - assertEquals("", FilenameUtils.getFullPathNoEndSeparator("C:")); + assertEquals("", FilenameUtils.getFullPathNoEndSeparator(DRIVE_C)); } assertEquals("C:/", FilenameUtils.getFullPathNoEndSeparator("C:/")); @@ -334,7 +336,7 @@ void testGetFullPathNoEndSeparator() { assertEquals("a/b", FilenameUtils.getFullPathNoEndSeparator("a/b/c.txt")); assertEquals("/a/b", FilenameUtils.getFullPathNoEndSeparator("/a/b/c.txt")); - assertEquals("C:", FilenameUtils.getFullPathNoEndSeparator("C:a")); + assertEquals(DRIVE_C, FilenameUtils.getFullPathNoEndSeparator("C:a")); assertEquals("C:a/b", FilenameUtils.getFullPathNoEndSeparator("C:a/b/c.txt")); assertEquals("C:/a/b", FilenameUtils.getFullPathNoEndSeparator("C:/a/b/c.txt")); assertEquals("//server/a/b", FilenameUtils.getFullPathNoEndSeparator("//server/a/b/c.txt")); @@ -390,7 +392,7 @@ void testGetPath() { assertNull(FilenameUtils.getPath("//a")); assertEquals("", FilenameUtils.getPath("")); - assertEquals("", FilenameUtils.getPath("C:")); + assertEquals("", FilenameUtils.getPath(DRIVE_C)); assertEquals("", FilenameUtils.getPath("C:/")); assertEquals("", FilenameUtils.getPath("//server/")); assertEquals("", FilenameUtils.getPath("~")); @@ -432,7 +434,7 @@ void testGetPathNoEndSeparator() { assertNull(FilenameUtils.getPathNoEndSeparator("//a")); assertEquals("", FilenameUtils.getPathNoEndSeparator("")); - assertEquals("", FilenameUtils.getPathNoEndSeparator("C:")); + assertEquals("", FilenameUtils.getPathNoEndSeparator(DRIVE_C)); assertEquals("", FilenameUtils.getPathNoEndSeparator("C:/")); assertEquals("", FilenameUtils.getPathNoEndSeparator("//server/")); assertEquals("", FilenameUtils.getPathNoEndSeparator("~")); @@ -469,10 +471,10 @@ void testGetPrefix() { assertEquals("\\", FilenameUtils.getPrefix("\\")); if (SystemUtils.IS_OS_WINDOWS) { - assertEquals("C:", FilenameUtils.getPrefix("C:")); + assertEquals(DRIVE_C, FilenameUtils.getPrefix(DRIVE_C)); } if (SystemUtils.IS_OS_LINUX) { - assertEquals("", FilenameUtils.getPrefix("C:")); + assertEquals("", FilenameUtils.getPrefix(DRIVE_C)); } assertEquals("C:\\", FilenameUtils.getPrefix("C:\\")); @@ -519,10 +521,10 @@ void testGetPrefixLength() { assertEquals(1, FilenameUtils.getPrefixLength("\\")); if (SystemUtils.IS_OS_WINDOWS) { - assertEquals(2, FilenameUtils.getPrefixLength("C:")); + assertEquals(2, FilenameUtils.getPrefixLength(DRIVE_C)); } if (SystemUtils.IS_OS_LINUX) { - assertEquals(0, FilenameUtils.getPrefixLength("C:")); + assertEquals(0, FilenameUtils.getPrefixLength(DRIVE_C)); } assertEquals(3, FilenameUtils.getPrefixLength("C:\\")); @@ -758,7 +760,7 @@ void testNormalize() { assertEquals("a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("a\\b/c.txt")); assertEquals("" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("\\a\\b/c.txt")); - assertEquals("C:" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("C:\\a\\b/c.txt")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("C:\\a\\b/c.txt")); assertEquals("" + SEP + "" + SEP + "server" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("\\\\server\\a\\b/c.txt")); assertEquals("~" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("~\\a\\b/c.txt")); assertEquals("~user" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalize("~user\\a\\b/c.txt")); @@ -841,41 +843,41 @@ void testNormalize() { assertEquals("~user" + SEP, FilenameUtils.normalize("~user/")); assertEquals("~user" + SEP, FilenameUtils.normalize("~user")); - assertEquals("C:" + SEP + "a", FilenameUtils.normalize("C:/a")); - assertEquals("C:" + SEP + "a" + SEP, FilenameUtils.normalize("C:/a/")); - assertEquals("C:" + SEP + "a" + SEP + "c", FilenameUtils.normalize("C:/a/b/../c")); - assertEquals("C:" + SEP + "c", FilenameUtils.normalize("C:/a/b/../../c")); + assertEquals(DRIVE_C + SEP + "a", FilenameUtils.normalize("C:/a")); + assertEquals(DRIVE_C + SEP + "a" + SEP, FilenameUtils.normalize("C:/a/")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "c", FilenameUtils.normalize("C:/a/b/../c")); + assertEquals(DRIVE_C + SEP + "c", FilenameUtils.normalize("C:/a/b/../../c")); assertNull(FilenameUtils.normalize("C:/a/b/../../../c")); - assertEquals("C:" + SEP + "a" + SEP, FilenameUtils.normalize("C:/a/b/..")); - assertEquals("C:" + SEP + "", FilenameUtils.normalize("C:/a/b/../..")); + assertEquals(DRIVE_C + SEP + "a" + SEP, FilenameUtils.normalize("C:/a/b/..")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalize("C:/a/b/../..")); assertNull(FilenameUtils.normalize("C:/a/b/../../..")); - assertEquals("C:" + SEP + "a" + SEP + "d", FilenameUtils.normalize("C:/a/b/../c/../d")); - assertEquals("C:" + SEP + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalize("C:/a/b//d")); - assertEquals("C:" + SEP + "a" + SEP + "b" + SEP, FilenameUtils.normalize("C:/a/b/././.")); - assertEquals("C:" + SEP + "a", FilenameUtils.normalize("C:/./a")); - assertEquals("C:" + SEP + "", FilenameUtils.normalize("C:/./")); - assertEquals("C:" + SEP + "", FilenameUtils.normalize("C:/.")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "d", FilenameUtils.normalize("C:/a/b/../c/../d")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalize("C:/a/b//d")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b" + SEP, FilenameUtils.normalize("C:/a/b/././.")); + assertEquals(DRIVE_C + SEP + "a", FilenameUtils.normalize("C:/./a")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalize("C:/./")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalize("C:/.")); assertNull(FilenameUtils.normalize("C:/../a")); assertNull(FilenameUtils.normalize("C:/..")); - assertEquals("C:" + SEP + "", FilenameUtils.normalize("C:/")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalize("C:/")); - assertEquals("C:" + "a", FilenameUtils.normalize("C:a")); - assertEquals("C:" + "a" + SEP, FilenameUtils.normalize("C:a/")); - assertEquals("C:" + "a" + SEP + "c", FilenameUtils.normalize("C:a/b/../c")); - assertEquals("C:" + "c", FilenameUtils.normalize("C:a/b/../../c")); + assertEquals(DRIVE_C + "a", FilenameUtils.normalize("C:a")); + assertEquals(DRIVE_C + "a" + SEP, FilenameUtils.normalize("C:a/")); + assertEquals(DRIVE_C + "a" + SEP + "c", FilenameUtils.normalize("C:a/b/../c")); + assertEquals(DRIVE_C + "c", FilenameUtils.normalize("C:a/b/../../c")); assertNull(FilenameUtils.normalize("C:a/b/../../../c")); - assertEquals("C:" + "a" + SEP, FilenameUtils.normalize("C:a/b/..")); - assertEquals("C:" + "", FilenameUtils.normalize("C:a/b/../..")); + assertEquals(DRIVE_C + "a" + SEP, FilenameUtils.normalize("C:a/b/..")); + assertEquals(DRIVE_C + "", FilenameUtils.normalize("C:a/b/../..")); assertNull(FilenameUtils.normalize("C:a/b/../../..")); - assertEquals("C:" + "a" + SEP + "d", FilenameUtils.normalize("C:a/b/../c/../d")); - assertEquals("C:" + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalize("C:a/b//d")); - assertEquals("C:" + "a" + SEP + "b" + SEP, FilenameUtils.normalize("C:a/b/././.")); - assertEquals("C:" + "a", FilenameUtils.normalize("C:./a")); - assertEquals("C:" + "", FilenameUtils.normalize("C:./")); - assertEquals("C:" + "", FilenameUtils.normalize("C:.")); + assertEquals(DRIVE_C + "a" + SEP + "d", FilenameUtils.normalize("C:a/b/../c/../d")); + assertEquals(DRIVE_C + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalize("C:a/b//d")); + assertEquals(DRIVE_C + "a" + SEP + "b" + SEP, FilenameUtils.normalize("C:a/b/././.")); + assertEquals(DRIVE_C + "a", FilenameUtils.normalize("C:./a")); + assertEquals(DRIVE_C + "", FilenameUtils.normalize("C:./")); + assertEquals(DRIVE_C + "", FilenameUtils.normalize("C:.")); assertNull(FilenameUtils.normalize("C:../a")); assertNull(FilenameUtils.normalize("C:..")); - assertEquals("C:" + "", FilenameUtils.normalize("C:")); + assertEquals(DRIVE_C + "", FilenameUtils.normalize(DRIVE_C)); assertEquals(SEP + SEP + "server" + SEP + "a", FilenameUtils.normalize("//server/a")); assertEquals(SEP + SEP + "server" + SEP + "a" + SEP, FilenameUtils.normalize("//server/a/")); @@ -948,8 +950,8 @@ void testNormalizeFromJavaDoc() { assertEquals("bar", FilenameUtils.normalize("foo" + SEP + ".." + SEP + "bar")); assertEquals(SEP + SEP + "server" + SEP + "bar", FilenameUtils.normalize(SEP + SEP + "server" + SEP + "foo" + SEP + ".." + SEP + "bar")); assertNull(FilenameUtils.normalize(SEP + SEP + "server" + SEP + ".." + SEP + "bar")); - assertEquals("C:" + SEP + "bar", FilenameUtils.normalize("C:" + SEP + "foo" + SEP + ".." + SEP + "bar")); - assertNull(FilenameUtils.normalize("C:" + SEP + ".." + SEP + "bar")); + assertEquals(DRIVE_C + SEP + "bar", FilenameUtils.normalize(DRIVE_C + SEP + "foo" + SEP + ".." + SEP + "bar")); + assertNull(FilenameUtils.normalize(DRIVE_C + SEP + ".." + SEP + "bar")); assertEquals("~" + SEP + "bar" + SEP, FilenameUtils.normalize("~" + SEP + "foo" + SEP + ".." + SEP + "bar" + SEP)); assertNull(FilenameUtils.normalize("~" + SEP + ".." + SEP + "bar")); @@ -969,11 +971,11 @@ void testNormalizeNoEndSeparator() { assertEquals("a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("a\\b/c.txt")); assertEquals("" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("\\a\\b/c.txt")); - assertEquals("C:" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("C:\\a\\b/c.txt")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("C:\\a\\b/c.txt")); assertEquals("" + SEP + "" + SEP + "server" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("\\\\server\\a\\b/c.txt")); assertEquals("~" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("~\\a\\b/c.txt")); assertEquals("~user" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("~user\\a\\b/c.txt")); - assertEquals("C:" + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("C:\\\\a\\\\b\\\\c.txt")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b" + SEP + "c.txt", FilenameUtils.normalizeNoEndSeparator("C:\\\\a\\\\b\\\\c.txt")); assertEquals("a" + SEP + "c", FilenameUtils.normalizeNoEndSeparator("a/b/../c")); assertEquals("c", FilenameUtils.normalizeNoEndSeparator("a/b/../../c")); @@ -1053,41 +1055,41 @@ void testNormalizeNoEndSeparator() { assertEquals("~user" + SEP, FilenameUtils.normalizeNoEndSeparator("~user/")); assertEquals("~user" + SEP, FilenameUtils.normalizeNoEndSeparator("~user")); - assertEquals("C:" + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/a")); - assertEquals("C:" + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/a/")); - assertEquals("C:" + SEP + "a" + SEP + "c", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../c")); - assertEquals("C:" + SEP + "c", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../../c")); + assertEquals(DRIVE_C + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/a")); + assertEquals(DRIVE_C + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/a/")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "c", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../c")); + assertEquals(DRIVE_C + SEP + "c", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../../c")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:/a/b/../../../c")); - assertEquals("C:" + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/a/b/..")); - assertEquals("C:" + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../..")); + assertEquals(DRIVE_C + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/a/b/..")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../..")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:/a/b/../../..")); - assertEquals("C:" + SEP + "a" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../c/../d")); - assertEquals("C:" + SEP + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:/a/b//d")); - assertEquals("C:" + SEP + "a" + SEP + "b", FilenameUtils.normalizeNoEndSeparator("C:/a/b/././.")); - assertEquals("C:" + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/./a")); - assertEquals("C:" + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/./")); - assertEquals("C:" + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/.")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:/a/b/../c/../d")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:/a/b//d")); + assertEquals(DRIVE_C + SEP + "a" + SEP + "b", FilenameUtils.normalizeNoEndSeparator("C:/a/b/././.")); + assertEquals(DRIVE_C + SEP + "a", FilenameUtils.normalizeNoEndSeparator("C:/./a")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/./")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/.")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:/../a")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:/..")); - assertEquals("C:" + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/")); + assertEquals(DRIVE_C + SEP + "", FilenameUtils.normalizeNoEndSeparator("C:/")); - assertEquals("C:" + "a", FilenameUtils.normalizeNoEndSeparator("C:a")); - assertEquals("C:" + "a", FilenameUtils.normalizeNoEndSeparator("C:a/")); - assertEquals("C:" + "a" + SEP + "c", FilenameUtils.normalizeNoEndSeparator("C:a/b/../c")); - assertEquals("C:" + "c", FilenameUtils.normalizeNoEndSeparator("C:a/b/../../c")); + assertEquals(DRIVE_C + "a", FilenameUtils.normalizeNoEndSeparator("C:a")); + assertEquals(DRIVE_C + "a", FilenameUtils.normalizeNoEndSeparator("C:a/")); + assertEquals(DRIVE_C + "a" + SEP + "c", FilenameUtils.normalizeNoEndSeparator("C:a/b/../c")); + assertEquals(DRIVE_C + "c", FilenameUtils.normalizeNoEndSeparator("C:a/b/../../c")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:a/b/../../../c")); - assertEquals("C:" + "a", FilenameUtils.normalizeNoEndSeparator("C:a/b/..")); - assertEquals("C:" + "", FilenameUtils.normalizeNoEndSeparator("C:a/b/../..")); + assertEquals(DRIVE_C + "a", FilenameUtils.normalizeNoEndSeparator("C:a/b/..")); + assertEquals(DRIVE_C + "", FilenameUtils.normalizeNoEndSeparator("C:a/b/../..")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:a/b/../../..")); - assertEquals("C:" + "a" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:a/b/../c/../d")); - assertEquals("C:" + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:a/b//d")); - assertEquals("C:" + "a" + SEP + "b", FilenameUtils.normalizeNoEndSeparator("C:a/b/././.")); - assertEquals("C:" + "a", FilenameUtils.normalizeNoEndSeparator("C:./a")); - assertEquals("C:" + "", FilenameUtils.normalizeNoEndSeparator("C:./")); - assertEquals("C:" + "", FilenameUtils.normalizeNoEndSeparator("C:.")); + assertEquals(DRIVE_C + "a" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:a/b/../c/../d")); + assertEquals(DRIVE_C + "a" + SEP + "b" + SEP + "d", FilenameUtils.normalizeNoEndSeparator("C:a/b//d")); + assertEquals(DRIVE_C + "a" + SEP + "b", FilenameUtils.normalizeNoEndSeparator("C:a/b/././.")); + assertEquals(DRIVE_C + "a", FilenameUtils.normalizeNoEndSeparator("C:./a")); + assertEquals(DRIVE_C + "", FilenameUtils.normalizeNoEndSeparator("C:./")); + assertEquals(DRIVE_C + "", FilenameUtils.normalizeNoEndSeparator("C:.")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:../a")); assertNull(FilenameUtils.normalizeNoEndSeparator("C:..")); - assertEquals("C:" + "", FilenameUtils.normalizeNoEndSeparator("C:")); + assertEquals(DRIVE_C + "", FilenameUtils.normalizeNoEndSeparator(DRIVE_C)); assertEquals(SEP + SEP + "server" + SEP + "a", FilenameUtils.normalizeNoEndSeparator("//server/a")); assertEquals(SEP + SEP + "server" + SEP + "a", FilenameUtils.normalizeNoEndSeparator("//server/a/")); diff --git a/src/test/java/org/apache/commons/io/IOCaseTest.java b/src/test/java/org/apache/commons/io/IOCaseTest.java index 97a990f6369..54073da8096 100644 --- a/src/test/java/org/apache/commons/io/IOCaseTest.java +++ b/src/test/java/org/apache/commons/io/IOCaseTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -29,6 +30,8 @@ import java.io.ObjectOutputStream; import java.util.Arrays; +import org.apache.commons.io.IOUtils.ScratchBytes; +import org.apache.commons.io.IOUtils.ScratchChars; import org.junit.jupiter.api.Test; /** @@ -296,34 +299,49 @@ void test_getName() { @Test void test_getScratchByteArray() { - final byte[] array = IOUtils.getScratchByteArray(); - assert0(array); - Arrays.fill(array, (byte) 1); - assert0(IOUtils.getScratchCharArray()); - } - - @Test - void test_getScratchByteArrayWriteOnly() { - final byte[] array = IOUtils.getScratchByteArrayWriteOnly(); - assert0(array); - Arrays.fill(array, (byte) 1); - assert0(IOUtils.getScratchCharArray()); + final byte[] array; + try (ScratchBytes scratch = IOUtils.ScratchBytes.get()) { + array = scratch.array(); + assert0(array); + Arrays.fill(array, (byte) 1); + // Get another array, while the first is still in use + // The test doesn't need the try here but that's the pattern. + try (ScratchBytes scratch2 = IOUtils.ScratchBytes.get()) { + assertNotSame(scratch, scratch2); + final byte[] array2 = scratch2.array(); + assert0(array2); + assertNotSame(array, array2); + } + } + // The first array should be reset and reusable + try (ScratchBytes scratch = IOUtils.ScratchBytes.get()) { + final byte[] array3 = scratch.array(); + assert0(array3); + assertSame(array, array3); + } } @Test void test_getScratchCharArray() { - final char[] array = IOUtils.getScratchCharArray(); - assert0(array); - Arrays.fill(array, (char) 1); - assert0(IOUtils.getScratchCharArray()); - } - - @Test - void test_getScratchCharArrayWriteOnly() { - final char[] array = IOUtils.getScratchCharArrayWriteOnly(); - assert0(array); - Arrays.fill(array, (char) 1); - assert0(IOUtils.getScratchCharArray()); + final char[] array; + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + array = scratch.array(); + assert0(array); + Arrays.fill(array, (char) 1); + // Get another array, while the first is still in use + // The test doesn't need the try here but that's the pattern. + try (ScratchChars scratch2 = IOUtils.ScratchChars.get()) { + final char[] array2 = scratch2.array(); + assert0(array2); + assertNotSame(array, array2); + } + } + // The first array should be reset and reusable + try (ScratchChars scratch = IOUtils.ScratchChars.get()) { + final char[] array3 = scratch.array(); + assert0(array3); + assertSame(array, array3); + } } @Test diff --git a/src/test/java/org/apache/commons/io/IOUtilsConcurrentTest.java b/src/test/java/org/apache/commons/io/IOUtilsConcurrentTest.java new file mode 100644 index 00000000000..1f1fb2a13fe --- /dev/null +++ b/src/test/java/org/apache/commons/io/IOUtilsConcurrentTest.java @@ -0,0 +1,212 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.Charset; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import java.util.zip.CRC32; +import java.util.zip.Checksum; + +import org.apache.commons.io.function.IOConsumer; +import org.apache.commons.io.input.ChecksumInputStream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link IOUtils} methods in a concurrent environment. + */ +class IOUtilsConcurrentTest { + + private static class ChecksumReader extends Reader { + private final CRC32 checksum; + private final long expectedChecksumValue; + private final Reader reader; + + ChecksumReader(final Reader reader, final long expectedChecksumValue) { + this.reader = reader; + this.checksum = new CRC32(); + this.expectedChecksumValue = expectedChecksumValue; + } + + @Override + public void close() throws IOException { + reader.close(); + } + + public long getValue() { + return checksum.getValue(); + } + + @Override + public int read() throws IOException { + return super.read(); + } + + @Override + public int read(final char[] cbuf, final int off, final int len) throws IOException { + final int n = reader.read(cbuf, off, len); + if (n > 0) { + final byte[] bytes = new String(cbuf, off, n).getBytes(Charset.defaultCharset()); + checksum.update(bytes, 0, bytes.length); + } + if (n == -1) { + final long actual = checksum.getValue(); + if (actual != expectedChecksumValue) { + throw new IOException("Checksum mismatch: expected " + expectedChecksumValue + " but got " + actual); + } + } + return n; + } + } + + /** + * Test data for InputStream tests. + */ + private static final byte[][] BYTE_DATA; + /** + * Checksum values for {@link #BYTE_DATA}. + */ + private static final long[] BYTE_DATA_CHECKSUM; + /** + * Number of runs per thread (to increase the chance of collisions). + */ + private static final int RUNS_PER_THREAD = 16; + /** + * Size of test data. + */ + private static final int SIZE = IOUtils.DEFAULT_BUFFER_SIZE; + /** + * Test data for Reader tests. + */ + private static final String[] STRING_DATA; + /** + * Checksum values for {@link #STRING_DATA}. + */ + private static final long[] STRING_DATA_CHECKSUM; + /** + * Number of threads to use. + */ + private static final int THREAD_COUNT = 16; + /** + * Number of data variants (to increase the chance of collisions). + */ + private static final int VARIANTS = 16; + + static { + final Checksum checksum = new CRC32(); + // Byte data + BYTE_DATA = new byte[VARIANTS][]; + BYTE_DATA_CHECKSUM = new long[VARIANTS]; + for (int variant = 0; variant < VARIANTS; variant++) { + final byte[] data = new byte[SIZE]; + for (int i = 0; i < SIZE; i++) { + data[i] = (byte) ((i + variant) % 256); + } + BYTE_DATA[variant] = data; + checksum.reset(); + checksum.update(data, 0 , data.length); + BYTE_DATA_CHECKSUM[variant] = checksum.getValue(); + } + // Char data + final char[] cdata = new char[SIZE]; + STRING_DATA = new String[VARIANTS]; + STRING_DATA_CHECKSUM = new long[VARIANTS]; + for (int variant = 0; variant < VARIANTS; variant++) { + for (int i = 0; i < SIZE; i++) { + cdata[i] = (char) ((i + variant) % Character.MAX_VALUE); + } + STRING_DATA[variant] = new String(cdata); + checksum.reset(); + final byte[] bytes = STRING_DATA[variant].getBytes(Charset.defaultCharset()); + checksum.update(bytes, 0, bytes.length); + STRING_DATA_CHECKSUM[variant] = checksum.getValue(); + } + } + + static Stream> testConcurrentInputStreamTasks() { + return Stream.of( + IOUtils::consume, + in -> IOUtils.skip(in, Long.MAX_VALUE), + in -> IOUtils.skipFully(in, SIZE), + IOUtils::toByteArray, + in -> IOUtils.toByteArray(in, SIZE), + in -> IOUtils.toByteArray(in, SIZE, 512) + ); + } + + static Stream> testConcurrentReaderTasks() { + return Stream.of( + IOUtils::consume, + reader -> IOUtils.skip(reader, Long.MAX_VALUE), + reader -> IOUtils.skipFully(reader, SIZE), + reader -> IOUtils.toByteArray(reader, Charset.defaultCharset()) + ); + } + + @ParameterizedTest + @MethodSource + void testConcurrentInputStreamTasks(final IOConsumer consumer) throws InterruptedException { + final ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT); + try { + final List> futures = IntStream.range(0, THREAD_COUNT * RUNS_PER_THREAD) + .>mapToObj(i -> threadPool.submit(() -> { + try (InputStream in = ChecksumInputStream + .builder() + .setByteArray(BYTE_DATA[i % VARIANTS]) + .setChecksum(new CRC32()) + .setExpectedChecksumValue(BYTE_DATA_CHECKSUM[i % VARIANTS]) + .get()) { + consumer.accept(in); + } + return null; + })).collect(Collectors.toList()); + futures.forEach(f -> assertDoesNotThrow(() -> f.get())); + } finally { + threadPool.shutdownNow(); + } + } + + @ParameterizedTest + @MethodSource + void testConcurrentReaderTasks(final IOConsumer consumer) throws InterruptedException { + final ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT); + try { + final List> futures = IntStream.range(0, THREAD_COUNT * RUNS_PER_THREAD) + .>mapToObj(i -> threadPool.submit(() -> { + try (Reader reader = new ChecksumReader(new StringReader(STRING_DATA[i % VARIANTS]), STRING_DATA_CHECKSUM[i % VARIANTS])) { + consumer.accept(reader); + } + return null; + })).collect(Collectors.toList()); + futures.forEach(f -> assertDoesNotThrow(() -> f.get())); + } finally { + threadPool.shutdownNow(); + } + } +} diff --git a/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java b/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java index 57870b32ed1..881ecec1d92 100644 --- a/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java +++ b/src/test/java/org/apache/commons/io/IOUtilsMultithreadedSkipTest.java @@ -110,7 +110,7 @@ private void testSkipFullyOnInflaterInputStream(final Supplier baSupplie final int c = is.read(); assertEquals(expected[skipIndex], c, "failed on seed=" + seed + " iteration=" + iteration); } catch (final EOFException e) { - assertEquals(expected[skipIndex], is.read(), "failed on " + "seed=" + seed + " iteration=" + iteration); + assertEquals(expected[skipIndex], is.read(), "failed on seed=" + seed + " iteration=" + iteration); } } } diff --git a/src/test/java/org/apache/commons/io/IOUtilsTest.java b/src/test/java/org/apache/commons/io/IOUtilsTest.java index 56fd1307eb1..033fdffdd76 100644 --- a/src/test/java/org/apache/commons/io/IOUtilsTest.java +++ b/src/test/java/org/apache/commons/io/IOUtilsTest.java @@ -27,6 +27,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -48,6 +50,7 @@ import java.io.SequenceInputStream; import java.io.StringReader; import java.io.Writer; +import java.lang.reflect.InvocationTargetException; import java.net.ServerSocket; import java.net.Socket; import java.net.URI; @@ -63,6 +66,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Supplier; @@ -80,9 +84,12 @@ import org.apache.commons.io.output.NullOutputStream; import org.apache.commons.io.output.NullWriter; import org.apache.commons.io.output.StringBuilderWriter; +import org.apache.commons.io.output.UnsynchronizedByteArrayOutputStream; import org.apache.commons.io.test.TestUtils; import org.apache.commons.io.test.ThrowOnCloseReader; +import org.apache.commons.lang3.JavaVersion; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.SystemUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -90,6 +97,12 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentMatchers; +import org.mockito.MockedStatic; +import org.mockito.Mockito; /** * This is used to test {@link IOUtils} for correctness. The following checks are performed: @@ -125,6 +138,97 @@ public static void beforeAll() { IO.clear(); } + static Stream invalidRead_InputStream_Offset_ArgumentsProvider() { + final InputStream input = new ByteArrayInputStream(new byte[10]); + final byte[] b = new byte[10]; + return Stream.of( + // input is null + Arguments.of(null, b, 0, 1, NullPointerException.class), + // b is null + Arguments.of(input, null, 0, 1, NullPointerException.class), + // off is negative + Arguments.of(input, b, -1, 1, IndexOutOfBoundsException.class), + // len is negative + Arguments.of(input, b, 0, -1, IndexOutOfBoundsException.class), + // off + len is too big + Arguments.of(input, b, 1, 10, IndexOutOfBoundsException.class), + // off + len is too big + Arguments.of(input, b, 10, 1, IndexOutOfBoundsException.class) + ); + } + + static Stream testCheckFromIndexSizeInvalidCases() { + return Stream.of( + Arguments.of(-1, 0, 42), + Arguments.of(0, -1, 42), + Arguments.of(0, 0, -1), + // off + len > arrayLength + Arguments.of(1, 42, 42), + Arguments.of(Integer.MAX_VALUE, 1, Integer.MAX_VALUE) + ); + } + + static Stream testCheckFromIndexSizeValidCases() { + return Stream.of( + // Valid cases + Arguments.of(0, 0, 42), + Arguments.of(0, 1, 42), + Arguments.of(0, 42, 42), + Arguments.of(41, 1, 42), + Arguments.of(42, 0, 42) + ); + } + + static Stream testCheckFromToIndexInvalidCases() { + return Stream.of( + Arguments.of(-1, 0, 42), + Arguments.of(0, -1, 42), + Arguments.of(0, 0, -1), + // from > to + Arguments.of(1, 0, 42), + // to > arrayLength + Arguments.of(0, 43, 42), + Arguments.of(1, 43, 42) + ); + } + + static Stream testCheckFromToIndexValidCases() { + return Stream.of( + // Valid cases + Arguments.of(0, 0, 42), + Arguments.of(0, 1, 42), + Arguments.of(0, 42, 42), + Arguments.of(41, 42, 42), + Arguments.of(42, 42, 42) + ); + } + + private static Stream testToByteArray_InputStream_Size_BufferSize_Succeeds() { + final byte[] data = new byte[1024]; + for (int i = 0; i < 1024; i++) { + data[i] = (byte) i; + } + return Stream.of( + // Eager reading + Arguments.of(data.clone(), 512, 1024), + // Incremental reading + Arguments.of(data.clone(), 1024, 512), + // No reading + Arguments.of(data.clone(), 0, 128)); + } + + static Stream testToByteArray_InputStream_Size_BufferSize_Throws() { + return Stream.of( + // Negative size + Arguments.of(-1, 128, IllegalArgumentException.class), + // Invalid buffer size + Arguments.of(0, 0, IllegalArgumentException.class), + // Truncation with requested size < chunk size + Arguments.of(64, 128, EOFException.class), + // Truncation with requested size > chunk size + Arguments.of(Integer.MAX_VALUE, 128, EOFException.class)); + } + @TempDir public File temporaryFolder; @@ -315,6 +419,58 @@ void testByteArrayWithNegativeSize() { assertThrows(NegativeArraySizeException.class, () -> IOUtils.byteArray(-1)); } + @ParameterizedTest + @MethodSource + void testCheckFromIndexSizeInvalidCases(final int off, final int len, final int arrayLength) { + final IndexOutOfBoundsException ex = assertThrows(IndexOutOfBoundsException.class, () -> IOUtils.checkFromIndexSize(off, len, arrayLength)); + assertTrue(ex.getMessage().contains(String.valueOf(off))); + assertTrue(ex.getMessage().contains(String.valueOf(len))); + assertTrue(ex.getMessage().contains(String.valueOf(arrayLength))); + // Optional requirement: compare the exception message for Java 8 and Java 9+ + if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9)) { + final IndexOutOfBoundsException jreEx = assertThrows(IndexOutOfBoundsException.class, () -> { + try { + Objects.class.getDeclaredMethod("checkFromIndexSize", int.class, int.class, int.class).invoke(null, off, len, arrayLength); + } catch (final InvocationTargetException ite) { + throw ite.getTargetException(); + } + }); + assertEquals(jreEx.getMessage(), ex.getMessage()); + } + } + + @ParameterizedTest + @MethodSource + void testCheckFromIndexSizeValidCases(final int off, final int len, final int arrayLength) { + assertDoesNotThrow(() -> IOUtils.checkFromIndexSize(off, len, arrayLength)); + } + + @ParameterizedTest + @MethodSource + void testCheckFromToIndexInvalidCases(final int from, final int to, final int arrayLength) { + final IndexOutOfBoundsException ex = assertThrows(IndexOutOfBoundsException.class, () -> IOUtils.checkFromToIndex(from, to, arrayLength)); + assertTrue(ex.getMessage().contains(String.valueOf(from))); + assertTrue(ex.getMessage().contains(String.valueOf(to))); + assertTrue(ex.getMessage().contains(String.valueOf(arrayLength))); + // Optional requirement: compare the exception message for Java 8 and Java 9+ + if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_9)) { + final IndexOutOfBoundsException jreEx = assertThrows(IndexOutOfBoundsException.class, () -> { + try { + Objects.class.getDeclaredMethod("checkFromToIndex", int.class, int.class, int.class).invoke(null, from, to, arrayLength); + } catch (final InvocationTargetException ite) { + throw ite.getTargetException(); + } + }); + assertEquals(jreEx.getMessage(), ex.getMessage()); + } + } + + @ParameterizedTest + @MethodSource + void testCheckFromToIndexValidCases(final int from, final int to, final int arrayLength) { + assertDoesNotThrow(() -> IOUtils.checkFromToIndex(from, to, arrayLength)); + } + @Test void testClose() { assertDoesNotThrow(() -> IOUtils.close((Closeable) null)); @@ -546,13 +702,10 @@ void testConsumeReader() throws Exception { final long size = (long) Integer.MAX_VALUE + (long) 1; final Reader in = new NullReader(size); final Writer out = NullWriter.INSTANCE; - // Test copy() method assertEquals(-1, IOUtils.copy(in, out)); - // reset the input in.close(); - // Test consume() method assertEquals(size, IOUtils.consume(in), "consume()"); } @@ -776,12 +929,9 @@ void testCopy_ByteArray_OutputStream() throws Exception { // Create our byte[]. Rely on testInputStreamToByteArray() to make sure this is valid. in = IOUtils.toByteArray(fin); } - try (OutputStream fout = Files.newOutputStream(destination.toPath())) { CopyUtils.copy(in, fout); - fout.flush(); - TestUtils.checkFile(destination, testFile); TestUtils.checkWrite(fout); } @@ -796,7 +946,6 @@ void testCopy_ByteArray_Writer() throws Exception { // Create our byte[]. Rely on testInputStreamToByteArray() to make sure this is valid. in = IOUtils.toByteArray(fin); } - try (Writer fout = Files.newBufferedWriter(destination.toPath())) { CopyUtils.copy(in, fout); fout.flush(); @@ -814,11 +963,9 @@ void testCopy_String_Writer() throws Exception { // Create our String. Rely on testReaderToString() to make sure this is valid. str = IOUtils.toString(fin); } - try (Writer fout = Files.newBufferedWriter(destination.toPath())) { CopyUtils.copy(str, fout); fout.flush(); - TestUtils.checkFile(destination, testFile); TestUtils.checkWrite(fout); } @@ -833,19 +980,16 @@ void testCopyLarge_CharExtraLength() throws IOException { // Create streams is = new CharArrayReader(carr); os = new CharArrayWriter(); - // Test our copy method // for extra length, it reads till EOF assertEquals(200, IOUtils.copyLarge(is, os, 0, 2000)); final char[] oarr = os.toCharArray(); - // check that output length is correct assertEquals(200, oarr.length); // check that output data corresponds to input data assertEquals(1, oarr[1]); assertEquals(79, oarr[79]); assertEquals((char) -1, oarr[80]); - } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); @@ -860,18 +1004,15 @@ void testCopyLarge_CharFullLength() throws IOException { // Create streams is = new CharArrayReader(carr); os = new CharArrayWriter(); - // Test our copy method assertEquals(200, IOUtils.copyLarge(is, os, 0, -1)); final char[] oarr = os.toCharArray(); - // check that output length is correct assertEquals(200, oarr.length); // check that output data corresponds to input data assertEquals(1, oarr[1]); assertEquals(79, oarr[79]); assertEquals((char) -1, oarr[80]); - } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); @@ -886,18 +1027,15 @@ void testCopyLarge_CharNoSkip() throws IOException { // Create streams is = new CharArrayReader(carr); os = new CharArrayWriter(); - // Test our copy method assertEquals(100, IOUtils.copyLarge(is, os, 0, 100)); final char[] oarr = os.toCharArray(); - // check that output length is correct assertEquals(100, oarr.length); // check that output data corresponds to input data assertEquals(1, oarr[1]); assertEquals(79, oarr[79]); assertEquals((char) -1, oarr[80]); - } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); @@ -912,18 +1050,15 @@ void testCopyLarge_CharSkip() throws IOException { // Create streams is = new CharArrayReader(carr); os = new CharArrayWriter(); - // Test our copy method assertEquals(100, IOUtils.copyLarge(is, os, 10, 100)); final char[] oarr = os.toCharArray(); - // check that output length is correct assertEquals(100, oarr.length); // check that output data corresponds to input data assertEquals(11, oarr[1]); assertEquals(79, oarr[69]); assertEquals((char) -1, oarr[70]); - } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); @@ -939,15 +1074,12 @@ void testCopyLarge_CharSkipInvalid() { @Test void testCopyLarge_ExtraLength() throws IOException { - try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); - ByteArrayOutputStream os = new ByteArrayOutputStream()) { + try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); ByteArrayOutputStream os = new ByteArrayOutputStream()) { // Create streams - // Test our copy method // for extra length, it reads till EOF assertEquals(200, IOUtils.copyLarge(is, os, 0, 2000)); final byte[] oarr = os.toByteArray(); - // check that output length is correct assertEquals(200, oarr.length); // check that output data corresponds to input data @@ -959,12 +1091,10 @@ void testCopyLarge_ExtraLength() throws IOException { @Test void testCopyLarge_FullLength() throws IOException { - try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); - ByteArrayOutputStream os = new ByteArrayOutputStream()) { + try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); ByteArrayOutputStream os = new ByteArrayOutputStream()) { // Test our copy method assertEquals(200, IOUtils.copyLarge(is, os, 0, -1)); final byte[] oarr = os.toByteArray(); - // check that output length is correct assertEquals(200, oarr.length); // check that output data corresponds to input data @@ -976,12 +1106,10 @@ void testCopyLarge_FullLength() throws IOException { @Test void testCopyLarge_NoSkip() throws IOException { - try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); - ByteArrayOutputStream os = new ByteArrayOutputStream()) { + try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); ByteArrayOutputStream os = new ByteArrayOutputStream()) { // Test our copy method assertEquals(100, IOUtils.copyLarge(is, os, 0, 100)); final byte[] oarr = os.toByteArray(); - // check that output length is correct assertEquals(100, oarr.length); // check that output data corresponds to input data @@ -993,12 +1121,10 @@ void testCopyLarge_NoSkip() throws IOException { @Test void testCopyLarge_Skip() throws IOException { - try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); - ByteArrayOutputStream os = new ByteArrayOutputStream()) { + try (ByteArrayInputStream is = new ByteArrayInputStream(iarr); ByteArrayOutputStream os = new ByteArrayOutputStream()) { // Test our copy method assertEquals(100, IOUtils.copyLarge(is, os, 10, 100)); final byte[] oarr = os.toByteArray(); - // check that output length is correct assertEquals(100, oarr.length); // check that output data corresponds to input data @@ -1025,24 +1151,28 @@ void testCopyLarge_SkipWithInvalidOffset() throws IOException { // Create streams is = new ByteArrayInputStream(iarr); os = new ByteArrayOutputStream(); - // Test our copy method assertEquals(100, IOUtils.copyLarge(is, os, -10, 100)); final byte[] oarr = os.toByteArray(); - // check that output length is correct assertEquals(100, oarr.length); // check that output data corresponds to input data assertEquals(1, oarr[1]); assertEquals(79, oarr[79]); assertEquals(-1, oarr[80]); - } finally { IOUtils.closeQuietly(is); IOUtils.closeQuietly(os); } } + @ParameterizedTest + @MethodSource("invalidRead_InputStream_Offset_ArgumentsProvider") + void testRead_InputStream_Offset_ArgumentsValidation(final InputStream input, final byte[] b, final int off, final int len, + final Class expected) { + assertThrows(expected, () -> IOUtils.read(input, b, off, len)); + } + @Test void testRead_ReadableByteChannel() throws Exception { final ByteBuffer buffer = ByteBuffer.allocate(FILE_SIZE); @@ -1054,7 +1184,7 @@ void testRead_ReadableByteChannel() throws Exception { assertEquals(0, buffer.remaining()); assertEquals(0, input.read(buffer)); buffer.clear(); - assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer), "Should have failed with EOFException"); + assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer)); } finally { IOUtils.closeQuietly(input, fileInputStream); } @@ -1064,11 +1194,8 @@ void testRead_ReadableByteChannel() throws Exception { void testReadFully_InputStream__ReturnByteArray() throws Exception { final byte[] bytes = "abcd1234".getBytes(StandardCharsets.UTF_8); final ByteArrayInputStream stream = new ByteArrayInputStream(bytes); - final byte[] result = IOUtils.readFully(stream, bytes.length); - IOUtils.closeQuietly(stream); - assertEqualContent(result, bytes); } @@ -1078,11 +1205,11 @@ void testReadFully_InputStream_ByteArray() throws Exception { final byte[] buffer = new byte[size]; final InputStream input = new ByteArrayInputStream(new byte[size]); - assertThrows(IllegalArgumentException.class, () -> IOUtils.readFully(input, buffer, 0, -1), "Should have failed with IllegalArgumentException"); + assertThrows(IndexOutOfBoundsException.class, () -> IOUtils.readFully(input, buffer, 0, -1)); IOUtils.readFully(input, buffer, 0, 0); IOUtils.readFully(input, buffer, 0, size - 1); - assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer, 0, 2), "Should have failed with EOFException"); + assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer, 0, 2)); IOUtils.closeQuietly(input); } @@ -1095,6 +1222,13 @@ void testReadFully_InputStream_Offset() throws Exception { IOUtils.closeQuietly(stream); } + @ParameterizedTest + @MethodSource("invalidRead_InputStream_Offset_ArgumentsProvider") + void testReadFully_InputStream_Offset_ArgumentsValidation(final InputStream input, final byte[] b, final int off, final int len, + final Class expected) { + assertThrows(expected, () -> IOUtils.read(input, b, off, len)); + } + @Test void testReadFully_ReadableByteChannel() throws Exception { final ByteBuffer buffer = ByteBuffer.allocate(FILE_SIZE); @@ -1111,7 +1245,7 @@ void testReadFully_ReadableByteChannel() throws Exception { assertEquals(0, input.read(buffer)); IOUtils.readFully(input, buffer); buffer.clear(); - assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer), "Should have failed with EOFxception"); + assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer)); } finally { IOUtils.closeQuietly(input, fileInputStream); } @@ -1125,8 +1259,8 @@ void testReadFully_Reader() throws Exception { IOUtils.readFully(input, buffer, 0, 0); IOUtils.readFully(input, buffer, 0, size - 3); - assertThrows(IllegalArgumentException.class, () -> IOUtils.readFully(input, buffer, 0, -1), "Should have failed with IllegalArgumentException"); - assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer, 0, 5), "Should have failed with EOFException"); + assertThrows(IndexOutOfBoundsException.class, () -> IOUtils.readFully(input, buffer, 0, -1)); + assertThrows(EOFException.class, () -> IOUtils.readFully(input, buffer, 0, 5)); IOUtils.closeQuietly(input); } @@ -1241,16 +1375,16 @@ void testResourceToByteArray_ExistingResourceAtRootPackage_WithClassLoader() thr @Test void testResourceToByteArray_ExistingResourceAtSubPackage() throws Exception { - final long fileSize = TestResources.getFile("FileUtilsTestDataCR.dat").length(); - final byte[] bytes = IOUtils.resourceToByteArray("/org/apache/commons/io/FileUtilsTestDataCR.dat"); + final long fileSize = TestResources.getFile("FileUtilsTestDataCR.bin").length(); + final byte[] bytes = IOUtils.resourceToByteArray("/org/apache/commons/io/FileUtilsTestDataCR.bin"); assertNotNull(bytes); assertEquals(fileSize, bytes.length); } @Test void testResourceToByteArray_ExistingResourceAtSubPackage_WithClassLoader() throws Exception { - final long fileSize = TestResources.getFile("FileUtilsTestDataCR.dat").length(); - final byte[] bytes = IOUtils.resourceToByteArray("org/apache/commons/io/FileUtilsTestDataCR.dat", + final long fileSize = TestResources.getFile("FileUtilsTestDataCR.bin").length(); + final byte[] bytes = IOUtils.resourceToByteArray("org/apache/commons/io/FileUtilsTestDataCR.bin", ClassLoader.getSystemClassLoader()); assertNotNull(bytes); assertEquals(fileSize, bytes.length); @@ -1288,6 +1422,8 @@ void testResourceToString_ExistingResourceAtRootPackage() throws Exception { assertEquals(fileSize, content.getBytes().length); } + // Tests from IO-305 + @Test void testResourceToString_ExistingResourceAtRootPackage_WithClassLoader() throws Exception { final long fileSize = TestResources.getFile("test-file-simple-utf8.bin").length(); @@ -1300,20 +1436,18 @@ void testResourceToString_ExistingResourceAtRootPackage_WithClassLoader() throws @Test void testResourceToString_ExistingResourceAtSubPackage() throws Exception { - final long fileSize = TestResources.getFile("FileUtilsTestDataCR.dat").length(); - final String content = IOUtils.resourceToString("/org/apache/commons/io/FileUtilsTestDataCR.dat", + final long fileSize = TestResources.getFile("FileUtilsTestDataCR.bin").length(); + final String content = IOUtils.resourceToString("/org/apache/commons/io/FileUtilsTestDataCR.bin", StandardCharsets.UTF_8); assertNotNull(content); assertEquals(fileSize, content.getBytes().length); } - // Tests from IO-305 - @Test void testResourceToString_ExistingResourceAtSubPackage_WithClassLoader() throws Exception { - final long fileSize = TestResources.getFile("FileUtilsTestDataCR.dat").length(); - final String content = IOUtils.resourceToString("org/apache/commons/io/FileUtilsTestDataCR.dat", + final long fileSize = TestResources.getFile("FileUtilsTestDataCR.bin").length(); + final String content = IOUtils.resourceToString("org/apache/commons/io/FileUtilsTestDataCR.bin", StandardCharsets.UTF_8, ClassLoader.getSystemClassLoader()); assertNotNull(content); @@ -1372,18 +1506,18 @@ void testResourceToURL_ExistingResourceAtRootPackage_WithClassLoader() throws Ex @Test void testResourceToURL_ExistingResourceAtSubPackage() throws Exception { - final URL url = IOUtils.resourceToURL("/org/apache/commons/io/FileUtilsTestDataCR.dat"); + final URL url = IOUtils.resourceToURL("/org/apache/commons/io/FileUtilsTestDataCR.bin"); assertNotNull(url); - assertTrue(url.getFile().endsWith("/org/apache/commons/io/FileUtilsTestDataCR.dat")); + assertTrue(url.getFile().endsWith("/org/apache/commons/io/FileUtilsTestDataCR.bin")); } @Test void testResourceToURL_ExistingResourceAtSubPackage_WithClassLoader() throws Exception { - final URL url = IOUtils.resourceToURL("org/apache/commons/io/FileUtilsTestDataCR.dat", + final URL url = IOUtils.resourceToURL("org/apache/commons/io/FileUtilsTestDataCR.bin", ClassLoader.getSystemClassLoader()); assertNotNull(url); - assertTrue(url.getFile().endsWith("/org/apache/commons/io/FileUtilsTestDataCR.dat")); + assertTrue(url.getFile().endsWith("/org/apache/commons/io/FileUtilsTestDataCR.bin")); } @Test @@ -1460,13 +1594,11 @@ void testSkip_ReadableByteChannel() throws Exception { @Test void testSkipFully_InputStream() throws Exception { final int size = 1027; - try (InputStream input = new ByteArrayInputStream(new byte[size])) { - assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1), "Should have failed with IllegalArgumentException"); - + assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1)); IOUtils.skipFully(input, 0); IOUtils.skipFully(input, size - 1); - assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2), "Should have failed with IOException"); + assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2)); } } @@ -1475,11 +1607,10 @@ void testSkipFully_InputStream_Buffer_New_bytes() throws Exception { final int size = 1027; final Supplier bas = () -> new byte[size]; try (InputStream input = new ByteArrayInputStream(new byte[size])) { - assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1, bas), "Should have failed with IllegalArgumentException"); - + assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1, bas)); IOUtils.skipFully(input, 0, bas); IOUtils.skipFully(input, size - 1, bas); - assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2, bas), "Should have failed with IOException"); + assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2, bas)); } } @@ -1489,11 +1620,10 @@ void testSkipFully_InputStream_Buffer_Reuse_bytes() throws Exception { final byte[] ba = new byte[size]; final Supplier bas = () -> ba; try (InputStream input = new ByteArrayInputStream(new byte[size])) { - assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1, bas), "Should have failed with IllegalArgumentException"); - + assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1, bas)); IOUtils.skipFully(input, 0, bas); IOUtils.skipFully(input, size - 1, bas); - assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2, bas), "Should have failed with IOException"); + assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2, bas)); } } @@ -1502,11 +1632,10 @@ void testSkipFully_InputStream_Buffer_Reuse_ThreadLocal() throws Exception { final int size = 1027; final ThreadLocal tl = ThreadLocal.withInitial(() -> new byte[size]); try (InputStream input = new ByteArrayInputStream(new byte[size])) { - assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1, tl::get), "Should have failed with IllegalArgumentException"); - + assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1, tl::get)); IOUtils.skipFully(input, 0, tl::get); IOUtils.skipFully(input, size - 1, tl::get); - assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2, tl::get), "Should have failed with IOException"); + assertThrows(IOException.class, () -> IOUtils.skipFully(input, 2, tl::get)); } } @@ -1515,10 +1644,10 @@ void testSkipFully_ReadableByteChannel() throws Exception { final FileInputStream fileInputStream = new FileInputStream(testFile); final FileChannel fileChannel = fileInputStream.getChannel(); try { - assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(fileChannel, -1), "Should have failed with IllegalArgumentException"); + assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(fileChannel, -1)); IOUtils.skipFully(fileChannel, 0); IOUtils.skipFully(fileChannel, FILE_SIZE - 1); - assertThrows(IOException.class, () -> IOUtils.skipFully(fileChannel, 2), "Should have failed with IOException"); + assertThrows(IOException.class, () -> IOUtils.skipFully(fileChannel, 2)); } finally { IOUtils.closeQuietly(fileChannel, fileInputStream); } @@ -1530,8 +1659,8 @@ void testSkipFully_Reader() throws Exception { try (Reader input = new CharArrayReader(new char[size])) { IOUtils.skipFully(input, 0); IOUtils.skipFully(input, size - 3); - assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1), "Should have failed with IllegalArgumentException"); - assertThrows(IOException.class, () -> IOUtils.skipFully(input, 5), "Should have failed with IOException"); + assertThrows(IllegalArgumentException.class, () -> IOUtils.skipFully(input, -1)); + assertThrows(IOException.class, () -> IOUtils.skipFully(input, 5)); } } @@ -1543,7 +1672,6 @@ void testStringToOutputStream() throws Exception { // Create our String. Rely on testReaderToString() to make sure this is valid. str = IOUtils.toString(fin); } - try (OutputStream fout = Files.newOutputStream(destination.toPath())) { CopyUtils.copy(str, fout); // Note: this method *does* flush. It is equivalent to: @@ -1552,7 +1680,6 @@ void testStringToOutputStream() throws Exception { // _out.flush(); // out = fout; // note: we don't flush here; this IOUtils method does it for us - TestUtils.checkFile(destination, testFile); TestUtils.checkWrite(fout); } @@ -1604,10 +1731,8 @@ void testToByteArray_InputStream_LongerThanIntegerMaxValue() throws Exception { @Test void testToByteArray_InputStream_NegativeSize() throws Exception { try (InputStream fin = Files.newInputStream(testFilePath)) { - final IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> IOUtils.toByteArray(fin, -1), - "Should have failed with IllegalArgumentException"); - assertTrue(exc.getMessage().startsWith("Size must be equal or greater than zero"), - "Exception message does not start with \"Size must be equal or greater than zero\""); + final IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> IOUtils.toByteArray(fin, -1)); + assertTrue(exc.getMessage().startsWith("size < 0"), exc.getMessage()); } } @@ -1622,22 +1747,44 @@ void testToByteArray_InputStream_Size() throws Exception { } } + @ParameterizedTest + @MethodSource + void testToByteArray_InputStream_Size_BufferSize_Succeeds(final byte[] data, final int size, final int bufferSize) throws IOException { + final ByteArrayInputStream input = new ByteArrayInputStream(data); + final byte[] expected = Arrays.copyOf(data, size); + final byte[] actual = IOUtils.toByteArray(input, size, bufferSize); + assertArrayEquals(expected, actual); + } + + @ParameterizedTest + @MethodSource + void testToByteArray_InputStream_Size_BufferSize_Throws( + final int size, final int bufferSize, final Class exceptionClass) throws IOException { + try (InputStream input = new NullInputStream(0)) { + assertThrows(exceptionClass, () -> IOUtils.toByteArray(input, size, bufferSize)); + } + } + + @Test + void testToByteArray_InputStream_Size_Truncated() throws Exception { + try (InputStream in = new NullInputStream(0)) { + assertThrows(EOFException.class, () -> IOUtils.toByteArray(in, 1)); + } + } + @Test void testToByteArray_InputStream_SizeIllegal() throws Exception { try (InputStream fin = Files.newInputStream(testFilePath)) { - final IOException exc = assertThrows(IOException.class, () -> IOUtils.toByteArray(fin, testFile.length() + 1), - "Should have failed with IOException"); - assertTrue(exc.getMessage().startsWith("Unexpected read size"), "Exception message does not start with \"Unexpected read size\""); + final IOException exc = assertThrows(IOException.class, () -> IOUtils.toByteArray(fin, testFile.length() + 1)); + assertTrue(exc.getMessage().startsWith("Expected read size"), exc.getMessage()); } } @Test void testToByteArray_InputStream_SizeLong() throws Exception { try (InputStream fin = Files.newInputStream(testFilePath)) { - final IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> IOUtils.toByteArray(fin, (long) Integer.MAX_VALUE + 1), - "Should have failed with IllegalArgumentException"); - assertTrue(exc.getMessage().startsWith("Size cannot be greater than Integer max value"), - "Exception message does not start with \"Size cannot be greater than Integer max value\""); + final IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> IOUtils.toByteArray(fin, (long) Integer.MAX_VALUE + 1)); + assertTrue(exc.getMessage().startsWith("size > Integer.MAX_VALUE"), exc.getMessage()); } } @@ -1674,12 +1821,28 @@ void testToByteArray_String() throws Exception { try (Reader fin = Files.newBufferedReader(testFilePath)) { // Create our String. Rely on testReaderToString() to make sure this is valid. final String str = IOUtils.toString(fin); - final byte[] out = IOUtils.toByteArray(str); assertEqualContent(str.getBytes(), out); } } + @Test + void testToByteArray_ThrowsIOExceptionOnHugeStream() throws IOException { + try (MockedStatic utils = Mockito.mockStatic(IOUtils.class, Mockito.CALLS_REAL_METHODS); + UnsynchronizedByteArrayOutputStream mockOutputStream = mock(UnsynchronizedByteArrayOutputStream.class)) { + // Prepare the mocks + utils.when(() -> IOUtils.copyToOutputStream(ArgumentMatchers.any(InputStream.class), ArgumentMatchers.anyLong(), ArgumentMatchers.anyInt())) + .thenReturn(mockOutputStream); + when(mockOutputStream.size()).thenReturn(IOUtils.SOFT_MAX_ARRAY_LENGTH + 1); + // Test and check + try (InputStream mockInputStream = mock(InputStream.class)) { + final IOException exception = assertThrows(IOException.class, () -> IOUtils.toByteArray(mockInputStream)); + assertTrue(exception.getMessage().contains(String.format("%,d", IOUtils.SOFT_MAX_ARRAY_LENGTH)), + "Exception message does not contain the maximum length"); + } + } + } + @Test void testToByteArray_URI() throws Exception { final URI url = testFile.toURI(); @@ -1919,5 +2082,4 @@ void testWriteLittleString() throws IOException { } } } - } diff --git a/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java b/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java index e55da41d39f..d40cbf05d63 100644 --- a/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/AbstractOriginTest.java @@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.Closeable; import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -30,18 +31,25 @@ import java.io.RandomAccessFile; import java.io.Reader; import java.io.Writer; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.OpenOption; import java.nio.file.Path; -import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Objects; import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; import org.apache.commons.io.build.AbstractOrigin.RandomAccessFileOrigin; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; @@ -55,18 +63,59 @@ public abstract class AbstractOriginTest> { protected static final String FILE_RES_RO = "/org/apache/commons/io/test-file-20byteslength.bin"; protected static final String FILE_NAME_RO = "src/test/resources" + FILE_RES_RO; - protected static final String FILE_NAME_RW = "target/" + AbstractOriginTest.class.getSimpleName() + ".txt"; + protected static final String FILE_NAME_RW = AbstractOriginTest.class.getSimpleName() + ".txt"; private static final int RO_LENGTH = 20; protected AbstractOrigin originRo; protected AbstractOrigin originRw; + @TempDir + protected Path tempPath; + @BeforeEach - public void beforeEach() throws IOException { + void beforeEach() throws IOException { setOriginRo(newOriginRo()); + resetOriginRw(); setOriginRw(newOriginRw()); } + private void checkRead(final ReadableByteChannel channel) throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(RO_LENGTH); + int read = channel.read(buffer); + assertEquals(RO_LENGTH, read); + assertArrayEquals(getFixtureByteArray(), buffer.array()); + // Channel is at EOF + buffer.clear(); + read = channel.read(buffer); + assertEquals(-1, read); + } + + private void checkWrite(final WritableByteChannel channel) throws IOException { + final ByteBuffer buffer = ByteBuffer.wrap(getFixtureByteArray()); + final int written = channel.write(buffer); + assertEquals(RO_LENGTH, written); + } + + @AfterEach + void cleanup() { + final T originRo = getOriginRo().get(); + if (originRo instanceof Closeable) { + IOUtils.closeQuietly((Closeable) originRo); + } + final T originRw = getOriginRw().get(); + if (originRw instanceof Closeable) { + IOUtils.closeQuietly((Closeable) originRw); + } + } + + byte[] getFixtureByteArray() throws IOException { + return IOUtils.resourceToByteArray(FILE_RES_RO); + } + + String getFixtureString() throws IOException { + return IOUtils.resourceToString(FILE_RES_RO, StandardCharsets.UTF_8); + } + protected AbstractOrigin getOriginRo() { return Objects.requireNonNull(originRo, "originRo"); } @@ -84,6 +133,10 @@ private boolean isValid(final RandomAccessFile raf) throws IOException { protected abstract B newOriginRw() throws IOException; + protected void resetOriginRw() throws IOException { + // No-op + } + protected void setOriginRo(final AbstractOrigin origin) { this.originRo = origin; } @@ -94,7 +147,7 @@ protected void setOriginRw(final AbstractOrigin origin) { @Test void testGetByteArray() throws IOException { - assertArrayEquals(Files.readAllBytes(Paths.get(FILE_NAME_RO)), getOriginRo().getByteArray()); + assertArrayEquals(getFixtureByteArray(), getOriginRo().getByteArray()); } @Test @@ -114,7 +167,9 @@ void testGetByteArrayAt_1_1() throws IOException { @Test void testGetCharSequence() throws IOException { - assertNotNull(getOriginRo().getCharSequence(Charset.defaultCharset())); + final CharSequence charSequence = getOriginRo().getCharSequence(StandardCharsets.UTF_8); + assertNotNull(charSequence); + assertEquals(getFixtureString(), charSequence.toString()); } @Test @@ -135,6 +190,7 @@ private void testGetFile(final File file, final long expectedLen) throws IOExcep void testGetInputStream() throws IOException { try (InputStream inputStream = getOriginRo().getInputStream()) { assertNotNull(inputStream); + assertArrayEquals(getFixtureByteArray(), IOUtils.toByteArray(inputStream)); } } @@ -219,14 +275,84 @@ void testGetRandomAccessFile(final OpenOption openOption) throws IOException { } } + @Test + void testGetReadableByteChannel() throws IOException { + try (ReadableByteChannel channel = getOriginRo().getChannel(ReadableByteChannel.class, StandardOpenOption.READ)) { + final SeekableByteChannel seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + assertNotNull(channel); + assertTrue(channel.isOpen()); + if (seekable != null) { + assertEquals(0, seekable.position()); + assertEquals(RO_LENGTH, seekable.size()); + } + checkRead(channel); + if (seekable != null) { + assertEquals(RO_LENGTH, seekable.position()); + } + } + } + @Test void testGetReader() throws IOException { try (Reader reader = getOriginRo().getReader(Charset.defaultCharset())) { assertNotNull(reader); } + setOriginRo(newOriginRo()); try (Reader reader = getOriginRo().getReader(null)) { assertNotNull(reader); } + setOriginRo(newOriginRo()); + try (Reader reader = getOriginRo().getReader(StandardCharsets.UTF_8)) { + assertNotNull(reader); + assertEquals(getFixtureString(), IOUtils.toString(reader)); + } + } + + @Test + void testGetWritableByteChannel() throws IOException { + final boolean supportsRead; + try (WritableByteChannel channel = getOriginRw().getChannel(WritableByteChannel.class, StandardOpenOption.WRITE)) { + supportsRead = channel instanceof ReadableByteChannel; + final SeekableByteChannel seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + assertNotNull(channel); + assertTrue(channel.isOpen()); + if (seekable != null) { + assertEquals(0, seekable.position()); + assertEquals(0, seekable.size()); + } + checkWrite(channel); + if (seekable != null) { + assertEquals(RO_LENGTH, seekable.position()); + assertEquals(RO_LENGTH, seekable.size()); + } + } + if (supportsRead) { + setOriginRw(newOriginRw()); + try (ReadableByteChannel channel = getOriginRw().getChannel(ReadableByteChannel.class, StandardOpenOption.READ)) { + assertNotNull(channel); + assertTrue(channel.isOpen()); + checkRead(channel); + } + } + setOriginRw(newOriginRw()); + try (WritableByteChannel channel = getOriginRw().getChannel(WritableByteChannel.class, StandardOpenOption.WRITE)) { + final SeekableByteChannel seekable = channel instanceof SeekableByteChannel ? (SeekableByteChannel) channel : null; + assertNotNull(channel); + assertTrue(channel.isOpen()); + if (seekable != null) { + seekable.position(RO_LENGTH); + assertEquals(RO_LENGTH, seekable.position()); + assertEquals(RO_LENGTH, seekable.size()); + // Truncate + final int newSize = RO_LENGTH / 2; + seekable.truncate(newSize); + assertEquals(newSize, seekable.position()); + assertEquals(newSize, seekable.size()); + // Rewind + seekable.position(0); + assertEquals(0, seekable.position()); + } + } } @Test @@ -242,6 +368,6 @@ void testGetWriter() throws IOException { @Test void testSize() throws IOException { - assertEquals(Files.size(Paths.get(FILE_NAME_RO)), getOriginRo().getByteArray().length); + assertEquals(RO_LENGTH, getOriginRo().getByteArray().length); } } diff --git a/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java b/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java index 369ce295ddc..28644f3c877 100644 --- a/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/AbstractRandomAccessFileOriginTest.java @@ -17,12 +17,17 @@ package org.apache.commons.io.build; +import java.io.IOException; import java.io.RandomAccessFile; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.IORandomAccessFile; import org.apache.commons.io.build.AbstractOrigin.AbstractRandomAccessFileOrigin; import org.apache.commons.io.build.AbstractOrigin.IORandomAccessFileOrigin; import org.apache.commons.io.build.AbstractOrigin.RandomAccessFileOrigin; +import org.apache.commons.lang3.ArrayUtils; /** * Tests {@link RandomAccessFileOrigin} and {@link IORandomAccessFileOrigin}. @@ -35,4 +40,10 @@ public abstract class AbstractRandomAccessFileOriginTest> extends AbstractOriginTest { + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } } diff --git a/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java b/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java index f94957f5351..36c994c185b 100644 --- a/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java +++ b/src/test/java/org/apache/commons/io/build/AbstractStreamBuilderTest.java @@ -17,13 +17,30 @@ package org.apache.commons.io.build; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.FileInputStream; +import java.io.RandomAccessFile; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; +import org.apache.commons.io.function.IOConsumer; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; /** * Tests {@link AbstractStreamBuilder}. @@ -41,6 +58,25 @@ public char[] get() { } + private static Stream> fileBasedConfigurers() throws URISyntaxException { + final URI uri = Objects.requireNonNull(AbstractStreamBuilderTest.class.getResource(AbstractOriginTest.FILE_RES_RO)).toURI(); + final Path path = Paths.get(AbstractOriginTest.FILE_NAME_RO); + // @formatter:off + return Stream.of( + b -> b.setByteArray(ArrayUtils.EMPTY_BYTE_ARRAY), + b -> b.setFile(AbstractOriginTest.FILE_NAME_RO), + b -> b.setFile(path.toFile()), + b -> b.setPath(AbstractOriginTest.FILE_NAME_RO), + b -> b.setPath(path), + b -> b.setRandomAccessFile(new RandomAccessFile(AbstractOriginTest.FILE_NAME_RO, "r")), + // We can convert FileInputStream to ReadableByteChannel, but not the reverse. + // Therefore, we don't use Files.newInputStream. + b -> b.setInputStream(new FileInputStream(AbstractOriginTest.FILE_NAME_RO)), + b -> b.setChannel(Files.newByteChannel(path)), + b -> b.setURI(uri)); + // @formatter:on + } + private void assertResult(final char[] arr, final int size) { assertNotNull(arr); assertEquals(size, arr.length); @@ -53,6 +89,21 @@ protected Builder builder() { return new Builder(); } + /** + * Tests various ways to obtain a {@link SeekableByteChannel}. + * + * @param configurer Lambda to configure the builder. + */ + @ParameterizedTest + @MethodSource("fileBasedConfigurers") + void getGetSeekableByteChannel(final IOConsumer configurer) throws Exception { + final Builder builder = builder(); + configurer.accept(builder); + try (ReadableByteChannel channel = assertDoesNotThrow(() -> builder.getChannel(SeekableByteChannel.class))) { + assertTrue(channel.isOpen()); + } + } + @Test void testBufferSizeChecker() { // sanity @@ -65,4 +116,17 @@ void testBufferSizeChecker() { // resize assertResult(builder().setBufferSizeMax(2).setBufferSizeChecker(i -> 100).setBufferSize(3).get(), 100); } + + /** + * Tests various ways to obtain a {@link java.io.InputStream}. + * + * @param configurer Lambda to configure the builder. + */ + @ParameterizedTest + @MethodSource("fileBasedConfigurers") + void testGetInputStream(final IOConsumer configurer) throws Exception { + final Builder builder = builder(); + configurer.accept(builder); + assertNotNull(builder.getInputStream()); + } } diff --git a/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java b/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java index 41a6931482c..b4fd3bf0b24 100644 --- a/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/ByteArrayOriginTest.java @@ -82,11 +82,17 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a byte[] to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } + @Override @Test void testGetWriter() { // Cannot convert a byte[] to a Writer. assertThrows(UnsupportedOperationException.class, super::testGetWriter); } - } diff --git a/src/test/java/org/apache/commons/io/build/ChannelOriginTest.java b/src/test/java/org/apache/commons/io/build/ChannelOriginTest.java new file mode 100644 index 00000000000..a4199286872 --- /dev/null +++ b/src/test/java/org/apache/commons/io/build/ChannelOriginTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.io.build; + +import static java.nio.file.StandardOpenOption.READ; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; + +import org.apache.commons.io.build.AbstractOrigin.ChannelOrigin; +import org.apache.commons.lang3.ArrayUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class ChannelOriginTest extends AbstractOriginTest { + @Override + protected ChannelOrigin newOriginRo() throws IOException { + return new ChannelOrigin(Files.newByteChannel(Paths.get(FILE_NAME_RO), Collections.singleton(READ))); + } + + @Override + protected ChannelOrigin newOriginRw() throws IOException { + return new ChannelOrigin(Files.newByteChannel( + tempPath.resolve(FILE_NAME_RW), + new HashSet<>(Arrays.asList(StandardOpenOption.READ, StandardOpenOption.WRITE)))); + } + + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } + + @Override + @Test + void testGetFile() { + // A FileByteChannel cannot be converted into a File. + assertThrows(UnsupportedOperationException.class, super::testGetFile); + } + + @Override + @Test + void testGetPath() { + // A FileByteChannel cannot be converted into a Path. + assertThrows(UnsupportedOperationException.class, super::testGetPath); + } + + @Override + @Test + void testGetRandomAccessFile() { + // A FileByteChannel cannot be converted into a RandomAccessFile. + assertThrows(UnsupportedOperationException.class, super::testGetRandomAccessFile); + } + + @Override + @ParameterizedTest + @EnumSource(StandardOpenOption.class) + void testGetRandomAccessFile(final OpenOption openOption) { + // A FileByteChannel cannot be converted into a RandomAccessFile. + assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); + } + + @Test + void testUnsupportedOperations_ReadableByteChannel() { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + final ChannelOrigin origin = new ChannelOrigin(channel); + assertThrows(UnsupportedOperationException.class, origin::getOutputStream); + assertThrows(UnsupportedOperationException.class, () -> origin.getWriter(null)); + assertThrows(UnsupportedOperationException.class, () -> origin.getChannel(WritableByteChannel.class)); + } + + @Test + void testUnsupportedOperations_WritableByteChannel() { + final Channel channel = mock(WritableByteChannel.class); + final ChannelOrigin origin = new ChannelOrigin(channel); + assertThrows(UnsupportedOperationException.class, origin::getInputStream); + assertThrows(UnsupportedOperationException.class, () -> origin.getReader(null)); + assertThrows(UnsupportedOperationException.class, () -> origin.getChannel(ReadableByteChannel.class)); + } +} diff --git a/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java b/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java index df29f453af8..0fc5b9c5cc6 100644 --- a/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/CharSequenceOriginTest.java @@ -109,6 +109,13 @@ void testGetReaderIgnoreCharsetNull() throws IOException { } } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a CharSequence to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } + @Override @Test void testGetWriter() { diff --git a/src/test/java/org/apache/commons/io/build/FileOriginTest.java b/src/test/java/org/apache/commons/io/build/FileOriginTest.java index 4f0cb2514c7..548d788851e 100644 --- a/src/test/java/org/apache/commons/io/build/FileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/FileOriginTest.java @@ -17,8 +17,13 @@ package org.apache.commons.io.build; import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.build.AbstractOrigin.FileOrigin; +import org.apache.commons.lang3.ArrayUtils; /** * Tests {@link FileOrigin}. @@ -35,8 +40,14 @@ protected FileOrigin newOriginRo() { } @Override - protected FileOrigin newOriginRw() { - return new FileOrigin(new File(FILE_NAME_RW)); + protected FileOrigin newOriginRw() throws IOException { + return new FileOrigin(tempPath.resolve(FILE_NAME_RW).toFile()); } + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } } diff --git a/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java b/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java index 8d83bba1c63..b51118c657e 100644 --- a/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/IORandomAccessFileOriginTest.java @@ -17,6 +17,7 @@ package org.apache.commons.io.build; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.RandomAccessFile; import org.apache.commons.io.IORandomAccessFile; @@ -38,8 +39,7 @@ protected IORandomAccessFileOrigin newOriginRo() throws FileNotFoundException { @SuppressWarnings("resource") @Override - protected IORandomAccessFileOrigin newOriginRw() throws FileNotFoundException { - return new IORandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.io(FILE_NAME_RW)); + protected IORandomAccessFileOrigin newOriginRw() throws IOException { + return new IORandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.io(tempPath.resolve(FILE_NAME_RW).toFile().getPath())); } - } diff --git a/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java b/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java index ac83acbecfb..7329fdb5783 100644 --- a/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/InputStreamOriginTest.java @@ -20,6 +20,7 @@ import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; @@ -87,11 +88,17 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a InputStream to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } + @Override @Test void testGetWriter() { // Cannot convert a InputStream to a Writer. assertThrows(UnsupportedOperationException.class, super::testGetWriter); } - } diff --git a/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java b/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java index 5e66e244d3a..463ad83fb61 100644 --- a/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/OutputStreamOriginTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; import java.io.OutputStream; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; @@ -118,6 +119,13 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetReadableByteChannel() throws IOException { + // Cannot convert a OutputStream to a ReadableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetReadableByteChannel); + } + @Override @Test void testGetReader() { diff --git a/src/test/java/org/apache/commons/io/build/PathOriginTest.java b/src/test/java/org/apache/commons/io/build/PathOriginTest.java index 81c4fc8d900..bc29df52b54 100644 --- a/src/test/java/org/apache/commons/io/build/PathOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/PathOriginTest.java @@ -16,10 +16,14 @@ */ package org.apache.commons.io.build; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.build.AbstractOrigin.PathOrigin; +import org.apache.commons.lang3.ArrayUtils; /** * Tests {@link PathOrigin}. @@ -36,8 +40,14 @@ protected PathOrigin newOriginRo() { } @Override - protected PathOrigin newOriginRw() { - return new PathOrigin(Paths.get(FILE_NAME_RW)); + protected PathOrigin newOriginRw() throws IOException { + return new PathOrigin(tempPath.resolve(FILE_NAME_RW)); } + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); + } } diff --git a/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java b/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java index 7cebf778db0..d7306c12cbc 100644 --- a/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/RandomAccessFileOriginTest.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.RandomAccessFile; import org.apache.commons.io.RandomAccessFileMode; @@ -41,8 +42,8 @@ protected RandomAccessFileOrigin newOriginRo() throws FileNotFoundException { @SuppressWarnings("resource") @Override - protected RandomAccessFileOrigin newOriginRw() throws FileNotFoundException { - return new RandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.create(FILE_NAME_RW)); + protected RandomAccessFileOrigin newOriginRw() throws IOException { + return new RandomAccessFileOrigin(RandomAccessFileMode.READ_WRITE.create(tempPath.resolve(FILE_NAME_RW))); } @Override diff --git a/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java b/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java index 6897d241868..3c1e3032fb4 100644 --- a/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/ReaderOriginTest.java @@ -20,6 +20,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.io.Reader; import java.nio.file.OpenOption; import java.nio.file.StandardOpenOption; @@ -85,11 +86,17 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + @Test + void testGetWritableByteChannel() throws IOException { + // Cannot convert a InputStream to a WritableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetWritableByteChannel); + } + @Override @Test void testGetWriter() { // Cannot convert a Reader to a Writer. assertThrows(UnsupportedOperationException.class, super::testGetWriter); } - } diff --git a/src/test/java/org/apache/commons/io/build/URIOriginTest.java b/src/test/java/org/apache/commons/io/build/URIOriginTest.java index 591a6c4ce04..6fd2043eb12 100644 --- a/src/test/java/org/apache/commons/io/build/URIOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/URIOriginTest.java @@ -18,11 +18,16 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; +import java.io.IOException; import java.io.InputStream; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; import org.apache.commons.io.build.AbstractOrigin.URIOrigin; +import org.apache.commons.lang3.ArrayUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -43,13 +48,20 @@ protected URIOrigin newOriginRo() { @Override protected URIOrigin newOriginRw() { - return new URIOrigin(Paths.get(FILE_NAME_RW).toUri()); + return new URIOrigin(tempPath.resolve(FILE_NAME_RW).toUri()); + } + + @Override + protected void resetOriginRw() throws IOException { + // Reset the file + final Path rwPath = tempPath.resolve(FILE_NAME_RW); + Files.write(rwPath, ArrayUtils.EMPTY_BYTE_ARRAY, StandardOpenOption.CREATE); } @ParameterizedTest @ValueSource(strings = { - "http://example.com", - "https://example.com" + "http://apache.com", + "https://apache.com" }) void testGetInputStream(final String uri) throws Exception { final AbstractOrigin.URIOrigin origin = new AbstractOrigin.URIOrigin(new URI(uri)); diff --git a/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java b/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java index f0aa525127b..9d1f2339bb0 100644 --- a/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java +++ b/src/test/java/org/apache/commons/io/build/WriterStreamOriginTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; +import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.nio.file.OpenOption; @@ -118,6 +119,12 @@ void testGetRandomAccessFile(final OpenOption openOption) { assertThrows(UnsupportedOperationException.class, () -> super.testGetRandomAccessFile(openOption)); } + @Override + void testGetReadableByteChannel() throws IOException { + // Cannot convert a Writer to a ReadableByteChannel. + assertThrows(UnsupportedOperationException.class, super::testGetReadableByteChannel); + } + @Override @Test void testGetReader() { diff --git a/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java new file mode 100644 index 00000000000..abf96d8f47d --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/AbstractSeekableByteChannelTest.java @@ -0,0 +1,392 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.channels; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Random; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Comprehensive test suite for SeekableByteChannel implementations. Tests can be run against any SeekableByteChannel implementation by overriding the + * createChannel() method in a subclass. + */ +abstract class AbstractSeekableByteChannelTest { + + private SeekableByteChannel channel; + + @TempDir + protected Path tempDir; + + protected Path tempFile; + + private final Random random = new Random(42); + + /** + * Creates the SeekableByteChannel to test. + * + * @return a new SeekableByteChannel. + * @throws IOException Thrown when the SeekableByteChannel cannot be created. + */ + protected abstract SeekableByteChannel createChannel() throws IOException; + + @BeforeEach + void setUp() throws IOException { + tempFile = tempDir.resolve(getClass().getSimpleName() + ".tmp"); + channel = createChannel(); + } + + @AfterEach + void tearDown() throws IOException { + if (channel != null && channel.isOpen()) { + channel.close(); + } + if (tempFile != null && Files.exists(tempFile)) { + Files.delete(tempFile); + } + } + + @Test + void testCloseMultipleTimes() throws IOException { + channel.close(); + channel.close(); // Should not throw + assertFalse(channel.isOpen()); + } + + @Test + void testConcurrentPositionAndSizeQueries() throws IOException { + final byte[] data = "test data".getBytes(); + channel.write(ByteBuffer.wrap(data)); + final long size = channel.size(); + final long position = channel.position(); + assertEquals(data.length, size); + assertEquals(data.length, position); + // These values should be consistent + assertEquals(channel.size(), size); + assertEquals(channel.position(), position); + } + + @Test + void testIsOpenAfterClose() throws IOException { + channel.close(); + assertFalse(channel.isOpen()); + } + + @Test + void testIsOpennOnNew() { + assertTrue(channel.isOpen()); + } + + @Test + void testPartialWritesAndReads() throws IOException { + final byte[] data = "0123456789".getBytes(); + // Write in chunks + channel.write(ByteBuffer.wrap(data, 0, 5)); + channel.write(ByteBuffer.wrap(data, 5, 5)); + assertEquals(10, channel.size()); + // Read back in different chunks + channel.position(0); + final ByteBuffer buffer1 = ByteBuffer.allocate(3); + final ByteBuffer buffer2 = ByteBuffer.allocate(7); + assertEquals(3, channel.read(buffer1)); + assertEquals(7, channel.read(buffer2)); + assertArrayEquals(Arrays.copyOf(data, 3), buffer1.array()); + assertArrayEquals(Arrays.copyOfRange(data, 3, 10), buffer2.array()); + } + + @Test + void testPositionBeyondSize() throws IOException { + channel.write(ByteBuffer.wrap("test".getBytes())); + channel.position(100); + assertEquals(100, channel.position()); + assertEquals(4, channel.size()); // Size should not change + } + + @ParameterizedTest + @CsvSource({ "0, 0", "5, 5", "10, 10", "100, 100" }) + void testPositionInBounds(final long newPosition, final long expectedPosition) throws IOException { + // Create file with enough data + final byte[] data = new byte[200]; + random.nextBytes(data); + channel.write(ByteBuffer.wrap(data)); + @SuppressWarnings("resource") // returns "this". + final SeekableByteChannel result = channel.position(newPosition); + assertSame(channel, result); // Javadoc: "This channel" + assertEquals(expectedPosition, channel.position()); + } + + @Test + void testPositionNegative() { + assertThrows(IllegalArgumentException.class, () -> channel.position(-1)); + } + + @Test + void testPositionOnClosed() throws IOException { + channel.close(); + assertThrows(ClosedChannelException.class, () -> channel.position()); + assertThrows(ClosedChannelException.class, () -> channel.position(0)); + } + + @Test + void testPositionOnNew() throws IOException { + assertEquals(0, channel.position()); + } + + @Test + void testRandomAccess() throws IOException { + final byte[] data = new byte[1000]; + random.nextBytes(data); + // Write initial data + channel.write(ByteBuffer.wrap(data)); + // Perform random access operations + final int[] positions = { 100, 500, 0, 999, 250 }; + for (final int pos : positions) { + channel.position(pos); + assertEquals(pos, channel.position()); + final ByteBuffer buffer = ByteBuffer.allocate(1); + final int read = channel.read(buffer); + if (pos < data.length) { + assertEquals(1, read); + assertEquals(data[pos], buffer.get(0)); + } + } + } + + @Test + void testReadAtEndOfFile() throws IOException { + channel.write(ByteBuffer.wrap("test".getBytes())); + // Position is already at end after write + final ByteBuffer buffer = ByteBuffer.allocate(10); + final int read = channel.read(buffer); + assertEquals(-1, read); + } + + @Test + void testReadBuffer() throws IOException { + final byte[] data = "test".getBytes(); + channel.write(ByteBuffer.wrap(data)); + channel.position(0); + final ByteBuffer buffer = ByteBuffer.allocate(100); + final int read = channel.read(buffer); + assertEquals(data.length, read); + assertEquals(data.length, channel.position()); + // Verify only the expected bytes were read + final byte[] readData = new byte[read]; + buffer.flip(); + buffer.get(readData); + assertArrayEquals(data, readData); + } + + @Test + void testReadBytes() throws IOException { + final byte[] data = "Hello, World!".getBytes(); + channel.write(ByteBuffer.wrap(data)); + channel.position(0); + final ByteBuffer buffer = ByteBuffer.allocate(data.length); + final int read = channel.read(buffer); + assertEquals(data.length, read); + assertArrayEquals(data, buffer.array()); + assertEquals(data.length, channel.position()); + } + + @Test + void testReadClosed() throws IOException { + channel.close(); + final ByteBuffer buffer = ByteBuffer.allocate(10); + assertThrows(ClosedChannelException.class, () -> channel.read(buffer)); + } + + @Test + void testReadEmpty() throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(10); + final int read = channel.read(buffer); + assertEquals(-1, read); + assertEquals(0, buffer.position()); + } + + @Test + void testReadNull() { + assertThrows(NullPointerException.class, () -> channel.read(null)); + } + + @Test + void testReadSingleByte() throws IOException { + // Write first + channel.write(ByteBuffer.wrap(new byte[] { 42 })); + channel.position(0); + final ByteBuffer buffer = ByteBuffer.allocate(1); + final int read = channel.read(buffer); + assertEquals(1, read); + assertEquals(42, buffer.get(0)); + assertEquals(1, channel.position()); + } + + @Test + void testSizeAfterTruncateToLargerSize() throws IOException { + channel.write(ByteBuffer.wrap("Hello".getBytes())); + assertEquals(5, channel.size()); + channel.truncate(10); + assertEquals(5, channel.size()); // Size should remain unchanged + } + + @Test + void testSizeAfterWrite() throws IOException { + assertEquals(0, channel.size()); + channel.write(ByteBuffer.wrap("Hello".getBytes())); + assertEquals(5, channel.size()); + channel.write(ByteBuffer.wrap(" World".getBytes())); + assertEquals(11, channel.size()); + } + + @Test + void testSizeOnClosed() throws IOException { + channel.close(); + assertThrows(ClosedChannelException.class, () -> channel.size()); + } + + @Test + void testSizeOnNew() throws IOException { + assertEquals(0, channel.size()); + } + + @Test + void testSizeSameOnOverwrite() throws IOException { + channel.write(ByteBuffer.wrap("Hello World".getBytes())); + assertEquals(11, channel.size()); + channel.position(6); + channel.write(ByteBuffer.wrap("Test".getBytes())); + assertEquals(11, channel.size()); // Size should not change + } + + @Test + void testTruncateNegative() { + assertThrows(IllegalArgumentException.class, () -> channel.truncate(-1)); + } + + @Test + void testTruncateShrinks() throws IOException { + channel.write(ByteBuffer.wrap("Hello World".getBytes())); + assertEquals(11, channel.size()); + channel.truncate(5); + assertEquals(5, channel.size()); + // Position should be adjusted if it was beyond new size + if (channel.position() > 5) { + assertEquals(5, channel.position()); + } + } + + @Test + void testWriteBeyondSizeGrows() throws IOException { + channel.position(100); + final byte[] data = "test".getBytes(); + channel.write(ByteBuffer.wrap(data)); + assertEquals(104, channel.size()); + assertEquals(104, channel.position()); + // Verify the gap contains zeros (implementation dependent) + channel.position(0); + final ByteBuffer buffer = ByteBuffer.allocate(100); + channel.read(buffer); + // Most implementations will fill gaps with zeros + final byte[] expectedGap = new byte[100]; + assertArrayEquals(expectedGap, buffer.array()); + } + + @ParameterizedTest + @ValueSource(ints = { 1, 10, 100, 1000, 10000 }) + void testWriteDifferentSizes(final int size) throws IOException { + final byte[] data = new byte[size]; + random.nextBytes(data); + final ByteBuffer buffer = ByteBuffer.wrap(data); + final int byteCount = channel.write(buffer); + assertEquals(size, byteCount); + assertEquals(size, channel.position()); + assertEquals(size, channel.size()); + } + @Test + void testWriteEmpty() throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(0); + final int byteCount = channel.write(buffer); + assertEquals(0, byteCount); + assertEquals(0, channel.position()); + assertEquals(0, channel.size()); + } + @Test + void testWriteNull() { + assertThrows(NullPointerException.class, () -> channel.write(null)); + } + @Test + void testWritePositionReadVerify() throws IOException { + final byte[] originalData = "Hello, SeekableByteChannel World!".getBytes(); + // Write data + channel.write(ByteBuffer.wrap(originalData)); + // Seek to beginning + channel.position(0); + // Read data back + final ByteBuffer readBuffer = ByteBuffer.allocate(originalData.length); + final int bytesRead = channel.read(readBuffer); + assertEquals(originalData.length, bytesRead); + assertArrayEquals(originalData, readBuffer.array()); + } + + @Test + void testWriteSingleByte() throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(1); + buffer.put((byte) 42); + buffer.flip(); + final int written = channel.write(buffer); + assertEquals(1, written); + assertEquals(1, channel.position()); + assertEquals(1, channel.size()); + } + + @Test + void testWriteToClosedChannel() throws IOException { + channel.close(); + final ByteBuffer buffer = ByteBuffer.wrap("test".getBytes()); + assertThrows(ClosedChannelException.class, () -> channel.write(buffer)); + } + + @Test + void tesWriteBytes() throws IOException { + final byte[] data = "Hello, World!".getBytes(); + final ByteBuffer buffer = ByteBuffer.wrap(data); + final int byteCount = channel.write(buffer); + assertEquals(data.length, byteCount); + assertEquals(data.length, channel.position()); + assertEquals(data.length, channel.size()); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java new file mode 100644 index 00000000000..66c8e60b91a --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelCompressTest.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.commons.io.channels; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** + * Tests {@link ByteArraySeekableByteChannel} in the same way Apache Commons Compress tests {@code SeekableInMemoryByteChannel}. + */ +class ByteArraySeekableByteChannelCompressTest { + + private static final byte[] testData = "Some data".getBytes(StandardCharsets.UTF_8); + + @AfterEach + void afterEach() { + // Reading tests don't modify the data + assertArrayEquals("Some data".getBytes(StandardCharsets.UTF_8), testData); + } + + /* + * If the stream is already closed then invoking this method has no effect. + */ + @Test + void testCloseIsIdempotent() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + c.close(); + assertFalse(c.isOpen()); + c.close(); + assertFalse(c.isOpen()); + } + } + + /* + * Setting the position to a value that is greater than the current size is legal but does not change the size of the entity. A later attempt to read + * bytes at such a position will immediately return an end-of-file indication + */ + @ParameterizedTest + @ValueSource(ints = { 0, 1, 2, 3, 4, 5, 6 }) + void testReadingFromAPositionAfterEndReturnsEOF(final int size) throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(new byte[size])) { + final int position = 2; + c.position(position); + assertEquals(position, c.position()); + final int readSize = 5; + final ByteBuffer readBuffer = ByteBuffer.allocate(readSize); + assertEquals(position >= size ? -1 : size - position, c.read(readBuffer)); + } + } + + @Test + void testShouldReadContentsProperly() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + final ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + final int readCount = c.read(readBuffer); + assertEquals(testData.length, readCount); + assertArrayEquals(testData, readBuffer.array()); + assertEquals(testData.length, c.position()); + } + } + + @Test + void testShouldReadContentsWhenBiggerBufferSupplied() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + final ByteBuffer readBuffer = ByteBuffer.allocate(testData.length + 1); + final int readCount = c.read(readBuffer); + assertEquals(testData.length, readCount); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + assertEquals(testData.length, c.position()); + } + } + + @Test + void testShouldReadDataFromSetPosition() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + final ByteBuffer readBuffer = ByteBuffer.allocate(4); + c.position(5L); + final int readCount = c.read(readBuffer); + assertEquals(4L, readCount); + assertEquals("data", new String(readBuffer.array(), StandardCharsets.UTF_8)); + assertEquals(testData.length, c.position()); + } + } + + @Test + void testShouldSetProperPosition() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + final long posAtFour = c.position(4L).position(); + final long posAtTheEnd = c.position(testData.length).position(); + final long posPastTheEnd = c.position(testData.length + 1L).position(); + assertEquals(4L, posAtFour); + assertEquals(c.size(), posAtTheEnd); + assertEquals(testData.length + 1L, posPastTheEnd); + } + } + + @Test + void testShouldSetProperPositionOnTruncate() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + c.position(testData.length); + c.truncate(4L); + assertEquals(4L, c.position()); + assertEquals(4L, c.size()); + } + } + + @Test + void testShouldSignalEOFWhenPositionAtTheEnd() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + final ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + c.position(testData.length + 1); + final int readCount = c.read(readBuffer); + assertEquals(0L, readBuffer.position()); + assertEquals(-1, readCount); + assertEquals(-1, c.read(readBuffer)); + } + } + + @Test + void testShouldThrowExceptionOnReadingClosedChannel() { + final ByteArraySeekableByteChannel c = new ByteArraySeekableByteChannel(); + c.close(); + assertThrows(ClosedChannelException.class, () -> c.read(ByteBuffer.allocate(1))); + } + + @Test + void testShouldThrowExceptionOnWritingToClosedChannel() { + final ByteArraySeekableByteChannel c = new ByteArraySeekableByteChannel(); + c.close(); + assertThrows(ClosedChannelException.class, () -> c.write(ByteBuffer.allocate(1))); + } + + @Test + void testShouldThrowExceptionWhenSettingIncorrectPosition() { + try (ByteArraySeekableByteChannel c = new ByteArraySeekableByteChannel()) { + assertThrows(IllegalArgumentException.class, () -> c.position(Integer.MAX_VALUE + 1L)); + } + } + + @Test + void testShouldThrowExceptionWhenTruncatingToIncorrectSize() { + try (ByteArraySeekableByteChannel c = new ByteArraySeekableByteChannel()) { + assertThrows(IllegalArgumentException.class, () -> c.truncate(Integer.MAX_VALUE + 1L)); + } + } + + @Test + void testShouldTruncateContentsProperly() throws ClosedChannelException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + c.truncate(4); + final byte[] bytes = Arrays.copyOf(c.array(), (int) c.size()); + assertEquals("Some", new String(bytes, StandardCharsets.UTF_8)); + } + } + // Contract Tests added in response to https://issues.apache.org/jira/browse/COMPRESS-499 + // https://docs.oracle.com/javase/8/docs/api/java/io/Closeable.html#close() + + @Test + void testShouldWriteDataProperly() throws IOException { + try (ByteArraySeekableByteChannel c = new ByteArraySeekableByteChannel()) { + final ByteBuffer inData = ByteBuffer.wrap(testData); + final int writeCount = c.write(inData); + assertEquals(testData.length, writeCount); + assertEquals(testData.length, c.position()); + assertArrayEquals(testData, Arrays.copyOf(c.array(), (int) c.position())); + } + } + // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#position() + + @Test + void testShouldWriteDataProperlyAfterPositionSet() throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData.clone())) { + final ByteBuffer inData = ByteBuffer.wrap(testData); + final ByteBuffer expectedData = ByteBuffer.allocate(testData.length + 5).put(testData, 0, 5).put(testData); + c.position(5L); + final int writeCount = c.write(inData); + assertEquals(testData.length, writeCount); + assertArrayEquals(expectedData.array(), Arrays.copyOf(c.array(), (int) c.size())); + assertEquals(testData.length + 5, c.position()); + } + } + // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#size() + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + void testThrowsClosedChannelExceptionWhenPositionIsSetOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + c.close(); + assertThrows(ClosedChannelException.class, () -> c.position(0)); + } + } + // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#position(long) + + /* + * IllegalArgumentException - If the new position is negative + */ + @Test + void testThrowsIllegalArgumentExceptionWhenTruncatingToANegativeSize() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + assertThrows(IllegalArgumentException.class, () -> c.truncate(-1)); + } + } + + /* + * IOException - If the new position is negative + */ + @Test + void testThrowsIOExceptionWhenPositionIsSetToANegativeValue() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + assertThrows(IllegalArgumentException.class, () -> c.position(-1)); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + void testTruncateDoesntChangeSmallPosition() throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + c.position(1); + c.truncate(testData.length - 1); + assertEquals(testData.length - 1, c.size()); + assertEquals(1, c.position()); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + void testTruncateMovesPositionWhenNewSizeIsBiggerThanSizeAndPositionIsEvenBigger() throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + c.position(2 * testData.length); + c.truncate(testData.length + 1); + assertEquals(testData.length, c.size()); + assertEquals(testData.length + 1, c.position()); + } + } + // https://docs.oracle.com/javase/8/docs/api/java/nio/channels/SeekableByteChannel.html#truncate(long) + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + void testTruncateMovesPositionWhenNotResizingButPositionBiggerThanSize() throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + c.position(2 * testData.length); + c.truncate(testData.length); + assertEquals(testData.length, c.size()); + assertEquals(testData.length, c.position()); + } + } + + /* + * In either case, if the current position is greater than the given size then it is set to that size. + */ + @Test + void testTruncateMovesPositionWhenShrinkingBeyondPosition() throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + c.position(4); + c.truncate(3); + assertEquals(3, c.size()); + assertEquals(3, c.position()); + } + } + + /* + * If the given size is greater than or equal to the current size then the entity is not modified. + */ + @Test + void testTruncateToBiggerSizeDoesntChangeAnything() throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + assertEquals(testData.length, c.size()); + c.truncate(testData.length + 1); + assertEquals(testData.length, c.size()); + final ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + assertEquals(testData.length, c.read(readBuffer)); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + } + } + + /* + * If the given size is greater than or equal to the current size then the entity is not modified. + */ + @Test + void testTruncateToCurrentSizeDoesntChangeAnything() throws Exception { + try (SeekableByteChannel c = ByteArraySeekableByteChannel.wrap(testData)) { + assertEquals(testData.length, c.size()); + c.truncate(testData.length); + assertEquals(testData.length, c.size()); + final ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + assertEquals(testData.length, c.read(readBuffer)); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + } + } + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + public void throwsClosedChannelExceptionWhenPositionIsReadOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + c.close(); + assertThrows(ClosedChannelException.class, c::position); + } + } + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + public void throwsClosedChannelExceptionWhenSizeIsReadOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + c.close(); + assertThrows(ClosedChannelException.class, c::size); + } + } + + /* + * ClosedChannelException - If this channel is closed + */ + @Test + public void throwsClosedChannelExceptionWhenTruncateIsCalledOnClosedChannel() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + c.close(); + assertThrows(ClosedChannelException.class, () -> c.truncate(0)); + } + } + + /* + * Setting the position to a value that is greater than the current size is legal but does not change the size of the entity. A later attempt to write + * bytes at such a position will cause the entity to grow to accommodate the new bytes; the values of any bytes between the previous end-of-file and the + * newly-written bytes are unspecified. + */ + public void writingToAPositionAfterEndGrowsChannel() throws Exception { + try (SeekableByteChannel c = new ByteArraySeekableByteChannel()) { + c.position(2); + assertEquals(2, c.position()); + final ByteBuffer inData = ByteBuffer.wrap(testData); + assertEquals(testData.length, c.write(inData)); + assertEquals(testData.length + 2, c.size()); + c.position(2); + final ByteBuffer readBuffer = ByteBuffer.allocate(testData.length); + c.read(readBuffer); + assertArrayEquals(testData, Arrays.copyOf(readBuffer.array(), testData.length)); + } + } +} diff --git a/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java new file mode 100644 index 00000000000..5b29b3f1be0 --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/ByteArraySeekableByteChannelTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.channels; + +import static org.apache.commons.lang3.ArrayUtils.EMPTY_BYTE_ARRAY; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.util.stream.Stream; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.function.IOSupplier; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * A sanity test to make sure {@link AbstractSeekableByteChannelTest} works for files. + */ +public class ByteArraySeekableByteChannelTest extends AbstractSeekableByteChannelTest { + + private static final byte[] testData = "Some data".getBytes(StandardCharsets.UTF_8); + + static Stream testConstructor() { + return Stream.of( + Arguments.of((IOSupplier) ByteArraySeekableByteChannel::new, EMPTY_BYTE_ARRAY, IOUtils.DEFAULT_BUFFER_SIZE), + Arguments.of((IOSupplier) () -> new ByteArraySeekableByteChannel(8), EMPTY_BYTE_ARRAY, 8), + Arguments.of((IOSupplier) () -> new ByteArraySeekableByteChannel(16), EMPTY_BYTE_ARRAY, 16), + Arguments.of((IOSupplier) () -> ByteArraySeekableByteChannel.wrap(EMPTY_BYTE_ARRAY), EMPTY_BYTE_ARRAY, 0), + Arguments.of((IOSupplier) () -> ByteArraySeekableByteChannel.wrap(testData), testData, testData.length)); + } + + static Stream testShouldResizeWhenWritingMoreDataThanCapacity() { + return Stream.of( + // Resize from 0 + Arguments.of(EMPTY_BYTE_ARRAY, 1), + // Resize less than double + Arguments.of(new byte[8], 1), + // Resize more that double + Arguments.of(new byte[8], 20)); + } + + @Override + protected SeekableByteChannel createChannel() throws IOException { + return new ByteArraySeekableByteChannel(); + } + + @ParameterizedTest + @MethodSource + void testConstructor(final IOSupplier supplier, final byte[] expected, final int capacity) throws IOException { + try (ByteArraySeekableByteChannel channel = supplier.get()) { + assertEquals(0, channel.position()); + assertEquals(expected.length, channel.size()); + assertEquals(capacity, channel.array().length); + assertArrayEquals(expected, channel.toByteArray()); + } + } + + @Test + void testConstructorInvalid() { + assertThrows(IllegalArgumentException.class, () -> new ByteArraySeekableByteChannel(-1)); + assertThrows(NullPointerException.class, () -> ByteArraySeekableByteChannel.wrap(null)); + } + + @ParameterizedTest + @MethodSource + void testShouldResizeWhenWritingMoreDataThanCapacity(final byte[] data, final int wanted) throws IOException { + try (ByteArraySeekableByteChannel c = ByteArraySeekableByteChannel.wrap(data)) { + c.position(data.length); + final ByteBuffer inData = ByteBuffer.wrap(new byte[wanted]); + final int writeCount = c.write(inData); + assertEquals(wanted, writeCount); + assertTrue(c.array().length >= data.length + wanted, "Capacity not increased sufficiently"); + } + } + +} diff --git a/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java b/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java new file mode 100644 index 00000000000..4676e64c060 --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/CloseShieldChannelTest.java @@ -0,0 +1,307 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.channels; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.channels.AsynchronousChannel; +import java.nio.channels.ByteChannel; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.InterruptibleChannel; +import java.nio.channels.NetworkChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ClassUtils; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests {@link CloseShieldChannel}. + */ +class CloseShieldChannelTest { + + /** + * JRE {@link Channel} interfaces. + */ + static Stream> channelInterfaces() { + // @formatter:off + return Stream.of( + AsynchronousChannel.class, + ByteChannel.class, + Channel.class, + GatheringByteChannel.class, + InterruptibleChannel.class, + NetworkChannel.class, + ReadableByteChannel.class, + ScatteringByteChannel.class, + SeekableByteChannel.class, + WritableByteChannel.class); + // @formatter:on + } + + /** + * Gets all interfaces implemented by the class {@link FileChannel}. + */ + static List> fileChannelInterfaces() { + return ClassUtils.getAllInterfaces(FileChannel.class); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testCloseDoesNotCloseDelegate(final Class channelClass) throws Exception { + final Channel channel = mock(channelClass); + final Channel shield = CloseShieldChannel.wrap(channel); + shield.close(); + verify(channel, never()).close(); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testCloseIsIdempotent(final Class channelClass) throws Exception { + final Channel channel = mock(channelClass); + final Channel shield = CloseShieldChannel.wrap(channel); + shield.close(); + assertFalse(shield.isOpen()); + shield.close(); + assertFalse(shield.isOpen()); + verifyNoInteractions(channel); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testCloseIsShielded(final Class channelInterface) throws Exception { + final Channel channel = mock(channelInterface); + when(channel.isOpen()).thenReturn(true, false, true, false); + final Channel shield = CloseShieldChannel.wrap(channel); + // Reflects delegate state initially + assertTrue(shield.isOpen(), "isOpen reflects delegate state"); + assertFalse(shield.isOpen(), "isOpen reflects delegate state"); + verify(channel, times(2)).isOpen(); + shield.close(); + // Reflects shield state after close + assertFalse(shield.isOpen(), "isOpen reflects shield state"); + assertFalse(shield.isOpen(), "isOpen reflects shield state"); + verify(channel, times(2)).isOpen(); + } + + @Test + void testDoesNotDoubleWrap() { + final ByteChannel channel = mock(ByteChannel.class); + final ByteChannel shield1 = CloseShieldChannel.wrap(channel); + final ByteChannel shield2 = CloseShieldChannel.wrap(shield1); + assertSame(shield1, shield2); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testEquals(final Class channelClass) throws Exception { + final Channel channel = mock(channelClass); + final Channel shield = CloseShieldChannel.wrap(channel); + final Channel anotherShield = CloseShieldChannel.wrap(channel); + assertTrue(shield.equals(shield), "reflexive"); + assertFalse(shield.equals(null), "null is not equal"); + assertFalse(shield.equals(channel), "shield not equal to delegate"); + assertTrue(shield.equals(anotherShield), "shields of same delegate are equal"); + } + + @Test + void testGatheringByteChannelMethods() throws Exception { + final GatheringByteChannel channel = mock(GatheringByteChannel.class); + when(channel.isOpen()).thenReturn(true); + final GatheringByteChannel shield = CloseShieldChannel.wrap(channel); + // Before close write() should delegate + when(channel.write(null, 0, 0)).thenReturn(42L); + assertEquals(42, shield.write(null, 0, 0)); + verify(channel).write(null, 0, 0); + // After close write() should throw ClosedChannelException + shield.close(); + assertThrows(ClosedChannelException.class, () -> shield.write(null, 0, 0)); + verifyNoMoreInteractions(channel); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testHashCode(final Class channelClass) throws Exception { + final Channel channel = mock(channelClass); + final Channel shield = CloseShieldChannel.wrap(channel); + final Channel anotherShield = CloseShieldChannel.wrap(channel); + assertEquals(shield.hashCode(), channel.hashCode(), "delegates hashCode"); + assertEquals(shield.hashCode(), anotherShield.hashCode(), "shields of same delegate have same hashCode"); + } + + @Test + void testNetworkChannelMethods() throws Exception { + final NetworkChannel channel = mock(NetworkChannel.class); + when(channel.isOpen()).thenReturn(true); + final NetworkChannel shield = CloseShieldChannel.wrap(channel); + // Before close getOption(), setOption(), getLocalAddress() and bind() should delegate + when(channel.getOption(null)).thenReturn("foo"); + when(channel.setOption(null, null)).thenReturn(channel); + when(channel.getLocalAddress()).thenReturn(null); + when(channel.bind(null)).thenReturn(channel); + assertEquals("foo", shield.getOption(null)); + assertEquals(shield, shield.setOption(null, null)); + assertEquals(null, shield.getLocalAddress()); + assertEquals(shield, shield.bind(null)); + verify(channel).getOption(null); + verify(channel).setOption(null, null); + verify(channel).getLocalAddress(); + verify(channel).bind(null); + // After close supportedOptions() should still work + shield.close(); + assertDoesNotThrow(shield::supportedOptions); + verify(channel).supportedOptions(); + // But the remaining methods should throw ClosedChannelException + assertThrows(ClosedChannelException.class, () -> shield.setOption(null, null)); + assertThrows(ClosedChannelException.class, () -> shield.getOption(null)); + assertThrows(ClosedChannelException.class, shield::getLocalAddress); + assertThrows(ClosedChannelException.class, () -> shield.bind(null)); + verifyNoMoreInteractions(channel); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testPreservesInterfaces(final Class channelClass) { + final Channel channel = mock(channelClass); + final Channel shield = CloseShieldChannel.wrap(channel); + assertNotSame(channel, shield); + assertTrue(channelClass.isInstance(shield)); + } + + @Test + void testReadableByteChannelMethods() throws Exception { + final ReadableByteChannel channel = mock(ReadableByteChannel.class); + when(channel.isOpen()).thenReturn(true); + final ReadableByteChannel shield = CloseShieldChannel.wrap(channel); + // Before close read() should delegate + when(channel.read(null)).thenReturn(42); + assertEquals(42, shield.read(null)); + verify(channel).read(null); + // After close read() should throw ClosedChannelException + shield.close(); + assertThrows(ClosedChannelException.class, () -> shield.read(null)); + verifyNoMoreInteractions(channel); + } + + @Test + void testScatteringByteChannelMethods() throws Exception { + final ScatteringByteChannel channel = mock(ScatteringByteChannel.class); + when(channel.isOpen()).thenReturn(true); + final ScatteringByteChannel shield = CloseShieldChannel.wrap(channel); + // Before close read() should delegate + when(channel.read(null, 0, 0)).thenReturn(42L); + assertEquals(42, shield.read(null, 0, 0)); + verify(channel).read(null, 0, 0); + // After close read() should throw ClosedChannelException + shield.close(); + assertThrows(ClosedChannelException.class, () -> shield.read(null, 0, 0)); + verifyNoMoreInteractions(channel); + } + + @Test + void testSeekableByteChannelMethods() throws Exception { + final SeekableByteChannel channel = mock(SeekableByteChannel.class); + when(channel.isOpen()).thenReturn(true); + final SeekableByteChannel shield = CloseShieldChannel.wrap(channel); + // Before close position() and size() should delegate + when(channel.position()).thenReturn(42L); + when(channel.size()).thenReturn(84L); + assertEquals(42, shield.position()); + assertEquals(84, shield.size()); + verify(channel).position(); + verify(channel).size(); + // Before close position(long) and truncate(long) should delegate + when(channel.position(21)).thenReturn(channel); + when(channel.truncate(21)).thenReturn(channel); + assertEquals(shield, shield.position(21)); + assertEquals(shield, shield.truncate(21)); + verify(channel).position(21); + verify(channel).truncate(21); + // After close position() should throw ClosedChannelException + shield.close(); + assertThrows(ClosedChannelException.class, shield::position); + assertThrows(ClosedChannelException.class, () -> shield.position(0)); + assertThrows(ClosedChannelException.class, shield::size); + assertThrows(ClosedChannelException.class, () -> shield.truncate(0)); + verifyNoMoreInteractions(channel); + } + + @ParameterizedTest + @MethodSource("channelInterfaces") + void testToString(final Class channelClass) throws Exception { + final Channel channel = mock(channelClass); + when(channel.toString()).thenReturn("MyChannel"); + final Channel shield = CloseShieldChannel.wrap(channel); + final String shieldString = shield.toString(); + assertTrue(shieldString.contains("CloseShield")); + assertTrue(shieldString.contains("MyChannel")); + } + + @Test + void testWrapFileChannel(final @TempDir Path tempDir) throws IOException { + final Path testFile = tempDir.resolve("test.txt"); + FileUtils.touch(testFile.toFile()); + try (FileChannel channel = FileChannel.open(testFile); Channel shield = CloseShieldChannel.wrap(channel)) { + fileChannelInterfaces().forEach(iface -> assertInstanceOf(iface, shield)); + // FileChannel is not an interface, so can not be implemented. + assertFalse(shield instanceof FileChannel, "not FileChannel"); + } + } + + @Test + void testWritableByteChannelMethods() throws Exception { + final WritableByteChannel channel = mock(WritableByteChannel.class); + when(channel.isOpen()).thenReturn(true); + final WritableByteChannel shield = CloseShieldChannel.wrap(channel); + // Before close write() should delegate + when(channel.write(null)).thenReturn(42); + assertEquals(42, shield.write(null)); + verify(channel).write(null); + // After close write() should throw ClosedChannelException + shield.close(); + assertThrows(ClosedChannelException.class, () -> shield.write(null)); + verifyNoMoreInteractions(channel); + } +} diff --git a/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java b/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java index 1543b957830..bbfbee9c575 100644 --- a/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java +++ b/src/test/java/org/apache/commons/io/channels/FileChannelsTest.java @@ -59,7 +59,7 @@ enum FileChannelType { private static final String CONTENT = StringUtils.repeat("x", SMALL_BUFFER_SIZE); @SuppressWarnings("resource") // Caller closes - private static FileChannel getChannel(final FileInputStream inNotEmpty, final FileChannelType fileChannelType, final int readSize) throws IOException { + private static FileChannel getChannel(final FileInputStream inNotEmpty, final FileChannelType fileChannelType, final int readSize) { return wrap(inNotEmpty.getChannel(), fileChannelType, readSize); } @@ -84,7 +84,7 @@ private static byte reverse(final byte b) { return (byte) (~b & 0xff); } - private static FileChannel wrap(final FileChannel fc, final FileChannelType fileChannelType, final int readSize) throws IOException { + private static FileChannel wrap(final FileChannel fc, final FileChannelType fileChannelType, final int readSize) { switch (fileChannelType) { case NON_BLOCKING: return new NonBlockingFileChannelProxy(fc); diff --git a/src/test/java/org/apache/commons/io/channels/FileSeekableByteChannelTest.java b/src/test/java/org/apache/commons/io/channels/FileSeekableByteChannelTest.java new file mode 100644 index 00000000000..132cf42ed6f --- /dev/null +++ b/src/test/java/org/apache/commons/io/channels/FileSeekableByteChannelTest.java @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.io.channels; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; + +/** + * A sanity test to make sure {@link AbstractSeekableByteChannelTest} works for files. + */ +public class FileSeekableByteChannelTest extends AbstractSeekableByteChannelTest { + + @Override + protected SeekableByteChannel createChannel() throws IOException { + return Files.newByteChannel(tempFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE, + StandardOpenOption.TRUNCATE_EXISTING); + } + +} diff --git a/src/test/java/org/apache/commons/io/comparator/CompositeFileComparatorTest.java b/src/test/java/org/apache/commons/io/comparator/CompositeFileComparatorTest.java index 7fb102907ca..5cc700d7215 100644 --- a/src/test/java/org/apache/commons/io/comparator/CompositeFileComparatorTest.java +++ b/src/test/java/org/apache/commons/io/comparator/CompositeFileComparatorTest.java @@ -116,6 +116,7 @@ void testConstructorIterable_order() { assertTrue(c.compare(moreFile, lessFile) > 0, "more"); } + @Override @Test void testToString() { final List> list = new ArrayList<>(); diff --git a/src/test/java/org/apache/commons/io/file/CopyDirectoryVisitorTest.java b/src/test/java/org/apache/commons/io/file/CopyDirectoryVisitorTest.java index c17a366ce9a..322b23083fd 100644 --- a/src/test/java/org/apache/commons/io/file/CopyDirectoryVisitorTest.java +++ b/src/test/java/org/apache/commons/io/file/CopyDirectoryVisitorTest.java @@ -107,7 +107,7 @@ void testCopyDirectoryEmptyFolderFilters(final PathCounters pathCounters) throws void testCopyDirectoryFilters(final PathCounters pathCounters) throws IOException { final Path sourceDir = Paths.get("src/test/resources/org/apache/commons/io/dirs-2-file-size-4"); final CopyDirectoryVisitor visitFileTree = PathUtils.visitFileTree(new CopyDirectoryVisitor(pathCounters, new NameFileFilter("file-size-1.bin"), - new NameFileFilter("dirs-2-file-size-4", "dirs-a-file-size-1"), sourceDir, targetDir, null), + new NameFileFilter("dirs-2-file-size-4", "dirs-a-file-size-1"), sourceDir, targetDir, (CopyOption[]) null), sourceDir); assertCounts(2, 1, 2, visitFileTree); assertArrayEquals(PathUtils.EMPTY_COPY_OPTIONS, visitFileTree.getCopyOptions()); diff --git a/src/test/java/org/apache/commons/io/file/PathUtilsTest.java b/src/test/java/org/apache/commons/io/file/PathUtilsTest.java index 14c432d5162..a533f5280ad 100644 --- a/src/test/java/org/apache/commons/io/file/PathUtilsTest.java +++ b/src/test/java/org/apache/commons/io/file/PathUtilsTest.java @@ -351,6 +351,19 @@ void testGetLastModifiedFileTime_URL_Present() throws IOException, URISyntaxExce assertNotNull(PathUtils.getLastModifiedFileTime(current().toUri().toURL())); } + @Test + void testGetPath() { + final String validKey = "user.dir"; + final Path value = Paths.get(System.getProperty(validKey)); + assertEquals(value, PathUtils.getPath(validKey, null)); + assertEquals(value, PathUtils.getPath(validKey, validKey)); + final String invalidKey = "this property key does not exist"; + assertEquals(value, PathUtils.getPath(invalidKey, value.toString())); + assertNull(PathUtils.getPath(invalidKey, null)); + assertEquals(value, PathUtils.getPath(null, value.toString())); + assertEquals(value, PathUtils.getPath("", value.toString())); + } + @Test void testGetTempDirectory() { final Path tempDirectory = Paths.get(SystemProperties.getJavaIoTmpdir()); diff --git a/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java b/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java index a6db833a1bf..fae4d5f7b2e 100644 --- a/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java +++ b/src/test/java/org/apache/commons/io/filefilter/AbstractConditionalFileFilterTest.java @@ -60,7 +60,7 @@ public abstract class AbstractConditionalFileFilterTest extends AbstractIOFileFi @BeforeEach public void setUp() { - this.workingPath = determineWorkingDirectoryPath(getWorkingPathNamePropertyKey(), getDefaultWorkingPath()); + this.workingPath = getWorkingDirectoryPath(getWorkingPathNamePropertyKey(), getDefaultWorkingPath()); this.file = new File(this.workingPath, TEST_FILE_NAME_PREFIX + 1 + TEST_FILE_TYPE); this.trueFilters = new TesterTrueFileFilter[4]; this.falseFilters = new TesterFalseFileFilter[4]; diff --git a/src/test/java/org/apache/commons/io/filefilter/AbstractIOFileFilterTest.java b/src/test/java/org/apache/commons/io/filefilter/AbstractIOFileFilterTest.java index 3521dd4f006..905c837a841 100644 --- a/src/test/java/org/apache/commons/io/filefilter/AbstractIOFileFilterTest.java +++ b/src/test/java/org/apache/commons/io/filefilter/AbstractIOFileFilterTest.java @@ -22,6 +22,8 @@ import java.util.Objects; import java.util.stream.Stream; +import org.apache.commons.io.file.PathUtils; + public abstract class AbstractIOFileFilterTest { final class TesterFalseFileFilter extends FalseFileFilter { @@ -125,10 +127,9 @@ public static void assertTrueFiltersInvoked(final int testNumber, final TesterTr } } - public static File determineWorkingDirectoryPath(final String key, final String defaultPath) { + public static File getWorkingDirectoryPath(final String key, final String defaultPath) { // Look for a system property to specify the working directory - final String workingPathName = System.getProperty(key, defaultPath); - return new File(workingPathName); + return PathUtils.getPath(key, defaultPath).toFile(); } public static void resetFalseFilters(final TesterFalseFileFilter[] filters) { diff --git a/src/test/java/org/apache/commons/io/filefilter/FileFilterTest.java b/src/test/java/org/apache/commons/io/filefilter/FileFilterTest.java index dfa13d68572..6e11b5e770f 100644 --- a/src/test/java/org/apache/commons/io/filefilter/FileFilterTest.java +++ b/src/test/java/org/apache/commons/io/filefilter/FileFilterTest.java @@ -745,7 +745,7 @@ void testHidden() throws IOException { @Test void testMagicNumberFileFilterBytes() throws Exception { final byte[] classFileMagicNumber = {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}; - final String xmlFileContent = "\n" + "text"; + final String xmlFileContent = "\ntext"; final File classAFile = new File(temporaryFolder, "A.class"); final Path classAPath = classAFile.toPath(); @@ -826,7 +826,7 @@ void testMagicNumberFileFilterBytesOffset() throws Exception { @Test void testMagicNumberFileFilterString() throws Exception { final byte[] classFileMagicNumber = {(byte) 0xCA, (byte) 0xFE, (byte) 0xBA, (byte) 0xBE}; - final String xmlFileContent = "\n" + "text"; + final String xmlFileContent = "\ntext"; final String xmlMagicNumber = " ref.incrementAndGet()); + assertEquals(2, ref.get()); + } + @Test void testForEach() throws IOException { final AtomicInteger ref = new AtomicInteger(); @@ -77,14 +84,14 @@ void testIterator() throws IOException { } @Test - void testSpliterator() throws IOException { + void testSpliterator() { final AtomicInteger ref = new AtomicInteger(); iterable.spliterator().forEachRemaining(e -> ref.incrementAndGet()); assertEquals(2, ref.get()); } @Test - void testUnrwap() throws IOException { + void testUnrwap() { assertSame(fixture.list, iterable.unwrap()); assertSame(fixture.unwrap(), iterable.unwrap()); } diff --git a/src/test/java/org/apache/commons/io/input/AbstractInputStreamTest.java b/src/test/java/org/apache/commons/io/input/AbstractInputStreamTest.java index 9f15d2b3075..7d653790d33 100644 --- a/src/test/java/org/apache/commons/io/input/AbstractInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/AbstractInputStreamTest.java @@ -53,6 +53,7 @@ public abstract class AbstractInputStreamTest { protected static byte[] ExpectedBytes; protected static Path InputPath; + @TempDir static Path tempDir; diff --git a/src/test/java/org/apache/commons/io/input/BoundedInputStreamTest.java b/src/test/java/org/apache/commons/io/input/BoundedInputStreamTest.java index 9c8f679375b..c03f061acfc 100644 --- a/src/test/java/org/apache/commons/io/input/BoundedInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/BoundedInputStreamTest.java @@ -23,18 +23,27 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrowsExactly; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import org.apache.commons.io.IOUtils; import org.apache.commons.io.test.CustomIOException; import org.apache.commons.lang3.mutable.MutableInt; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; /** @@ -42,6 +51,72 @@ */ class BoundedInputStreamTest { + static Stream testAvailableAfterClose() throws IOException { + // Case 1: behaves like ByteArrayInputStream — close() is a no-op, available() still returns a value (e.g., 42). + final InputStream noOpClose = mock(InputStream.class); + when(noOpClose.available()).thenReturn(42, 42); + + // Case 2: returns 0 after close (Commons memory-backed streams that ignore close but report 0 when exhausted). + final InputStream returnsZeroAfterClose = mock(InputStream.class); + when(returnsZeroAfterClose.available()).thenReturn(42, 0); + + // Case 3: throws IOException after close (e.g., FileInputStream-like behavior). + final InputStream throwsAfterClose = mock(InputStream.class); + when(throwsAfterClose.available()).thenReturn(42).thenThrow(new IOException("Stream closed")); + + return Stream.of( + Arguments.of("underlying stream still returns 42 after close", noOpClose, 42), + Arguments.of("underlying stream returns 0 after close", returnsZeroAfterClose, 42), + Arguments.of("underlying stream throws IOException after close", throwsAfterClose, 42)); + } + + static Stream testAvailableUpperLimit() { + final byte[] helloWorld = "Hello World".getBytes(StandardCharsets.UTF_8); + return Stream.of( + // Limited by maxCount + Arguments.of(new ByteArrayInputStream(helloWorld), helloWorld.length - 1, helloWorld.length - 1, 0), + // Limited by data length + Arguments.of(new ByteArrayInputStream(helloWorld), helloWorld.length + 1, helloWorld.length, 0), + // Limited by Integer.MAX_VALUE + Arguments.of( + new NullInputStream(Long.MAX_VALUE), Long.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + static Stream testReadAfterClose() throws IOException { + // Case 1: no-op close (ByteArrayInputStream-like): read() still returns a value after close + final InputStream noOpClose = mock(InputStream.class); + when(noOpClose.read()).thenReturn(42); + + // Case 2: returns EOF (-1) after close + final InputStream returnsEofAfterClose = mock(InputStream.class); + when(returnsEofAfterClose.read()).thenReturn(IOUtils.EOF); + + // Case 3: throws IOException after close (FileInputStream-like) + final InputStream throwsAfterClose = mock(InputStream.class); + final IOException closed = new IOException("Stream closed"); + when(throwsAfterClose.read()).thenThrow(closed); + + return Stream.of( + Arguments.of("underlying stream still reads data after close", noOpClose, 42), + Arguments.of("underlying stream returns EOF after close", returnsEofAfterClose, IOUtils.EOF), + Arguments.of("underlying stream throws IOException after close", throwsAfterClose, closed)); + } + + static Stream testRemaining() { + return Stream.of( + // Unbounded: any negative maxCount is treated as "no limit". + Arguments.of("unbounded (EOF constant)", IOUtils.EOF, Long.MAX_VALUE), + Arguments.of("unbounded (arbitrary negative)", Long.MIN_VALUE, Long.MAX_VALUE), + + // Bounded: remaining equals the configured limit, regardless of underlying data size. + Arguments.of("bounded (zero)", 0L, 0L), + Arguments.of("bounded (small)", 1024L, 1024L), + Arguments.of("bounded (Integer.MAX_VALUE)", Integer.MAX_VALUE, (long) Integer.MAX_VALUE), + + // Bounded but extremely large: still not 'unbounded'. + Arguments.of("bounded (Long.MAX_VALUE)", Long.MAX_VALUE, Long.MAX_VALUE)); + } + private void compare(final String message, final byte[] expected, final byte[] actual) { assertEquals(expected.length, actual.length, () -> message + " (array length equals check)"); final MutableInt mi = new MutableInt(); @@ -80,21 +155,43 @@ void testAfterReadConsumer() throws Exception { // @formatter:on } - @SuppressWarnings("resource") - @Test - void testAvailableAfterClose() throws Exception { + @ParameterizedTest(name = "{index} — {0}") + @MethodSource + void testAvailableAfterClose(final String caseName, final InputStream delegate, final int expectedBeforeClose) + throws Exception { final InputStream shadow; - try (InputStream in = BoundedInputStream.builder().setCharSequence("Hi").get()) { - assertTrue(in.available() > 0); - shadow = in; - } - assertEquals(0, shadow.available()); + try (InputStream in = BoundedInputStream.builder() + .setInputStream(delegate) + .setPropagateClose(true) + .get()) { + // Before close: pass-through behavior + assertEquals(expectedBeforeClose, in.available(), caseName + " (before close)"); + shadow = in; // keep reference to call after close + } + // Verify the underlying stream was closed + verify(delegate, times(1)).close(); + // After close: behavior depends on the underlying stream + assertEquals(0, shadow.available(), caseName + " (after close)"); + // Interactions: available called only once before close. + verify(delegate, times(1)).available(); + verifyNoMoreInteractions(delegate); } - @Test - void testAvailableAfterOpen() throws Exception { - try (InputStream in = BoundedInputStream.builder().setCharSequence("Hi").get()) { - assertTrue(in.available() > 0); + @ParameterizedTest + @MethodSource + void testAvailableUpperLimit(final InputStream input, final long maxCount, final int expectedBeforeSkip, final int expectedAfterSkip) + throws Exception { + try (BoundedInputStream bounded = BoundedInputStream.builder() + .setInputStream(input) + .setMaxCount(maxCount) + .get()) { + assertEquals( + expectedBeforeSkip, bounded.available(), "available should be limited by maxCount and data length"); + IOUtils.skip(bounded, expectedBeforeSkip); + assertEquals( + expectedAfterSkip, + bounded.available(), + "after skipping available should be limited by maxCount and data length"); } } @@ -444,15 +541,38 @@ void testPublicConstructors() throws IOException { } } - @SuppressWarnings("resource") - @Test - void testReadAfterClose() throws Exception { - final InputStream shadow; - try (InputStream in = BoundedInputStream.builder().setCharSequence("Hi").get()) { - assertTrue(in.available() > 0); - shadow = in; + @ParameterizedTest(name = "{index} — {0}") + @MethodSource("testReadAfterClose") + void testReadAfterClose( + final String caseName, + final InputStream delegate, + final Object expectedAfterClose // Integer (value) or IOException (expected thrown) + ) throws Exception { + + final InputStream bounded; + try (InputStream in = BoundedInputStream.builder() + .setInputStream(delegate) + .setPropagateClose(true) + .get()) { + bounded = in; // call read() only after close + } + + // Underlying stream should be closed exactly once + verify(delegate, times(1)).close(); + + if (expectedAfterClose instanceof Integer) { + assertEquals(expectedAfterClose, bounded.read(), caseName + " (after close)"); + } else if (expectedAfterClose instanceof IOException) { + final IOException actual = assertThrows(IOException.class, bounded::read, caseName + " (after close)"); + // verify it's the exact instance we configured + assertSame(expectedAfterClose, actual, caseName + " (exception instance)"); + } else { + fail("Unexpected expectedAfterClose type: " + expectedAfterClose); } - assertEquals(IOUtils.EOF, shadow.read()); + + // We only performed one read() (after close) + verify(delegate, times(1)).read(); + verifyNoMoreInteractions(delegate); } @Test @@ -532,6 +652,31 @@ void testReadSingle() throws Exception { } } + @ParameterizedTest(name = "{index}: {0} -> initial remaining {2}") + @MethodSource + void testRemaining(final String caseName, final long maxCount, final long expectedInitialRemaining) + throws Exception { + final byte[] data = "Hello World".getBytes(StandardCharsets.UTF_8); // 11 bytes + + try (BoundedInputStream in = BoundedInputStream.builder() + .setByteArray(data) + .setMaxCount(maxCount) + .get()) { + // Initial remaining respects the imposed limit (or is Long.MAX_VALUE if unbounded). + assertEquals(expectedInitialRemaining, in.getRemaining(), caseName + " (initial)"); + + // Skip more than the data length to exercise both bounded and unbounded paths. + final long skipped = IOUtils.skip(in, 42); + + // For unbounded streams (EOF == -1), remaining stays the same. + // For bounded, it decreases by 'skipped'. + final long expectedAfterSkip = + in.getMaxCount() == IOUtils.EOF ? expectedInitialRemaining : expectedInitialRemaining - skipped; + + assertEquals(expectedAfterSkip, in.getRemaining(), caseName + " (after skip)"); + } + } + @Test void testReset() throws Exception { final byte[] helloWorld = "Hello World".getBytes(StandardCharsets.UTF_8); diff --git a/src/test/java/org/apache/commons/io/input/BrokenReaderTest.java b/src/test/java/org/apache/commons/io/input/BrokenReaderTest.java index aca91077e6b..a47687abf3d 100644 --- a/src/test/java/org/apache/commons/io/input/BrokenReaderTest.java +++ b/src/test/java/org/apache/commons/io/input/BrokenReaderTest.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.Reader; +import java.nio.CharBuffer; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -45,7 +46,7 @@ void testClose(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenReader brokenReader = createBrokenReader(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenReader.close())); + assertEquals(exception, assertThrows(clazz, brokenReader::close)); } @Test @@ -68,7 +69,7 @@ void testRead(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenReader brokenReader = createBrokenReader(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenReader.read())); + assertEquals(exception, assertThrows(clazz, brokenReader::read)); } @ParameterizedTest @@ -86,7 +87,24 @@ void testReadCharArrayIndexed(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenReader brokenReader = createBrokenReader(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(new char[1], 0, 1))); + final char[] cbuf = new char[1]; + assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(cbuf, 0, 1))); + // Also throws the exception before checking arguments + assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(cbuf, -1, 1))); + assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(cbuf, 0, -1))); + assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(cbuf, 1, 1))); + assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(null, 0, 0))); + + } + + @ParameterizedTest + @MethodSource("org.apache.commons.io.BrokenTestFactories#parameters") + void testReadCharBuffer(final Class clazz) throws Exception { + final Throwable exception = clazz.newInstance(); + @SuppressWarnings("resource") + final BrokenReader brokenReader = createBrokenReader(exception); + final CharBuffer charBuffer = CharBuffer.allocate(1); + assertEquals(exception, assertThrows(clazz, () -> brokenReader.read(charBuffer))); } @ParameterizedTest @@ -95,7 +113,7 @@ void testReady(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenReader brokenReader = createBrokenReader(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenReader.ready())); + assertEquals(exception, assertThrows(clazz, brokenReader::ready)); } @ParameterizedTest @@ -104,7 +122,7 @@ void testReset(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenReader brokenReader = createBrokenReader(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenReader.reset())); + assertEquals(exception, assertThrows(clazz, brokenReader::reset)); } @ParameterizedTest diff --git a/src/test/java/org/apache/commons/io/input/ClosedInputStreamTest.java b/src/test/java/org/apache/commons/io/input/ClosedInputStreamTest.java index b19364d8a17..2586806b3a5 100644 --- a/src/test/java/org/apache/commons/io/input/ClosedInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/ClosedInputStreamTest.java @@ -22,6 +22,7 @@ import java.io.InputStream; +import org.apache.commons.io.IOUtils; import org.junit.jupiter.api.Test; /** @@ -86,7 +87,7 @@ void testReadArray() throws Exception { try (ClosedInputStream cis = new ClosedInputStream()) { assertEquals(EOF, cis.read(new byte[4096])); assertEquals(EOF, cis.read(new byte[1])); - assertEquals(EOF, cis.read(new byte[0])); + assertEquals(0, cis.read(IOUtils.EMPTY_BYTE_ARRAY)); } } @@ -95,7 +96,7 @@ void testReadArrayIndex() throws Exception { try (ClosedInputStream cis = new ClosedInputStream()) { assertEquals(EOF, cis.read(new byte[4096], 0, 1)); assertEquals(EOF, cis.read(new byte[1], 0, 1)); - assertEquals(EOF, cis.read(new byte[0], 0, 0)); + assertEquals(0, cis.read(IOUtils.EMPTY_BYTE_ARRAY, 0, 0)); } } diff --git a/src/test/java/org/apache/commons/io/input/ClosedReaderTest.java b/src/test/java/org/apache/commons/io/input/ClosedReaderTest.java index cb57f9e6f91..57b87ddfb09 100644 --- a/src/test/java/org/apache/commons/io/input/ClosedReaderTest.java +++ b/src/test/java/org/apache/commons/io/input/ClosedReaderTest.java @@ -18,6 +18,7 @@ import static org.apache.commons.io.IOUtils.EOF; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.io.IOException; import java.io.Reader; @@ -46,25 +47,39 @@ void testReadArray() throws Exception { try (Reader reader = new ClosedReader()) { assertEquals(EOF, reader.read(new char[4096])); assertEquals(EOF, reader.read(new char[1])); - assertEquals(EOF, reader.read(new char[0])); + assertEquals(0, reader.read(new char[0])); + assertThrows(NullPointerException.class, () -> reader.read((char[]) null)); } } @Test void testReadArrayIndex() throws Exception { try (Reader reader = new ClosedReader()) { - assertEquals(EOF, reader.read(CharBuffer.wrap(new char[4096]))); - assertEquals(EOF, reader.read(CharBuffer.wrap(new char[1]))); - assertEquals(EOF, reader.read(CharBuffer.wrap(new char[0]))); + final char[] cbuf = new char[4096]; + assertEquals(EOF, reader.read(cbuf, 0, 2048)); + assertEquals(EOF, reader.read(cbuf, 2048, 2048)); + assertEquals(0, reader.read(cbuf, 4096, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(cbuf, -1, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(cbuf, 0, 4097)); + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(cbuf, 1, -1)); + + assertEquals(EOF, reader.read(new char[1])); + assertEquals(0, reader.read(new char[0])); + assertThrows(NullPointerException.class, () -> reader.read(null, 0, 0)); } } @Test void testReadCharBuffer() throws Exception { try (Reader reader = new ClosedReader()) { - assertEquals(EOF, reader.read(new char[4096])); - assertEquals(EOF, reader.read(new char[1])); - assertEquals(EOF, reader.read(new char[0])); + final CharBuffer charBuffer = CharBuffer.wrap(new char[4096]); + assertEquals(EOF, reader.read(charBuffer)); + charBuffer.position(4096); + assertEquals(0, reader.read(charBuffer)); + + assertEquals(EOF, reader.read(CharBuffer.wrap(new char[1]))); + assertEquals(0, reader.read(CharBuffer.wrap(new char[0]))); + assertThrows(NullPointerException.class, () -> reader.read((CharBuffer) null)); } } diff --git a/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java b/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java index a1e4d17f457..2566f8b51e6 100644 --- a/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/MarkShieldInputStreamTest.java @@ -152,7 +152,7 @@ void testReadByteArrayIntIntAfterClose(final int len) throws Exception { MarkShieldInputStream msis = new MarkShieldInputStream(in)) { assertEquals(len, in.available()); in.close(); - assertEquals(0, in.read(new byte[0], 0, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> in.read(new byte[0], 0, 1)); assertEquals(0, in.read(new byte[1], 0, 0)); assertThrows(IOException.class, () -> in.read(new byte[2], 0, 1)); } diff --git a/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java b/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java index b79555d8c02..9b59ac90c0e 100644 --- a/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/NullInputStreamTest.java @@ -249,7 +249,7 @@ void testReadByteArrayIntIntAfterClose() throws Exception { try (InputStream in = new NullInputStream()) { assertEquals(0, in.available()); in.close(); - assertEquals(0, in.read(new byte[0], 0, 1)); + assertThrows(IndexOutOfBoundsException.class, () -> in.read(new byte[0], 0, 1)); assertEquals(0, in.read(new byte[1], 0, 0)); assertThrows(IOException.class, () -> in.read(new byte[2], 0, 1)); } diff --git a/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java b/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java index f33979ce1b8..681ab90eae0 100644 --- a/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java +++ b/src/test/java/org/apache/commons/io/input/ProxyReaderTest.java @@ -38,6 +38,11 @@ public int read(final char[] chars) throws IOException { return chars == null ? 0 : super.read(chars); } + @Override + public int read(final char[] chars, final int offset, final int length) throws IOException { + return chars == null ? 0 : super.read(chars, offset, length); + } + @Override public int read(final CharBuffer target) throws IOException { return target == null ? 0 : super.read(target); diff --git a/src/test/java/org/apache/commons/io/input/TailerTest.java b/src/test/java/org/apache/commons/io/input/TailerTest.java index 4e764bf94ad..dcada2f8f3f 100644 --- a/src/test/java/org/apache/commons/io/input/TailerTest.java +++ b/src/test/java/org/apache/commons/io/input/TailerTest.java @@ -659,7 +659,7 @@ void testTailerEndOfFileReached() throws Exception { // write a few lines writeLines(file, "line7", "line8", "line9"); TestUtils.sleep(testDelayMillis); - // May be > 3 times due to underlying OS behavior wrt streams + // May be > 3 times due to underlying OS behavior and streams. assertTrue(listener.reachedEndOfFile >= 3, "end of file reached at least 3 times"); } } diff --git a/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java index 9c7dbf6fd81..2a2b21a22ad 100644 --- a/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java +++ b/src/test/java/org/apache/commons/io/input/UnsynchronizedBufferedReaderTest.java @@ -347,41 +347,79 @@ void testRead() throws IOException { } } + @Test + void testReadArray_HARMONY_54() throws IOException { + // Regression for HARMONY-54 + final char[] ch = {}; + @SuppressWarnings("resource") + final UnsynchronizedBufferedReader reader = new UnsynchronizedBufferedReader(new CharArrayReader(ch)); + // Check exception thrown when the reader is open. + assertThrows(NullPointerException.class, () -> reader.read(null, 1, 0)); + + // Now check IOException is thrown in preference to + // NullPointerexception when the reader is closed. + reader.close(); + assertThrows(IOException.class, () -> reader.read(null, 1, 0)); + + // And check that the IOException is thrown before + // ArrayIndexOutOfBoundException + assertThrows(IOException.class, () -> reader.read(ch, 0, 42)); + } + + @Test + void testReadArray_HARMONY_831() throws IOException { + // regression for HARMONY-831 + try (Reader reader = new UnsynchronizedBufferedReader(new PipedReader(), 9)) { + assertThrows(IndexOutOfBoundsException.class, () -> reader.read(new char[] {}, 7, 0)); + } + } + /** * Tests {@link UnsynchronizedBufferedReader#read(char[], int, int)}. * * @throws IOException test failure. */ @Test - void testReadArray() throws IOException { + void testReadArray1() throws IOException { final char[] ca = new char[2]; try (UnsynchronizedBufferedReader toRet = new UnsynchronizedBufferedReader(new InputStreamReader(new ByteArrayInputStream(new byte[0])))) { - /* Null buffer should throw NPE even when len == 0 */ + /* Validate parameters, before returning 0 */ assertThrows(NullPointerException.class, () -> toRet.read(null, 1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> toRet.read(ca, 1, 5)); + /* Read zero bytes should return 0 */ + assertEquals(0, toRet.read(ca, 0, 0)); toRet.close(); - assertThrows(IOException.class, () -> toRet.read(null, 1, 0)); - /* Closed reader should throw IOException reading zero bytes */ - assertThrows(IOException.class, () -> toRet.read(ca, 0, 0)); /* - * Closed reader should throw IOException in preference to index out of bounds + * After close, readers in java.io consistently throw IOException before checking parameters or returning 0. */ - // Read should throw IOException before - // ArrayIndexOutOfBoundException + assertThrows(IOException.class, () -> toRet.read(null, 1, 0)); assertThrows(IOException.class, () -> toRet.read(ca, 1, 5)); + assertThrows(IOException.class, () -> toRet.read(ca, 0, 0)); } + } + + @Test + void testReadArray2() throws IOException { + final char[] ca = new char[2]; // Test to ensure that a drained stream returns 0 at EOF try (UnsynchronizedBufferedReader toRet2 = new UnsynchronizedBufferedReader(new InputStreamReader(new ByteArrayInputStream(new byte[2])))) { assertEquals(2, toRet2.read(ca, 0, 2)); assertEquals(-1, toRet2.read(ca, 0, 2)); assertEquals(0, toRet2.read(ca, 0, 0)); } + } + @Test + void testReadArray3() throws IOException { // Test for method int UnsynchronizedBufferedReader.read(char [], int, int) final char[] buf = new char[testString.length()]; br = new UnsynchronizedBufferedReader(new StringReader(testString)); br.read(buf, 50, 500); assertTrue(new String(buf, 50, 500).equals(testString.substring(0, 500))); + } + @Test + void testReadArray4() throws IOException { try (UnsynchronizedBufferedReader bufin = new UnsynchronizedBufferedReader(new Reader() { int size = 2; int pos; @@ -424,26 +462,6 @@ public boolean ready() throws IOException { final int result = bufin.read(new char[2], 0, 2); assertEquals(result, 1); } - // regression for HARMONY-831 - try (Reader reader = new UnsynchronizedBufferedReader(new PipedReader(), 9)) { - assertThrows(IndexOutOfBoundsException.class, () -> reader.read(new char[] {}, 7, 0)); - } - - // Regression for HARMONY-54 - final char[] ch = {}; - @SuppressWarnings("resource") - final UnsynchronizedBufferedReader reader = new UnsynchronizedBufferedReader(new CharArrayReader(ch)); - // Check exception thrown when the reader is open. - assertThrows(NullPointerException.class, () -> reader.read(null, 1, 0)); - - // Now check IOException is thrown in preference to - // NullPointerexception when the reader is closed. - reader.close(); - assertThrows(IOException.class, () -> reader.read(null, 1, 0)); - - // And check that the IOException is thrown before - // ArrayIndexOutOfBoundException - assertThrows(IOException.class, () -> reader.read(ch, 0, 42)); } /** @@ -456,13 +474,12 @@ void testReadArrayException() throws IOException { br = new UnsynchronizedBufferedReader(new StringReader(testString)); final char[] nullCharArray = null; final char[] charArray = testString.toCharArray(); - assertThrows(IndexOutOfBoundsException.class, () -> br.read(nullCharArray, -1, -1)); - assertThrows(IndexOutOfBoundsException.class, () -> br.read(nullCharArray, -1, 0)); + assertThrows(NullPointerException.class, () -> br.read(nullCharArray, -1, 0)); assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 0, -1)); - assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 0, 0)); - assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 0, 1)); - assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, -1, -1)); + assertThrows(NullPointerException.class, () -> br.read(nullCharArray, 1, 1)); assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> br.read(charArray, charArray.length, 1)); br.read(charArray, 0, 0); br.read(charArray, 0, charArray.length); diff --git a/src/test/java/org/apache/commons/io/output/BrokenWriterTest.java b/src/test/java/org/apache/commons/io/output/BrokenWriterTest.java index f8db50a6729..dffeb7907ae 100644 --- a/src/test/java/org/apache/commons/io/output/BrokenWriterTest.java +++ b/src/test/java/org/apache/commons/io/output/BrokenWriterTest.java @@ -55,6 +55,7 @@ void testAppendCharSequence(final Class clazz) throws Exception { @SuppressWarnings("resource") final BrokenWriter brokenWriter = createBrokenWriter(exception); assertEquals(exception, assertThrows(clazz, () -> brokenWriter.append("01"))); + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.append(null))); } @ParameterizedTest @@ -64,6 +65,7 @@ void testAppendCharSequenceIndexed(final Class clazz) throws Exceptio @SuppressWarnings("resource") final BrokenWriter brokenWriter = createBrokenWriter(exception); assertEquals(exception, assertThrows(clazz, () -> brokenWriter.append("01", 0, 1))); + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.append(null, 0, 4))); } @ParameterizedTest @@ -72,7 +74,7 @@ void testClose(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenWriter brokenWriter = createBrokenWriter(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenWriter.close())); + assertEquals(exception, assertThrows(clazz, brokenWriter::close)); } @ParameterizedTest @@ -81,7 +83,7 @@ void testFlush(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenWriter brokenWriter = createBrokenWriter(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenWriter.flush())); + assertEquals(exception, assertThrows(clazz, brokenWriter::flush)); } @Test @@ -119,7 +121,13 @@ void testWriteCharArrayIndexed(final Class clazz) throws Exception { final Throwable exception = clazz.newInstance(); @SuppressWarnings("resource") final BrokenWriter brokenWriter = createBrokenWriter(exception); - assertEquals(exception, assertThrows(clazz, () -> brokenWriter.write(new char[1], 0, 1))); + final char[] cbuf = new char[1]; + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.write(cbuf, 0, 1))); + // Verify that the exception is thrown before checking the parameters. + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.write(cbuf, -1, 0))); + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.write(cbuf, 0, -1))); + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.write(cbuf, 0, 2))); + assertEquals(exception, assertThrows(clazz, () -> brokenWriter.write((char[]) null, 0, 0))); } @ParameterizedTest diff --git a/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java index 57fc7ae2f27..9bae1cb5f62 100644 --- a/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java +++ b/src/test/java/org/apache/commons/io/output/ByteArrayOutputStreamTest.java @@ -43,7 +43,7 @@ import org.junit.jupiter.params.provider.MethodSource; /** - * Tests the alternative ByteArrayOutputStream implementations. + * Tests the alternative {@link ByteArrayOutputStream} implementations. */ class ByteArrayOutputStreamTest { @@ -177,7 +177,7 @@ void testInvalidWriteOffsetOver(final String baosName, final BAOSFactory baos @MethodSource("baosFactories") void testInvalidWriteOffsetUnder(final String baosName, final BAOSFactory baosFactory) throws IOException { try (AbstractByteArrayOutputStream baout = baosFactory.newInstance()) { - assertThrows(IndexOutOfBoundsException.class, () -> baout.write(null, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> baout.write(IOUtils.EMPTY_BYTE_ARRAY, -1, 0)); } } @@ -504,7 +504,7 @@ private int writeByte(final AbstractByteArrayOutputStream baout, final java.i return count; } - private int writeByte(final AbstractByteArrayOutputStream baout, final java.io.ByteArrayOutputStream ref, final int[] instructions) throws IOException { + private int writeByte(final AbstractByteArrayOutputStream baout, final java.io.ByteArrayOutputStream ref, final int[] instructions) { int written = 0; for (final int instruction : instructions) { written += writeByte(baout, ref, instruction); diff --git a/src/test/java/org/apache/commons/io/output/ClosedWriterTest.java b/src/test/java/org/apache/commons/io/output/ClosedWriterTest.java index 4f57957a885..b9758729f1b 100644 --- a/src/test/java/org/apache/commons/io/output/ClosedWriterTest.java +++ b/src/test/java/org/apache/commons/io/output/ClosedWriterTest.java @@ -43,7 +43,13 @@ void testFlush() throws IOException { @Test void testWrite() throws IOException { try (ClosedWriter cw = new ClosedWriter()) { - assertThrows(IOException.class, () -> cw.write(new char[0], 0, 0)); + final char[] cbuf = new char[1]; + assertThrows(IOException.class, () -> cw.write(new char[0], 0, 1)); + // In writers, testing for closed always comes before argument validation + assertThrows(IOException.class, () -> cw.write(cbuf, -1, 0)); + assertThrows(IOException.class, () -> cw.write(cbuf, 0, -1)); + assertThrows(IOException.class, () -> cw.write(cbuf, 0, 2)); + assertThrows(IOException.class, () -> cw.write((char[]) null, 0, 0)); } } diff --git a/src/test/java/org/apache/commons/io/output/NullAppendableTest.java b/src/test/java/org/apache/commons/io/output/NullAppendableTest.java index 6caa15023d9..aa5ef7137ed 100644 --- a/src/test/java/org/apache/commons/io/output/NullAppendableTest.java +++ b/src/test/java/org/apache/commons/io/output/NullAppendableTest.java @@ -16,6 +16,8 @@ */ package org.apache.commons.io.output; +import static org.junit.jupiter.api.Assertions.assertThrows; + import java.io.IOException; import org.junit.jupiter.api.Test; @@ -32,7 +34,12 @@ void testNull() throws IOException { appendable.append("A"); appendable.append("A", 0, 1); appendable.append(null, 0, 1); - appendable.append(null, -1, -1); + // Check argument validation + final CharSequence csq = "ABCDE"; + assertThrows(IndexOutOfBoundsException.class, () -> appendable.append(csq, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> appendable.append(csq, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> appendable.append(csq, 1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> appendable.append(csq, 0, 6)); } } diff --git a/src/test/java/org/apache/commons/io/output/NullOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/NullOutputStreamTest.java index bdecc0e0a68..097566fc6e2 100644 --- a/src/test/java/org/apache/commons/io/output/NullOutputStreamTest.java +++ b/src/test/java/org/apache/commons/io/output/NullOutputStreamTest.java @@ -16,6 +16,8 @@ */ package org.apache.commons.io.output; +import static org.junit.jupiter.api.Assertions.assertThrows; + import java.io.IOException; import org.junit.jupiter.api.Test; @@ -34,11 +36,18 @@ private void process(final NullOutputStream nos) throws IOException { nos.close(); nos.write("allowed".getBytes()); nos.write(255); + // Test arguments validation + final byte[] b = new byte[1]; + assertThrows(IndexOutOfBoundsException.class, () -> nos.write(b, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> nos.write(b, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> nos.write(b, 0, 2)); + assertThrows(NullPointerException.class, () -> nos.write(null, 0, 0)); } @Test + @SuppressWarnings("deprecation") void testNewInstance() throws IOException { - try (NullOutputStream nos = NullOutputStream.INSTANCE) { + try (NullOutputStream nos = new NullOutputStream()) { process(nos); } } diff --git a/src/test/java/org/apache/commons/io/output/NullWriterTest.java b/src/test/java/org/apache/commons/io/output/NullWriterTest.java index f1b603a3776..424d076ee21 100644 --- a/src/test/java/org/apache/commons/io/output/NullWriterTest.java +++ b/src/test/java/org/apache/commons/io/output/NullWriterTest.java @@ -16,6 +16,9 @@ */ package org.apache.commons.io.output; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + import org.junit.jupiter.api.Test; /** @@ -24,17 +27,96 @@ */ class NullWriterTest { + private static final String TEST_STRING = "ABC"; + private static final char[] TEST_CHARS = TEST_STRING.toCharArray(); + + @Test + void testAppendChar() { + try (NullWriter writer = NullWriter.INSTANCE) { + assertSame(writer, writer.append('X')); + } + } + + @Test + void testAppendCharSequence() { + try (NullWriter writer = NullWriter.INSTANCE) { + assertSame(writer, writer.append(TEST_STRING)); + assertSame(writer, writer.append(null)); + } + } + @Test - void testNull() { - final char[] chars = { 'A', 'B', 'C' }; + void testAppendCharSequenceWithRange() { + try (NullWriter writer = NullWriter.INSTANCE) { + assertSame(writer, writer.append(TEST_STRING, 1, 2)); + assertSame(writer, writer.append(null, 0, 4)); + // Test argument validation + assertThrows(IndexOutOfBoundsException.class, () -> writer.append(TEST_STRING, -1, 2)); + assertThrows(IndexOutOfBoundsException.class, () -> writer.append(TEST_STRING, 1, 5)); + assertThrows(IndexOutOfBoundsException.class, () -> writer.append(TEST_STRING, 2, 1)); + } + } + + @Test + void testCloseNoOp() { + final NullWriter writer = NullWriter.INSTANCE; + writer.close(); + writer.write(TEST_CHARS); + } + + @Test + void testFlush() { try (NullWriter writer = NullWriter.INSTANCE) { - writer.write(1); - writer.write(chars); - writer.write(chars, 1, 1); - writer.write("some string"); - writer.write("some string", 2, 2); writer.flush(); } } + @Test + void testWriteCharArray() { + try (NullWriter writer = NullWriter.INSTANCE) { + writer.write(TEST_CHARS); + // Test argument validation + assertThrows(NullPointerException.class, () -> writer.write((char[]) null)); + } + } + + @Test + void testWriteCharArrayWithOffset() { + try (NullWriter writer = NullWriter.INSTANCE) { + writer.write(TEST_CHARS, 1, 2); + // Test argument validation + assertThrows(IndexOutOfBoundsException.class, () -> writer.write(TEST_CHARS, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> writer.write(TEST_CHARS, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> writer.write(TEST_CHARS, 0, 4)); + assertThrows(NullPointerException.class, () -> writer.write((char[]) null, 0, 0)); + } + } + + @Test + void testWriteInt() { + try (NullWriter writer = NullWriter.INSTANCE) { + writer.write(42); + } + } + + @Test + void testWriteString() { + try (NullWriter writer = NullWriter.INSTANCE) { + writer.write(TEST_STRING); + // Test argument validation + assertThrows(NullPointerException.class, () -> writer.write((String) null)); + } + } + + @Test + void testWriteStringWithOffset() { + try (NullWriter writer = NullWriter.INSTANCE) { + writer.write(TEST_STRING, 1, 1); + // Test argument validation + assertThrows(IndexOutOfBoundsException.class, () -> writer.write(TEST_STRING, -1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> writer.write(TEST_STRING, 0, -1)); + assertThrows(IndexOutOfBoundsException.class, () -> writer.write(TEST_STRING, 0, 4)); + assertThrows(NullPointerException.class, () -> writer.write((String) null, 0, 0)); + } + } } diff --git a/src/test/java/org/apache/commons/io/output/ProxyOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/ProxyOutputStreamTest.java index 4f120af3c1f..45b9e759cd5 100644 --- a/src/test/java/org/apache/commons/io/output/ProxyOutputStreamTest.java +++ b/src/test/java/org/apache/commons/io/output/ProxyOutputStreamTest.java @@ -30,67 +30,177 @@ import org.junit.jupiter.api.Test; /** - * Tests {@link CloseShieldOutputStream}. + * Tests {@link ProxyOutputStream}. */ class ProxyOutputStreamTest { - private ByteArrayOutputStream original; + private ByteArrayOutputStream target; private ProxyOutputStream proxied; - private final AtomicBoolean hit = new AtomicBoolean(); + private final AtomicBoolean hitByteArray = new AtomicBoolean(); + private final AtomicBoolean hitByteArrayAt = new AtomicBoolean(); + private final AtomicBoolean hitInt = new AtomicBoolean(); @BeforeEach public void setUp() { - original = new ByteArrayOutputStream() { + target = new ByteArrayOutputStream() { @Override public void write(final byte[] ba) { - hit.set(true); + hitByteArray.set(true); super.write(ba); } + @Override + public void write(final byte[] b, final int off, final int len) { + hitByteArrayAt.set(true); + super.write(b, off, len); + } + @Override public synchronized void write(final int ba) { - hit.set(true); + hitInt.set(true); super.write(ba); } }; - proxied = new ProxyOutputStream(original); + proxied = new ProxyOutputStream(target); } @Test void testBuilder() throws Exception { - assertSame(original, new ProxyOutputStream.Builder().setOutputStream(original).get().unwrap()); + assertSame(target, new ProxyOutputStream.Builder().setOutputStream(target).get().unwrap()); } @SuppressWarnings("resource") @Test void testSetReference() throws Exception { - assertFalse(hit.get()); + assertFalse(hitByteArray.get()); proxied.setReference(new ByteArrayOutputStream()); proxied.write('y'); - assertFalse(hit.get()); - assertEquals(0, original.size()); - assertArrayEquals(ArrayUtils.EMPTY_BYTE_ARRAY, original.toByteArray()); + assertFalse(hitByteArray.get()); + assertEquals(0, target.size()); + assertArrayEquals(ArrayUtils.EMPTY_BYTE_ARRAY, target.toByteArray()); + } + + @Test + void testWriteByteArray() throws Exception { + assertFalse(hitByteArray.get()); + proxied.write(new byte[] { 'y', 'z' }); + assertTrue(hitByteArray.get()); + assertEquals(2, target.size()); + assertArrayEquals(new byte[] { 'y', 'z' }, target.toByteArray()); + } + + @Test + void testWriteByteArrayAt() throws Exception { + assertFalse(hitByteArrayAt.get()); + proxied.write(new byte[] { 'y', 'z' }, 1, 1); + assertTrue(hitByteArrayAt.get()); + assertEquals(1, target.size()); + assertArrayEquals(new byte[] { 'z' }, target.toByteArray()); } @Test - void testWrite() throws Exception { - assertFalse(hit.get()); + void testWriteByteArrayAtRepeat() throws Exception { + // repeat -1 + proxied.writeRepeat(new byte[] { 'y', 'z' }, 1, 1, 0); + assertFalse(hitByteArrayAt.get()); + hitByteArray.set(false); + assertEquals(0, target.size()); + assertArrayEquals(new byte[] {}, target.toByteArray()); + // repeat 0 + proxied.writeRepeat(new byte[] { 'y', 'z' }, 1, 1, 0); + assertFalse(hitByteArrayAt.get()); + hitByteArray.set(false); + assertEquals(0, target.size()); + assertArrayEquals(new byte[] {}, target.toByteArray()); + // repeat 1 + proxied.writeRepeat(new byte[] { 'y', 'z' }, 1, 1, 1); + assertTrue(hitByteArrayAt.get()); + hitByteArray.set(false); + assertEquals(1, target.size()); + assertArrayEquals(new byte[] { 'z' }, target.toByteArray()); + // repeat 2 + proxied.writeRepeat(new byte[] { 'y', 'x' }, 1, 1, 2); + assertTrue(hitByteArrayAt.get()); + assertEquals(3, target.size()); + assertArrayEquals(new byte[] { 'z', 'x', 'x' }, target.toByteArray()); + } + + @Test + void testWriteByteArrayRepeat() throws Exception { + // repeat -1 + proxied.writeRepeat(new byte[] { 'y', 'z' }, -1); + assertFalse(hitByteArray.get()); + hitByteArray.set(false); + assertEquals(0, target.size()); + assertArrayEquals(new byte[] {}, target.toByteArray()); + // repeat 0 + proxied.writeRepeat(new byte[] { 'y', 'z' }, 0); + assertFalse(hitByteArray.get()); + hitByteArray.set(false); + assertEquals(0, target.size()); + assertArrayEquals(new byte[] {}, target.toByteArray()); + // repeat 1 + proxied.writeRepeat(new byte[] { 'y', 'z' }, 1); + assertTrue(hitByteArray.get()); + hitByteArray.set(false); + assertEquals(2, target.size()); + assertArrayEquals(new byte[] { 'y', 'z' }, target.toByteArray()); + // repeat 2 + proxied.writeRepeat(new byte[] { 'y', 'z' }, 2); + assertTrue(hitByteArray.get()); + assertEquals(6, target.size()); + assertArrayEquals(new byte[] { 'y', 'z', 'y', 'z' , 'y', 'z' }, target.toByteArray()); + } + + @Test + void testWriteInt() throws Exception { + assertFalse(hitInt.get()); proxied.write('y'); - assertTrue(hit.get()); - assertEquals(1, original.size()); - assertEquals('y', original.toByteArray()[0]); + assertTrue(hitInt.get()); + assertEquals(1, target.size()); + assertEquals('y', target.toByteArray()[0]); + } + + @Test + void testWriteIntRepeat() throws Exception { + // repeat -1 + assertFalse(hitInt.get()); + proxied.writeRepeat('y', -1); + assertFalse(hitInt.get()); + assertEquals(0, target.size()); + assertArrayEquals(new byte[] {}, target.toByteArray()); + // repeat 0 + assertFalse(hitInt.get()); + proxied.writeRepeat('y', 0); + assertFalse(hitInt.get()); + assertEquals(0, target.size()); + assertArrayEquals(new byte[] {}, target.toByteArray()); + // repeat 1 + assertFalse(hitInt.get()); + proxied.writeRepeat('y', 1); + assertTrue(hitInt.get()); + hitInt.set(false); + assertEquals(1, target.size()); + assertArrayEquals(new byte[] { 'y' }, target.toByteArray()); + // repeat 2 + assertFalse(hitInt.get()); + proxied.writeRepeat('z', 2); + assertTrue(hitInt.get()); + hitInt.set(false); + assertEquals(3, target.size()); + assertArrayEquals(new byte[] { 'y', 'z', 'z' }, target.toByteArray()); } @Test void testWriteNullArrayProxiesToUnderlying() throws Exception { - assertFalse(hit.get()); + assertFalse(hitByteArray.get()); final byte[] ba = null; - assertThrows(NullPointerException.class, () -> original.write(ba)); - assertTrue(hit.get()); + assertThrows(NullPointerException.class, () -> target.write(ba)); + assertTrue(hitByteArray.get()); assertThrows(NullPointerException.class, () -> proxied.write(ba)); - assertTrue(hit.get()); + assertTrue(hitByteArray.get()); } } diff --git a/src/test/java/org/apache/commons/io/output/ProxyWriterTest.java b/src/test/java/org/apache/commons/io/output/ProxyWriterTest.java index 9954ca06803..e810d39d726 100644 --- a/src/test/java/org/apache/commons/io/output/ProxyWriterTest.java +++ b/src/test/java/org/apache/commons/io/output/ProxyWriterTest.java @@ -205,8 +205,8 @@ public void write(final String str, final int off, final int len) throws IOExcep @Test void testNullCharArray() throws Exception { try (ProxyWriter proxy = new ProxyWriter(NullWriter.INSTANCE)) { - proxy.write((char[]) null); - proxy.write((char[]) null, 0, 0); + assertThrows(NullPointerException.class, () -> proxy.write((char[]) null)); + assertThrows(NullPointerException.class, () -> proxy.write((char[]) null, 0, 0)); } } @@ -220,8 +220,9 @@ void testNullCharSequence() throws Exception { @Test void testNullString() throws Exception { try (ProxyWriter proxy = new ProxyWriter(NullWriter.INSTANCE)) { - proxy.write((String) null); - proxy.write((String) null, 0, 0); + // Default implementation delegates to write(char[], int, int) + assertThrows(NullPointerException.class, () -> proxy.write((String) null)); + assertThrows(NullPointerException.class, () -> proxy.write((String) null, 0, 0)); } } diff --git a/src/test/java/org/apache/commons/io/output/RandomAccessFileOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/RandomAccessFileOutputStreamTest.java index 857a707bd84..38db420377a 100644 --- a/src/test/java/org/apache/commons/io/output/RandomAccessFileOutputStreamTest.java +++ b/src/test/java/org/apache/commons/io/output/RandomAccessFileOutputStreamTest.java @@ -134,7 +134,7 @@ void testWriteGet() throws IOException { } @Test - void testWriteGetDefault() throws IOException { + void testWriteGetDefault() { assertThrows(IllegalStateException.class, () -> { try (RandomAccessFileOutputStream os = RandomAccessFileOutputStream.builder().get()) { validateState(os); diff --git a/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java b/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java index 2fe4356653d..5e1e2c00d9d 100644 --- a/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java +++ b/src/test/java/org/apache/commons/io/output/ThresholdingOutputStreamTest.java @@ -72,7 +72,7 @@ void testResetByteCount() throws IOException { } @Test - void testResetByteCountBrokenOutputStream() throws IOException { + void testResetByteCountBrokenOutputStream() { final int threshold = 1; final AtomicInteger counter = new AtomicInteger(); final IOException e = assertThrows(IOException.class, () -> { diff --git a/src/test/java/org/apache/commons/io/serialization/AbstractCloseableListTest.java b/src/test/java/org/apache/commons/io/serialization/AbstractCloseableListTest.java index 53696ae7758..43ec7d55d8e 100644 --- a/src/test/java/org/apache/commons/io/serialization/AbstractCloseableListTest.java +++ b/src/test/java/org/apache/commons/io/serialization/AbstractCloseableListTest.java @@ -27,17 +27,20 @@ import org.junit.jupiter.api.BeforeEach; /** - * Test base class that keeps track of Closeable objects and cleans them up. + * Abstract test class that tracks {@link Closeable} objects and cleans them up. + * + * @see Closeable */ public abstract class AbstractCloseableListTest { private final List closeableList = new ArrayList<>(); /** - * Adds a Closeable to close after each test. + * Adds a {@link Closeable} to close after each test. * * @param The Closeable type * @param t The Closeable. * @return The Closeable. + * @see Closeable */ protected T addCloseable(final T t) { closeableList.add(t); diff --git a/src/test/java/org/apache/commons/io/serialization/ProxyTest.java b/src/test/java/org/apache/commons/io/serialization/ProxyTest.java new file mode 100644 index 00000000000..64b82551e49 --- /dev/null +++ b/src/test/java/org/apache/commons/io/serialization/ProxyTest.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.commons.io.serialization; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InvalidClassException; +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.apache.commons.lang3.SerializationUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ValidatingObjectInputStream}. + */ +class ProxyTest { + + public interface IFoo extends Serializable { + + void foo(); + } + + public static class InvocationHandlerImpl implements InvocationHandler, Serializable { + + @Override + public Object invoke(final Object proxy, final Method method, final Object[] args) { + return "InvocationHandlerImpl.invoke()"; + } + } + + Object newProxy() { + return Proxy.newProxyInstance(ProxyTest.class.getClassLoader(), new Class[] { IFoo.class }, new InvocationHandlerImpl()); + } + + @Test + void testAcceptProxy() throws IOException, ClassNotFoundException { + final Object proxy = newProxy(); + final byte[] serialized = SerializationUtils.serialize((Serializable) proxy); + final Class ifaceClass = IFoo.class; + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .accept("*") + .get()) { + // @formatter:on + assertTrue(assertInstanceOf(ifaceClass, vois.readObject()).toString().endsWith("InvocationHandlerImpl.invoke()")); + } + } + + @Test + void testRejectProxy() throws IOException, ClassNotFoundException { + final Object proxy = newProxy(); + final byte[] serialized = SerializationUtils.serialize((Serializable) proxy); + final Class ifaceClass = IFoo.class; + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .accept("*") + .reject(ifaceClass) + .get()) { + // @formatter:on + assertThrows(InvalidClassException.class, vois::readObject); + } + } +} diff --git a/src/test/java/org/apache/commons/io/test/ThrowOnCloseInputStream.java b/src/test/java/org/apache/commons/io/test/ThrowOnCloseInputStream.java index 984e53ad20e..19ea7f4ef66 100644 --- a/src/test/java/org/apache/commons/io/test/ThrowOnCloseInputStream.java +++ b/src/test/java/org/apache/commons/io/test/ThrowOnCloseInputStream.java @@ -46,7 +46,7 @@ public ThrowOnCloseInputStream(final InputStream proxy) { /** * Always throws IOException. * - * @see java.io.InputStream#close() + * @see InputStream#close() */ @Override public void close() throws IOException { diff --git a/src/test/java/org/apache/commons/io/test/ThrowOnCloseOutputStream.java b/src/test/java/org/apache/commons/io/test/ThrowOnCloseOutputStream.java index afb41113aef..cde3fdc8741 100644 --- a/src/test/java/org/apache/commons/io/test/ThrowOnCloseOutputStream.java +++ b/src/test/java/org/apache/commons/io/test/ThrowOnCloseOutputStream.java @@ -41,7 +41,7 @@ public ThrowOnCloseOutputStream(final OutputStream proxy) { super(proxy); } - /** @see java.io.OutputStream#close() */ + /** @see OutputStream#close() */ @Override public void close() throws IOException { throw new IOException(getClass().getSimpleName() + ".close() called."); diff --git a/src/test/java/org/apache/commons/io/test/ThrowOnCloseReader.java b/src/test/java/org/apache/commons/io/test/ThrowOnCloseReader.java index 069cd0ad17f..0df6a3a8a6e 100644 --- a/src/test/java/org/apache/commons/io/test/ThrowOnCloseReader.java +++ b/src/test/java/org/apache/commons/io/test/ThrowOnCloseReader.java @@ -41,7 +41,7 @@ public ThrowOnCloseReader(final Reader proxy) { super(proxy); } - /** @see java.io.Reader#close() */ + /** @see Reader#close() */ @Override public void close() throws IOException { throw new IOException(getClass().getSimpleName() + ".close() called."); diff --git a/src/test/java/org/apache/commons/io/test/ThrowOnCloseWriter.java b/src/test/java/org/apache/commons/io/test/ThrowOnCloseWriter.java index 5845f1fd8f8..d50d6047309 100644 --- a/src/test/java/org/apache/commons/io/test/ThrowOnCloseWriter.java +++ b/src/test/java/org/apache/commons/io/test/ThrowOnCloseWriter.java @@ -41,7 +41,7 @@ public ThrowOnCloseWriter(final Writer proxy) { super(proxy); } - /** @see java.io.Writer#close() */ + /** @see Writer#close() */ @Override public void close() throws IOException { throw new IOException(getClass().getSimpleName() + ".close() called."); diff --git a/src/test/java/org/apache/commons/io/test/ThrowOnFlushAndCloseOutputStream.java b/src/test/java/org/apache/commons/io/test/ThrowOnFlushAndCloseOutputStream.java index 97c67197e71..f5096673a78 100644 --- a/src/test/java/org/apache/commons/io/test/ThrowOnFlushAndCloseOutputStream.java +++ b/src/test/java/org/apache/commons/io/test/ThrowOnFlushAndCloseOutputStream.java @@ -43,7 +43,7 @@ public ThrowOnFlushAndCloseOutputStream(final OutputStream proxy, final boolean this.throwOnClose = throwOnClose; } - /** @see java.io.OutputStream#close() */ + /** @see OutputStream#close() */ @Override public void close() throws IOException { if (throwOnClose) { @@ -52,7 +52,7 @@ public void close() throws IOException { super.close(); } - /** @see java.io.OutputStream#flush() */ + /** @see OutputStream#flush() */ @Override public void flush() throws IOException { if (throwOnFlush) { diff --git a/src/test/resources/org/apache/commons/io/FileUtilsTestDataCR.dat b/src/test/resources/org/apache/commons/io/FileUtilsTestDataCR.bin similarity index 100% rename from src/test/resources/org/apache/commons/io/FileUtilsTestDataCR.dat rename to src/test/resources/org/apache/commons/io/FileUtilsTestDataCR.bin diff --git a/src/test/resources/org/apache/commons/io/FileUtilsTestDataCRLF.dat b/src/test/resources/org/apache/commons/io/FileUtilsTestDataCRLF.bin similarity index 100% rename from src/test/resources/org/apache/commons/io/FileUtilsTestDataCRLF.dat rename to src/test/resources/org/apache/commons/io/FileUtilsTestDataCRLF.bin diff --git a/src/test/resources/org/apache/commons/io/FileUtilsTestDataLF.dat b/src/test/resources/org/apache/commons/io/FileUtilsTestDataLF.bin similarity index 100% rename from src/test/resources/org/apache/commons/io/FileUtilsTestDataLF.dat rename to src/test/resources/org/apache/commons/io/FileUtilsTestDataLF.bin