diff --git a/.asf.yaml b/.asf.yaml index c56b33138c..cdb8cd101b 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -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/.gitattributes b/.gitattributes index bec231c194..f42866e4bd 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, diff --git a/.github/GH-ROBOTS.txt b/.github/GH-ROBOTS.txt index e3329e55fb..64a88674fe 100644 --- a/.github/GH-ROBOTS.txt +++ b/.github/GH-ROBOTS.txt @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9ebcd0ebb1..90ec55f742 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -18,10 +18,8 @@ updates: - package-ecosystem: "maven" directory: "/" schedule: - interval: "weekly" - day: "friday" + interval: "quarterly" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" - day: "friday" + interval: "quarterly" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d126a970ce..9ff35c83e7 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -7,7 +7,7 @@ "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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 @@ -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. Which AI tool was used to create this pull request, and to what extent did it contribute? - [ ] 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 84d9df4ce0..cca38e5121 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -37,6 +37,7 @@ jobs: security-events: write strategy: + max-parallel: 20 fail-fast: false matrix: language: [ 'java' ] @@ -45,10 +46,10 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.0 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -57,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # 3.28.18 + uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -68,7 +69,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@ff0a06e83cb2de871e5a09832bc6a81e7276941f # 3.28.18 + uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -82,4 +83,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # 3.28.18 + uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 9d226a4585..7bc02bdd23 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -6,7 +6,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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 @@ -26,6 +26,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: 'Dependency Review PR' - uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1 + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 02b3996bd7..17ba7dd386 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -15,7 +15,11 @@ name: Java CI -on: [push, pull_request] +on: + push: + branches: + - 'master' + pull_request: {} permissions: contents: read @@ -23,30 +27,35 @@ permissions: jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} strategy: + max-parallel: 20 + fail-fast: false matrix: - java: [ 8, 11, 17, 21, 24 ] + os: [ubuntu-latest, macos-latest] + java: [ 8, 11, 17, 21, 25, 26 ] experimental: [false] + # Keep the same parameter order as the matrix above include: - - java: 25-ea - experimental: true + - os: ubuntu-latest + java: 27-ea + experimental: true steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 #v6.1.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@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0 with: - distribution: 'temurin' + distribution: ${{ runner.os == 'macOS' && matrix.java == '8' && 'zulu' || 'temurin' }} java-version: ${{ matrix.java }} - name: Build with Maven run: mvn -Ddoclint=all --show-version --batch-mode --no-transfer-progress diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml index 8026752067..e1868cb462 100644 --- a/.github/workflows/scorecards-analysis.yml +++ b/.github/workflows/scorecards-analysis.yml @@ -5,7 +5,7 @@ # (the "License"); you may not use this file except in compliance with # the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, @@ -40,12 +40,12 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # 7.0.0 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@f49aabe0b5af0936a0987cfb85d86b75731b0186 # 2.4.1 + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # 2.4.3 with: results_file: results.sarif results_format: sarif @@ -57,13 +57,13 @@ jobs: publish_results: true - name: "Upload artifact" - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # 4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: SARIF file path: results.sarif retention-days: 5 - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # 3.28.18 + uses: github/codeql-action/upload-sarif@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2 with: sarif_file: results.sarif diff --git a/.gitignore b/.gitignore index 4b377d5762..2ff17ae4a8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,8 @@ buildNumber.properties *.iml /.vscode/ +/.DS_Store + +# NetBeans files +nb-configuration.xml +nbactions.xml diff --git a/BENCHMARK.md b/BENCHMARK.md index e8b579b2e2..c45918a289 100644 --- a/BENCHMARK.md +++ b/BENCHMARK.md @@ -6,7 +6,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 3ed501501d..b4342f33ca 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -6,7 +6,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index beb9a235a3..3423e18ad2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,13 +48,13 @@ Getting Started --------------- + Make sure you have a [JIRA account](https://issues.apache.org/jira/). -+ Make sure you have a [GitHub account](https://github.com/signup/free). This is not essential, but makes providing patches much easier. ++ Make sure you have a [GitHub account](https://github.com/signup). This is not essential, but makes providing patches much easier. + If you're planning to implement a new feature it makes sense to discuss your changes on the [dev list](https://commons.apache.org/mail-lists.html) first. This way you can make sure you're not wasting your time on something that isn't considered to be in Apache Commons CSV's scope. + Submit a [Jira Ticket][jira] for your issue, assuming one does not already exist. + Clearly describe the issue including steps to reproduce when it is a bug. + Make sure you fill in the earliest version that you know has the issue. + Find the corresponding [repository on GitHub](https://github.com/apache/?query=commons-), -[fork](https://help.github.com/articles/fork-a-repo/) and check out your forked repository. If you don't have a GitHub account, you can still clone the Commons repository. +[fork](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) and check out your forked repository. If you don't have a GitHub account, you can still clone the Commons repository. Making Changes -------------- @@ -69,7 +69,7 @@ Making Changes + Respect the original code style: + Only use spaces for indentation; you can check for unnecessary whitespace with `git diff` before committing. + 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 first. -+ 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 is a best practice. Unit tests are typically in the `src/test/java` directory. + 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 a pull request description that is detailed enough to understand what the pull request does, how, and why. @@ -108,8 +108,8 @@ Additional Resources + [Contributing patches](https://commons.apache.org/patches.html) + [Apache Commons CSV JIRA project page][jira] + [Contributor License Agreement][cla] -+ [General GitHub documentation](https://help.github.com/) -+ [GitHub pull request documentation](https://help.github.com/articles/creating-a-pull-request/) ++ [General GitHub documentation](https://docs.github.com/) ++ [GitHub pull request documentation](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request) + [Apache Commons Twitter Account](https://twitter.com/ApacheCommons) [cla]:https://www.apache.org/licenses/#clas diff --git a/LICENSE.txt b/LICENSE.txt index d645695673..ff9ad4530f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -193,7 +193,7 @@ you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/NOTICE.txt b/NOTICE.txt index b5c1f1445b..06d3824a28 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Apache Commons CSV -Copyright 2005-2025 The Apache Software Foundation +Copyright 2005-2026 The Apache Software Foundation This product includes software developed at The Apache Software Foundation (https://www.apache.org/). diff --git a/README.md b/README.md index 42c9894d17..f30de4b9c9 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Apache Commons CSV [![Java CI](https://github.com/apache/commons-csv/actions/workflows/maven.yml/badge.svg)](https://github.com/apache/commons-csv/actions/workflows/maven.yml) [![Maven Central](https://img.shields.io/maven-central/v/org.apache.commons/commons-csv?label=Maven%20Central)](https://search.maven.org/artifact/org.apache.commons/commons-csv) -[![Javadocs](https://javadoc.io/badge/org.apache.commons/commons-csv/1.14.0.svg)](https://javadoc.io/doc/org.apache.commons/commons-csv/1.14.0) +[![Javadocs](https://javadoc.io/badge/org.apache.commons/commons-csv/1.14.1.svg)](https://javadoc.io/doc/org.apache.commons/commons-csv/1.14.1) [![CodeQL](https://github.com/apache/commons-csv/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/apache/commons-csv/actions/workflows/codeql-analysis.yml) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/apache/commons-csv/badge)](https://api.securityscorecards.dev/projects/github.com/apache/commons-csv) @@ -68,7 +68,7 @@ Alternatively, you can pull it from the central Maven repositories: org.apache.commons commons-csv - 1.14.0 + 1.14.1 ``` @@ -90,7 +90,7 @@ There are some guidelines which will make applying PRs easier for us: + 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. -+ To see the code coverage report, regardless of coverage failures, run `mvn clean site -Dcommons.jacoco.haltOnFailure=false` ++ 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). You can learn more about contributing via GitHub in our [contribution guidelines](CONTRIBUTING.md). diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 599f0d1f8a..bfeb4bb8de 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -1,4 +1,54 @@ +Apache Commons CSV 1.14.1 Release Notes +--------------------------------------- + +The Apache Commons CSV team is pleased to announce the release of Apache Commons CSV 1.14.1. + + +This document contains the release notes for the 1.14.1 version of Apache Commons CSV. +Commons CSV reads and writes files in Comma Separated Value (CSV) format variations. + +Commons CSV requires at least Java 8. + +The Apache Commons CSV library provides a simple interface for reading and writing CSV files of various types. + +This is a feature and maintenance release. Java 8 or later is required. + +Changes in this version include: + + +Fixed Bugs +---------- + +* CSV-318: CSVPrinter.printRecord(Stream) hangs if given a parallel stream. Thanks to Joseph Shraibman, Gary Gregory. +* CSV-318: CSVPrinter now uses an internal lock instead of synchronized methods. Thanks to Joseph Shraibman, Gary Gregory. +* org.apache.commons.csv.CSVPrinter.printRecords(ResultSet) now writes one record at a time using a lock. Thanks to Gary Gregory. + +Changes +------- + +* Bump org.apache.commons:commons-parent from 81 to 85 #542. Thanks to Gary Gregory, Dependabot. +* Bump commons-io:commons-io from 2.18.0 to 2.20.0. Thanks to Gary Gregory. +* Bump com.opencsv:opencsv from 5.10 to 5.11.2 #545, #551, #553. Thanks to Gary Gregory, Dependabot. +* Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 #556. Thanks to Gary Gregory, Dependabot. +* Bump commons-codec:commons-codec from 1.18.0 to 1.19.0. Thanks to Gary Gregory. + + +Historical list of changes: https://commons.apache.org/proper/commons-csv/changes.html + +For complete information on Apache Commons CSV, including instructions on how to submit bug reports, +patches, or suggestions for improvement, see the Apache Commons CSV website: + +https://commons.apache.org/proper/commons-csv/ + +Download page: https://commons.apache.org/proper/commons-csv/download_csv.cgi + +Have fun! +-Apache Commons CSV team + +------------------------------------------------------------------------------ + Apache Commons CSV 1.14.0 Release Notes +--------------------------------------- This document contains the release notes for the 1.14.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -59,7 +109,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.13.0 Release Notes +Apache Commons CSV 1.13.0 Release Notes +--------------------------------------- This document contains the release notes for the 1.13.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -111,7 +162,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.12.0 Release Notes +Apache Commons CSV 1.12.0 Release Notes +--------------------------------------- This document contains the release notes for the 1.12.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -167,7 +219,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.11.0 Release Notes +Apache Commons CSV 1.11.0 Release Notes +--------------------------------------- This document contains the release notes for the 1.11.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -227,7 +280,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.10.0 Release Notes +Apache Commons CSV 1.10.0 Release Notes +--------------------------------------- This document contains the release notes for the 1.10.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -308,7 +362,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.9.0 Release Notes +Apache Commons CSV 1.9.0 Release Notes +-------------------------------------- This document contains the release notes for the 1.9.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -410,7 +465,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.8 Release Notes +Apache Commons CSV 1.8 Release Notes +------------------------------------ This document contains the release notes for the 1.8 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -465,7 +521,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.7 Release Notes +Apache Commons CSV 1.7 Release Notes +------------------------------------ This document contains the release notes for the 1.7 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -512,7 +569,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.6 Release Notes +Apache Commons CSV 1.6 Release Notes +------------------------------------ This document contains the release notes for the 1.6 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the @@ -561,7 +619,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.5 Release Notes +Apache Commons CSV 1.5 Release Notes +------------------------------------ This document contains the release notes for the 1.5 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -613,7 +672,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.4 Release Notes +Apache Commons CSV 1.4 Release Notes +------------------------------------ This document contains the release notes for the 1.4 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -652,7 +712,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.3 Release Notes +Apache Commons CSV 1.3 Release Notes +------------------------------------ This document contains the release notes for the 1.3 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -696,7 +757,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.2 Release Notes +Apache Commons CSV 1.2 Release Notes +------------------------------------ This document contains the release notes for the 1.2 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -734,7 +796,8 @@ Have fun! ------------------------------------------------------------------------------ -Apache Commons CSV Version 1.1 Release Notes +Apache Commons CSV 1.1 Release Notes +------------------------------------ This document contains the release notes for the 1.1 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. @@ -775,7 +838,8 @@ Have fun! ------------------------------------------------------------------------------- -Apache Commons CSV Version 1.0 Release Notes +Apache Commons CSV 1.0 Release Notes +------------------------------------ This document contains the release notes for the 1.0 version of Apache Commons CSV. Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format. diff --git a/SECURITY.md b/SECURITY.md index 51943ba7b4..744d4cddbb 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,7 +6,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/benchmark-prereq.sh b/benchmark-prereq.sh index 1d03f6773a..bd1db91821 100755 --- a/benchmark-prereq.sh +++ b/benchmark-prereq.sh @@ -8,7 +8,7 @@ # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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, diff --git a/pom.xml b/pom.xml index df1462775a..8cb13ed7c2 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, @@ -20,10 +20,10 @@ org.apache.commons commons-parent - 83 + 102 commons-csv - 1.14.1-SNAPSHOT + 1.15.0-SNAPSHOT Apache Commons CSV https://commons.apache.org/proper/commons-csv/ 2005 @@ -53,12 +53,13 @@ org.apache.commons commons-lang3 - 3.17.0 + 3.20.0 test com.h2database h2 + 2.2.224 test @@ -68,20 +69,13 @@ ${commons.jmh.version} test - - com.github.spotbugs - spotbugs-annotations - ${commons.spotbugs.impl.version} - true - - scm:git:http://gitbox.apache.org/repos/asf/commons-csv.git + scm:git:https://gitbox.apache.org/repos/asf/commons-csv.git scm:git:https://gitbox.apache.org/repos/asf/commons-csv.git https://gitbox.apache.org/repos/asf?p=commons-csv.git - jira https://issues.apache.org/jira/browse/CSV @@ -96,12 +90,12 @@ - 1.14.0 + 1.15.0 (Java 8 or above) RC1 - 1.13.0 - 1.14.1 + 1.14.1 + 1.15.1 csv org.apache.commons.csv CSV @@ -114,9 +108,9 @@ UTF-8 false true - 2025-03-19T22:12:04Z - 1.18.0 - 2.19.0 + 2025-07-30T14:51:35Z + 1.22.0 + 2.22.0 org.apache.commons.codec.binary;version="${commons.codec.version}", @@ -174,34 +168,35 @@ apache-rat-plugin - + - src/test/resources/org/apache/commons/csv/empty.txt - src/test/resources/org/apache/commons/csv/CSV-141/csv-141.csv - src/test/resources/org/apache/commons/csv/csv-167/sample1.csv - src/test/resources/org/apache/commons/csv/CSV-198/optd_por_public.csv - src/test/resources/org/apache/commons/csv/CSV-196/emoji.csv - src/test/resources/org/apache/commons/csv/CSV-196/japanese.csv - src/test/resources/org/apache/commons/csv/CSV-213/999751170.patch.csv - src/test/resources/org/apache/commons/csv/CSVFileParser/bom.csv - src/test/resources/org/apache/commons/csv/CSVFileParser/test.csv - src/test/resources/org/apache/commons/csv/CSVFileParser/test_default.txt - src/test/resources/org/apache/commons/csv/CSVFileParser/test_default_comment.txt - src/test/resources/org/apache/commons/csv/CSVFileParser/test_rfc4180.txt - src/test/resources/org/apache/commons/csv/CSVFileParser/test_rfc4180_trim.txt - src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV85.csv - src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV85_default.txt - src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV85_ignoreEmpty.txt + src/test/resources/org/apache/commons/csv/empty.txt + src/test/resources/org/apache/commons/csv/CSV-141/csv-141.csv + src/test/resources/org/apache/commons/csv/csv-167/sample1.csv + src/test/resources/org/apache/commons/csv/CSV-198/optd_por_public.csv + src/test/resources/org/apache/commons/csv/CSV-196/emoji.csv + src/test/resources/org/apache/commons/csv/CSV-196/japanese.csv + src/test/resources/org/apache/commons/csv/CSV-213/999751170.patch.csv + src/test/resources/org/apache/commons/csv/CSV-254/csv-254.csv + src/test/resources/org/apache/commons/csv/CSVFileParser/bom.csv + src/test/resources/org/apache/commons/csv/CSVFileParser/test.csv + src/test/resources/org/apache/commons/csv/CSVFileParser/test_default.txt + src/test/resources/org/apache/commons/csv/CSVFileParser/test_default_comment.txt + src/test/resources/org/apache/commons/csv/CSVFileParser/test_rfc4180.txt + src/test/resources/org/apache/commons/csv/CSVFileParser/test_rfc4180_trim.txt + src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV85.csv + src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV85_default.txt + src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV85_ignoreEmpty.txt - src/test/resources/org/apache/commons/csv/ferc.gov/contract.txt - src/test/resources/org/apache/commons/csv/ferc.gov/transaction.txt - src/test/resources/**/*.bin - src/test/resources/org/apache/commons/csv/CSV-259/sample.txt - src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV246.csv - src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV246_checkWithNoComment.txt - src/test/resources/org/apache/commons/csv/CSV-290/psql.csv - src/test/resources/org/apache/commons/csv/CSV-290/psql.tsv - + src/test/resources/org/apache/commons/csv/ferc.gov/contract.txt + src/test/resources/org/apache/commons/csv/ferc.gov/transaction.txt + src/test/resources/**/*.bin + src/test/resources/org/apache/commons/csv/CSV-259/sample.txt + src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV246.csv + src/test/resources/org/apache/commons/csv/CSVFileParser/testCSV246_checkWithNoComment.txt + src/test/resources/org/apache/commons/csv/CSV-290/psql.csv + src/test/resources/org/apache/commons/csv/CSV-290/psql.tsv + @@ -386,7 +381,7 @@ com.opencsv opencsv - 5.11 + 5.12.0 test @@ -415,7 +410,7 @@ org.apache.commons commons-lang3 - 3.17.0 + 3.20.0 diff --git a/src/assembly/bin.xml b/src/assembly/bin.xml index f73d62f6df..3526ca9e91 100644 --- a/src/assembly/bin.xml +++ b/src/assembly/bin.xml @@ -6,7 +6,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/assembly/src.xml b/src/assembly/src.xml index 9f33f58f20..1330db01f6 100644 --- a/src/assembly/src.xml +++ b/src/assembly/src.xml @@ -6,7 +6,7 @@ Licensed to the Apache Software Foundation (ASF) under one or more (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 3cb97f78c1..93952e9f18 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -7,7 +7,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, @@ -40,16 +40,51 @@ Apache Commons CSV Release Notes - + + + Remove Spotbugs dependency and use exclude-filter instead #564. + Remove broken website link #577. + Fix Apache RAT plugin console warnings. + [Javadoc] Clarify behavior of deprecated CSVFormat#withFirstRecordAsHeader() #2413. + CSVFormat.equals()/hashCode() ignores maxRows (#600). + ExtendedBufferedReader byte tracking leads to an incorrect CSVRecord.getBytePosition() (#601). + CSVFormat.Builder.setQuote() does not refresh quotedNullString (#2447). + Lexer.isDelimiter() accepts a partial multi-character delimiter at EOF (#603). + CSVParser applies characterOffset to bytePosition (#604). + CSVPrinter Reader printing with quote and escape can emit CSV that its parser cannot read back. + CSVParser applies maxRows to record numbers instead of rows produced when setRecordNumber(...) is used. + CSVParser with trackBytes enabled throws on multi-character delimiters containing supplementary Unicode characters. + CSVFormat.Builder.setNullString(String) can build an invalid quoted null string after setQuote(null). + Escape Reader values with quote and escape (#606). + Clear escape delimiter buffer before peek in Lexer.isEscapeDelimiter() (#608, #611). + Escape quote char in printWithEscapes when QuoteMode is NONE (#609). + Quote value starting with comment marker in minimal quote mode (#610). + Escape leading comment marker in printWithEscapes (#614). + Skip byte counting at EOF in ExtendedBufferedReader.read (#615). + Keep quoted empty trailing field with trailingDelimiter (#616). + Evaluate isDelimiter once in nextToken whitespace skip (#618).. + + Add an "Android Compatibility" section to the web site. + Add CSVParser.Builder.setByteOffset(long) (#604). + + Bump org.apache.commons:commons-parent from 85 to 102 #573, #595. + [test] Bump com.opencsv:opencsv from 5.11.2 to 5.12.0 #558. + Bump org.apache.commons:commons-lang3 from 3.18.0 to 3.20.0. + Bump commons-codec:commons-codec from 1.19.0 to 1.22.0. + Bump commons-io:commons-io from 2.20.0 to 2.22.0 #594. + + CSVPrinter.printRecord(Stream) hangs if given a parallel stream. CSVPrinter now uses an internal lock instead of synchronized methods. org.apache.commons.csv.CSVPrinter.printRecords(ResultSet) now writes one record at a time using a lock. - Bump commons-io:commons-io from 2.18.0 to 2.19.0. - Bump org.apache.commons:commons-parent from 81 to 83 #542. - Bump com.opencsv:opencsv from 5.10 to 5.11 #545. + Bump org.apache.commons:commons-parent from 81 to 85 #542. + Bump commons-io:commons-io from 2.18.0 to 2.20.0. + Bump com.opencsv:opencsv from 5.10 to 5.11.2 #545, #551, #553. + Bump org.apache.commons:commons-lang3 from 3.17.0 to 3.18.0 #556. + Bump commons-codec:commons-codec from 1.18.0 to 1.19.0. diff --git a/src/changes/release-notes.vm b/src/changes/release-notes.vm index 5f55012690..5769829552 100644 --- a/src/changes/release-notes.vm +++ b/src/changes/release-notes.vm @@ -6,7 +6,7 @@ ## "License"); you may not use this file except in compliance ## with the License. You may obtain a copy of the License at ## -## http://www.apache.org/licenses/LICENSE-2.0 +## 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 diff --git a/src/conf/checkstyle/checkstyle-suppressions.xml b/src/conf/checkstyle/checkstyle-suppressions.xml index e1a4807ccb..f1eab03c40 100644 --- a/src/conf/checkstyle/checkstyle-suppressions.xml +++ b/src/conf/checkstyle/checkstyle-suppressions.xml @@ -7,7 +7,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/conf/checkstyle/checkstyle.xml b/src/conf/checkstyle/checkstyle.xml index 1010cb262b..4e8691243c 100644 --- a/src/conf/checkstyle/checkstyle.xml +++ b/src/conf/checkstyle/checkstyle.xml @@ -7,7 +7,7 @@ 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 - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/main/java/org/apache/commons/csv/CSVFormat.java b/src/main/java/org/apache/commons/csv/CSVFormat.java index 9bcf9d1890..7145d23d3b 100644 --- a/src/main/java/org/apache/commons/csv/CSVFormat.java +++ b/src/main/java/org/apache/commons/csv/CSVFormat.java @@ -216,7 +216,7 @@ public static Builder create() { .setRecordSeparator(Constants.CRLF) .setIgnoreEmptyLines(true) .setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_ALL); - // @formatter:on + // @formatter:on } /** @@ -614,6 +614,7 @@ public Builder setHeader(final ResultSetMetaData resultSetMetaData) throws SQLEx *

* This method keeps a copy of the input array. *

+ * * @param header the header, {@code null} if disabled, empty if parsed automatically, user-specified otherwise. * @return This instance. */ @@ -695,6 +696,7 @@ public Builder setHeaderComments(final Object... headerComments) { *

* This method keeps a copy of the input array. *

+ * * @param headerComments the headerComments which will be printed by the Printer before the CSV data. * @return This instance. */ @@ -778,8 +780,7 @@ public Builder setMaxRows(final long maxRows) { */ public Builder setNullString(final String nullString) { this.nullString = nullString; - this.quotedNullString = quoteCharacter + nullString + quoteCharacter; - return this; + return setQuotedNullString(); } /** @@ -804,6 +805,12 @@ public Builder setQuote(final Character quoteCharacter) { throw new IllegalArgumentException("The quoteCharacter cannot be a line break"); } this.quoteCharacter = quoteCharacter; + return setQuotedNullString(); + } + + private Builder setQuotedNullString() { + final Character quote = quoteCharacter != null ? quoteCharacter : Constants.DOUBLE_QUOTE_CHAR; + this.quotedNullString = quote + nullString + quote; return this; } @@ -876,6 +883,16 @@ public Builder setTrailingData(final boolean trailingData) { /** * Sets whether to add a trailing delimiter. * + *

+ * When writing, a delimiter is appended after the last value of each record. When reading, the empty field + * that such a trailing delimiter produces is dropped so the output round-trips back to the original record; + * a quoted empty trailing field ({@code ""}) is a real value rather than a trailing delimiter and is kept. + *

+ *

+ * This is unrelated to {@link #setTrailingData(boolean) trailing data}, which controls whether characters + * after the closing quote of an encapsulated value are tolerated when reading. + *

+ * * @param trailingDelimiter whether to add a trailing delimiter. * @return This instance. */ @@ -898,7 +915,7 @@ public Builder setTrim(final boolean trim) { } /** - * Predefines formats. + * Enumerates predefines formats. * * @since 1.2 */ @@ -1475,7 +1492,7 @@ private static boolean isLineBreak(final char c) { * @return true if {@code c} is a line break character (and not null). */ private static boolean isLineBreak(final Character c) { - return c != null && isLineBreak(c.charValue()); // Explicit (un)boxing is intentional + return c != null && isLineBreak(c.charValue()); // Explicit unboxing is intentional } /** Same test as in as {@link String#trim()}. */ @@ -1688,15 +1705,15 @@ public boolean equals(final Object obj) { duplicateHeaderMode == other.duplicateHeaderMode && Objects.equals(escapeCharacter, other.escapeCharacter) && Arrays.equals(headerComments, other.headerComments) && Arrays.equals(headers, other.headers) && ignoreEmptyLines == other.ignoreEmptyLines && ignoreHeaderCase == other.ignoreHeaderCase && - ignoreSurroundingSpaces == other.ignoreSurroundingSpaces && lenientEof == other.lenientEof && - Objects.equals(nullString, other.nullString) && Objects.equals(quoteCharacter, other.quoteCharacter) && - quoteMode == other.quoteMode && Objects.equals(quotedNullString, other.quotedNullString) && - Objects.equals(recordSeparator, other.recordSeparator) && skipHeaderRecord == other.skipHeaderRecord && - trailingData == other.trailingData && trailingDelimiter == other.trailingDelimiter && trim == other.trim; + ignoreSurroundingSpaces == other.ignoreSurroundingSpaces && lenientEof == other.lenientEof && maxRows == other.maxRows && + Objects.equals(nullString, other.nullString) && Objects.equals(quoteCharacter, other.quoteCharacter) && quoteMode == other.quoteMode && + Objects.equals(quotedNullString, other.quotedNullString) && Objects.equals(recordSeparator, other.recordSeparator) && + skipHeaderRecord == other.skipHeaderRecord && trailingData == other.trailingData && trailingDelimiter == other.trailingDelimiter && + trim == other.trim; } private void escape(final char c, final Appendable appendable) throws IOException { - append(escapeCharacter.charValue(), appendable); // Explicit (un)boxing is intentional + append(escapeCharacter.charValue(), appendable); // Explicit unboxing is intentional append(c, appendable); } @@ -1834,7 +1851,7 @@ public DuplicateHeaderMode getDuplicateHeaderMode() { * @return the escape character, may be {@code 0} */ char getEscapeChar() { - return escapeCharacter != null ? escapeCharacter.charValue() : 0; // Explicit (un)boxing is intentional + return escapeCharacter != null ? escapeCharacter.charValue() : 0; // Explicit unboxing is intentional } /** @@ -2005,6 +2022,16 @@ public boolean getTrailingData() { /** * Gets whether to add a trailing delimiter. * + *

+ * When writing, a delimiter is appended after the last value of each record. When reading, the empty field + * that such a trailing delimiter produces is dropped so the output round-trips back to the original record; + * a quoted empty trailing field ({@code ""}) is a real value rather than a trailing delimiter and is kept. + *

+ *

+ * This is unrelated to {@link #getTrailingData() trailing data}, which controls whether characters after the + * closing quote of an encapsulated value are tolerated when reading. + *

+ * * @return whether to add a trailing delimiter. * @since 1.3 */ @@ -2027,9 +2054,10 @@ public int hashCode() { int result = 1; result = prime * result + Arrays.hashCode(headerComments); result = prime * result + Arrays.hashCode(headers); - return prime * result + Objects.hash(allowMissingColumnNames, autoFlush, commentMarker, delimiter, duplicateHeaderMode, escapeCharacter, - ignoreEmptyLines, ignoreHeaderCase, ignoreSurroundingSpaces, lenientEof, nullString, quoteCharacter, quoteMode, quotedNullString, + result = prime * result + Objects.hash(allowMissingColumnNames, autoFlush, commentMarker, delimiter, duplicateHeaderMode, escapeCharacter, + ignoreEmptyLines, ignoreHeaderCase, ignoreSurroundingSpaces, lenientEof, maxRows, nullString, quoteCharacter, quoteMode, quotedNullString, recordSeparator, skipHeaderRecord, trailingData, trailingDelimiter, trim); + return result; } /** @@ -2156,7 +2184,7 @@ private void print(final InputStream inputStream, final Appendable out, final bo } final boolean quoteCharacterSet = isQuoteCharacterSet(); if (quoteCharacterSet) { - append(getQuoteCharacter().charValue(), out); // Explicit (un)boxing is intentional + append(getQuoteCharacter().charValue(), out); // Explicit unboxing is intentional } // Stream the input to the output without reading or holding the whole value in memory. // AppendableOutputStream cannot "close" an Appendable. @@ -2164,7 +2192,7 @@ private void print(final InputStream inputStream, final Appendable out, final bo IOUtils.copy(inputStream, outputStream); } if (quoteCharacterSet) { - append(getQuoteCharacter().charValue(), out); // Explicit (un)boxing is intentional + append(getQuoteCharacter().charValue(), out); // Explicit unboxing is intentional } } @@ -2319,12 +2347,18 @@ private void printWithEscapes(final CharSequence charSeq, final Appendable appen final char[] delimArray = getDelimiterCharArray(); final int delimLength = delimArray.length; final char escape = getEscapeChar(); + final boolean quoteSet = isQuoteCharacterSet(); + final char quote = quoteSet ? getQuoteCharacter().charValue() : 0; + final boolean commentMarkerSet = isCommentMarkerSet(); + final char commentChar = commentMarkerSet ? commentMarker.charValue() : 0; // Explicit unboxing is intentional while (pos < end) { char c = charSeq.charAt(pos); final boolean isDelimiterStart = isDelimiter(c, charSeq, pos, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; - if (isCr || isLf || c == escape || isDelimiterStart) { + // A leading comment marker would be read back as a comment, so escape it. + final boolean isComment = commentMarkerSet && pos == 0 && c == commentChar; + if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart || isComment) { // write out segment up until this char if (pos > start) { appendable.append(charSeq, start, pos); @@ -2363,8 +2397,13 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) final char[] delimArray = getDelimiterCharArray(); final int delimLength = delimArray.length; final char escape = getEscapeChar(); + final boolean quoteSet = isQuoteCharacterSet(); + final char quote = quoteSet ? getQuoteCharacter().charValue() : 0; + final boolean commentMarkerSet = isCommentMarkerSet(); + final char commentChar = commentMarkerSet ? commentMarker.charValue() : 0; // Explicit unboxing is intentional final StringBuilder builder = new StringBuilder(IOUtils.DEFAULT_BUFFER_SIZE); int c; + boolean firstChar = true; final char[] lookAheadBuffer = new char[delimLength - 1]; while (EOF != (c = bufferedReader.read())) { builder.append((char) c); @@ -2374,7 +2413,10 @@ private void printWithEscapes(final Reader reader, final Appendable appendable) final boolean isDelimiterStart = isDelimiter((char) c, test, pos, delimArray, delimLength); final boolean isCr = c == Constants.CR; final boolean isLf = c == Constants.LF; - if (isCr || isLf || c == escape || isDelimiterStart) { + // A leading comment marker would be read back as a comment, so escape it. + final boolean isComment = commentMarkerSet && firstChar && c == commentChar; + firstChar = false; + if (isCr || isLf || c == escape || quoteSet && c == quote || isDelimiterStart || isComment) { // write out segment up until this char if (pos > start) { append(builder.substring(start, pos), appendable); @@ -2413,7 +2455,7 @@ private void printWithQuotes(final Object object, final CharSequence charSeq, fi final int len = charSeq.length(); final char[] delim = getDelimiterCharArray(); final int delimLength = delim.length; - final char quoteChar = getQuoteCharacter().charValue(); // Explicit (un)boxing is intentional + final char quoteChar = getQuoteCharacter().charValue(); // Explicit unboxing is intentional // If escape char not specified, default to the quote char // This avoids having to keep checking whether there is an escape character // at the cost of checking against quote twice @@ -2445,10 +2487,11 @@ private void printWithQuotes(final Object object, final CharSequence charSeq, fi } } else { char c = charSeq.charAt(pos); - if (c <= Constants.COMMENT) { + if (c <= Constants.COMMENT || isCommentMarkerSet() && c == commentMarker.charValue()) { // Some other chars at the start of a value caused the parser to fail, so for now // encapsulate if we start in anything less than '#'. We are being conservative - // by including the default comment char too. + // by including the default comment char and any configured comment marker too, + // which the parser would otherwise read back as a comment line. quote = true; } else { while (pos < len) { @@ -2516,15 +2559,16 @@ private void printWithQuotes(final Reader reader, final Appendable appendable) t printWithEscapes(reader, appendable); return; } - final char quote = getQuoteCharacter().charValue(); // Explicit (un)boxing is intentional + final char quote = getQuoteCharacter().charValue(); // Explicit unboxing is intentional + final char escape = isEscapeCharacterSet() ? getEscapeChar() : quote; // (1) Append opening quote append(quote, appendable); - // (2) Append Reader contents, doubling quotes + // (2) Append Reader contents, doubling quotes and escape characters int c; while (EOF != (c = reader.read())) { append((char) c, appendable); - if (c == quote) { - append(quote, appendable); + if (c == quote || c == escape) { + append((char) c, appendable); } } // (3) Append closing quote @@ -2536,27 +2580,27 @@ public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("Delimiter=<").append(delimiter).append('>'); if (isEscapeCharacterSet()) { - sb.append(' '); + sb.append(Constants.SP); sb.append("Escape=<").append(escapeCharacter).append('>'); } if (isQuoteCharacterSet()) { - sb.append(' '); + sb.append(Constants.SP); sb.append("QuoteChar=<").append(quoteCharacter).append('>'); } if (quoteMode != null) { - sb.append(' '); + sb.append(Constants.SP); sb.append("QuoteMode=<").append(quoteMode).append('>'); } if (isCommentMarkerSet()) { - sb.append(' '); + sb.append(Constants.SP); sb.append("CommentStart=<").append(commentMarker).append('>'); } if (isNullStringSet()) { - sb.append(' '); + sb.append(Constants.SP); sb.append("NullString=<").append(nullString).append('>'); } if (recordSeparator != null) { - sb.append(' '); + sb.append(Constants.SP); sb.append("RecordSeparator=<").append(recordSeparator).append('>'); } if (getIgnoreEmptyLines()) { @@ -2570,11 +2614,11 @@ public String toString() { } sb.append(" SkipHeaderRecord:").append(skipHeaderRecord); if (headerComments != null) { - sb.append(' '); + sb.append(Constants.SP); sb.append("HeaderComments:").append(Arrays.toString(headerComments)); } if (headers != null) { - sb.append(' '); + sb.append(Constants.SP); sb.append("Header:").append(Arrays.toString(headers)); } return sb.toString(); @@ -2602,13 +2646,13 @@ boolean useRow(final long rowNum) { * @throws IllegalArgumentException Throw when any attribute is invalid or inconsistent with other attributes. */ private void validate() throws IllegalArgumentException { - if (quoteCharacter != null && contains(delimiter, quoteCharacter.charValue())) { // Explicit (un)boxing is intentional + if (quoteCharacter != null && contains(delimiter, quoteCharacter.charValue())) { // Explicit unboxing is intentional throw new IllegalArgumentException("The quoteChar character and the delimiter cannot be the same ('" + quoteCharacter + "')"); } - if (escapeCharacter != null && contains(delimiter, escapeCharacter.charValue())) { // Explicit (un)boxing is intentional + if (escapeCharacter != null && contains(delimiter, escapeCharacter.charValue())) { // Explicit unboxing is intentional throw new IllegalArgumentException("The escape character and the delimiter cannot be the same ('" + escapeCharacter + "')"); } - if (commentMarker != null && contains(delimiter, commentMarker.charValue())) { // Explicit (un)boxing is intentional + if (commentMarker != null && contains(delimiter, commentMarker.charValue())) { // Explicit unboxing is intentional throw new IllegalArgumentException("The comment start character and the delimiter cannot be the same ('" + commentMarker + "')"); } if (quoteCharacter != null && quoteCharacter.equals(commentMarker)) { @@ -2786,6 +2830,9 @@ public CSVFormat withEscape(final Character escape) { * .get(); * * + *

Any previously set headers are reset to empty. + * The resulting format will have {@code skipHeaderRecord = true}.

+ * * @return A new CSVFormat that is equal to this but using the first record as header. * @see Builder#setSkipHeaderRecord(boolean) * @see Builder#setHeader(String...) diff --git a/src/main/java/org/apache/commons/csv/CSVParser.java b/src/main/java/org/apache/commons/csv/CSVParser.java index 33ae785675..141eba732c 100644 --- a/src/main/java/org/apache/commons/csv/CSVParser.java +++ b/src/main/java/org/apache/commons/csv/CSVParser.java @@ -154,6 +154,7 @@ public final class CSVParser implements Iterable, Closeable { public static class Builder extends AbstractStreamBuilder { private CSVFormat format; + private long byteOffset = -1; private long characterOffset; private long recordNumber = 1; private boolean trackBytes; @@ -165,17 +166,33 @@ protected Builder() { // empty } - @SuppressWarnings("resource") @Override public CSVParser get() throws IOException { - return new CSVParser(getReader(), format != null ? format : CSVFormat.DEFAULT, characterOffset, recordNumber, getCharset(), trackBytes); + return new CSVParser(this); } /** - * Sets the lexer offset when the parser does not start parsing at the beginning of the source. + * Sets the lexer byte offset when the parser does not start parsing at the beginning of the source. + *

+ * By default, the value is {@code -1}, which reuses the character offset for the byte offset. + *

* - * @param characterOffset the lexer offset. - * @return this instance. + * @param byteOffset the lexer byte offset. + * @return {@code this} instance. + * @see #setCharacterOffset(long) + * @since 1.15.0 + */ + public Builder setByteOffset(final long byteOffset) { + this.byteOffset = byteOffset; + return asThis(); + } + + /** + * Sets the lexer character offset when the parser does not start parsing at the beginning of the source. + * + * @param characterOffset the lexer character offset. + * @return {@code this} instance. + * @see #setByteOffset(long) */ public Builder setCharacterOffset(final long characterOffset) { this.characterOffset = characterOffset; @@ -186,7 +203,7 @@ public Builder setCharacterOffset(final long characterOffset) { * Sets the CSV format. A copy of the given format is kept. * * @param format the CSV format, {@code null} resets to {@link CSVFormat#DEFAULT}. - * @return this instance. + * @return {@code this} instance. */ public Builder setFormat(final CSVFormat format) { this.format = CSVFormat.copy(format); @@ -197,7 +214,7 @@ public Builder setFormat(final CSVFormat format) { * Sets the next record number to assign, defaults to {@code 1}. * * @param recordNumber the next record number to assign. - * @return this instance. + * @return {@code this} instance. */ public Builder setRecordNumber(final long recordNumber) { this.recordNumber = recordNumber; @@ -208,7 +225,7 @@ public Builder setRecordNumber(final long recordNumber) { * Sets whether to enable byte tracking for the parser. * * @param trackBytes {@code true} to enable byte tracking; {@code false} to disable it. - * @return this instance. + * @return {@code this} instance. * @since 1.13.0 */ public Builder setTrackBytes(final boolean trackBytes) { @@ -220,6 +237,7 @@ public Builder setTrackBytes(final boolean trackBytes) { final class CSVRecordIterator implements Iterator { private CSVRecord current; + private long recordCount; /** * Gets the next record or null at the end of stream or max rows read. @@ -230,8 +248,11 @@ final class CSVRecordIterator implements Iterator { */ private CSVRecord getNextRecord() { CSVRecord record = null; - if (format.useRow(recordNumber + 1)) { + if (format.useRow(recordCount + 1)) { record = Uncheck.get(CSVParser.this::nextRecord); + if (record != null) { + recordCount++; + } } return record; } @@ -269,6 +290,7 @@ public void remove() { throw new UnsupportedOperationException(); } } + /** * Header information based on name and position. */ @@ -465,6 +487,12 @@ public static CSVParser parse(final URL url, final Charset charset, final CSVFor */ private long recordNumber; + /** + * Lexer offset when the parser does not start parsing at the beginning of the source. Usually used in combination + * with {@link #recordNumber}. + */ + private final long byteOffset; + /** * Lexer offset when the parser does not start parsing at the beginning of the source. Usually used in combination * with {@link #recordNumber}. @@ -473,6 +501,23 @@ public static CSVParser parse(final URL url, final Charset charset, final CSVFor private final Token reusableToken = new Token(); + /** + * Constructs a new instance from a builder. + * + * @param builder The source builder. + * @throws IOException if an I/O error occurs. + */ + @SuppressWarnings("resource") // Lexer manages ExtendedBufferedReader. + private CSVParser(final Builder builder) throws IOException { + this.format = (builder.format != null ? builder.format : CSVFormat.DEFAULT).copy(); + this.lexer = new Lexer(format, new ExtendedBufferedReader(builder.getReader(), builder.getCharset(), builder.trackBytes)); + this.csvRecordIterator = new CSVRecordIterator(); + this.headers = createHeaders(); + this.byteOffset = builder.byteOffset != -1 ? builder.byteOffset : builder.characterOffset; + this.characterOffset = builder.characterOffset; + this.recordNumber = builder.recordNumber - 1; + } + /** * Constructs a new instance using the given {@link CSVFormat}. * @@ -523,51 +568,21 @@ public CSVParser(final Reader reader, final CSVFormat format) throws IOException */ @Deprecated public CSVParser(final Reader reader, final CSVFormat format, final long characterOffset, final long recordNumber) throws IOException { - this(reader, format, characterOffset, recordNumber, null, false); - } - - /** - * Constructs a new instance using the given {@link CSVFormat}. - * - *

- * If you do not read all records from the given {@code reader}, you should call {@link #close()} on the parser, - * unless you close the {@code reader}. - *

- * - * @param reader - * a Reader containing CSV-formatted input. Must not be null. - * @param format - * the CSVFormat used for CSV parsing. Must not be null. - * @param characterOffset - * Lexer offset when the parser does not start parsing at the beginning of the source. - * @param recordNumber - * The next record number to assign. - * @param charset - * The character encoding to be used for the reader when enableByteTracking is true. - * @param trackBytes - * {@code true} to enable byte tracking for the parser; {@code false} to disable it. - * @throws IllegalArgumentException - * If the parameters of the format are inconsistent or if either the reader or format is null. - * @throws IOException - * If there is a problem reading the header or skipping the first record. - * @throws CSVException Thrown on invalid CSV input data. - */ - private CSVParser(final Reader reader, final CSVFormat format, final long characterOffset, final long recordNumber, - final Charset charset, final boolean trackBytes) - throws IOException { - Objects.requireNonNull(reader, "reader"); - Objects.requireNonNull(format, "format"); - this.format = format.copy(); - this.lexer = new Lexer(format, new ExtendedBufferedReader(reader, charset, trackBytes)); - this.csvRecordIterator = new CSVRecordIterator(); - this.headers = createHeaders(); - this.characterOffset = characterOffset; - this.recordNumber = recordNumber - 1; + // @formatter:off + this(builder() + .setReader(reader) + .setFormat(Objects.requireNonNull(format, "format")) // requireNonNull for full compatibility + .setCharacterOffset(characterOffset) + .setRecordNumber(recordNumber) + .setCharset((Charset) null).setTrackBytes(false)); + // @formatter:off } private void addRecordValue(final boolean lastRecord) { final String input = format.trim(reusableToken.content.toString()); - if (lastRecord && input.isEmpty() && format.getTrailingDelimiter()) { + // Only drop the empty field produced by an actual trailing delimiter. A quoted empty + // field ("") is a real value, not a trailing delimiter, so it must be kept. + if (lastRecord && input.isEmpty() && format.getTrailingDelimiter() && !reusableToken.isQuoted) { return; } recordList.add(handleNull(input)); @@ -641,7 +656,7 @@ private Headers createHeaders() throws IOException { } observedMissing |= blankHeader; if (header != null) { - headerMap.put(header, Integer.valueOf(i)); // Explicit (un)boxing is intentional + headerMap.put(header, Integer.valueOf(i)); // Explicit boxing is intentional if (headerNames == null) { headerNames = new ArrayList<>(headerRecord.length); } @@ -886,7 +901,7 @@ CSVRecord nextRecord() throws IOException { recordList.clear(); StringBuilder sb = null; final long startCharPosition = lexer.getCharacterPosition() + characterOffset; - final long startBytePosition = lexer.getBytesRead() + this.characterOffset; + final long startBytePosition = lexer.getBytesRead() + byteOffset; do { reusableToken.reset(); lexer.nextToken(reusableToken); @@ -919,12 +934,10 @@ CSVRecord nextRecord() throws IOException { throw new CSVException("Unexpected Token type: %s", reusableToken.type); } } while (reusableToken.type == TOKEN); - if (!recordList.isEmpty()) { recordNumber++; - final String comment = Objects.toString(sb, null); - result = new CSVRecord(this, recordList.toArray(Constants.EMPTY_STRING_ARRAY), comment, - recordNumber, startCharPosition, startBytePosition); + result = new CSVRecord(this, recordList.toArray(Constants.EMPTY_STRING_ARRAY), Objects.toString(sb, null), recordNumber, startCharPosition, + startBytePosition); } return result; } diff --git a/src/main/java/org/apache/commons/csv/CSVPrinter.java b/src/main/java/org/apache/commons/csv/CSVPrinter.java index 68d8d40eff..a7048fd625 100644 --- a/src/main/java/org/apache/commons/csv/CSVPrinter.java +++ b/src/main/java/org/apache/commons/csv/CSVPrinter.java @@ -40,8 +40,6 @@ import org.apache.commons.io.function.IOStream; -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - /** * Prints values in a {@link CSVFormat CSV format}. * @@ -153,7 +151,6 @@ public void close(final boolean flush) throws IOException { * @throws IOException * If an I/O error occurs */ - @SuppressFBWarnings(value = "AT_NONATOMIC_OPERATIONS_ON_SHARED_VARIABLE", justification = "https://github.com/spotbugs/spotbugs/issues/3428") private void endOfRecord() throws IOException { println(); recordCount++; @@ -208,19 +205,6 @@ public void print(final Object value) throws IOException { } } - /** - * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed. - * - * @param value - * value to be output. - * @throws IOException - * If an I/O error occurs - */ - private void printRaw(final Object value) throws IOException { - format.print(value, appendable, newRecord); - newRecord = false; - } - /** * Prints a comment on a new line among the delimiter-separated values. * @@ -251,7 +235,7 @@ public void printComment(final String comment) throws IOException { if (!newRecord) { println(); } - appendable.append(format.getCommentMarker().charValue()); // Explicit (un)boxing is intentional + appendable.append(format.getCommentMarker().charValue()); // Explicit unboxing is intentional appendable.append(SP); for (int i = 0; i < comment.length(); i++) { final char c = comment.charAt(i); @@ -263,7 +247,7 @@ public void printComment(final String comment) throws IOException { // falls-through: break intentionally excluded. case LF: println(); - appendable.append(format.getCommentMarker().charValue()); // Explicit (un)boxing is intentional + appendable.append(format.getCommentMarker().charValue()); // Explicit unboxing is intentional appendable.append(SP); break; default: @@ -313,6 +297,19 @@ public void println() throws IOException { } } + /** + * Prints the string as the next value on the line. The value will be escaped or encapsulated as needed. + * + * @param value + * value to be output. + * @throws IOException + * If an I/O error occurs + */ + private void printRaw(final Object value) throws IOException { + format.print(value, appendable, newRecord); + newRecord = false; + } + /** * Prints the given values as a single record of delimiter-separated values followed by the record separator. * diff --git a/src/main/java/org/apache/commons/csv/CSVRecord.java b/src/main/java/org/apache/commons/csv/CSVRecord.java index c04725af7c..8dab14d907 100644 --- a/src/main/java/org/apache/commons/csv/CSVRecord.java +++ b/src/main/java/org/apache/commons/csv/CSVRecord.java @@ -55,13 +55,13 @@ public final class CSVRecord implements Serializable, Iterable { */ private final long bytePosition; - /** The accumulated comments (if any) */ + /** The accumulated comments (if any). */ private final String comment; /** The record number. */ private final long recordNumber; - /** The values of the record */ + /** The values of the record. */ private final String[] values; /** The parser that originates this record. This is not serialized. */ @@ -114,9 +114,9 @@ public String get(final int i) { * the name of the column to be retrieved. * @return the column value, maybe null depending on {@link CSVFormat#getNullString()}. * @throws IllegalStateException - * if no header mapping was provided + * if no header mapping was provided. * @throws IllegalArgumentException - * if {@code name} is not mapped or if the record is inconsistent + * if {@code name} is not mapped or if the record is inconsistent. * @see #isMapped(String) * @see #isConsistent() * @see #getParser() @@ -125,20 +125,18 @@ public String get(final int i) { public String get(final String name) { final Map headerMap = getHeaderMapRaw(); if (headerMap == null) { - throw new IllegalStateException( - "No header mapping was specified, the record values can't be accessed by name"); + throw new IllegalStateException("No header mapping was specified, the record values can't be accessed by name"); } final Integer index = headerMap.get(name); if (index == null) { - throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name, - headerMap.keySet())); + throw new IllegalArgumentException(String.format("Mapping for %s not found, expected one of %s", name, headerMap.keySet())); } try { - return values[index.intValue()]; // Explicit (un)boxing is intentional + return values[index.intValue()]; // Explicit unboxing is intentional } catch (final ArrayIndexOutOfBoundsException e) { - throw new IllegalArgumentException(String.format( - "Index for header '%s' is %d but CSVRecord only has %d values!", name, index, - Integer.valueOf(values.length))); // Explicit (un)boxing is intentional + // Explicit boxing is intentional + throw new IllegalArgumentException( + String.format("Index for header '%s' is %d but CSVRecord only has %d values!", name, index, Integer.valueOf(values.length))); } } @@ -214,7 +212,7 @@ public long getRecordNumber() { * If there is no following record (that is, the comment is at EOF), * then the comment will be ignored. * - * @return true if this record has a comment, false otherwise + * @return true if this record has a comment, false otherwise. * @since 1.3 */ public boolean hasComment() { @@ -229,7 +227,7 @@ public boolean hasComment() { * test but still produce parsable files. *

* - * @return true of this record is valid, false if not + * @return true of this record is valid, false if not. */ public boolean isConsistent() { final Map headerMap = getHeaderMapRaw(); @@ -252,8 +250,8 @@ public boolean isMapped(final String name) { * Checks whether a column with a given index has a value. * * @param index - * a column index (0-based) - * @return whether a column with a given index has a value + * a column index (0-based). + * @return whether a column with a given index has a value. */ public boolean isSet(final int index) { return 0 <= index && index < values.length; @@ -264,10 +262,10 @@ public boolean isSet(final int index) { * * @param name * the name of the column to be retrieved. - * @return whether a given column is mapped and has a value + * @return whether a given column is mapped and has a value. */ public boolean isSet(final String name) { - return isMapped(name) && getHeaderMapRaw().get(name).intValue() < values.length; // Explicit (un)boxing is intentional + return isMapped(name) && getHeaderMapRaw().get(name).intValue() < values.length; // Explicit unboxing is intentional } /** @@ -283,7 +281,7 @@ public Iterator iterator() { /** * Puts all values of this record into the given Map. * - * @param the map type + * @param The map type. * @param map The Map to populate. * @return the given map. * @since 1.9.0 @@ -354,14 +352,13 @@ public Map toMap() { */ @Override public String toString() { - return "CSVRecord [comment='" + comment + "', recordNumber=" + recordNumber + ", values=" + - Arrays.toString(values) + "]"; + return "CSVRecord [comment='" + comment + "', recordNumber=" + recordNumber + ", values=" + Arrays.toString(values) + "]"; } /** - * Gets the values for this record. This is not a copy. + * Gets the values for this record. This is not a copy. * - * @return the values for this record. + * @return the values for this record, never null. * @since 1.10.0 */ public String[] values() { diff --git a/src/main/java/org/apache/commons/csv/Constants.java b/src/main/java/org/apache/commons/csv/Constants.java index 0b9476e1cd..9dd276eccc 100644 --- a/src/main/java/org/apache/commons/csv/Constants.java +++ b/src/main/java/org/apache/commons/csv/Constants.java @@ -40,7 +40,7 @@ final class Constants { /** RFC 4180 defines line breaks as CRLF. */ static final String CRLF = "\r\n"; - static final Character DOUBLE_QUOTE_CHAR = Character.valueOf('"'); // Explicit (un)boxing is intentional. + static final Character DOUBLE_QUOTE_CHAR = Character.valueOf('"'); // Explicit boxing is intentional. static final String EMPTY = ""; diff --git a/src/main/java/org/apache/commons/csv/DuplicateHeaderMode.java b/src/main/java/org/apache/commons/csv/DuplicateHeaderMode.java index 01989d6640..8087f16eeb 100644 --- a/src/main/java/org/apache/commons/csv/DuplicateHeaderMode.java +++ b/src/main/java/org/apache/commons/csv/DuplicateHeaderMode.java @@ -20,7 +20,7 @@ package org.apache.commons.csv; /** - * Determines how duplicate header fields should be handled + * Enumerates how duplicate header fields should be handled * if {@link CSVFormat.Builder#setHeader(Class)} is not null. * * @since 1.10.0 diff --git a/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java b/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java index 8c0a034a22..20c1ef5444 100644 --- a/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java +++ b/src/main/java/org/apache/commons/csv/ExtendedBufferedReader.java @@ -37,26 +37,30 @@ /** * A special buffered reader which supports sophisticated read access. *

- * In particular the reader supports a look-ahead option, which allows you to see the next char returned by - * {@link #read()}. This reader also tracks how many characters have been read with {@link #getPosition()}. + * In particular the reader supports a look-ahead option, which allows you to see the next char returned by {@link #read()}. This reader also tracks how many + * characters have been read with {@link #getPosition()}. *

*/ final class ExtendedBufferedReader extends UnsynchronizedBufferedReader { /** The last char returned */ private int lastChar = UNDEFINED; + private int lastCharMark = UNDEFINED; /** The count of EOLs (CR/LF/CRLF) seen so far */ private long lineNumber; + private long lineNumberMark; /** The position, which is the number of characters read so far */ private long position; + private long positionMark; /** The number of bytes read so far. */ private long bytesRead; + private long bytesReadMark; /** Encoder for calculating the number of bytes for each character read. */ @@ -70,12 +74,11 @@ final class ExtendedBufferedReader extends UnsynchronizedBufferedReader { } /** - * Constructs a new instance with the specified reader, character set, - * and byte tracking option. Initializes an encoder if byte tracking is enabled - * and a character set is provided. + * Constructs a new instance with the specified reader, character set, and byte tracking option. Initializes an encoder if byte tracking is enabled and a + * character set is provided. * - * @param reader the reader supports a look-ahead option. - * @param charset the character set for encoding, or {@code null} if not applicable. + * @param reader the reader supports a look-ahead option. + * @param charset the character set for encoding, or {@code null} if not applicable. * @param trackBytes {@code true} to enable byte tracking; {@code false} to disable it. */ ExtendedBufferedReader(final Reader reader, final Charset charset, final boolean trackBytes) { @@ -86,8 +89,7 @@ final class ExtendedBufferedReader extends UnsynchronizedBufferedReader { /** * Closes the stream. * - * @throws IOException - * If an I/O error occurs + * @throws IOException If an I/O error occurs */ @Override public void close() throws IOException { @@ -105,26 +107,35 @@ long getBytesRead() { return this.bytesRead; } + private long getEncodedCharLength(final char[] buf, final int offset, final int length) throws CharacterCodingException { + long len = 0; + int previous = lastChar; + for (int i = offset; i < offset + length; i++) { + len += getEncodedCharLength(previous, buf[i]); + previous = buf[i]; + } + return len; + } + /** - * Gets the byte length of the given character based on the the original Unicode - * specification, which defined characters as fixed-width 16-bit entities. + * Gets the byte length of the given character based on the original Unicode specification, which defined characters as fixed-width 16-bit entities. *

* The Unicode characters are divided into two main ranges: *

    - *
  • U+0000 to U+FFFF (Basic Multilingual Plane, BMP): - *
      - *
    • Represented using a single 16-bit {@code char}.
    • - *
    • Includes UTF-8 encodings of 1-byte, 2-byte, and some 3-byte characters.
    • - *
    - *
  • - *
  • U+10000 to U+10FFFF (Supplementary Characters): - *
      - *
    • Represented as a pair of {@code char}s:
    • - *
    • The first {@code char} is from the high-surrogates range (\uD800-\uDBFF).
    • - *
    • The second {@code char} is from the low-surrogates range (\uDC00-\uDFFF).
    • - *
    • Includes UTF-8 encodings of some 3-byte characters and all 4-byte characters.
    • - *
    - *
  • + *
  • U+0000 to U+FFFF (Basic Multilingual Plane, BMP): + *
      + *
    • Represented using a single 16-bit {@code char}.
    • + *
    • Includes UTF-8 encodings of 1-byte, 2-byte, and some 3-byte characters.
    • + *
    + *
  • + *
  • U+10000 to U+10FFFF (Supplementary Characters): + *
      + *
    • Represented as a pair of {@code char}s:
    • + *
    • The first {@code char} is from the high-surrogates range (\uD800-\uDBFF).
    • + *
    • The second {@code char} is from the low-surrogates range (\uDC00-\uDFFF).
    • + *
    • Includes UTF-8 encodings of some 3-byte characters and all 4-byte characters.
    • + *
    + *
  • *
* * @param current the current character to process. @@ -132,26 +143,29 @@ long getBytesRead() { * @throws CharacterCodingException if the character cannot be encoded. */ private int getEncodedCharLength(final int current) throws CharacterCodingException { + return getEncodedCharLength(lastChar, current); + } + + private int getEncodedCharLength(final int previous, final int current) throws CharacterCodingException { final char cChar = (char) current; - final char lChar = (char) lastChar; + final char lChar = (char) previous; if (!Character.isSurrogate(cChar)) { return encoder.encode(CharBuffer.wrap(new char[] { cChar })).limit(); } if (Character.isHighSurrogate(cChar)) { // Move on to the next char (low surrogate) return 0; - } else if (Character.isSurrogatePair(lChar, cChar)) { + } + if (Character.isSurrogatePair(lChar, cChar)) { return encoder.encode(CharBuffer.wrap(new char[] { lChar, cChar })).limit(); - } else { - throw new CharacterCodingException(); } + throw new CharacterCodingException(); } /** - * Returns the last character that was read as an integer (0 to 65535). This will be the last character returned by - * any of the read methods. This will not include a character read using the {@link #peek()} method. If no - * character has been read then this will return {@link Constants#UNDEFINED}. If the end of the stream was reached - * on the last read then this will return {@link IOUtils#EOF}. + * Returns the last character that was read as an integer (0 to 65535). This will be the last character returned by any of the read methods. This will not + * include a character read using the {@link #peek()} method. If no character has been read then this will return {@link Constants#UNDEFINED}. If the end of + * the stream was reached on the last read then this will return {@link IOUtils#EOF}. * * @return the last character that was read */ @@ -193,11 +207,10 @@ public void mark(final int readAheadLimit) throws IOException { @Override public int read() throws IOException { final int current = super.read(); - if (current == CR || current == LF && lastChar != CR || - current == EOF && lastChar != CR && lastChar != LF && lastChar != EOF) { + if (current == CR || current == LF && lastChar != CR || current == EOF && lastChar != CR && lastChar != LF && lastChar != EOF) { lineNumber++; } - if (encoder != null) { + if (encoder != null && current != EOF) { this.bytesRead += getEncodedCharLength(current); } lastChar = current; @@ -211,6 +224,9 @@ public int read(final char[] buf, final int offset, final int length) throws IOE return 0; } final int len = super.read(buf, offset, length); + if (encoder != null && len > 0) { + this.bytesRead += getEncodedCharLength(buf, offset, len); + } if (len > 0) { for (int i = offset; i < offset + len; i++) { final char ch = buf[i]; @@ -231,8 +247,7 @@ public int read(final char[] buf, final int offset, final int length) throws IOE } /** - * Gets the next line, dropping the line terminator(s). This method should only be called when processing a - * comment, otherwise, information can be lost. + * Gets the next line, dropping the line terminator(s). This method should only be called when processing a comment, otherwise, information can be lost. *

* Increments {@link #lineNumber} and updates {@link #position}. *

@@ -272,5 +287,4 @@ public void reset() throws IOException { bytesRead = bytesReadMark; super.reset(); } - } diff --git a/src/main/java/org/apache/commons/csv/Lexer.java b/src/main/java/org/apache/commons/csv/Lexer.java index 0e5f368665..fe964480a4 100644 --- a/src/main/java/org/apache/commons/csv/Lexer.java +++ b/src/main/java/org/apache/commons/csv/Lexer.java @@ -23,6 +23,7 @@ import java.io.Closeable; import java.io.IOException; +import java.util.Arrays; import org.apache.commons.io.IOUtils; @@ -68,8 +69,8 @@ final class Lexer implements Closeable { /** * Appends the next escaped character to the token's content. * - * @param token the current token - * @throws IOException on stream access error + * @param token the current token. + * @throws IOException on stream access error. * @throws CSVException Thrown on invalid input. */ private void appendNextEscapedCharacterToToken(final Token token) throws IOException { @@ -89,7 +90,7 @@ private void appendNextEscapedCharacterToToken(final Token token) throws IOExcep * Closes resources. * * @throws IOException - * If an I/O error occurs + * If an I/O error occurs. */ @Override public void close() throws IOException { @@ -97,27 +98,27 @@ public void close() throws IOException { } /** - * Gets the number of bytes read + * Gets the number of bytes read. * - * @return the number of bytes read + * @return the number of bytes read. */ long getBytesRead() { return reader.getBytesRead(); } /** - * Returns the current character position + * Gets the current character position. * - * @return the current character position + * @return the current character position. */ long getCharacterPosition() { return reader.getPosition(); } /** - * Returns the current line number + * Gets the current line number. * - * @return the current line number + * @return the current line number. */ long getCurrentLineNumber() { return reader.getLineNumber(); @@ -136,7 +137,7 @@ boolean isCommentStart(final int ch) { } /** - * Determine whether the next characters constitute a delimiter through {@link ExtendedBufferedReader#peek(char[])}. + * Tests whether the next characters constitute a delimiter through {@link ExtendedBufferedReader#peek(char[])}. * * @param ch * the current character. @@ -152,6 +153,7 @@ boolean isDelimiter(final int ch) throws IOException { isLastTokenDelimiter = true; return true; } + Arrays.fill(delimiterBuf, '\0'); reader.peek(delimiterBuf); for (int i = 0; i < delimiterBuf.length; i++) { if (delimiterBuf[i] != delimiter[i + 1]) { @@ -190,6 +192,7 @@ boolean isEscape(final int ch) { * @throws IOException If an I/O error occurs. */ boolean isEscapeDelimiter() throws IOException { + Arrays.fill(escapeDelimiterBuf, '\0'); reader.peek(escapeDelimiterBuf); if (escapeDelimiterBuf[0] != delimiter[0]) { return false; @@ -214,7 +217,7 @@ boolean isQuoteChar(final int ch) { /** * Tests if the current character represents the start of a line: a CR, LF, or is at the start of the file. * - * @param ch the character to check + * @param ch the character to check. * @return true if the character is at the start of a line. */ boolean isStartOfLine(final int ch) { @@ -274,15 +277,22 @@ Token nextToken(final Token token) throws IOException { } // Important: make sure a new char gets consumed in each iteration while (token.type == Token.Type.INVALID) { + // isDelimiter consumes the trailing characters of a multi-character delimiter as a side effect, so it must + // only be evaluated once per character. Remember a match found while skipping whitespace below. + boolean delimiter = false; // ignore whitespaces at beginning of a token if (ignoreSurroundingSpaces) { - while (Character.isWhitespace((char) c) && !isDelimiter(c) && !eol) { + while (Character.isWhitespace((char) c) && !eol) { + if (isDelimiter(c)) { + delimiter = true; + break; + } c = reader.read(); eol = readEndOfLine(c); } } // ok, start of token reached: encapsulated, or token - if (isDelimiter(c)) { + if (delimiter || isDelimiter(c)) { // empty token return TOKEN("") token.type = Token.Type.TOKEN; } else if (eol) { @@ -340,7 +350,6 @@ private Token parseEncapsulatedToken(final Token token) throws IOException { int c; while (true) { c = reader.read(); - if (isQuoteChar(c)) { if (isQuoteChar(reader.peek())) { // double or escaped encapsulator -> add single encapsulator to token @@ -401,10 +410,10 @@ private Token parseEncapsulatedToken(final Token token) throws IOException { *
  • An unescaped delimiter has been reached (TOKEN)
  • * * - * @param token the current token - * @param ch the current character - * @return the filled token - * @throws IOException on stream access error + * @param token the current token. + * @param ch the current character. + * @return the filled token. + * @throws IOException on stream access error. * @throws CSVException Thrown on invalid input. */ private Token parseSimpleToken(final Token token, final int ch) throws IOException { @@ -443,7 +452,7 @@ private Token parseSimpleToken(final Token token, final int ch) throws IOExcepti /** * Greedily accepts \n, \r and \r\n This checker consumes silently the second control-character... * - * @return true if the given or next character is a line-terminator + * @return true if the given or next character is a line-terminator. */ boolean readEndOfLine(final int ch) throws IOException { // check if we have \r\n... diff --git a/src/main/java/org/apache/commons/csv/QuoteMode.java b/src/main/java/org/apache/commons/csv/QuoteMode.java index d9c032ffc4..ae64ab4863 100644 --- a/src/main/java/org/apache/commons/csv/QuoteMode.java +++ b/src/main/java/org/apache/commons/csv/QuoteMode.java @@ -19,7 +19,9 @@ package org.apache.commons.csv; /** - * Defines quoting behavior. + * Enumerates quoting behavior. + * + * @see CSVFormat.Builder#setQuoteMode(QuoteMode) */ public enum QuoteMode { diff --git a/src/main/java/org/apache/commons/csv/Token.java b/src/main/java/org/apache/commons/csv/Token.java index 17eb4c77c3..87af335678 100644 --- a/src/main/java/org/apache/commons/csv/Token.java +++ b/src/main/java/org/apache/commons/csv/Token.java @@ -30,6 +30,7 @@ final class Token { enum Type { + /** Token has no valid content, that is, is in its initialized state. */ INVALID, diff --git a/src/media/commons-logo-component-100.xcf b/src/media/commons-logo-component-100.xcf new file mode 100644 index 0000000000..77d92f2779 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 0000000000..3670221da7 Binary files /dev/null and b/src/media/commons-logo-component.xcf differ diff --git a/src/media/logo.png b/src/media/logo.png new file mode 100644 index 0000000000..93bb6c0148 Binary files /dev/null and b/src/media/logo.png differ diff --git a/src/site/resources/images/logo.png b/src/site/resources/images/logo.png index 77e721d2c7..93bb6c0148 100644 Binary files a/src/site/resources/images/logo.png and b/src/site/resources/images/logo.png differ diff --git a/src/site/resources/images/logo.xcf b/src/site/resources/images/logo.xcf deleted file mode 100644 index 98ff21ec1d..0000000000 Binary files a/src/site/resources/images/logo.xcf and /dev/null differ diff --git a/src/site/resources/pmd/pmd-ruleset.xml b/src/site/resources/pmd/pmd-ruleset.xml index 5acc764021..74e41f991d 100644 --- a/src/site/resources/pmd/pmd-ruleset.xml +++ b/src/site/resources/pmd/pmd-ruleset.xml @@ -7,7 +7,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, @@ -59,7 +59,6 @@ - diff --git a/src/site/resources/spotbugs/spotbugs-exclude-filter.xml b/src/site/resources/spotbugs/spotbugs-exclude-filter.xml index a7364d40db..79c57d3ae4 100644 --- a/src/site/resources/spotbugs/spotbugs-exclude-filter.xml +++ b/src/site/resources/spotbugs/spotbugs-exclude-filter.xml @@ -6,7 +6,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, @@ -54,5 +54,12 @@ + + + + + + + diff --git a/src/site/site.xml b/src/site/site.xml index 86c1afa1eb..232c2056c5 100644 --- a/src/site/site.xml +++ b/src/site/site.xml @@ -7,7 +7,7 @@ (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/site/xdoc/download_csv.xml b/src/site/xdoc/download_csv.xml index b5e7881dc1..151c3f69ec 100644 --- a/src/site/xdoc/download_csv.xml +++ b/src/site/xdoc/download_csv.xml @@ -115,32 +115,32 @@ limitations under the License.

    -
    +
    - - - + + + - - - + + +
    commons-csv-1.14.0-bin.tar.gzsha512pgpcommons-csv-1.14.1-bin.tar.gzsha512pgp
    commons-csv-1.14.0-bin.zipsha512pgpcommons-csv-1.14.1-bin.zipsha512pgp
    - - - + + + - - - + + +
    commons-csv-1.14.0-src.tar.gzsha512pgpcommons-csv-1.14.1-src.tar.gzsha512pgp
    commons-csv-1.14.0-src.zipsha512pgpcommons-csv-1.14.1-src.zipsha512pgp
    diff --git a/src/site/xdoc/index.xml b/src/site/xdoc/index.xml index 491a384b45..ac5b8cfa9f 100644 --- a/src/site/xdoc/index.xml +++ b/src/site/xdoc/index.xml @@ -7,7 +7,7 @@ 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 - http://www.apache.org/licenses/LICENSE-2.0 + 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, @@ -20,13 +20,14 @@ limitations under the License. Home Apache Commons Team + + + -

    Commons CSV reads and writes files in variations of the Comma Separated Value (CSV) format.

    Read the documentation starting with the Javadoc Overview.

    -

    An overview of the functionality is provided in the @@ -45,7 +46,6 @@ The git repository can be browsed.

    -
    -

    The latest code can be checked out from our git repository at https://gitbox.apache.org/repos/asf/commons-csv.git. You can build the component using Apache Maven using mvn clean package.

    - +
    +

    + Apache Commons CSV requires Java 8 or above. +

    + + + + + + + + + + + + + + + +
    Commons CSVJavaAndroid
    1.10.0+8Android 7.0 (API level 24)
    +

    The commons developer mailing list is the main channel of communication for contributors. Please remember that the lists are shared between all commons components, so prefix your email by [csv].

    @@ -84,7 +103,6 @@ For previous releases, see the TagList report.

    If you'd like to offer up pull requests via GitHub rather than applying patches to JIRA, we have a GitHub mirror.

    -

    The commons mailing lists act as the main support forum. @@ -98,14 +116,12 @@ For previous releases, see the

    Commons CSV was started to unify a common and simple interface for reading and writing CSV files under an ASL license. It has been bootstrapped by a code donation from Netcetera in Switzerland. There are three pre-existing BSD compatible CSV parsers which this component will hopefully make redundant (authors willing):

    In addition to the code from Netcetera (org.apache.commons.csv), Martin van den Bemt has added an additional writer API.

    Other CSV implementations:

    @@ -113,7 +129,5 @@ For previous releases, see the Super CSV
    - - diff --git a/src/site/xdoc/security.xml b/src/site/xdoc/security.xml index ab00560494..47edf5d116 100644 --- a/src/site/xdoc/security.xml +++ b/src/site/xdoc/security.xml @@ -47,5 +47,10 @@

    None.

    +
    +

    + For information about safe deserialization, please see Safe Deserialization. +

    +
    \ No newline at end of file diff --git a/src/site/xdoc/user-guide.xml b/src/site/xdoc/user-guide.xml index 64d9a40397..d5a1f26850 100644 --- a/src/site/xdoc/user-guide.xml +++ b/src/site/xdoc/user-guide.xml @@ -7,7 +7,7 @@ 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 - http://www.apache.org/licenses/LICENSE-2.0 + 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, diff --git a/src/test/java/org/apache/commons/csv/CSVDuplicateHeaderTest.java b/src/test/java/org/apache/commons/csv/CSVDuplicateHeaderTest.java index 124e9efce6..2f518a1206 100644 --- a/src/test/java/org/apache/commons/csv/CSVDuplicateHeaderTest.java +++ b/src/test/java/org/apache/commons/csv/CSVDuplicateHeaderTest.java @@ -19,13 +19,16 @@ package org.apache.commons.csv; +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 java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -34,7 +37,7 @@ * Tests parsing of duplicate column names in a CSV header. * The test verifies that headers are consistently handled by CSVFormat and CSVParser. */ -public class CSVDuplicateHeaderTest { +class CSVDuplicateHeaderTest { /** * Return test cases for duplicate header data for use in CSVFormat. @@ -272,7 +275,7 @@ static Stream duplicateHeaderData() { */ @ParameterizedTest @MethodSource(value = {"duplicateHeaderAllowsMissingColumnsNamesData"}) - public void testCSVFormat(final DuplicateHeaderMode duplicateHeaderMode, + void testCSVFormat(final DuplicateHeaderMode duplicateHeaderMode, final boolean allowMissingColumnNames, final boolean ignoreHeaderCase, final String[] headers, @@ -285,11 +288,11 @@ public void testCSVFormat(final DuplicateHeaderMode duplicateHeaderMode, .setHeader(headers); if (valid) { final CSVFormat format = builder.get(); - Assertions.assertEquals(duplicateHeaderMode, format.getDuplicateHeaderMode(), "DuplicateHeaderMode"); - Assertions.assertEquals(allowMissingColumnNames, format.getAllowMissingColumnNames(), "AllowMissingColumnNames"); - Assertions.assertArrayEquals(headers, format.getHeader(), "Header"); + assertEquals(duplicateHeaderMode, format.getDuplicateHeaderMode(), "DuplicateHeaderMode"); + assertEquals(allowMissingColumnNames, format.getAllowMissingColumnNames(), "AllowMissingColumnNames"); + assertArrayEquals(headers, format.getHeader(), "Header"); } else { - Assertions.assertThrows(IllegalArgumentException.class, builder::get); + assertThrows(IllegalArgumentException.class, builder::get); } } @@ -305,7 +308,7 @@ public void testCSVFormat(final DuplicateHeaderMode duplicateHeaderMode, */ @ParameterizedTest @MethodSource(value = {"duplicateHeaderData"}) - public void testCSVParser(final DuplicateHeaderMode duplicateHeaderMode, + void testCSVParser(final DuplicateHeaderMode duplicateHeaderMode, final boolean allowMissingColumnNames, final boolean ignoreHeaderCase, final String[] headers, @@ -327,10 +330,10 @@ public void testCSVParser(final DuplicateHeaderMode duplicateHeaderMode, try (CSVParser parser = CSVParser.parse(input, format)) { // Parser ignores null headers final List expected = Arrays.stream(headers).filter(s -> s != null).collect(Collectors.toList()); - Assertions.assertEquals(expected, parser.getHeaderNames(), "HeaderNames"); + assertEquals(expected, parser.getHeaderNames(), "HeaderNames"); } } else { - Assertions.assertThrows(IllegalArgumentException.class, () -> CSVParser.parse(input, format)); + assertThrows(IllegalArgumentException.class, () -> CSVParser.parse(input, format)); } } } diff --git a/src/test/java/org/apache/commons/csv/CSVFileParserTest.java b/src/test/java/org/apache/commons/csv/CSVFileParserTest.java index fd989779dd..e74d0e6884 100644 --- a/src/test/java/org/apache/commons/csv/CSVFileParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFileParserTest.java @@ -40,7 +40,7 @@ /** * Parse tests using test files */ -public class CSVFileParserTest { +class CSVFileParserTest { private static final File BASE_DIR = new File("src/test/resources/org/apache/commons/csv/CSVFileParser"); @@ -59,7 +59,7 @@ private String readTestData(final BufferedReader reader) throws IOException { @ParameterizedTest @MethodSource("generateData") - public void testCSVFile(final File testFile) throws Exception { + void testCSVFile(final File testFile) throws Exception { try (FileReader fr = new FileReader(testFile); BufferedReader testDataReader = new BufferedReader(fr)) { String line = readTestData(testDataReader); assertNotNull("file must contain config line", line); @@ -104,7 +104,7 @@ public void testCSVFile(final File testFile) throws Exception { @ParameterizedTest @MethodSource("generateData") - public void testCSVUrl(final File testFile) throws Exception { + void testCSVUrl(final File testFile) throws Exception { try (FileReader fr = new FileReader(testFile); BufferedReader testData = new BufferedReader(fr)) { String line = readTestData(testData); assertNotNull("file must contain config line", line); diff --git a/src/test/java/org/apache/commons/csv/CSVFormatPredefinedTest.java b/src/test/java/org/apache/commons/csv/CSVFormatPredefinedTest.java index e907d4f17f..dad08cdb1d 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatPredefinedTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatPredefinedTest.java @@ -26,7 +26,7 @@ /** * Tests {@link CSVFormat.Predefined}. */ -public class CSVFormatPredefinedTest { +class CSVFormatPredefinedTest { private void test(final CSVFormat format, final String enumName) { assertEquals(format, CSVFormat.Predefined.valueOf(enumName).getFormat()); @@ -34,52 +34,52 @@ private void test(final CSVFormat format, final String enumName) { } @Test - public void testDefault() { + void testDefault() { test(CSVFormat.DEFAULT, "Default"); } @Test - public void testExcel() { + void testExcel() { test(CSVFormat.EXCEL, "Excel"); } @Test - public void testMongoDbCsv() { + void testMongoDbCsv() { test(CSVFormat.MONGODB_CSV, "MongoDBCsv"); } @Test - public void testMongoDbTsv() { + void testMongoDbTsv() { test(CSVFormat.MONGODB_TSV, "MongoDBTsv"); } @Test - public void testMySQL() { + void testMySQL() { test(CSVFormat.MYSQL, "MySQL"); } @Test - public void testOracle() { + void testOracle() { test(CSVFormat.ORACLE, "Oracle"); } @Test - public void testPostgreSqlCsv() { + void testPostgreSqlCsv() { test(CSVFormat.POSTGRESQL_CSV, "PostgreSQLCsv"); } @Test - public void testPostgreSqlText() { + void testPostgreSqlText() { test(CSVFormat.POSTGRESQL_TEXT, "PostgreSQLText"); } @Test - public void testRFC4180() { + void testRFC4180() { test(CSVFormat.RFC4180, "RFC4180"); } @Test - public void testTDF() { + void testTDF() { test(CSVFormat.TDF, "TDF"); } } diff --git a/src/test/java/org/apache/commons/csv/CSVFormatTest.java b/src/test/java/org/apache/commons/csv/CSVFormatTest.java index 0c7e763e5b..ed20898de9 100644 --- a/src/test/java/org/apache/commons/csv/CSVFormatTest.java +++ b/src/test/java/org/apache/commons/csv/CSVFormatTest.java @@ -26,6 +26,7 @@ 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.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertNull; @@ -48,13 +49,12 @@ import java.util.Objects; import org.apache.commons.csv.CSVFormat.Builder; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; /** * Tests {@link CSVFormat}. */ -public class CSVFormatTest { +class CSVFormatTest { public enum EmptyEnum { // empty enum. @@ -64,16 +64,16 @@ public enum Header { Name, Email, Phone } - private static void assertNotEquals(final Object right, final Object left) { - Assertions.assertNotEquals(right, left); - Assertions.assertNotEquals(left, right); + private static void assertNotEqualsFlip(final Object right, final Object left) { + assertNotEquals(right, left); + assertNotEquals(left, right); } private static CSVFormat copy(final CSVFormat format) { return format.builder().setDelimiter(format.getDelimiter()).get(); } - private void assertNotEquals(final String name, final String type, final Object left, final Object right) { + private void assertNotEqualsHash(final String name, final String type, final Object left, final Object right) { if (left.equals(right) || right.equals(left)) { fail("Objects must not compare equal for " + name + "(" + type + ")"); } @@ -83,65 +83,65 @@ private void assertNotEquals(final String name, final String type, final Object } @Test - public void testBuildVsGet() { + void testBuildVsGet() { final Builder builder = CSVFormat.DEFAULT.builder(); assertNotSame(builder.get(), builder.build()); } @Test - public void testDelimiterCharLineBreakCrThrowsException1() { + void testDelimiterCharLineBreakCrThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter(Constants.CR).get()); } @Test - public void testDelimiterCharLineBreakLfThrowsException1() { + void testDelimiterCharLineBreakLfThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter(Constants.LF).get()); } @Test - public void testDelimiterEmptyStringThrowsException1() { + void testDelimiterEmptyStringThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter("").get()); } @SuppressWarnings("deprecation") @Test - public void testDelimiterSameAsCommentStartThrowsException_Deprecated() { + void testDelimiterSameAsCommentStartThrowsException_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withDelimiter('!').withCommentMarker('!')); } @Test - public void testDelimiterSameAsCommentStartThrowsException1() { + void testDelimiterSameAsCommentStartThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter('!').setCommentMarker('!').get()); } @SuppressWarnings("deprecation") @Test - public void testDelimiterSameAsEscapeThrowsException_Deprecated() { + void testDelimiterSameAsEscapeThrowsException_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withDelimiter('!').withEscape('!')); } @Test - public void testDelimiterSameAsEscapeThrowsException1() { + void testDelimiterSameAsEscapeThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter('!').setEscape('!').get()); } @Test - public void testDelimiterSameAsRecordSeparatorThrowsException() { + void testDelimiterSameAsRecordSeparatorThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.newFormat(CR)); } @Test - public void testDelimiterStringLineBreakCrThrowsException1() { + void testDelimiterStringLineBreakCrThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter(String.valueOf(Constants.CR)).get()); } @Test - public void testDelimiterStringLineBreakLfThrowsException1() { + void testDelimiterStringLineBreakLfThrowsException1() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setDelimiter(String.valueOf(Constants.LF)).get()); } @Test - public void testDuplicateHeaderElements() { + void testDuplicateHeaderElements() { final String[] header = { "A", "A" }; final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader(header).get(); assertEquals(2, format.getHeader().length); @@ -150,7 +150,7 @@ public void testDuplicateHeaderElements() { @SuppressWarnings("deprecation") @Test - public void testDuplicateHeaderElements_Deprecated() { + void testDuplicateHeaderElements_Deprecated() { final String[] header = { "A", "A" }; final CSVFormat format = CSVFormat.DEFAULT.withHeader(header); assertEquals(2, format.getHeader().length); @@ -158,48 +158,48 @@ public void testDuplicateHeaderElements_Deprecated() { } @Test - public void testDuplicateHeaderElementsFalse() { + void testDuplicateHeaderElementsFalse() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setAllowDuplicateHeaderNames(false).setHeader("A", "A").get()); } @SuppressWarnings("deprecation") @Test - public void testDuplicateHeaderElementsFalse_Deprecated() { + void testDuplicateHeaderElementsFalse_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withAllowDuplicateHeaderNames(false).withHeader("A", "A")); } @Test - public void testDuplicateHeaderElementsTrue() { + void testDuplicateHeaderElementsTrue() { CSVFormat.DEFAULT.builder().setAllowDuplicateHeaderNames(true).setHeader("A", "A").get(); } @SuppressWarnings("deprecation") @Test - public void testDuplicateHeaderElementsTrue_Deprecated() { + void testDuplicateHeaderElementsTrue_Deprecated() { CSVFormat.DEFAULT.withAllowDuplicateHeaderNames(true).withHeader("A", "A"); } @Test - public void testDuplicateHeaderElementsTrueContainsEmpty1() { + void testDuplicateHeaderElementsTrueContainsEmpty1() { CSVFormat.DEFAULT.builder().setAllowDuplicateHeaderNames(false).setHeader("A", "", "B", "").get(); } @Test - public void testDuplicateHeaderElementsTrueContainsEmpty2() { + void testDuplicateHeaderElementsTrueContainsEmpty2() { CSVFormat.DEFAULT.builder().setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_EMPTY).setHeader("A", "", "B", "").get(); } @Test - public void testDuplicateHeaderElementsTrueContainsEmpty3() { + void testDuplicateHeaderElementsTrueContainsEmpty3() { CSVFormat.DEFAULT.builder().setAllowDuplicateHeaderNames(false).setAllowMissingColumnNames(true).setHeader("A", "", "B", "").get(); } @Test - public void testEquals() { + void testEquals() { final CSVFormat right = CSVFormat.DEFAULT; final CSVFormat left = copy(right); - Assertions.assertNotEquals(null, right); - Assertions.assertNotEquals("A String Instance", right); + assertNotEquals(null, right); + assertNotEquals("A String Instance", right); assertEquals(right, right); assertEquals(right, left); assertEquals(left, right); @@ -208,49 +208,49 @@ public void testEquals() { } @Test - public void testEqualsCommentStart() { + void testEqualsCommentStart() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setQuote('"').setCommentMarker('#').setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setCommentMarker('!').get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsCommentStart_Deprecated() { + void testEqualsCommentStart_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withQuote('"').withCommentMarker('#').withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withCommentMarker('!'); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsDelimiter() { + void testEqualsDelimiter() { final CSVFormat right = CSVFormat.newFormat('!'); final CSVFormat left = CSVFormat.newFormat('?'); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsEscape() { + void testEqualsEscape() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setQuote('"').setCommentMarker('#').setEscape('+').setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setEscape('!').get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsEscape_Deprecated() { + void testEqualsEscape_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withQuote('"').withCommentMarker('#').withEscape('+').withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withEscape('!'); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsHash() throws Exception { + void testEqualsHash() throws Exception { final Method[] methods = CSVFormat.class.getDeclaredMethods(); for (final Method method : methods) { if (Modifier.isPublic(method.getModifiers())) { @@ -262,49 +262,49 @@ public void testEqualsHash() throws Exception { case "boolean": { final Object defTrue = method.invoke(CSVFormat.DEFAULT, Boolean.TRUE); final Object defFalse = method.invoke(CSVFormat.DEFAULT, Boolean.FALSE); - assertNotEquals(name, type, defTrue, defFalse); + assertNotEqualsHash(name, type, defTrue, defFalse); break; } case "char": { final Object a = method.invoke(CSVFormat.DEFAULT, 'a'); final Object b = method.invoke(CSVFormat.DEFAULT, 'b'); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } case "java.lang.Character": { final Object a = method.invoke(CSVFormat.DEFAULT, new Object[] { null }); final Object b = method.invoke(CSVFormat.DEFAULT, Character.valueOf('d')); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } case "java.lang.String": { final Object a = method.invoke(CSVFormat.DEFAULT, new Object[] { null }); final Object b = method.invoke(CSVFormat.DEFAULT, "e"); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } case "java.lang.String[]": { final Object a = method.invoke(CSVFormat.DEFAULT, new Object[] { new String[] { null, null } }); final Object b = method.invoke(CSVFormat.DEFAULT, new Object[] { new String[] { "f", "g" } }); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } case "org.apache.commons.csv.QuoteMode": { final Object a = method.invoke(CSVFormat.DEFAULT, QuoteMode.MINIMAL); final Object b = method.invoke(CSVFormat.DEFAULT, QuoteMode.ALL); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } case "org.apache.commons.csv.DuplicateHeaderMode": { final Object a = method.invoke(CSVFormat.DEFAULT, DuplicateHeaderMode.ALLOW_ALL); final Object b = method.invoke(CSVFormat.DEFAULT, DuplicateHeaderMode.DISALLOW); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } case "java.lang.Object[]": { final Object a = method.invoke(CSVFormat.DEFAULT, new Object[] { new Object[] { null, null } }); final Object b = method.invoke(CSVFormat.DEFAULT, new Object[] { new Object[] { new Object(), new Object() } }); - assertNotEquals(name, type, a, b); + assertNotEqualsHash(name, type, a, b); break; } default: @@ -322,81 +322,89 @@ public void testEqualsHash() throws Exception { } @Test - public void testEqualsHeader() { + void testEqualsHeader() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setRecordSeparator(CR).setCommentMarker('#').setEscape('+').setHeader("One", "Two", "Three") .setIgnoreEmptyLines(true).setIgnoreSurroundingSpaces(true).setQuote('"').setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setHeader("Three", "Two", "One").get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsHeader_Deprecated() { + void testEqualsHeader_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withRecordSeparator(CR).withCommentMarker('#').withEscape('+').withHeader("One", "Two", "Three") .withIgnoreEmptyLines().withIgnoreSurroundingSpaces().withQuote('"').withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withHeader("Three", "Two", "One"); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsIgnoreEmptyLines() { + void testEqualsIgnoreEmptyLines() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setCommentMarker('#').setEscape('+').setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true).setQuote('"').setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setIgnoreEmptyLines(false).get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsIgnoreEmptyLines_Deprecated() { + void testEqualsIgnoreEmptyLines_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withCommentMarker('#').withEscape('+').withIgnoreEmptyLines().withIgnoreSurroundingSpaces() .withQuote('"').withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withIgnoreEmptyLines(false); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsIgnoreSurroundingSpaces() { + void testEqualsIgnoreSurroundingSpaces() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setCommentMarker('#').setEscape('+').setIgnoreSurroundingSpaces(true).setQuote('"') .setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setIgnoreSurroundingSpaces(false).get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsIgnoreSurroundingSpaces_Deprecated() { + void testEqualsIgnoreSurroundingSpaces_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withCommentMarker('#').withEscape('+').withIgnoreSurroundingSpaces().withQuote('"') .withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withIgnoreSurroundingSpaces(false); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsLeftNoQuoteRightQuote() { + void testEqualsLeftNoQuoteRightQuote() { final CSVFormat left = CSVFormat.newFormat(',').builder().setQuote(null).get(); final CSVFormat right = left.builder().setQuote('#').get(); - assertNotEquals(left, right); + assertNotEqualsFlip(left, right); } @SuppressWarnings("deprecation") @Test - public void testEqualsLeftNoQuoteRightQuote_Deprecated() { + void testEqualsLeftNoQuoteRightQuote_Deprecated() { final CSVFormat left = CSVFormat.newFormat(',').withQuote(null); final CSVFormat right = left.withQuote('#'); - assertNotEquals(left, right); + assertNotEqualsFlip(left, right); } @Test - public void testEqualsNoQuotes() { + void testEqualsMaxRows() { + final CSVFormat right = CSVFormat.DEFAULT.builder().setMaxRows(10).get(); + final CSVFormat left = CSVFormat.DEFAULT.builder().setMaxRows(1000).get(); + assertNotEqualsFlip(right, left); + assertNotEquals(right.hashCode(), left.hashCode()); + } + + @Test + void testEqualsNoQuotes() { final CSVFormat left = CSVFormat.newFormat(',').builder().setQuote(null).get(); final CSVFormat right = left.builder().setQuote(null).get(); @@ -405,7 +413,7 @@ public void testEqualsNoQuotes() { @SuppressWarnings("deprecation") @Test - public void testEqualsNoQuotes_Deprecated() { + void testEqualsNoQuotes_Deprecated() { final CSVFormat left = CSVFormat.newFormat(',').withQuote(null); final CSVFormat right = left.withQuote(null); @@ -413,26 +421,26 @@ public void testEqualsNoQuotes_Deprecated() { } @Test - public void testEqualsNullString() { + void testEqualsNullString() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setRecordSeparator(CR).setCommentMarker('#').setEscape('+').setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true).setQuote('"').setQuoteMode(QuoteMode.ALL).setNullString("null").get(); final CSVFormat left = right.builder().setNullString("---").get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsNullString_Deprecated() { + void testEqualsNullString_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withRecordSeparator(CR).withCommentMarker('#').withEscape('+').withIgnoreEmptyLines() .withIgnoreSurroundingSpaces().withQuote('"').withQuoteMode(QuoteMode.ALL).withNullString("null"); final CSVFormat left = right.withNullString("---"); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsOne() { + void testEqualsOne() { final CSVFormat csvFormatOne = CSVFormat.INFORMIX_UNLOAD; final CSVFormat csvFormatTwo = CSVFormat.MYSQL; @@ -495,7 +503,7 @@ public void testEqualsOne() { assertFalse(csvFormatTwo.isCommentMarkerSet()); assertNotSame(csvFormatTwo, csvFormatOne); - Assertions.assertNotEquals(csvFormatTwo, csvFormatOne); + assertNotEquals(csvFormatTwo, csvFormatOne); assertEquals('\\', (char) csvFormatOne.getEscapeCharacter()); assertNull(csvFormatOne.getQuoteMode()); @@ -554,86 +562,86 @@ public void testEqualsOne() { assertNotSame(csvFormatOne, csvFormatTwo); assertNotSame(csvFormatTwo, csvFormatOne); - Assertions.assertNotEquals(csvFormatOne, csvFormatTwo); - Assertions.assertNotEquals(csvFormatTwo, csvFormatOne); + assertNotEquals(csvFormatOne, csvFormatTwo); + assertNotEquals(csvFormatTwo, csvFormatOne); - Assertions.assertNotEquals(csvFormatTwo, csvFormatOne); + assertNotEquals(csvFormatTwo, csvFormatOne); } @Test - public void testEqualsQuoteChar() { + void testEqualsQuoteChar() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setQuote('"').get(); final CSVFormat left = right.builder().setQuote('!').get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsQuoteChar_Deprecated() { + void testEqualsQuoteChar_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withQuote('"'); final CSVFormat left = right.withQuote('!'); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsQuotePolicy() { + void testEqualsQuotePolicy() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setQuote('"').setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setQuoteMode(QuoteMode.MINIMAL).get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsQuotePolicy_Deprecated() { + void testEqualsQuotePolicy_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withQuote('"').withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withQuoteMode(QuoteMode.MINIMAL); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsRecordSeparator() { + void testEqualsRecordSeparator() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setRecordSeparator(CR).setCommentMarker('#').setEscape('+').setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true).setQuote('"').setQuoteMode(QuoteMode.ALL).get(); final CSVFormat left = right.builder().setRecordSeparator(LF).get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsRecordSeparator_Deprecated() { + void testEqualsRecordSeparator_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withRecordSeparator(CR).withCommentMarker('#').withEscape('+').withIgnoreEmptyLines() .withIgnoreSurroundingSpaces().withQuote('"').withQuoteMode(QuoteMode.ALL); final CSVFormat left = right.withRecordSeparator(LF); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } - public void testEqualsSkipHeaderRecord() { + void testEqualsSkipHeaderRecord() { final CSVFormat right = CSVFormat.newFormat('\'').builder().setRecordSeparator(CR).setCommentMarker('#').setEscape('+').setIgnoreEmptyLines(true) .setIgnoreSurroundingSpaces(true).setQuote('"').setQuoteMode(QuoteMode.ALL).setNullString("null").setSkipHeaderRecord(true).get(); final CSVFormat left = right.builder().setSkipHeaderRecord(false).get(); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @SuppressWarnings("deprecation") @Test - public void testEqualsSkipHeaderRecord_Deprecated() { + void testEqualsSkipHeaderRecord_Deprecated() { final CSVFormat right = CSVFormat.newFormat('\'').withRecordSeparator(CR).withCommentMarker('#').withEscape('+').withIgnoreEmptyLines() .withIgnoreSurroundingSpaces().withQuote('"').withQuoteMode(QuoteMode.ALL).withNullString("null").withSkipHeaderRecord(); final CSVFormat left = right.withSkipHeaderRecord(false); - assertNotEquals(right, left); + assertNotEqualsFlip(right, left); } @Test - public void testEqualsWithNull() { + void testEqualsWithNull() { final CSVFormat csvFormat = CSVFormat.POSTGRESQL_TEXT; @@ -691,23 +699,23 @@ public void testEqualsWithNull() { assertNull(csvFormat.getQuoteCharacter()); assertTrue(csvFormat.isNullStringSet()); - Assertions.assertNotEquals(null, csvFormat); + assertNotEquals(null, csvFormat); } @Test - public void testEscapeSameAsCommentStartThrowsException() { + void testEscapeSameAsCommentStartThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setEscape('!').setCommentMarker('!').get()); } @SuppressWarnings("deprecation") @Test - public void testEscapeSameAsCommentStartThrowsException_Deprecated() { + void testEscapeSameAsCommentStartThrowsException_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withEscape('!').withCommentMarker('!')); } @Test - public void testEscapeSameAsCommentStartThrowsExceptionForWrapperType() { + void testEscapeSameAsCommentStartThrowsExceptionForWrapperType() { // Cannot assume that callers won't use different Character objects assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setEscape(Character.valueOf('!')).setCommentMarker(Character.valueOf('!')).get()); @@ -715,13 +723,13 @@ public void testEscapeSameAsCommentStartThrowsExceptionForWrapperType() { @SuppressWarnings("deprecation") @Test - public void testEscapeSameAsCommentStartThrowsExceptionForWrapperType_Deprecated() { + void testEscapeSameAsCommentStartThrowsExceptionForWrapperType_Deprecated() { // Cannot assume that callers won't use different Character objects assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withEscape(Character.valueOf('!')).withCommentMarker(Character.valueOf('!'))); } @Test - public void testFormat() { + void testFormat() { final CSVFormat format = CSVFormat.DEFAULT; assertEquals("", format.format()); @@ -730,7 +738,7 @@ public void testFormat() { } @Test // I assume this to be a defect. - public void testFormatThrowsNullPointerException() { + void testFormatThrowsNullPointerException() { final CSVFormat csvFormat = CSVFormat.MYSQL; @@ -739,7 +747,7 @@ public void testFormatThrowsNullPointerException() { } @Test - public void testFormatToString() { + void testFormatToString() { // @formatter:off final CSVFormat format = CSVFormat.RFC4180 .withEscape('?') @@ -759,7 +767,7 @@ public void testFormatToString() { } @Test - public void testGetAllowDuplicateHeaderNames() { + void testGetAllowDuplicateHeaderNames() { final Builder builder = CSVFormat.DEFAULT.builder(); assertTrue(builder.get().getAllowDuplicateHeaderNames()); assertTrue(builder.setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_ALL).get().getAllowDuplicateHeaderNames()); @@ -768,7 +776,7 @@ public void testGetAllowDuplicateHeaderNames() { } @Test - public void testGetDuplicateHeaderMode() { + void testGetDuplicateHeaderMode() { final Builder builder = CSVFormat.DEFAULT.builder(); assertEquals(DuplicateHeaderMode.ALLOW_ALL, builder.get().getDuplicateHeaderMode()); @@ -778,7 +786,7 @@ public void testGetDuplicateHeaderMode() { } @Test - public void testGetHeader() { + void testGetHeader() { final String[] header = { "one", "two", "three" }; final CSVFormat formatWithHeader = CSVFormat.DEFAULT.withHeader(header); // getHeader() makes a copy of the header array. @@ -791,7 +799,7 @@ public void testGetHeader() { } @Test - public void testHashCodeAndWithIgnoreHeaderCase() { + void testHashCodeAndWithIgnoreHeaderCase() { final CSVFormat csvFormat = CSVFormat.INFORMIX_UNLOAD_CSV; final CSVFormat csvFormatTwo = csvFormat.withIgnoreHeaderCase(); @@ -801,7 +809,7 @@ public void testHashCodeAndWithIgnoreHeaderCase() { assertTrue(csvFormatTwo.getIgnoreHeaderCase()); // now different assertFalse(csvFormatTwo.getTrailingDelimiter()); - Assertions.assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal + assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal assertFalse(csvFormatTwo.getAllowMissingColumnNames()); assertFalse(csvFormatTwo.getTrim()); @@ -809,18 +817,18 @@ public void testHashCodeAndWithIgnoreHeaderCase() { } @Test - public void testJiraCsv236() { + void testJiraCsv236() { CSVFormat.DEFAULT.builder().setAllowDuplicateHeaderNames(true).setHeader("CC", "VV", "VV").get(); } @SuppressWarnings("deprecation") @Test - public void testJiraCsv236__Deprecated() { + void testJiraCsv236__Deprecated() { CSVFormat.DEFAULT.withAllowDuplicateHeaderNames().withHeader("CC", "VV", "VV"); } @Test - public void testNewFormat() { + void testNewFormat() { final CSVFormat csvFormat = CSVFormat.newFormat('X'); @@ -881,7 +889,7 @@ public void testNewFormat() { } @Test - public void testNullRecordSeparatorCsv106() { + void testNullRecordSeparatorCsv106() { final CSVFormat format = CSVFormat.newFormat(';').builder().setSkipHeaderRecord(true).setHeader("H1", "H2").get(); final String formatStr = format.format("A", "B"); assertNotNull(formatStr); @@ -890,7 +898,7 @@ public void testNullRecordSeparatorCsv106() { @SuppressWarnings("deprecation") @Test - public void testNullRecordSeparatorCsv106__Deprecated() { + void testNullRecordSeparatorCsv106__Deprecated() { final CSVFormat format = CSVFormat.newFormat(';').withSkipHeaderRecord().withHeader("H1", "H2"); final String formatStr = format.format("A", "B"); assertNotNull(formatStr); @@ -898,7 +906,7 @@ public void testNullRecordSeparatorCsv106__Deprecated() { } @Test - public void testPrintRecord() throws IOException { + void testPrintRecord() throws IOException { final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180; format.printRecord(out, "a", "b", "c"); @@ -906,7 +914,7 @@ public void testPrintRecord() throws IOException { } @Test - public void testPrintRecordEmpty() throws IOException { + void testPrintRecordEmpty() throws IOException { final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180; format.printRecord(out); @@ -914,7 +922,7 @@ public void testPrintRecordEmpty() throws IOException { } @Test - public void testPrintWithEscapesEndWithCRLF() throws IOException { + void testPrintWithEscapesEndWithCRLF() throws IOException { final Reader in = new StringReader("x,y,x\r\na,?b,c\r\n"); final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180.withEscape('?').withDelimiter(',').withQuote(null).withRecordSeparator(CRLF); @@ -923,7 +931,7 @@ public void testPrintWithEscapesEndWithCRLF() throws IOException { } @Test - public void testPrintWithEscapesEndWithoutCRLF() throws IOException { + void testPrintWithEscapesEndWithoutCRLF() throws IOException { final Reader in = new StringReader("x,y,x"); final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180.withEscape('?').withDelimiter(',').withQuote(null).withRecordSeparator(CRLF); @@ -932,7 +940,7 @@ public void testPrintWithEscapesEndWithoutCRLF() throws IOException { } @Test - public void testPrintWithoutQuotes() throws IOException { + void testPrintWithoutQuotes() throws IOException { final Reader in = new StringReader(""); final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180.withDelimiter(',').withQuote('"').withEscape('?').withQuoteMode(QuoteMode.NON_NUMERIC); @@ -941,7 +949,7 @@ public void testPrintWithoutQuotes() throws IOException { } @Test - public void testPrintWithQuoteModeIsNONE() throws IOException { + void testPrintWithQuoteModeIsNONE() throws IOException { final Reader in = new StringReader("a,b,c"); final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180.withDelimiter(',').withQuote('"').withEscape('?').withQuoteMode(QuoteMode.NONE); @@ -950,7 +958,7 @@ public void testPrintWithQuoteModeIsNONE() throws IOException { } @Test - public void testPrintWithQuotes() throws IOException { + void testPrintWithQuotes() throws IOException { final Reader in = new StringReader("\"a,b,c\r\nx,y,z"); final Appendable out = new StringBuilder(); final CSVFormat format = CSVFormat.RFC4180.withDelimiter(',').withQuote('"').withEscape('?').withQuoteMode(QuoteMode.NON_NUMERIC); @@ -958,43 +966,89 @@ public void testPrintWithQuotes() throws IOException { assertEquals("\"\"\"a,b,c\r\nx,y,z\"", out.toString()); } + /** + * Tests CSV-326. + */ + @Test + void testPrintWithQuotesEscapeBeforeQuote() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder() + .setEscape('\\') + .setQuote('"') + .get(); + final String value = "\\\""; + final Appendable out = new StringBuilder(); + format.print(new StringReader(value), out, true); + try (CSVParser parser = CSVParser.parse(out.toString(), format)) { + assertEquals(value, parser.getRecords().get(0).get(0)); + } + } + @Test - public void testQuoteCharSameAsCommentStartThrowsException() { + void testQuoteCharSameAsCommentStartThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setQuote('!').setCommentMarker('!').get()); } @SuppressWarnings("deprecation") @Test - public void testQuoteCharSameAsCommentStartThrowsException_Deprecated() { + void testQuoteCharSameAsCommentStartThrowsException_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withQuote('!').withCommentMarker('!')); } @Test - public void testQuoteCharSameAsCommentStartThrowsExceptionForWrapperType() { + void testQuoteCharSameAsCommentStartThrowsExceptionForWrapperType() { // Cannot assume that callers won't use different Character objects assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setQuote(Character.valueOf('!')).setCommentMarker('!').get()); } @SuppressWarnings("deprecation") @Test - public void testQuoteCharSameAsCommentStartThrowsExceptionForWrapperType_Deprecated() { + void testQuoteCharSameAsCommentStartThrowsExceptionForWrapperType_Deprecated() { // Cannot assume that callers won't use different Character objects assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withQuote(Character.valueOf('!')).withCommentMarker('!')); } @Test - public void testQuoteCharSameAsDelimiterThrowsException() { + void testQuoteCharSameAsDelimiterThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.builder().setQuote('!').setDelimiter('!').get()); } @SuppressWarnings("deprecation") @Test - public void testQuoteCharSameAsDelimiterThrowsException_Deprecated() { + void testQuoteCharSameAsDelimiterThrowsException_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withQuote('!').withDelimiter('!')); } @Test - public void testQuoteModeNoneShouldReturnMeaningfulExceptionMessage() { + void testQuotedNullStringTracksQuoteCharacter() throws IOException { + final StringBuilder out = new StringBuilder(); + // @formatter:off + final Builder builder = CSVFormat.DEFAULT.builder(); + final CSVFormat format = builder + .setQuoteMode(QuoteMode.ALL) + .setNullString("NULL") + .get(); + // @formatter:on + format.print(null, out, true); + assertEquals("\"NULL\"", out.toString()); + // set + out.setLength(0); + builder.setQuote('\''); + builder.get().print(null, out, true); + assertEquals("'NULL'", out.toString()); + // reset + out.setLength(0); + builder.setQuote((Character) null); + builder.get().print(null, out, true); + assertEquals("\"NULL\"", out.toString()); + // reset, reverse setter order + out.setLength(0); + builder.setNullString(null).setQuote((Character) null).setNullString("NULL"); + builder.get().print(null, out, true); + assertEquals("\"NULL\"", out.toString()); + } + + @Test + void testQuoteModeNoneShouldReturnMeaningfulExceptionMessage() { final Exception exception = assertThrows(IllegalArgumentException.class, () -> // @formatter:off CSVFormat.DEFAULT.builder() @@ -1009,18 +1063,18 @@ public void testQuoteModeNoneShouldReturnMeaningfulExceptionMessage() { } @Test - public void testQuotePolicyNoneWithoutEscapeThrowsException() { + void testQuotePolicyNoneWithoutEscapeThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.newFormat('!').builder().setQuoteMode(QuoteMode.NONE).get()); } @SuppressWarnings("deprecation") @Test - public void testQuotePolicyNoneWithoutEscapeThrowsException_Deprecated() { + void testQuotePolicyNoneWithoutEscapeThrowsException_Deprecated() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.newFormat('!').withQuoteMode(QuoteMode.NONE)); } @Test - public void testRFC4180() { + void testRFC4180() { assertNull(RFC4180.getCommentMarker()); assertEquals(',', RFC4180.getDelimiter()); assertNull(RFC4180.getEscapeCharacter()); @@ -1032,7 +1086,7 @@ public void testRFC4180() { @SuppressWarnings("boxing") // no need to worry about boxing here @Test - public void testSerialization() throws Exception { + void testSerialization() throws Exception { final ByteArrayOutputStream out = new ByteArrayOutputStream(); try (ObjectOutputStream oos = new ObjectOutputStream(out)) { @@ -1054,7 +1108,7 @@ public void testSerialization() throws Exception { } @Test - public void testToString() { + void testToString() { final String string = CSVFormat.INFORMIX_UNLOAD.toString(); @@ -1063,7 +1117,7 @@ public void testToString() { } @Test - public void testToStringAndWithCommentMarkerTakingCharacter() { + void testToStringAndWithCommentMarkerTakingCharacter() { final CSVFormat.Predefined csvFormatPredefined = CSVFormat.Predefined.Default; final CSVFormat csvFormat = csvFormatPredefined.getFormat(); @@ -1156,7 +1210,7 @@ public void testToStringAndWithCommentMarkerTakingCharacter() { assertNotSame(csvFormat, csvFormatTwo); assertNotSame(csvFormatTwo, csvFormat); - Assertions.assertNotEquals(csvFormatTwo, csvFormat); + assertNotEquals(csvFormatTwo, csvFormat); assertNull(csvFormat.getEscapeCharacter()); assertTrue(csvFormat.isQuoteCharacterSet()); @@ -1215,16 +1269,16 @@ public void testToStringAndWithCommentMarkerTakingCharacter() { assertNotSame(csvFormat, csvFormatTwo); assertNotSame(csvFormatTwo, csvFormat); - Assertions.assertNotEquals(csvFormat, csvFormatTwo); + assertNotEquals(csvFormat, csvFormatTwo); - Assertions.assertNotEquals(csvFormatTwo, csvFormat); - assertEquals("Delimiter=<,> QuoteChar=<\"> CommentStart= " + "RecordSeparator=<\r\n> EmptyLines:ignored SkipHeaderRecord:false", + assertNotEquals(csvFormatTwo, csvFormat); + assertEquals("Delimiter=<,> QuoteChar=<\"> CommentStart= RecordSeparator=<\r\n> EmptyLines:ignored SkipHeaderRecord:false", csvFormatTwo.toString()); } @Test - public void testTrim() throws IOException { + void testTrim() throws IOException { final CSVFormat formatWithTrim = CSVFormat.DEFAULT.withDelimiter(',').withTrim().withQuote(null).withRecordSeparator(CRLF); CharSequence in = "a,b,c"; @@ -1249,29 +1303,29 @@ public void testTrim() throws IOException { } @Test - public void testWithCommentStart() { + void testWithCommentStart() { final CSVFormat formatWithCommentStart = CSVFormat.DEFAULT.withCommentMarker('#'); assertEquals(Character.valueOf('#'), formatWithCommentStart.getCommentMarker()); } @Test - public void testWithCommentStartCRThrowsException() { + void testWithCommentStartCRThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withCommentMarker(CR)); } @Test - public void testWithDelimiter() { + void testWithDelimiter() { final CSVFormat formatWithDelimiter = CSVFormat.DEFAULT.withDelimiter('!'); assertEquals('!', formatWithDelimiter.getDelimiter()); } @Test - public void testWithDelimiterLFThrowsException() { + void testWithDelimiterLFThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withDelimiter(LF)); } @Test - public void testWithEmptyDuplicates() { + void testWithEmptyDuplicates() { final CSVFormat formatWithEmptyDuplicates = CSVFormat.DEFAULT.builder().setDuplicateHeaderMode(DuplicateHeaderMode.ALLOW_EMPTY).get(); assertEquals(DuplicateHeaderMode.ALLOW_EMPTY, formatWithEmptyDuplicates.getDuplicateHeaderMode()); @@ -1279,31 +1333,31 @@ public void testWithEmptyDuplicates() { } @Test - public void testWithEmptyEnum() { + void testWithEmptyEnum() { final CSVFormat formatWithHeader = CSVFormat.DEFAULT.withHeader(EmptyEnum.class); assertEquals(0, formatWithHeader.getHeader().length); } @Test - public void testWithEscape() { + void testWithEscape() { final CSVFormat formatWithEscape = CSVFormat.DEFAULT.withEscape('&'); assertEquals(Character.valueOf('&'), formatWithEscape.getEscapeCharacter()); } @Test - public void testWithEscapeCRThrowsExceptions() { + void testWithEscapeCRThrowsExceptions() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withEscape(CR)); } @Test - public void testWithFirstRecordAsHeader() { + void testWithFirstRecordAsHeader() { final CSVFormat formatWithFirstRecordAsHeader = CSVFormat.DEFAULT.withFirstRecordAsHeader(); assertTrue(formatWithFirstRecordAsHeader.getSkipHeaderRecord()); assertEquals(0, formatWithFirstRecordAsHeader.getHeader().length); } @Test - public void testWithHeader() { + void testWithHeader() { final String[] header = { "one", "two", "three" }; // withHeader() makes a copy of the header array. final CSVFormat formatWithHeader = CSVFormat.DEFAULT.withHeader(header); @@ -1312,7 +1366,7 @@ public void testWithHeader() { } @Test - public void testWithHeaderComments() { + void testWithHeaderComments() { final CSVFormat csvFormat = CSVFormat.DEFAULT; @@ -1403,7 +1457,7 @@ public void testWithHeaderComments() { assertNotSame(csvFormat, csvFormatTwo); assertNotSame(csvFormatTwo, csvFormat); - Assertions.assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal + assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal final String string = csvFormatTwo.format(objectArray); @@ -1465,88 +1519,88 @@ public void testWithHeaderComments() { assertNotSame(csvFormatTwo, csvFormat); assertNotNull(string); - Assertions.assertNotEquals(csvFormat, csvFormatTwo); // CSV-244 - should not be equal + assertNotEquals(csvFormat, csvFormatTwo); // CSV-244 - should not be equal - Assertions.assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal + assertNotEquals(csvFormatTwo, csvFormat); // CSV-244 - should not be equal assertEquals(",,,,,,,", string); } @Test - public void testWithHeaderEnum() { + void testWithHeaderEnum() { final CSVFormat formatWithHeader = CSVFormat.DEFAULT.withHeader(Header.class); assertArrayEquals(new String[] { "Name", "Email", "Phone" }, formatWithHeader.getHeader()); } @Test - public void testWithHeaderEnumNull() { + void testWithHeaderEnumNull() { final CSVFormat format = CSVFormat.DEFAULT; final Class> simpleName = null; format.withHeader(simpleName); } @Test - public void testWithHeaderResultSetNull() throws SQLException { + void testWithHeaderResultSetNull() throws SQLException { final CSVFormat format = CSVFormat.DEFAULT; final ResultSet resultSet = null; format.withHeader(resultSet); } @Test - public void testWithIgnoreEmptyLines() { + void testWithIgnoreEmptyLines() { assertFalse(CSVFormat.DEFAULT.withIgnoreEmptyLines(false).getIgnoreEmptyLines()); assertTrue(CSVFormat.DEFAULT.withIgnoreEmptyLines().getIgnoreEmptyLines()); } @Test - public void testWithIgnoreSurround() { + void testWithIgnoreSurround() { assertFalse(CSVFormat.DEFAULT.withIgnoreSurroundingSpaces(false).getIgnoreSurroundingSpaces()); assertTrue(CSVFormat.DEFAULT.withIgnoreSurroundingSpaces().getIgnoreSurroundingSpaces()); } @Test - public void testWithNullString() { + void testWithNullString() { final CSVFormat formatWithNullString = CSVFormat.DEFAULT.withNullString("null"); assertEquals("null", formatWithNullString.getNullString()); } @Test - public void testWithQuoteChar() { + void testWithQuoteChar() { final CSVFormat formatWithQuoteChar = CSVFormat.DEFAULT.withQuote('"'); assertEquals(Character.valueOf('"'), formatWithQuoteChar.getQuoteCharacter()); } @Test - public void testWithQuoteLFThrowsException() { + void testWithQuoteLFThrowsException() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withQuote(LF)); } @Test - public void testWithQuotePolicy() { + void testWithQuotePolicy() { final CSVFormat formatWithQuotePolicy = CSVFormat.DEFAULT.withQuoteMode(QuoteMode.ALL); assertEquals(QuoteMode.ALL, formatWithQuotePolicy.getQuoteMode()); } @Test - public void testWithRecordSeparatorCR() { + void testWithRecordSeparatorCR() { final CSVFormat formatWithRecordSeparator = CSVFormat.DEFAULT.withRecordSeparator(CR); assertEquals(String.valueOf(CR), formatWithRecordSeparator.getRecordSeparator()); } @Test - public void testWithRecordSeparatorCRLF() { + void testWithRecordSeparatorCRLF() { final CSVFormat formatWithRecordSeparator = CSVFormat.DEFAULT.withRecordSeparator(CRLF); assertEquals(CRLF, formatWithRecordSeparator.getRecordSeparator()); } @Test - public void testWithRecordSeparatorLF() { + void testWithRecordSeparatorLF() { final CSVFormat formatWithRecordSeparator = CSVFormat.DEFAULT.withRecordSeparator(LF); assertEquals(String.valueOf(LF), formatWithRecordSeparator.getRecordSeparator()); } @Test - public void testWithSystemRecordSeparator() { + void testWithSystemRecordSeparator() { final CSVFormat formatWithRecordSeparator = CSVFormat.DEFAULT.withSystemRecordSeparator(); assertEquals(System.lineSeparator(), formatWithRecordSeparator.getRecordSeparator()); } diff --git a/src/test/java/org/apache/commons/csv/CSVParserTest.java b/src/test/java/org/apache/commons/csv/CSVParserTest.java index 20ab9b6588..6d9bdd9e80 100644 --- a/src/test/java/org/apache/commons/csv/CSVParserTest.java +++ b/src/test/java/org/apache/commons/csv/CSVParserTest.java @@ -22,7 +22,7 @@ import static org.apache.commons.csv.Constants.CR; import static org.apache.commons.csv.Constants.CRLF; import static org.apache.commons.csv.Constants.LF; -import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.apache.commons.csv.CsvAssertions.assertValuesEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; @@ -71,7 +71,7 @@ * The test are organized in three different sections: The 'setter/getter' section, the lexer section and finally the parser section. In case a test fails, you * should follow a top-down approach for fixing a potential bug (its likely that the parser itself fails if the lexer has problems...). */ -public class CSVParserTest { +class CSVParserTest { private static final CSVFormat EXCEL_WITH_HEADER = CSVFormat.EXCEL.withHeader(); @@ -79,9 +79,13 @@ public class CSVParserTest { private static final String UTF_8_NAME = UTF_8.name(); - private static final String CSV_INPUT = "a,b,c,d\n" + " a , b , 1 2 \n" + "\"foo baar\", b,\n" + + // @formatter:off + private static final String CSV_INPUT = "a,b,c,d\n" + + " a , b , 1 2 \n" + + "\"foo baar\", b,\n" + // + " \"foo\n,,\n\"\",,\n\\\"\",d,e\n"; " \"foo\n,,\n\"\",,\n\"\"\",d,e\n"; // changed to use standard CSV escaping + // @formatter:on private static final String CSV_INPUT_1 = "a,b,c,d"; @@ -140,7 +144,7 @@ private void parseFully(final CSVParser parser) { } @Test - public void testBackslashEscaping() throws IOException { + void testBackslashEscaping() throws IOException { // To avoid confusion over the need for escaping chars in java code, // We will test with a forward slash as the escape char, and a single // quote as the encapsulator. @@ -176,12 +180,12 @@ public void testBackslashEscaping() throws IOException { } @Test - public void testBackslashEscaping2() throws IOException { + void testBackslashEscaping2() throws IOException { // To avoid confusion over the need for escaping chars in java code, // We will test with a forward slash as the escape char, and a single // quote as the encapsulator. // @formatter:off - final String code = "" + " , , \n" + // 1) + final String code = " , , \n" + // 1) " \t , , \n" + // 2) " // , /, , /,\n" + // 3) ""; @@ -200,9 +204,18 @@ public void testBackslashEscaping2() throws IOException { @Test @Disabled - public void testBackslashEscapingOld() throws IOException { - final String code = "one,two,three\n" + "on\\\"e,two\n" + "on\"e,two\n" + "one,\"tw\\\"o\"\n" + "one,\"t\\,wo\"\n" + "one,two,\"th,ree\"\n" + - "\"a\\\\\"\n" + "a\\,b\n" + "\"a\\\\,b\""; + void testBackslashEscapingOld() throws IOException { + // @formatter:off + final String code = "one,two,three\n" + + "on\\\"e,two\n" + + "on\"e,two\n" + + "one,\"tw\\\"o\"\n" + + "one,\"t\\,wo\"\n" + + "one,two,\"th,ree\"\n" + + "\"a\\\\\"\n" + + "a\\,b\n" + + "\"a\\\\,b\""; + // @formatter:on final String[][] res = { { "one", "two", "three" }, { "on\\\"e", "two" }, { "on\"e", "two" }, { "one", "tw\"o" }, { "one", "t\\,wo" }, // backslash in // quotes only // escapes a @@ -217,14 +230,14 @@ public void testBackslashEscapingOld() throws IOException { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } @Test @Disabled("CSV-107") - public void testBOM() throws IOException { + void testBOM() throws IOException { final URL url = ClassLoader.getSystemClassLoader().getResource("org/apache/commons/csv/CSVFileParser/bom.csv"); try (CSVParser parser = CSVParser.parse(url, StandardCharsets.UTF_8, EXCEL_WITH_HEADER)) { parser.forEach(record -> assertNotNull(record.get("Date"))); @@ -232,7 +245,7 @@ public void testBOM() throws IOException { } @Test - public void testBOMInputStreamParserWithInputStream() throws IOException { + void testBOMInputStreamParserWithInputStream() throws IOException { try (BOMInputStream inputStream = createBOMInputStream("org/apache/commons/csv/CSVFileParser/bom.csv"); CSVParser parser = CSVParser.parse(inputStream, UTF_8, EXCEL_WITH_HEADER)) { parser.forEach(record -> assertNotNull(record.get("Date"))); @@ -240,7 +253,7 @@ public void testBOMInputStreamParserWithInputStream() throws IOException { } @Test - public void testBOMInputStreamParserWithReader() throws IOException { + void testBOMInputStreamParserWithReader() throws IOException { try (Reader reader = new InputStreamReader(createBOMInputStream("org/apache/commons/csv/CSVFileParser/bom.csv"), UTF_8_NAME); CSVParser parser = CSVParser.builder() .setReader(reader) @@ -251,7 +264,7 @@ public void testBOMInputStreamParserWithReader() throws IOException { } @Test - public void testBOMInputStreamParseWithReader() throws IOException { + void testBOMInputStreamParseWithReader() throws IOException { try (Reader reader = new InputStreamReader(createBOMInputStream("org/apache/commons/csv/CSVFileParser/bom.csv"), UTF_8_NAME); CSVParser parser = CSVParser.builder() .setReader(reader) @@ -262,7 +275,7 @@ public void testBOMInputStreamParseWithReader() throws IOException { } @Test - public void testCarriageReturnEndings() throws IOException { + void testCarriageReturnEndings() throws IOException { final String string = "foo\rbaar,\rhello,world\r,kanu"; try (CSVParser parser = CSVParser.builder().setCharSequence(string).get()) { final List records = parser.getRecords(); @@ -271,7 +284,7 @@ public void testCarriageReturnEndings() throws IOException { } @Test - public void testCarriageReturnLineFeedEndings() throws IOException { + void testCarriageReturnLineFeedEndings() throws IOException { final String string = "foo\r\nbaar,\r\nhello,world\r\n,kanu"; try (CSVParser parser = CSVParser.builder().setCharSequence(string).get()) { final List records = parser.getRecords(); @@ -280,7 +293,7 @@ public void testCarriageReturnLineFeedEndings() throws IOException { } @Test - public void testClose() throws Exception { + void testClose() throws Exception { final Reader in = new StringReader("# comment\na,b,c\n1,2,3\nx,y,z"); final Iterator records; try (CSVParser parser = CSVFormat.DEFAULT.withCommentMarker('#').withHeader().parse(in)) { @@ -292,32 +305,32 @@ public void testClose() throws Exception { } @Test - public void testCSV141CSVFormat_DEFAULT() throws Exception { + void testCSV141CSVFormat_DEFAULT() throws Exception { testCSV141Failure(CSVFormat.DEFAULT, 3); } @Test - public void testCSV141CSVFormat_INFORMIX_UNLOAD() throws Exception { + void testCSV141CSVFormat_INFORMIX_UNLOAD() throws Exception { testCSV141Failure(CSVFormat.INFORMIX_UNLOAD, 1); } @Test - public void testCSV141CSVFormat_INFORMIX_UNLOAD_CSV() throws Exception { + void testCSV141CSVFormat_INFORMIX_UNLOAD_CSV() throws Exception { testCSV141Failure(CSVFormat.INFORMIX_UNLOAD_CSV, 3); } @Test - public void testCSV141CSVFormat_ORACLE() throws Exception { + void testCSV141CSVFormat_ORACLE() throws Exception { testCSV141Failure(CSVFormat.ORACLE, 2); } @Test - public void testCSV141CSVFormat_POSTGRESQL_CSV() throws Exception { + void testCSV141CSVFormat_POSTGRESQL_CSV() throws Exception { testCSV141Failure(CSVFormat.POSTGRESQL_CSV, 3); } @Test - public void testCSV141Excel() throws Exception { + void testCSV141Excel() throws Exception { testCSV141Ok(CSVFormat.EXCEL); } @@ -387,12 +400,12 @@ record = parser.nextRecord(); } @Test - public void testCSV141RFC4180() throws Exception { + void testCSV141RFC4180() throws Exception { testCSV141Failure(CSVFormat.RFC4180, 3); } @Test - public void testCSV235() throws IOException { + void testCSV235() throws IOException { final String dqString = "\"aaa\",\"b\"\"bb\",\"ccc\""; // "aaa","b""bb","ccc" try (CSVParser parser = CSVFormat.RFC4180.parse(new StringReader(dqString))) { final Iterator records = parser.iterator(); @@ -406,7 +419,7 @@ public void testCSV235() throws IOException { } @Test - public void testCSV57() throws Exception { + void testCSV57() throws Exception { try (CSVParser parser = CSVParser.parse("", CSVFormat.DEFAULT)) { final List list = parser.getRecords(); assertNotNull(list); @@ -415,9 +428,9 @@ public void testCSV57() throws Exception { } @Test - public void testDefaultFormat() throws IOException { + void testDefaultFormat() throws IOException { // @formatter:off - final String code = "" + "a,b#\n" + // 1) + final String code = "a,b#\n" + // 1) "\"\n\",\" \",#\n" + // 2) "#,\"\"\n" + // 3) "# Final comment\n" // 4) @@ -440,20 +453,45 @@ public void testDefaultFormat() throws IOException { } @Test - public void testDuplicateHeadersAllowedByDefault() throws Exception { + void testDuplicateHeadersAllowedByDefault() throws Exception { try (CSVParser parser = CSVParser.parse("a,b,a\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader())) { // noop } } @Test - public void testDuplicateHeadersNotAllowed() { + void testDuplicateHeadersNotAllowed() { assertThrows(IllegalArgumentException.class, () -> CSVParser.parse("a,b,a\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader().withAllowDuplicateHeaderNames(false))); } + /** + * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, + * the empty field at the delimiter boundary must survive. The delimiter look-ahead is consumed while skipping + * leading whitespace, so re-evaluating it would drop the empty field and merge the following field's value. + */ + @Test + void testEmptyFieldBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); + try (CSVParser parser = CSVParser.parse(" |a", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "", "a" }, records.get(0)); + } + try (CSVParser parser = CSVParser.parse("a | |b", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "a", "", "b" }, records.get(0)); + } + try (CSVParser parser = CSVParser.parse("a | |b |", format)) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertValuesEquals(new String[] { "a", "", "b", "" }, records.get(0)); + } + } + @Test - public void testEmptyFile() throws Exception { + void testEmptyFile() throws Exception { try (CSVParser parser = CSVParser.parse(Paths.get("src/test/resources/org/apache/commons/csv/empty.txt"), StandardCharsets.UTF_8, CSVFormat.DEFAULT)) { assertNull(parser.nextRecord()); @@ -461,7 +499,7 @@ public void testEmptyFile() throws Exception { } @Test - public void testEmptyFileHeaderParsing() throws Exception { + void testEmptyFileHeaderParsing() throws Exception { try (CSVParser parser = CSVParser.parse("", CSVFormat.DEFAULT.withFirstRecordAsHeader())) { assertNull(parser.nextRecord()); assertTrue(parser.getHeaderNames().isEmpty()); @@ -469,7 +507,7 @@ public void testEmptyFileHeaderParsing() throws Exception { } @Test - public void testEmptyLineBehaviorCSV() throws Exception { + void testEmptyLineBehaviorCSV() throws Exception { final String[] codes = { "hello,\r\n\r\n\r\n", "hello,\n\n\n", "hello,\"\"\r\n\r\n\r\n", "hello,\"\"\n\n\n" }; final String[][] res = { { "hello", "" } // CSV format ignores empty lines }; @@ -479,14 +517,14 @@ public void testEmptyLineBehaviorCSV() throws Exception { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } } @Test - public void testEmptyLineBehaviorExcel() throws Exception { + void testEmptyLineBehaviorExcel() throws Exception { final String[] codes = { "hello,\r\n\r\n\r\n", "hello,\n\n\n", "hello,\"\"\r\n\r\n\r\n", "hello,\"\"\n\n\n" }; final String[][] res = { { "hello", "" }, { "" }, // Excel format does not ignore empty lines { "" } }; @@ -496,21 +534,21 @@ public void testEmptyLineBehaviorExcel() throws Exception { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } } @Test - public void testEmptyString() throws Exception { + void testEmptyString() throws Exception { try (CSVParser parser = CSVParser.parse("", CSVFormat.DEFAULT)) { assertNull(parser.nextRecord()); } } @Test - public void testEndOfFileBehaviorCSV() throws Exception { + void testEndOfFileBehaviorCSV() throws Exception { final String[] codes = { "hello,\r\n\r\nworld,\r\n", "hello,\r\n\r\nworld,", "hello,\r\n\r\nworld,\"\"\r\n", "hello,\r\n\r\nworld,\"\"", "hello,\r\n\r\nworld,\n", "hello,\r\n\r\nworld,", "hello,\r\n\r\nworld,\"\"\n", "hello,\r\n\r\nworld,\"\"" }; final String[][] res = { { "hello", "" }, // CSV format ignores empty lines @@ -521,14 +559,14 @@ public void testEndOfFileBehaviorCSV() throws Exception { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } } @Test - public void testEndOfFileBehaviorExcel() throws Exception { + void testEndOfFileBehaviorExcel() throws Exception { final String[] codes = { "hello,\r\n\r\nworld,\r\n", "hello,\r\n\r\nworld,", "hello,\r\n\r\nworld,\"\"\r\n", "hello,\r\n\r\nworld,\"\"", "hello,\r\n\r\nworld,\n", "hello,\r\n\r\nworld,", "hello,\r\n\r\nworld,\"\"\n", "hello,\r\n\r\nworld,\"\"" }; final String[][] res = { { "hello", "" }, { "" }, // Excel format does not ignore empty lines @@ -540,15 +578,15 @@ public void testEndOfFileBehaviorExcel() throws Exception { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } } @Test - public void testExcelFormat1() throws IOException { - final String code = "value1,value2,value3,value4\r\na,b,c,d\r\n x,,," + "\r\n\r\n\"\"\"hello\"\"\",\" \"\"world\"\"\",\"abc\ndef\",\r\n"; + void testExcelFormat1() throws IOException { + final String code = "value1,value2,value3,value4\r\na,b,c,d\r\n x,,,\r\n\r\n\"\"\"hello\"\"\",\" \"\"world\"\"\",\"abc\ndef\",\r\n"; final String[][] res = { { "value1", "value2", "value3", "value4" }, { "a", "b", "c", "d" }, { " x", "", "", "" }, { "" }, { "\"hello\"", " \"world\"", "abc\ndef", "" } }; try (CSVParser parser = CSVParser.parse(code, CSVFormat.EXCEL)) { @@ -556,13 +594,13 @@ public void testExcelFormat1() throws IOException { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } @Test - public void testExcelFormat2() throws Exception { + void testExcelFormat2() throws Exception { final String code = "foo,baar\r\n\r\nhello,\r\n\r\nworld,\r\n"; final String[][] res = { { "foo", "baar" }, { "" }, { "hello", "" }, { "" }, { "world", "" } }; try (CSVParser parser = CSVParser.parse(code, CSVFormat.EXCEL)) { @@ -570,7 +608,7 @@ public void testExcelFormat2() throws Exception { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } @@ -579,7 +617,7 @@ public void testExcelFormat2() throws Exception { * Tests an exported Excel worksheet with a header row and rows that have more columns than the headers */ @Test - public void testExcelHeaderCountLessThanData() throws Exception { + void testExcelHeaderCountLessThanData() throws Exception { final String code = "A,B,C,,\r\na,b,c,d,e\r\n"; try (CSVParser parser = CSVParser.parse(code, EXCEL_WITH_HEADER)) { parser.getRecords().forEach(record -> { @@ -591,7 +629,7 @@ public void testExcelHeaderCountLessThanData() throws Exception { } @Test - public void testFirstEndOfLineCr() throws IOException { + void testFirstEndOfLineCr() throws IOException { final String data = "foo\rbaar,\rhello,world\r,kanu"; try (CSVParser parser = CSVParser.parse(data, CSVFormat.DEFAULT)) { final List records = parser.getRecords(); @@ -601,7 +639,7 @@ public void testFirstEndOfLineCr() throws IOException { } @Test - public void testFirstEndOfLineCrLf() throws IOException { + void testFirstEndOfLineCrLf() throws IOException { final String data = "foo\r\nbaar,\r\nhello,world\r\n,kanu"; try (CSVParser parser = CSVParser.parse(data, CSVFormat.DEFAULT)) { final List records = parser.getRecords(); @@ -611,7 +649,7 @@ public void testFirstEndOfLineCrLf() throws IOException { } @Test - public void testFirstEndOfLineLf() throws IOException { + void testFirstEndOfLineLf() throws IOException { final String data = "foo\nbaar,\nhello,world\n,kanu"; try (CSVParser parser = CSVParser.parse(data, CSVFormat.DEFAULT)) { final List records = parser.getRecords(); @@ -621,7 +659,7 @@ public void testFirstEndOfLineLf() throws IOException { } @Test - public void testForEach() throws Exception { + void testForEach() throws Exception { try (Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); CSVParser parser = CSVFormat.DEFAULT.parse(in)) { final List records = new ArrayList<>(); @@ -629,14 +667,108 @@ public void testForEach() throws Exception { records.add(record); } assertEquals(3, records.size()); - assertArrayEquals(new String[] { "a", "b", "c" }, records.get(0).values()); - assertArrayEquals(new String[] { "1", "2", "3" }, records.get(1).values()); - assertArrayEquals(new String[] { "x", "y", "z" }, records.get(2).values()); + assertValuesEquals(new String[] { "a", "b", "c" }, records.get(0)); + assertValuesEquals(new String[] { "1", "2", "3" }, records.get(1)); + assertValuesEquals(new String[] { "x", "y", "z" }, records.get(2)); } } @Test - public void testGetHeaderComment_HeaderComment1() throws IOException { + void testGetBytePositionMultiCharacterDelimiter() throws IOException { + final String code = "aa[|]bb\ncc[|]dd\n"; + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(code)) + .setFormat(format) + .setCharset(StandardCharsets.UTF_8) + .setTrackBytes(true) + .get()) { + final Iterator it = parser.iterator(); + final CSVRecord first = it.next(); + final CSVRecord second = it.next(); + assertEquals(0, first.getBytePosition()); + assertEquals(8, second.getBytePosition()); + } + } + + /** + * Tests CSV-329. + */ + @Test + void testGetBytePositionMultiCharacterDelimiterWithSupplementaryCharacter() throws IOException { + final String delimiter = "x😀"; + final String code = "ax😀b\ncx😀d\n"; + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(delimiter).get(); + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(code)) + .setFormat(format) + .setCharset(UTF_8) + .setTrackBytes(true) + .get()) { + final CSVRecord first = parser.nextRecord(); + final CSVRecord second = parser.nextRecord(); + assertNotNull(first); + assertNotNull(second); + assertValuesEquals(new String[] { "a", "b" }, first); + assertValuesEquals(new String[] { "c", "d" }, second); + assertEquals(0, first.getBytePosition()); + assertEquals("ax😀b\n".getBytes(UTF_8).length, second.getBytePosition()); + } + } + + @Test + void testGetBytePositionWithCharacterOffsetAndMultiBytePrefix() throws Exception { + final String row0 = "é,x\n"; + final Charset charset = UTF_8; + // row0 char count is 4 + assertEquals(4, row0.length()); + // row0 byte count is 5 + final int record1ByteOffset = row0.getBytes(charset).length; + assertEquals(5, record1ByteOffset); + final String row1 = "b,c\n"; + final String rows = row0 + row1; + final long record1CharOffset = row0.length(); + final long expectedByteOffset = row0.getBytes(charset).length; + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(row1)) + .setFormat(CSVFormat.DEFAULT) + .setCharset(charset) + .setTrackBytes(true) + .setByteOffset(record1ByteOffset) + .setCharacterOffset(record1CharOffset) + .setRecordNumber(2) // not relevant but a better use case example. + .get()) { + final CSVRecord record = parser.nextRecord(); + assertNotNull(record); + assertEquals(4, record.getCharacterPosition()); + assertEquals(record1CharOffset, record.getCharacterPosition()); + assertEquals(expectedByteOffset, record.getBytePosition()); + } + } + + @Test + void testGetBytePositionWithSingleByteCharset() throws IOException { + // A single-byte charset cannot encode U+FFFF, the char value of the EOF sentinel. + // Byte counting must skip the EOF read so a valid file parses without throwing. + final String code = "a,b\nc,d\n"; + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader(code)) + .setFormat(CSVFormat.DEFAULT) + .setCharset(StandardCharsets.ISO_8859_1) + .setTrackBytes(true) + .get()) { + final CSVRecord first = parser.nextRecord(); + final CSVRecord second = parser.nextRecord(); + assertNotNull(first); + assertNotNull(second); + assertNull(parser.nextRecord()); + assertEquals(0, first.getBytePosition()); + assertEquals(4, second.getBytePosition()); + } + } + + @Test + void testGetHeaderComment_HeaderComment1() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_AUTO_HEADER)) { parser.getRecords(); // Expect a header comment @@ -646,7 +778,7 @@ public void testGetHeaderComment_HeaderComment1() throws IOException { } @Test - public void testGetHeaderComment_HeaderComment2() throws IOException { + void testGetHeaderComment_HeaderComment2() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_EXPLICIT_HEADER)) { parser.getRecords(); // Expect a header comment @@ -656,7 +788,7 @@ public void testGetHeaderComment_HeaderComment2() throws IOException { } @Test - public void testGetHeaderComment_HeaderComment3() throws IOException { + void testGetHeaderComment_HeaderComment3() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_EXPLICIT_HEADER_NOSKIP)) { parser.getRecords(); // Expect no header comment - the text "comment" is attached to the first record @@ -666,7 +798,7 @@ public void testGetHeaderComment_HeaderComment3() throws IOException { } @Test - public void testGetHeaderComment_HeaderTrailerComment() throws IOException { + void testGetHeaderComment_HeaderTrailerComment() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_MULTILINE_HEADER_TRAILER_COMMENT, FORMAT_AUTO_HEADER)) { parser.getRecords(); // Expect a header comment @@ -676,7 +808,7 @@ public void testGetHeaderComment_HeaderTrailerComment() throws IOException { } @Test - public void testGetHeaderComment_NoComment1() throws IOException { + void testGetHeaderComment_NoComment1() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_NO_COMMENT, FORMAT_AUTO_HEADER)) { parser.getRecords(); // Expect no header comment @@ -686,7 +818,7 @@ public void testGetHeaderComment_NoComment1() throws IOException { } @Test - public void testGetHeaderComment_NoComment2() throws IOException { + void testGetHeaderComment_NoComment2() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_NO_COMMENT, FORMAT_EXPLICIT_HEADER)) { parser.getRecords(); // Expect no header comment @@ -696,7 +828,7 @@ public void testGetHeaderComment_NoComment2() throws IOException { } @Test - public void testGetHeaderComment_NoComment3() throws IOException { + void testGetHeaderComment_NoComment3() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_NO_COMMENT, FORMAT_EXPLICIT_HEADER_NOSKIP)) { parser.getRecords(); // Expect no header comment @@ -706,7 +838,7 @@ public void testGetHeaderComment_NoComment3() throws IOException { } @Test - public void testGetHeaderMap() throws Exception { + void testGetHeaderMap() throws Exception { try (CSVParser parser = CSVParser.parse("a,b,c\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) { final Map headerMap = parser.getHeaderMap(); final Iterator columnNames = headerMap.keySet().iterator(); @@ -730,7 +862,7 @@ public void testGetHeaderMap() throws Exception { } @Test - public void testGetHeaderNames() throws IOException { + void testGetHeaderNames() throws IOException { try (CSVParser parser = CSVParser.parse("a,b,c\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) { final Map nameIndexMap = parser.getHeaderMap(); final List headerNames = parser.getHeaderNames(); @@ -744,7 +876,7 @@ public void testGetHeaderNames() throws IOException { } @Test - public void testGetHeaderNamesReadOnly() throws IOException { + void testGetHeaderNamesReadOnly() throws IOException { try (CSVParser parser = CSVParser.parse("a,b,c\n1,2,3\nx,y,z", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) { final List headerNames = parser.getHeaderNames(); assertNotNull(headerNames); @@ -753,10 +885,10 @@ public void testGetHeaderNamesReadOnly() throws IOException { } @Test - public void testGetLine() throws IOException { + void testGetLine() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT, CSVFormat.DEFAULT.withIgnoreSurroundingSpaces())) { for (final String[] re : RESULT) { - assertArrayEquals(re, parser.nextRecord().values()); + assertValuesEquals(re, parser.nextRecord()); } assertNull(parser.nextRecord()); @@ -764,25 +896,25 @@ public void testGetLine() throws IOException { } @Test - public void testGetLineNumberWithCR() throws Exception { + void testGetLineNumberWithCR() throws Exception { validateLineNumbers(String.valueOf(CR)); } @Test - public void testGetLineNumberWithCRLF() throws Exception { + void testGetLineNumberWithCRLF() throws Exception { validateLineNumbers(CRLF); } @Test - public void testGetLineNumberWithLF() throws Exception { + void testGetLineNumberWithLF() throws Exception { validateLineNumbers(String.valueOf(LF)); } @Test - public void testGetOneLine() throws IOException { + void testGetOneLine() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_1, CSVFormat.DEFAULT)) { final CSVRecord record = parser.getRecords().get(0); - assertArrayEquals(RESULT[0], record.values()); + assertValuesEquals(RESULT[0], record); } } @@ -792,7 +924,7 @@ public void testGetOneLine() throws IOException { * @throws IOException when an I/O error occurs. */ @Test - public void testGetOneLineOneParser() throws IOException { + void testGetOneLineOneParser() throws IOException { final CSVFormat format = CSVFormat.DEFAULT; try (PipedWriter writer = new PipedWriter(); PipedReader origin = new PipedReader(writer); @@ -803,16 +935,16 @@ public void testGetOneLineOneParser() throws IOException { writer.append(CSV_INPUT_1); writer.append(format.getRecordSeparator()); final CSVRecord record1 = parser.nextRecord(); - assertArrayEquals(RESULT[0], record1.values()); + assertValuesEquals(RESULT[0], record1); writer.append(CSV_INPUT_2); writer.append(format.getRecordSeparator()); final CSVRecord record2 = parser.nextRecord(); - assertArrayEquals(RESULT[1], record2.values()); + assertValuesEquals(RESULT[1], record2); } } @Test - public void testGetRecordFourBytesRead() throws Exception { + void testGetRecordFourBytesRead() throws Exception { final String code = "id,a,b,c\n" + "1,😊,🤔,😂\n" + "2,😊,🤔,😂\n" + @@ -846,44 +978,44 @@ public void testGetRecordFourBytesRead() throws Exception { } @Test - public void testGetRecordNumberWithCR() throws Exception { + void testGetRecordNumberWithCR() throws Exception { validateRecordNumbers(String.valueOf(CR)); } @Test - public void testGetRecordNumberWithCRLF() throws Exception { + void testGetRecordNumberWithCRLF() throws Exception { validateRecordNumbers(CRLF); } @Test - public void testGetRecordNumberWithLF() throws Exception { + void testGetRecordNumberWithLF() throws Exception { validateRecordNumbers(String.valueOf(LF)); } @Test - public void testGetRecordPositionWithCRLF() throws Exception { + void testGetRecordPositionWithCRLF() throws Exception { validateRecordPosition(CRLF); } @Test - public void testGetRecordPositionWithLF() throws Exception { + void testGetRecordPositionWithLF() throws Exception { validateRecordPosition(String.valueOf(LF)); } @Test - public void testGetRecords() throws IOException { + void testGetRecords() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT, CSVFormat.DEFAULT.withIgnoreSurroundingSpaces())) { final List records = parser.getRecords(); assertEquals(RESULT.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < RESULT.length; i++) { - assertArrayEquals(RESULT[i], records.get(i).values()); + assertValuesEquals(RESULT[i], records.get(i)); } } } @Test - public void testGetRecordsFromBrokenInputStream() throws IOException { + void testGetRecordsFromBrokenInputStream() throws IOException { @SuppressWarnings("resource") // We also get an exception on close, which is OK but can't assert in a try. final CSVParser parser = CSVParser.parse(new BrokenInputStream(), UTF_8, CSVFormat.DEFAULT); assertThrows(UncheckedIOException.class, parser::getRecords); @@ -892,20 +1024,37 @@ public void testGetRecordsFromBrokenInputStream() throws IOException { @ParameterizedTest @ValueSource(longs = { -1, 0, 1, 2, 3, 4, Long.MAX_VALUE }) - public void testGetRecordsMaxRows(final long maxRows) throws IOException { + void testGetRecordsMaxRows(final long maxRows) throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT, CSVFormat.DEFAULT.builder().setIgnoreSurroundingSpaces(true).setMaxRows(maxRows).get())) { final List records = parser.getRecords(); final long expectedLength = maxRows <= 0 || maxRows > RESULT.length ? RESULT.length : maxRows; assertEquals(expectedLength, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < expectedLength; i++) { - assertArrayEquals(RESULT[i], records.get(i).values()); + assertValuesEquals(RESULT[i], records.get(i)); } } } + /** + * Tests CSV-327. + */ @Test - public void testGetRecordThreeBytesRead() throws Exception { + void testGetRecordsMaxRowsWithRecordNumberOffset() throws IOException { + try (CSVParser parser = CSVParser.builder() + .setReader(new StringReader("a,b\nc,d\n")) + .setFormat(CSVFormat.DEFAULT.builder().setMaxRows(1).get()) + .setRecordNumber(2) + .get()) { + final List records = parser.getRecords(); + assertEquals(1, records.size()); + assertEquals(2, records.get(0).getRecordNumber()); + assertValuesEquals(new String[] { "a", "b" }, records.get(0)); + } + } + + @Test + void testGetRecordThreeBytesRead() throws Exception { final String code = "id,date,val5,val4\n" + "11111111111111,'4017-09-01',きちんと節分近くには咲いてる~,v4\n" + "22222222222222,'4017-01-01',おはよう私の友人~,v4\n" + @@ -941,7 +1090,7 @@ public void testGetRecordThreeBytesRead() throws Exception { } @Test - public void testGetRecordWithMultiLineValues() throws Exception { + void testGetRecordWithMultiLineValues() throws Exception { try (CSVParser parser = CSVParser.parse("\"a\r\n1\",\"a\r\n2\"" + CRLF + "\"b\r\n1\",\"b\r\n2\"" + CRLF + "\"c\r\n1\",\"c\r\n2\"", CSVFormat.DEFAULT.withRecordSeparator(CRLF))) { CSVRecord record; @@ -966,7 +1115,7 @@ public void testGetRecordWithMultiLineValues() throws Exception { } @Test - public void testGetTrailerComment_HeaderComment1() throws IOException { + void testGetTrailerComment_HeaderComment1() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_AUTO_HEADER)) { parser.getRecords(); assertFalse(parser.hasTrailerComment()); @@ -975,7 +1124,7 @@ public void testGetTrailerComment_HeaderComment1() throws IOException { } @Test - public void testGetTrailerComment_HeaderComment2() throws IOException { + void testGetTrailerComment_HeaderComment2() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_EXPLICIT_HEADER)) { parser.getRecords(); assertFalse(parser.hasTrailerComment()); @@ -984,7 +1133,7 @@ public void testGetTrailerComment_HeaderComment2() throws IOException { } @Test - public void testGetTrailerComment_HeaderComment3() throws IOException { + void testGetTrailerComment_HeaderComment3() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_COMMENT, FORMAT_EXPLICIT_HEADER_NOSKIP)) { parser.getRecords(); assertFalse(parser.hasTrailerComment()); @@ -993,7 +1142,7 @@ public void testGetTrailerComment_HeaderComment3() throws IOException { } @Test - public void testGetTrailerComment_HeaderTrailerComment1() throws IOException { + void testGetTrailerComment_HeaderTrailerComment1() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_TRAILER_COMMENT, FORMAT_AUTO_HEADER)) { parser.getRecords(); assertTrue(parser.hasTrailerComment()); @@ -1002,7 +1151,7 @@ public void testGetTrailerComment_HeaderTrailerComment1() throws IOException { } @Test - public void testGetTrailerComment_HeaderTrailerComment2() throws IOException { + void testGetTrailerComment_HeaderTrailerComment2() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_TRAILER_COMMENT, FORMAT_EXPLICIT_HEADER)) { parser.getRecords(); assertTrue(parser.hasTrailerComment()); @@ -1011,7 +1160,7 @@ public void testGetTrailerComment_HeaderTrailerComment2() throws IOException { } @Test - public void testGetTrailerComment_HeaderTrailerComment3() throws IOException { + void testGetTrailerComment_HeaderTrailerComment3() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_HEADER_TRAILER_COMMENT, FORMAT_EXPLICIT_HEADER_NOSKIP)) { parser.getRecords(); assertTrue(parser.hasTrailerComment()); @@ -1020,7 +1169,7 @@ public void testGetTrailerComment_HeaderTrailerComment3() throws IOException { } @Test - public void testGetTrailerComment_MultilineComment() throws IOException { + void testGetTrailerComment_MultilineComment() throws IOException { try (CSVParser parser = CSVParser.parse(CSV_INPUT_MULTILINE_HEADER_TRAILER_COMMENT, FORMAT_AUTO_HEADER)) { parser.getRecords(); assertTrue(parser.hasTrailerComment()); @@ -1029,7 +1178,7 @@ public void testGetTrailerComment_MultilineComment() throws IOException { } @Test - public void testHeader() throws Exception { + void testHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader().parse(in)) { @@ -1048,7 +1197,7 @@ public void testHeader() throws Exception { } @Test - public void testHeaderComment() throws Exception { + void testHeaderComment() throws Exception { final Reader in = new StringReader("# comment\na,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withCommentMarker('#').withHeader().parse(in)) { final Iterator records = parser.iterator(); @@ -1064,7 +1213,7 @@ public void testHeaderComment() throws Exception { } @Test - public void testHeaderMissing() throws Exception { + void testHeaderMissing() throws Exception { final Reader in = new StringReader("a,,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader().withAllowMissingColumnNames().parse(in)) { final Iterator records = parser.iterator(); @@ -1079,7 +1228,7 @@ public void testHeaderMissing() throws Exception { } @Test - public void testHeaderMissingWithNull() throws Exception { + void testHeaderMissingWithNull() throws Exception { final Reader in = new StringReader("a,,c,,e\n1,2,3,4,5\nv,w,x,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader().withNullString("").withAllowMissingColumnNames().parse(in)) { parser.iterator(); @@ -1087,7 +1236,7 @@ public void testHeaderMissingWithNull() throws Exception { } @Test - public void testHeadersMissing() throws Exception { + void testHeadersMissing() throws Exception { try (Reader in = new StringReader("a,,c,,e\n1,2,3,4,5\nv,w,x,y,z"); CSVParser parser = CSVFormat.DEFAULT.withHeader().withAllowMissingColumnNames().parse(in)) { parser.iterator(); @@ -1095,19 +1244,19 @@ public void testHeadersMissing() throws Exception { } @Test - public void testHeadersMissingException() { + void testHeadersMissingException() { final Reader in = new StringReader("a,,c,,e\n1,2,3,4,5\nv,w,x,y,z"); assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withHeader().parse(in).iterator()); } @Test - public void testHeadersMissingOneColumnException() { + void testHeadersMissingOneColumnException() { final Reader in = new StringReader("a,,c,d,e\n1,2,3,4,5\nv,w,x,y,z"); assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withHeader().parse(in).iterator()); } @Test - public void testHeadersWithNullColumnName() throws IOException { + void testHeadersWithNullColumnName() throws IOException { final Reader in = new StringReader("header1,null,header3\n1,2,3\n4,5,6"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader().withNullString("null").withAllowMissingColumnNames().parse(in)) { final Iterator records = parser.iterator(); @@ -1121,7 +1270,7 @@ public void testHeadersWithNullColumnName() throws IOException { } @Test - public void testIgnoreCaseHeaderMapping() throws Exception { + void testIgnoreCaseHeaderMapping() throws Exception { final Reader reader = new StringReader("1,2,3"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("One", "TWO", "three").withIgnoreHeaderCase().parse(reader)) { final Iterator records = parser.iterator(); @@ -1133,7 +1282,7 @@ public void testIgnoreCaseHeaderMapping() throws Exception { } @Test - public void testIgnoreEmptyLines() throws IOException { + void testIgnoreEmptyLines() throws IOException { final String code = "\nfoo,baar\n\r\n,\n\n,world\r\n\n"; // String code = "world\r\n\n"; // String code = "foo;baar\r\n\r\nhello;\r\n\r\nworld;\r\n"; @@ -1144,23 +1293,23 @@ public void testIgnoreEmptyLines() throws IOException { } @Test - public void testInvalidFormat() { + void testInvalidFormat() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withDelimiter(CR)); } @Test - public void testIterator() throws Exception { + void testIterator() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.parse(in)) { final Iterator iterator = parser.iterator(); assertTrue(iterator.hasNext()); assertThrows(UnsupportedOperationException.class, iterator::remove); - assertArrayEquals(new String[] { "a", "b", "c" }, iterator.next().values()); - assertArrayEquals(new String[] { "1", "2", "3" }, iterator.next().values()); + assertValuesEquals(new String[] { "a", "b", "c" }, iterator.next()); + assertValuesEquals(new String[] { "1", "2", "3" }, iterator.next()); assertTrue(iterator.hasNext()); assertTrue(iterator.hasNext()); assertTrue(iterator.hasNext()); - assertArrayEquals(new String[] { "x", "y", "z" }, iterator.next().values()); + assertValuesEquals(new String[] { "x", "y", "z" }, iterator.next()); assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, iterator::next); } @@ -1168,26 +1317,26 @@ public void testIterator() throws Exception { @ParameterizedTest @ValueSource(longs = { -1, 0, 1, 2, 3, 4, 5, Long.MAX_VALUE }) - public void testIteratorMaxRows(final long maxRows) throws Exception { + void testIteratorMaxRows(final long maxRows) throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.builder().setMaxRows(maxRows).get().parse(in)) { final Iterator iterator = parser.iterator(); assertTrue(iterator.hasNext()); assertThrows(UnsupportedOperationException.class, iterator::remove); - assertArrayEquals(new String[] { "a", "b", "c" }, iterator.next().values()); + assertValuesEquals(new String[] { "a", "b", "c" }, iterator.next()); final boolean noLimit = maxRows <= 0; final int fixtureLen = 3; final long expectedLen = noLimit ? fixtureLen : Math.min(fixtureLen, maxRows); if (expectedLen > 1) { assertTrue(iterator.hasNext()); - assertArrayEquals(new String[] { "1", "2", "3" }, iterator.next().values()); + assertValuesEquals(new String[] { "1", "2", "3" }, iterator.next()); } assertEquals(expectedLen > 2, iterator.hasNext()); // again assertEquals(expectedLen > 2, iterator.hasNext()); if (expectedLen == fixtureLen) { assertTrue(iterator.hasNext()); - assertArrayEquals(new String[] { "x", "y", "z" }, iterator.next().values()); + assertValuesEquals(new String[] { "x", "y", "z" }, iterator.next()); } assertFalse(iterator.hasNext()); assertThrows(NoSuchElementException.class, iterator::next); @@ -1195,7 +1344,7 @@ public void testIteratorMaxRows(final long maxRows) throws Exception { } @Test - public void testIteratorSequenceBreaking() throws IOException { + void testIteratorSequenceBreaking() throws IOException { final String fiveRows = "1\n2\n3\n4\n5\n"; // Iterator hasNext() shouldn't break sequence try (CSVParser parser = CSVFormat.DEFAULT.parse(new StringReader(fiveRows))) { @@ -1250,7 +1399,7 @@ public void testIteratorSequenceBreaking() throws IOException { } @Test - public void testLineFeedEndings() throws IOException { + void testLineFeedEndings() throws IOException { final String code = "foo\nbaar,\nhello,world\n,kanu"; try (CSVParser parser = CSVParser.parse(code, CSVFormat.DEFAULT)) { final List records = parser.getRecords(); @@ -1259,7 +1408,7 @@ public void testLineFeedEndings() throws IOException { } @Test - public void testMappedButNotSetAsOutlook2007ContactExport() throws Exception { + void testMappedButNotSetAsOutlook2007ContactExport() throws Exception { final Reader in = new StringReader("a,b,c\n1,2\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("A", "B", "C").withSkipHeaderRecord().parse(in)) { final Iterator records = parser.iterator(); @@ -1294,7 +1443,7 @@ record = records.next(); @Test @Disabled - public void testMongoDbCsv() throws Exception { + void testMongoDbCsv() throws Exception { try (CSVParser parser = CSVParser.parse("\"a a\",b,c" + LF + "d,e,f", CSVFormat.MONGODB_CSV)) { final Iterator itr1 = parser.iterator(); final Iterator itr2 = parser.iterator(); @@ -1313,7 +1462,7 @@ public void testMongoDbCsv() throws Exception { @Test // TODO this may lead to strange behavior, throw an exception if iterator() has already been called? - public void testMultipleIterators() throws Exception { + void testMultipleIterators() throws Exception { try (CSVParser parser = CSVParser.parse("a,b,c" + CRLF + "d,e,f", CSVFormat.DEFAULT)) { final Iterator itr1 = parser.iterator(); @@ -1330,24 +1479,24 @@ public void testMultipleIterators() throws Exception { } @Test - public void testNewCSVParserNullReaderFormat() { + void testNewCSVParserNullReaderFormat() { assertThrows(NullPointerException.class, () -> new CSVParser(null, CSVFormat.DEFAULT)); } @Test - public void testNewCSVParserReaderNullFormat() { + void testNewCSVParserReaderNullFormat() { assertThrows(NullPointerException.class, () -> new CSVParser(new StringReader(""), null)); } @Test - public void testNoHeaderMap() throws Exception { + void testNoHeaderMap() throws Exception { try (CSVParser parser = CSVParser.parse("a,b,c\n1,2,3\nx,y,z", CSVFormat.DEFAULT)) { assertNull(parser.getHeaderMap()); } } @Test - public void testNotValueCSV() throws IOException { + void testNotValueCSV() throws IOException { final String source = "#"; final CSVFormat csvFormat = CSVFormat.DEFAULT.withCommentMarker('#'); try (CSVParser csvParser = csvFormat.parse(new StringReader(source))) { @@ -1357,7 +1506,7 @@ public void testNotValueCSV() throws IOException { } @Test - public void testParse() throws Exception { + void testParse() throws Exception { final URL url = ClassLoader.getSystemClassLoader().getResource("org/apache/commons/csv/CSVFileParser/test.csv"); final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader("A", "B", "C", "D").get(); final Charset charset = StandardCharsets.UTF_8; @@ -1424,7 +1573,7 @@ public void testParse() throws Exception { } @Test - public void testParseFileCharsetNullFormat() throws IOException { + void testParseFileCharsetNullFormat() throws IOException { final File file = new File("src/test/resources/org/apache/commons/csv/CSVFileParser/test.csv"); try (CSVParser parser = CSVParser.parse(file, Charset.defaultCharset(), null)) { // null maps to DEFAULT. @@ -1433,7 +1582,7 @@ public void testParseFileCharsetNullFormat() throws IOException { } @Test - public void testParseInputStreamCharsetNullFormat() throws IOException { + void testParseInputStreamCharsetNullFormat() throws IOException { try (InputStream in = Files.newInputStream(Paths.get("src/test/resources/org/apache/commons/csv/CSVFileParser/test.csv")); CSVParser parser = CSVParser.parse(in, Charset.defaultCharset(), null)) { // null maps to DEFAULT. @@ -1442,27 +1591,27 @@ public void testParseInputStreamCharsetNullFormat() throws IOException { } @Test - public void testParseNullFileFormat() { + void testParseNullFileFormat() { assertThrows(NullPointerException.class, () -> CSVParser.parse((File) null, Charset.defaultCharset(), CSVFormat.DEFAULT)); } @Test - public void testParseNullPathFormat() { + void testParseNullPathFormat() { assertThrows(NullPointerException.class, () -> CSVParser.parse((Path) null, Charset.defaultCharset(), CSVFormat.DEFAULT)); } @Test - public void testParseNullStringFormat() { + void testParseNullStringFormat() { assertThrows(NullPointerException.class, () -> CSVParser.parse((String) null, CSVFormat.DEFAULT)); } @Test - public void testParseNullUrlCharsetFormat() { + void testParseNullUrlCharsetFormat() { assertThrows(NullPointerException.class, () -> CSVParser.parse((URL) null, Charset.defaultCharset(), CSVFormat.DEFAULT)); } @Test - public void testParsePathCharsetNullFormat() throws IOException { + void testParsePathCharsetNullFormat() throws IOException { final Path path = Paths.get("src/test/resources/org/apache/commons/csv/CSVFileParser/test.csv"); try (CSVParser parser = CSVParser.parse(path, Charset.defaultCharset(), null)) { // null maps to DEFAULT. @@ -1471,7 +1620,7 @@ public void testParsePathCharsetNullFormat() throws IOException { } @Test - public void testParserUrlNullCharsetFormat() throws IOException { + void testParserUrlNullCharsetFormat() throws IOException { final URL url = ClassLoader.getSystemClassLoader().getResource("org/apache/commons/csv/CSVFileParser/test.csv"); try (CSVParser parser = CSVParser.parse(url, null, CSVFormat.DEFAULT)) { // null maps to DEFAULT. @@ -1480,7 +1629,7 @@ public void testParserUrlNullCharsetFormat() throws IOException { } @Test - public void testParseStringNullFormat() throws IOException { + void testParseStringNullFormat() throws IOException { try (CSVParser parser = CSVParser.parse("1,2,3", null)) { // null maps to DEFAULT. final List records = parser.getRecords(); @@ -1494,7 +1643,7 @@ public void testParseStringNullFormat() throws IOException { } @Test - public void testParseUrlCharsetNullFormat() throws IOException { + void testParseUrlCharsetNullFormat() throws IOException { final URL url = ClassLoader.getSystemClassLoader().getResource("org/apache/commons/csv/CSVFileParser/test.csv"); try (CSVParser parser = CSVParser.parse(url, Charset.defaultCharset(), null)) { // null maps to DEFAULT. @@ -1503,7 +1652,7 @@ public void testParseUrlCharsetNullFormat() throws IOException { } @Test - public void testParseWithDelimiterStringWithEscape() throws IOException { + void testParseWithDelimiterStringWithEscape() throws IOException { final String source = "a![!|!]b![|]c[|]xyz\r\nabc[abc][|]xyz"; final CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').get(); try (CSVParser csvParser = csvFormat.parse(new StringReader(source))) { @@ -1517,7 +1666,7 @@ public void testParseWithDelimiterStringWithEscape() throws IOException { } @Test - public void testParseWithDelimiterStringWithQuote() throws IOException { + void testParseWithDelimiterStringWithQuote() throws IOException { final String source = "'a[|]b[|]c'[|]xyz\r\nabc[abc][|]xyz"; final CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setQuote('\'').get(); try (CSVParser csvParser = csvFormat.parse(new StringReader(source))) { @@ -1531,7 +1680,7 @@ public void testParseWithDelimiterStringWithQuote() throws IOException { } @Test - public void testParseWithDelimiterWithEscape() throws IOException { + void testParseWithDelimiterWithEscape() throws IOException { final String source = "a!,b!,c,xyz"; final CSVFormat csvFormat = CSVFormat.DEFAULT.withEscape('!'); try (CSVParser csvParser = csvFormat.parse(new StringReader(source))) { @@ -1542,7 +1691,7 @@ public void testParseWithDelimiterWithEscape() throws IOException { } @Test - public void testParseWithDelimiterWithQuote() throws IOException { + void testParseWithDelimiterWithQuote() throws IOException { final String source = "'a,b,c',xyz"; final CSVFormat csvFormat = CSVFormat.DEFAULT.withQuote('\''); try (CSVParser csvParser = csvFormat.parse(new StringReader(source))) { @@ -1553,7 +1702,7 @@ public void testParseWithDelimiterWithQuote() throws IOException { } @Test - public void testParseWithQuoteThrowsException() { + void testParseWithQuoteThrowsException() { final CSVFormat csvFormat = CSVFormat.DEFAULT.withQuote('\''); assertThrows(IOException.class, () -> csvFormat.parse(new StringReader("'a,b,c','")).nextRecord()); assertThrows(IOException.class, () -> csvFormat.parse(new StringReader("'a,b,c'abc,xyz")).nextRecord()); @@ -1561,7 +1710,7 @@ public void testParseWithQuoteThrowsException() { } @Test - public void testParseWithQuoteWithEscape() throws IOException { + void testParseWithQuoteWithEscape() throws IOException { final String source = "'a?,b?,c?d',xyz"; final CSVFormat csvFormat = CSVFormat.DEFAULT.withQuote('\'').withEscape('?'); try (CSVParser csvParser = csvFormat.parse(new StringReader(source))) { @@ -1573,7 +1722,7 @@ public void testParseWithQuoteWithEscape() throws IOException { @ParameterizedTest @EnumSource(CSVFormat.Predefined.class) - public void testParsingPrintedEmptyFirstColumn(final CSVFormat.Predefined format) throws Exception { + void testParsingPrintedEmptyFirstColumn(final CSVFormat.Predefined format) throws Exception { final String[][] lines = { { "a", "b" }, { "", "x" } }; final StringWriter buf = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(buf, format.getFormat())) { @@ -1584,14 +1733,58 @@ public void testParsingPrintedEmptyFirstColumn(final CSVFormat.Predefined format .setFormat(format.getFormat()) .get()) { for (final String[] line : lines) { - assertArrayEquals(line, csvRecords.nextRecord().values()); + assertValuesEquals(line, csvRecords.nextRecord()); } assertNull(csvRecords.nextRecord()); } } + /** + * A truncated escaped multi-character delimiter at EOF must stay literal data and not be completed from a stale + * escape delimiter look-ahead. + */ + @Test + void testPartialEscapedMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').get(); + try (CSVParser parser = format.parse(new StringReader("x![!|!]y![!|"))) { + final CSVRecord record = parser.nextRecord(); + assertEquals("x[|]y![!|", record.get(0)); + assertEquals(1, record.size()); + } + } + + /** + * Tests CSV-324. + */ + @Test + void testPartialMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + try (CSVParser parser = format.parse(new StringReader("a[|]b[|"))) { + final CSVRecord record = parser.nextRecord(); + assertEquals("a", record.get(0)); + assertEquals("b[|", record.get(1)); + assertEquals(2, record.size()); + } + } + + /** + * A truncated multi-character delimiter at EOF must not be completed from the look-ahead buffer left dirty by an + * earlier non-matching peek in the same token. + */ @Test - public void testProvidedHeader() throws Exception { + void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + // The "[a]" peek leaves ']' in the look-ahead buffer; the trailing "[|" must not match "[|]". + final String recordString = "x[a][|"; + try (CSVParser parser = format.parse(new StringReader(recordString))) { + final CSVRecord record = parser.nextRecord(); + assertEquals(recordString, record.get(0)); + assertEquals(1, record.size()); + } + } + + @Test + void testProvidedHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("A", "B", "C").parse(in)) { final Iterator records = parser.iterator(); @@ -1611,7 +1804,7 @@ public void testProvidedHeader() throws Exception { } @Test - public void testProvidedHeaderAuto() throws Exception { + void testProvidedHeaderAuto() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader().parse(in)) { final Iterator records = parser.iterator(); @@ -1631,7 +1824,7 @@ public void testProvidedHeaderAuto() throws Exception { } @Test - public void testRepeatedHeadersAreReturnedInCSVRecordHeaderNames() throws IOException { + void testRepeatedHeadersAreReturnedInCSVRecordHeaderNames() throws IOException { final Reader in = new StringReader("header1,header2,header1\n1,2,3\n4,5,6"); try (CSVParser parser = CSVFormat.DEFAULT.withFirstRecordAsHeader().withTrim().parse(in)) { final Iterator records = parser.iterator(); @@ -1643,7 +1836,7 @@ public void testRepeatedHeadersAreReturnedInCSVRecordHeaderNames() throws IOExce } @Test - public void testRoundtrip() throws Exception { + void testRoundtrip() throws Exception { final StringWriter out = new StringWriter(); final String data = "a,b,c\r\n1,2,3\r\nx,y,z\r\n"; try (CSVPrinter printer = new CSVPrinter(out, CSVFormat.DEFAULT); @@ -1656,7 +1849,7 @@ public void testRoundtrip() throws Exception { } @Test - public void testSkipAutoHeader() throws Exception { + void testSkipAutoHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader().parse(in)) { final Iterator records = parser.iterator(); @@ -1668,7 +1861,7 @@ public void testSkipAutoHeader() throws Exception { } @Test - public void testSkipHeaderOverrideDuplicateHeaders() throws Exception { + void testSkipHeaderOverrideDuplicateHeaders() throws Exception { final Reader in = new StringReader("a,a,a\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("X", "Y", "Z").withSkipHeaderRecord().parse(in)) { final Iterator records = parser.iterator(); @@ -1680,7 +1873,7 @@ public void testSkipHeaderOverrideDuplicateHeaders() throws Exception { } @Test - public void testSkipSetAltHeaders() throws Exception { + void testSkipSetAltHeaders() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("X", "Y", "Z").withSkipHeaderRecord().parse(in)) { final Iterator records = parser.iterator(); @@ -1692,7 +1885,7 @@ public void testSkipSetAltHeaders() throws Exception { } @Test - public void testSkipSetHeader() throws Exception { + void testSkipSetHeader() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("a", "b", "c").withSkipHeaderRecord().parse(in)) { final Iterator records = parser.iterator(); @@ -1705,7 +1898,7 @@ public void testSkipSetHeader() throws Exception { @Test @Disabled - public void testStartWithEmptyLinesThenHeaders() throws Exception { + void testStartWithEmptyLinesThenHeaders() throws Exception { final String[] codes = { "\r\n\r\n\r\nhello,\r\n\r\n\r\n", "hello,\n\n\n", "hello,\"\"\r\n\r\n\r\n", "hello,\"\"\n\n\n" }; final String[][] res = { { "hello", "" }, { "" }, // Excel format does not ignore empty lines { "" } }; @@ -1715,43 +1908,43 @@ public void testStartWithEmptyLinesThenHeaders() throws Exception { assertEquals(res.length, records.size()); assertFalse(records.isEmpty()); for (int i = 0; i < res.length; i++) { - assertArrayEquals(res[i], records.get(i).values()); + assertValuesEquals(res[i], records.get(i)); } } } } @Test - public void testStream() throws Exception { + void testStream() throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.parse(in)) { final List list = parser.stream().collect(Collectors.toList()); assertFalse(list.isEmpty()); - assertArrayEquals(new String[] { "a", "b", "c" }, list.get(0).values()); - assertArrayEquals(new String[] { "1", "2", "3" }, list.get(1).values()); - assertArrayEquals(new String[] { "x", "y", "z" }, list.get(2).values()); + assertValuesEquals(new String[] { "a", "b", "c" }, list.get(0)); + assertValuesEquals(new String[] { "1", "2", "3" }, list.get(1)); + assertValuesEquals(new String[] { "x", "y", "z" }, list.get(2)); } } @ParameterizedTest @ValueSource(longs = { -1, 0, 1, 2, 3, 4, Long.MAX_VALUE }) - public void testStreamMaxRows(final long maxRows) throws Exception { + void testStreamMaxRows(final long maxRows) throws Exception { final Reader in = new StringReader("a,b,c\n1,2,3\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.builder().setMaxRows(maxRows).get().parse(in)) { final List list = parser.stream().collect(Collectors.toList()); assertFalse(list.isEmpty()); - assertArrayEquals(new String[] { "a", "b", "c" }, list.get(0).values()); + assertValuesEquals(new String[] { "a", "b", "c" }, list.get(0)); if (maxRows <= 0 || maxRows > 1) { - assertArrayEquals(new String[] { "1", "2", "3" }, list.get(1).values()); + assertValuesEquals(new String[] { "1", "2", "3" }, list.get(1)); } if (maxRows <= 0 || maxRows > 2) { - assertArrayEquals(new String[] { "x", "y", "z" }, list.get(2).values()); + assertValuesEquals(new String[] { "x", "y", "z" }, list.get(2)); } } } @Test - public void testThrowExceptionWithLineAndPosition() throws IOException { + void testThrowExceptionWithLineAndPosition() throws IOException { final String csvContent = "col1,col2,col3,col4,col5,col6,col7,col8,col9,col10\nrec1,rec2,rec3,rec4,rec5,rec6,rec7,rec8,\"\"rec9\"\",rec10"; final StringReader stringReader = new StringReader(csvContent); // @formatter:off @@ -1769,7 +1962,7 @@ public void testThrowExceptionWithLineAndPosition() throws IOException { } @Test - public void testTrailingDelimiter() throws Exception { + void testTrailingDelimiter() throws Exception { final Reader in = new StringReader("a,a,a,\n\"1\",\"2\",\"3\",\nx,y,z,"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("X", "Y", "Z").withSkipHeaderRecord().withTrailingDelimiter().parse(in)) { final Iterator records = parser.iterator(); @@ -1782,7 +1975,24 @@ public void testTrailingDelimiter() throws Exception { } @Test - public void testTrim() throws Exception { + void testTrailingDelimiterKeepsQuotedEmptyLastField() throws Exception { + final CSVFormat format = CSVFormat.DEFAULT.builder().setTrailingDelimiter(true).get(); + try (CSVParser parser = CSVParser.parse("a,b,\"\"", format)) { + final CSVRecord record = parser.iterator().next(); + assertEquals(3, record.size()); + assertEquals("a", record.get(0)); + assertEquals("b", record.get(1)); + assertEquals("", record.get(2)); + } + // An unquoted trailing delimiter still drops the empty field. + try (CSVParser parser = CSVParser.parse("a,b,", format)) { + final CSVRecord record = parser.iterator().next(); + assertEquals(2, record.size()); + } + } + + @Test + void testTrim() throws Exception { final Reader in = new StringReader("a,a,a\n\" 1 \",\" 2 \",\" 3 \"\nx,y,z"); try (CSVParser parser = CSVFormat.DEFAULT.withHeader("X", "Y", "Z").withSkipHeaderRecord().withTrim().parse(in)) { final Iterator records = parser.iterator(); diff --git a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java index a1a59cf4f7..9ae80c1e51 100644 --- a/src/test/java/org/apache/commons/csv/CSVPrinterTest.java +++ b/src/test/java/org/apache/commons/csv/CSVPrinterTest.java @@ -75,7 +75,7 @@ /** * Tests {@link CSVPrinter}. */ -public class CSVPrinterTest { +class CSVPrinterTest { private static final int TABLE_RECORD_COUNT = 2; private static final int TABLE_AND_HEADER_RECORD_COUNT = TABLE_RECORD_COUNT + 1; @@ -157,8 +157,8 @@ private void doRandom(final CSVFormat format, final int iter) throws Exception { } /** - * Converts an input CSV array into expected output values WRT NULLs. NULL strings are converted to null values because the parser will convert these - * strings to null. + * Converts an input CSV array into expected output values, including NULLs. NULL strings are converted to null values because the parser will convert + * these strings to null. */ private T[] expectNulls(final T[] original, final CSVFormat csvFormat) { final T[] fixed = original.clone(); @@ -265,7 +265,7 @@ private void setUpTable(final Connection connection) throws SQLException { } @Test - public void testCloseBackwardCompatibility() throws IOException { + void testCloseBackwardCompatibility() throws IOException { try (Writer writer = mock(Writer.class)) { final CSVFormat csvFormat = CSVFormat.DEFAULT; try (CSVPrinter printer = new CSVPrinter(writer, csvFormat)) { @@ -277,7 +277,7 @@ public void testCloseBackwardCompatibility() throws IOException { } @Test - public void testCloseWithCsvFormatAutoFlushOff() throws IOException { + void testCloseWithCsvFormatAutoFlushOff() throws IOException { try (Writer writer = mock(Writer.class)) { final CSVFormat csvFormat = CSVFormat.DEFAULT.withAutoFlush(false); try (CSVPrinter printer = new CSVPrinter(writer, csvFormat)) { @@ -289,7 +289,7 @@ public void testCloseWithCsvFormatAutoFlushOff() throws IOException { } @Test - public void testCloseWithCsvFormatAutoFlushOn() throws IOException { + void testCloseWithCsvFormatAutoFlushOn() throws IOException { // System.out.println("start method"); try (Writer writer = mock(Writer.class)) { final CSVFormat csvFormat = CSVFormat.DEFAULT.withAutoFlush(true); @@ -302,7 +302,7 @@ public void testCloseWithCsvFormatAutoFlushOn() throws IOException { } @Test - public void testCloseWithFlushOff() throws IOException { + void testCloseWithFlushOff() throws IOException { try (Writer writer = mock(Writer.class)) { final CSVFormat csvFormat = CSVFormat.DEFAULT; @SuppressWarnings("resource") @@ -316,7 +316,7 @@ public void testCloseWithFlushOff() throws IOException { } @Test - public void testCloseWithFlushOn() throws IOException { + void testCloseWithFlushOn() throws IOException { try (Writer writer = mock(Writer.class)) { @SuppressWarnings("resource") final CSVPrinter printer = new CSVPrinter(writer, CSVFormat.DEFAULT); @@ -328,7 +328,7 @@ public void testCloseWithFlushOn() throws IOException { } @Test - public void testCRComment() throws IOException { + void testCRComment() throws IOException { final StringWriter sw = new StringWriter(); final Object value = "abc"; try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withCommentMarker('#'))) { @@ -343,7 +343,7 @@ public void testCRComment() throws IOException { } @Test - public void testCSV135() throws IOException { + void testCSV135() throws IOException { final List list = new LinkedList<>(); list.add("\"\""); // "" list.add("\\\\"); // \\ @@ -366,7 +366,7 @@ public void testCSV135() throws IOException { } @Test - public void testCSV259() throws IOException { + void testCSV259() throws IOException { final StringWriter sw = new StringWriter(); try (Reader reader = new FileReader("src/test/resources/org/apache/commons/csv/CSV-259/sample.txt"); CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape('!').withQuote(null))) { @@ -377,7 +377,7 @@ public void testCSV259() throws IOException { } @Test - public void testDelimeterQuoted() throws IOException { + void testDelimeterQuoted() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\''))) { assertInitialState(printer); @@ -388,7 +388,7 @@ public void testDelimeterQuoted() throws IOException { } @Test - public void testDelimeterQuoteNone() throws IOException { + void testDelimeterQuoteNone() throws IOException { final StringWriter sw = new StringWriter(); final CSVFormat format = CSVFormat.DEFAULT.withEscape('!').withQuoteMode(QuoteMode.NONE); try (CSVPrinter printer = new CSVPrinter(sw, format)) { @@ -400,7 +400,7 @@ public void testDelimeterQuoteNone() throws IOException { } @Test - public void testDelimeterStringQuoted() throws IOException { + void testDelimeterStringQuoted() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.builder().setDelimiter("[|]").setQuote('\'').get())) { assertInitialState(printer); @@ -411,7 +411,7 @@ public void testDelimeterStringQuoted() throws IOException { } @Test - public void testDelimeterStringQuoteNone() throws IOException { + void testDelimeterStringQuoteNone() throws IOException { final StringWriter sw = new StringWriter(); final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').setQuoteMode(QuoteMode.NONE).get(); try (CSVPrinter printer = new CSVPrinter(sw, format)) { @@ -424,7 +424,7 @@ public void testDelimeterStringQuoteNone() throws IOException { } @Test - public void testDelimiterEscaped() throws IOException { + void testDelimiterEscaped() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape('!').withQuote(null))) { assertInitialState(printer); @@ -435,7 +435,7 @@ public void testDelimiterEscaped() throws IOException { } @Test - public void testDelimiterPlain() throws IOException { + void testDelimiterPlain() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null))) { assertInitialState(printer); @@ -446,7 +446,7 @@ public void testDelimiterPlain() throws IOException { } @Test - public void testDelimiterStringEscaped() throws IOException { + void testDelimiterStringEscaped() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.builder().setDelimiter("|||").setEscape('!').setQuote(null).get())) { assertInitialState(printer); @@ -457,7 +457,7 @@ public void testDelimiterStringEscaped() throws IOException { } @Test - public void testDisabledComment() throws IOException { + void testDisabledComment() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -468,7 +468,7 @@ public void testDisabledComment() throws IOException { } @Test - public void testDontQuoteEuroFirstChar() throws IOException { + void testDontQuoteEuroFirstChar() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.RFC4180)) { assertInitialState(printer); @@ -478,7 +478,7 @@ public void testDontQuoteEuroFirstChar() throws IOException { } @Test - public void testEolEscaped() throws IOException { + void testEolEscaped() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withEscape('!'))) { assertInitialState(printer); @@ -489,7 +489,7 @@ public void testEolEscaped() throws IOException { } @Test - public void testEolPlain() throws IOException { + void testEolPlain() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null))) { assertInitialState(printer); @@ -500,7 +500,7 @@ public void testEolPlain() throws IOException { } @Test - public void testEolQuoted() throws IOException { + void testEolQuoted() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\''))) { assertInitialState(printer); @@ -512,7 +512,7 @@ public void testEolQuoted() throws IOException { @SuppressWarnings("unlikely-arg-type") @Test - public void testEquals() throws IOException { + void testEquals() throws IOException { // Don't use assertNotEquals here assertFalse(CSVFormat.DEFAULT.equals(null)); // Don't use assertNotEquals here @@ -520,7 +520,7 @@ public void testEquals() throws IOException { } @Test - public void testEscapeBackslash1() throws IOException { + void testEscapeBackslash1() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(QUOTE_CH))) { assertInitialState(printer); @@ -530,7 +530,7 @@ public void testEscapeBackslash1() throws IOException { } @Test - public void testEscapeBackslash2() throws IOException { + void testEscapeBackslash2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(QUOTE_CH))) { assertInitialState(printer); @@ -540,7 +540,7 @@ public void testEscapeBackslash2() throws IOException { } @Test - public void testEscapeBackslash3() throws IOException { + void testEscapeBackslash3() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(QUOTE_CH))) { assertInitialState(printer); @@ -550,7 +550,7 @@ public void testEscapeBackslash3() throws IOException { } @Test - public void testEscapeBackslash4() throws IOException { + void testEscapeBackslash4() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(QUOTE_CH))) { assertInitialState(printer); @@ -560,7 +560,7 @@ public void testEscapeBackslash4() throws IOException { } @Test - public void testEscapeBackslash5() throws IOException { + void testEscapeBackslash5() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(QUOTE_CH))) { assertInitialState(printer); @@ -570,7 +570,58 @@ public void testEscapeBackslash5() throws IOException { } @Test - public void testEscapeNull1() throws IOException { + void testEscapeCommentMarkerFirstChar() throws IOException { + // No quoting available in escape mode, so a leading comment marker must be escaped or the + // record reads back as a comment and is dropped. Mirrors the quoting fix for QuoteMode.MINIMAL. + final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote(null).setEscape('\\').setCommentMarker(';').get(); + final StringWriter sw = new StringWriter(); + final String col1 = ";comment-like"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(col1, "b"); + printer.printRecord(new StringReader(col1), new StringReader("b")); + // The marker past the first character does not start a comment and is left alone. + printer.printRecord("a;b", ";c"); + } + final String string = sw.toString(); + assertEquals("\\;comment-like,b" + RECORD_SEPARATOR + + "\\;comment-like,b" + RECORD_SEPARATOR + + "a;b,\\;c" + RECORD_SEPARATOR, string); + // The emitted records must read back as the original values, none parsed as a comment. + try (CSVParser parser = CSVParser.parse(string, format)) { + final List records = parser.getRecords(); + assertEquals(3, records.size()); + assertEquals(col1, records.get(0).get(0)); + assertEquals("b", records.get(0).get(1)); + assertEquals(col1, records.get(1).get(0)); + assertEquals("b", records.get(1).get(1)); + assertEquals("a;b", records.get(2).get(0)); + assertEquals(";c", records.get(2).get(1)); + } + } + + @Test + void testEscapeCommentMarkerFirstCharWithQuoteModeNone() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setEscape('\\').setQuoteMode(QuoteMode.NONE).setCommentMarker(';').get(); + final StringWriter sw = new StringWriter(); + final String col1 = ";bar"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(col1, "b"); + printer.printRecord(new StringReader(col1), new StringReader("b")); + } + final String string = sw.toString(); + assertEquals("\\;bar,b" + RECORD_SEPARATOR + "\\;bar,b" + RECORD_SEPARATOR, string); + try (CSVParser parser = CSVParser.parse(string, format)) { + final List records = parser.getRecords(); + assertEquals(2, records.size()); + for (final CSVRecord record : records) { + assertEquals(col1, record.get(0)); + assertEquals("b", record.get(1)); + } + } + } + + @Test + void testEscapeNull1() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape(null))) { assertInitialState(printer); @@ -580,7 +631,7 @@ public void testEscapeNull1() throws IOException { } @Test - public void testEscapeNull2() throws IOException { + void testEscapeNull2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape(null))) { assertInitialState(printer); @@ -590,7 +641,7 @@ public void testEscapeNull2() throws IOException { } @Test - public void testEscapeNull3() throws IOException { + void testEscapeNull3() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape(null))) { assertInitialState(printer); @@ -600,7 +651,7 @@ public void testEscapeNull3() throws IOException { } @Test - public void testEscapeNull4() throws IOException { + void testEscapeNull4() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape(null))) { assertInitialState(printer); @@ -610,7 +661,7 @@ public void testEscapeNull4() throws IOException { } @Test - public void testEscapeNull5() throws IOException { + void testEscapeNull5() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withEscape(null))) { assertInitialState(printer); @@ -620,7 +671,7 @@ public void testEscapeNull5() throws IOException { } @Test - public void testExcelPrintAllArrayOfArrays() throws IOException { + void testExcelPrintAllArrayOfArrays() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -630,7 +681,7 @@ public void testExcelPrintAllArrayOfArrays() throws IOException { } @Test - public void testExcelPrintAllArrayOfArraysWithFirstEmptyValue2() throws IOException { + void testExcelPrintAllArrayOfArraysWithFirstEmptyValue2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -640,7 +691,7 @@ public void testExcelPrintAllArrayOfArraysWithFirstEmptyValue2() throws IOExcept } @Test - public void testExcelPrintAllArrayOfArraysWithFirstSpaceValue1() throws IOException { + void testExcelPrintAllArrayOfArraysWithFirstSpaceValue1() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -650,7 +701,7 @@ public void testExcelPrintAllArrayOfArraysWithFirstSpaceValue1() throws IOExcept } @Test - public void testExcelPrintAllArrayOfArraysWithFirstTabValue1() throws IOException { + void testExcelPrintAllArrayOfArraysWithFirstTabValue1() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -660,7 +711,7 @@ public void testExcelPrintAllArrayOfArraysWithFirstTabValue1() throws IOExceptio } @Test - public void testExcelPrintAllArrayOfLists() throws IOException { + void testExcelPrintAllArrayOfLists() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -670,7 +721,7 @@ public void testExcelPrintAllArrayOfLists() throws IOException { } @Test - public void testExcelPrintAllArrayOfListsWithFirstEmptyValue2() throws IOException { + void testExcelPrintAllArrayOfListsWithFirstEmptyValue2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -680,7 +731,7 @@ public void testExcelPrintAllArrayOfListsWithFirstEmptyValue2() throws IOExcepti } @Test - public void testExcelPrintAllIterableOfArrays() throws IOException { + void testExcelPrintAllIterableOfArrays() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -690,7 +741,7 @@ public void testExcelPrintAllIterableOfArrays() throws IOException { } @Test - public void testExcelPrintAllIterableOfArraysWithFirstEmptyValue2() throws IOException { + void testExcelPrintAllIterableOfArraysWithFirstEmptyValue2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -700,7 +751,7 @@ public void testExcelPrintAllIterableOfArraysWithFirstEmptyValue2() throws IOExc } @Test - public void testExcelPrintAllIterableOfLists() throws IOException { + void testExcelPrintAllIterableOfLists() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -711,7 +762,7 @@ public void testExcelPrintAllIterableOfLists() throws IOException { @ParameterizedTest @ValueSource(longs = { -1, 0, 1, 2, Long.MAX_VALUE }) - public void testExcelPrintAllStreamOfArrays(final long maxRows) throws IOException { + void testExcelPrintAllStreamOfArrays(final long maxRows) throws IOException { final StringWriter sw = new StringWriter(); final CSVFormat format = CSVFormat.EXCEL.builder().setMaxRows(maxRows).get(); try (CSVPrinter printer = new CSVPrinter(sw, format)) { @@ -726,7 +777,7 @@ public void testExcelPrintAllStreamOfArrays(final long maxRows) throws IOExcepti } @Test - public void testExcelPrinter1() throws IOException { + void testExcelPrinter1() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -736,7 +787,7 @@ public void testExcelPrinter1() throws IOException { } @Test - public void testExcelPrinter2() throws IOException { + void testExcelPrinter2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.EXCEL)) { assertInitialState(printer); @@ -746,7 +797,7 @@ public void testExcelPrinter2() throws IOException { } @Test - public void testHeader() throws IOException { + void testHeader() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3"))) { assertEquals(1, printer.getRecordCount()); @@ -757,7 +808,7 @@ public void testHeader() throws IOException { } @Test - public void testHeaderCommentExcel() throws IOException { + void testHeaderCommentExcel() throws IOException { final StringWriter sw = new StringWriter(); final Date now = new Date(); final CSVFormat format = CSVFormat.EXCEL; @@ -767,7 +818,7 @@ public void testHeaderCommentExcel() throws IOException { } @Test - public void testHeaderCommentTdf() throws IOException { + void testHeaderCommentTdf() throws IOException { final StringWriter sw = new StringWriter(); final Date now = new Date(); final CSVFormat format = CSVFormat.TDF; @@ -777,7 +828,7 @@ public void testHeaderCommentTdf() throws IOException { } @Test - public void testHeaderNotSet() throws IOException { + void testHeaderNotSet() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null))) { assertInitialState(printer); @@ -788,12 +839,12 @@ public void testHeaderNotSet() throws IOException { } @Test - public void testInvalidFormat() { + void testInvalidFormat() { assertThrows(IllegalArgumentException.class, () -> CSVFormat.DEFAULT.withDelimiter(CR)); } @Test - public void testJdbcPrinter() throws IOException, ClassNotFoundException, SQLException { + void testJdbcPrinter() throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter(); final CSVFormat csvFormat = CSVFormat.DEFAULT; try (Connection connection = getH2Connection()) { @@ -807,7 +858,7 @@ public void testJdbcPrinter() throws IOException, ClassNotFoundException, SQLExc } } final String csv = sw.toString(); - assertEquals("1,r1,\"long text 1\",\"YmluYXJ5IGRhdGEgMQ==\r\n\"" + RECORD_SEPARATOR + "2,r2,\"" + longText2 + "\",\"YmluYXJ5IGRhdGEgMg==\r\n\"" + + assertEquals("1,r1,\"long text 1\",\"YmluYXJ5IGRhdGEgMQ==\"" + RECORD_SEPARATOR + "2,r2,\"" + longText2 + "\",\"YmluYXJ5IGRhdGEgMg==\"" + RECORD_SEPARATOR, csv); // Round trip the data try (StringReader reader = new StringReader(csv); @@ -817,17 +868,17 @@ public void testJdbcPrinter() throws IOException, ClassNotFoundException, SQLExc assertEquals("1", record.get(0)); assertEquals("r1", record.get(1)); assertEquals("long text 1", record.get(2)); - assertEquals("YmluYXJ5IGRhdGEgMQ==\r\n", record.get(3)); + assertEquals("YmluYXJ5IGRhdGEgMQ==", record.get(3)); // Row 2 record = csvParser.nextRecord(); assertEquals("2", record.get(0)); assertEquals("r2", record.get(1)); - assertEquals("YmluYXJ5IGRhdGEgMg==\r\n", record.get(3)); + assertEquals("YmluYXJ5IGRhdGEgMg==", record.get(3)); } } @Test - public void testJdbcPrinterWithFirstEmptyValue2() throws IOException, ClassNotFoundException, SQLException { + void testJdbcPrinterWithFirstEmptyValue2() throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter(); try (Connection connection = getH2Connection()) { try (Statement stmt = connection.createStatement(); @@ -841,7 +892,7 @@ public void testJdbcPrinterWithFirstEmptyValue2() throws IOException, ClassNotFo @ParameterizedTest @ValueSource(longs = { -1, 0, 1, 2, 3, 4, Long.MAX_VALUE }) - public void testJdbcPrinterWithResultSet(final long maxRows) throws IOException, ClassNotFoundException, SQLException { + void testJdbcPrinterWithResultSet(final long maxRows) throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter(); final CSVFormat format = CSVFormat.DEFAULT.builder().setMaxRows(maxRows).get(); try (Connection connection = getH2Connection()) { @@ -869,7 +920,7 @@ public void testJdbcPrinterWithResultSet(final long maxRows) throws IOException, @ParameterizedTest @ValueSource(longs = { -1, 0, 3, 4, Long.MAX_VALUE }) - public void testJdbcPrinterWithResultSetHeader(final long maxRows) throws IOException, ClassNotFoundException, SQLException { + void testJdbcPrinterWithResultSetHeader(final long maxRows) throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter(); try (Connection connection = getH2Connection()) { setUpTable(connection); @@ -894,7 +945,7 @@ public void testJdbcPrinterWithResultSetHeader(final long maxRows) throws IOExce @ParameterizedTest @ValueSource(longs = { -1, 0, 3, 4, Long.MAX_VALUE }) - public void testJdbcPrinterWithResultSetMetaData(final long maxRows) throws IOException, ClassNotFoundException, SQLException { + void testJdbcPrinterWithResultSetMetaData(final long maxRows) throws IOException, ClassNotFoundException, SQLException { final StringWriter sw = new StringWriter(); try (Connection connection = getH2Connection()) { setUpTable(connection); @@ -914,7 +965,7 @@ public void testJdbcPrinterWithResultSetMetaData(final long maxRows) throws IOEx } @Test - public void testJira135_part1() throws IOException { + void testJira135_part1() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote(DQUOTE_CHAR).withEscape(BACKSLASH); final StringWriter sw = new StringWriter(); final List list = new LinkedList<>(); @@ -930,7 +981,7 @@ public void testJira135_part1() throws IOException { @Test @Disabled - public void testJira135_part2() throws IOException { + void testJira135_part2() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote(DQUOTE_CHAR).withEscape(BACKSLASH); final StringWriter sw = new StringWriter(); final List list = new LinkedList<>(); @@ -945,7 +996,7 @@ public void testJira135_part2() throws IOException { } @Test - public void testJira135_part3() throws IOException { + void testJira135_part3() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote(DQUOTE_CHAR).withEscape(BACKSLASH); final StringWriter sw = new StringWriter(); final List list = new LinkedList<>(); @@ -961,7 +1012,7 @@ public void testJira135_part3() throws IOException { @Test @Disabled - public void testJira135All() throws IOException { + void testJira135All() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.withRecordSeparator('\n').withQuote(DQUOTE_CHAR).withEscape(BACKSLASH); final StringWriter sw = new StringWriter(); final List list = new LinkedList<>(); @@ -978,7 +1029,7 @@ public void testJira135All() throws IOException { } @Test - public void testMongoDbCsvBasic() throws IOException { + void testMongoDbCsvBasic() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_CSV)) { printer.printRecord("a", "b"); @@ -988,7 +1039,7 @@ public void testMongoDbCsvBasic() throws IOException { } @Test - public void testMongoDbCsvCommaInValue() throws IOException { + void testMongoDbCsvCommaInValue() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_CSV)) { printer.printRecord("a,b", "c"); @@ -998,7 +1049,7 @@ public void testMongoDbCsvCommaInValue() throws IOException { } @Test - public void testMongoDbCsvDoubleQuoteInValue() throws IOException { + void testMongoDbCsvDoubleQuoteInValue() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_CSV)) { printer.printRecord("a \"c\" b", "d"); @@ -1008,7 +1059,7 @@ public void testMongoDbCsvDoubleQuoteInValue() throws IOException { } @Test - public void testMongoDbCsvTabInValue() throws IOException { + void testMongoDbCsvTabInValue() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_CSV)) { printer.printRecord("a\tb", "c"); @@ -1018,7 +1069,7 @@ public void testMongoDbCsvTabInValue() throws IOException { } @Test - public void testMongoDbTsvBasic() throws IOException { + void testMongoDbTsvBasic() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_TSV)) { printer.printRecord("a", "b"); @@ -1028,7 +1079,7 @@ public void testMongoDbTsvBasic() throws IOException { } @Test - public void testMongoDbTsvCommaInValue() throws IOException { + void testMongoDbTsvCommaInValue() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_TSV)) { printer.printRecord("a,b", "c"); @@ -1038,7 +1089,7 @@ public void testMongoDbTsvCommaInValue() throws IOException { } @Test - public void testMongoDbTsvTabInValue() throws IOException { + void testMongoDbTsvTabInValue() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.MONGODB_TSV)) { printer.printRecord("a\tb", "c"); @@ -1047,7 +1098,7 @@ public void testMongoDbTsvTabInValue() throws IOException { } @Test - public void testMultiLineComment() throws IOException { + void testMultiLineComment() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withCommentMarker('#'))) { printer.printComment("This is a comment\non multiple lines"); @@ -1057,7 +1108,7 @@ public void testMultiLineComment() throws IOException { } @Test - public void testMySqlNullOutput() throws IOException { + void testMySqlNullOutput() throws IOException { Object[] s = new String[] { "NULL", null }; CSVFormat format = CSVFormat.MYSQL.withQuote(DQUOTE_CHAR).withNullString("NULL").withQuoteMode(QuoteMode.NON_NUMERIC); StringWriter writer = new StringWriter(); @@ -1159,22 +1210,22 @@ public void testMySqlNullOutput() throws IOException { } @Test - public void testMySqlNullStringDefault() { + void testMySqlNullStringDefault() { assertEquals("\\N", CSVFormat.MYSQL.getNullString()); } @Test - public void testNewCsvPrinterAppendableNullFormat() { + void testNewCsvPrinterAppendableNullFormat() { assertThrows(NullPointerException.class, () -> new CSVPrinter(new StringWriter(), null)); } @Test - public void testNewCsvPrinterNullAppendableFormat() { + void testNewCsvPrinterNullAppendableFormat() { assertThrows(NullPointerException.class, () -> new CSVPrinter(null, CSVFormat.DEFAULT)); } @Test - public void testNotFlushable() throws IOException { + void testNotFlushable() throws IOException { final Appendable out = new StringBuilder(); try (CSVPrinter printer = new CSVPrinter(out, CSVFormat.DEFAULT)) { printer.printRecord("a", "b", "c"); @@ -1184,7 +1235,7 @@ public void testNotFlushable() throws IOException { } @Test - public void testParseCustomNullValues() throws IOException { + void testParseCustomNullValues() throws IOException { final StringWriter sw = new StringWriter(); final CSVFormat format = CSVFormat.DEFAULT.withNullString("NULL"); try (CSVPrinter printer = new CSVPrinter(sw, format)) { @@ -1203,7 +1254,7 @@ public void testParseCustomNullValues() throws IOException { } @Test - public void testPlainEscaped() throws IOException { + void testPlainEscaped() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withEscape('!'))) { printer.print("abc"); @@ -1213,7 +1264,7 @@ public void testPlainEscaped() throws IOException { } @Test - public void testPlainPlain() throws IOException { + void testPlainPlain() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null))) { printer.print("abc"); @@ -1223,7 +1274,7 @@ public void testPlainPlain() throws IOException { } @Test - public void testPlainQuoted() throws IOException { + void testPlainQuoted() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\''))) { printer.print("abc"); @@ -1233,7 +1284,7 @@ public void testPlainQuoted() throws IOException { @Test @Disabled - public void testPostgreSqlCsvNullOutput() throws IOException { + void testPostgreSqlCsvNullOutput() throws IOException { Object[] s = new String[] { "NULL", null }; CSVFormat format = CSVFormat.POSTGRESQL_CSV.withQuote(DQUOTE_CHAR).withNullString("NULL").withQuoteMode(QuoteMode.ALL_NON_NULL); StringWriter writer = new StringWriter(); @@ -1336,7 +1387,7 @@ public void testPostgreSqlCsvNullOutput() throws IOException { @Test @Disabled - public void testPostgreSqlCsvTextOutput() throws IOException { + void testPostgreSqlCsvTextOutput() throws IOException { Object[] s = new String[] { "NULL", null }; CSVFormat format = CSVFormat.POSTGRESQL_TEXT.withQuote(DQUOTE_CHAR).withNullString("NULL").withQuoteMode(QuoteMode.ALL_NON_NULL); StringWriter writer = new StringWriter(); @@ -1438,17 +1489,17 @@ public void testPostgreSqlCsvTextOutput() throws IOException { } @Test - public void testPostgreSqlNullStringDefaultCsv() { + void testPostgreSqlNullStringDefaultCsv() { assertEquals("", CSVFormat.POSTGRESQL_CSV.getNullString()); } @Test - public void testPostgreSqlNullStringDefaultText() { + void testPostgreSqlNullStringDefaultText() { assertEquals("\\N", CSVFormat.POSTGRESQL_TEXT.getNullString()); } @Test - public void testPrint() throws IOException { + void testPrint() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = CSVFormat.DEFAULT.print(sw)) { assertInitialState(printer); @@ -1458,7 +1509,7 @@ public void testPrint() throws IOException { } @Test - public void testPrintCSVParser() throws IOException { + void testPrintCSVParser() throws IOException { // @formatter:off final String code = "a1,b1\n" + // 1) "a2,b2\n" + // 2) @@ -1481,7 +1532,7 @@ public void testPrintCSVParser() throws IOException { } @Test - public void testPrintCSVRecord() throws IOException { + void testPrintCSVRecord() throws IOException { // @formatter:off final String code = "a1,b1\n" + // 1) "a2,b2\n" + // 2) @@ -1510,7 +1561,7 @@ public void testPrintCSVRecord() throws IOException { @ParameterizedTest @ValueSource(longs = { -1, 0, 3, 4, Long.MAX_VALUE }) - public void testPrintCSVRecords(final long maxRows) throws IOException { + void testPrintCSVRecords(final long maxRows) throws IOException { // @formatter:off final String code = "a1,b1\n" + // 1) "a2,b2\n" + // 2) @@ -1533,7 +1584,7 @@ public void testPrintCSVRecords(final long maxRows) throws IOException { } @Test - public void testPrintCustomNullValues() throws IOException { + void testPrintCustomNullValues() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withNullString("NULL"))) { assertInitialState(printer); @@ -1543,7 +1594,7 @@ public void testPrintCustomNullValues() throws IOException { } @Test - public void testPrinter1() throws IOException { + void testPrinter1() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1554,7 +1605,7 @@ public void testPrinter1() throws IOException { } @Test - public void testPrinter2() throws IOException { + void testPrinter2() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1564,7 +1615,7 @@ public void testPrinter2() throws IOException { } @Test - public void testPrinter3() throws IOException { + void testPrinter3() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1574,7 +1625,7 @@ public void testPrinter3() throws IOException { } @Test - public void testPrinter4() throws IOException { + void testPrinter4() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1584,7 +1635,7 @@ public void testPrinter4() throws IOException { } @Test - public void testPrinter5() throws IOException { + void testPrinter5() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1594,7 +1645,7 @@ public void testPrinter5() throws IOException { } @Test - public void testPrinter6() throws IOException { + void testPrinter6() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1604,7 +1655,7 @@ public void testPrinter6() throws IOException { } @Test - public void testPrinter7() throws IOException { + void testPrinter7() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1614,7 +1665,7 @@ public void testPrinter7() throws IOException { } @Test - public void testPrintNullValues() throws IOException { + void testPrintNullValues() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT)) { assertInitialState(printer); @@ -1624,7 +1675,7 @@ public void testPrintNullValues() throws IOException { } @Test - public void testPrintOnePositiveInteger() throws IOException { + void testPrintOnePositiveInteger() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuoteMode(QuoteMode.MINIMAL))) { assertInitialState(printer); @@ -1644,7 +1695,7 @@ public void testPrintOnePositiveInteger() throws IOException { * @throws IOException Not expected to happen */ @Test - public void testPrintReaderWithoutQuoteToAppendable() throws IOException { + void testPrintReaderWithoutQuoteToAppendable() throws IOException { final StringBuilder sb = new StringBuilder(); final String content = "testValue"; try (CSVPrinter printer = new CSVPrinter(sb, CSVFormat.DEFAULT.withQuote(null))) { @@ -1665,7 +1716,7 @@ public void testPrintReaderWithoutQuoteToAppendable() throws IOException { * @throws IOException Not expected to happen */ @Test - public void testPrintReaderWithoutQuoteToWriter() throws IOException { + void testPrintReaderWithoutQuoteToWriter() throws IOException { final StringWriter sw = new StringWriter(); final String content = "testValue"; try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null))) { @@ -1676,7 +1727,7 @@ public void testPrintReaderWithoutQuoteToWriter() throws IOException { } @Test - public void testPrintRecordStream() throws IOException { + void testPrintRecordStream() throws IOException { // @formatter:off final String code = "a1,b1\n" + // 1) "a2,b2\n" + // 2) @@ -1702,7 +1753,7 @@ public void testPrintRecordStream() throws IOException { } @Test - public void testPrintRecordsWithCSVRecord() throws IOException { + void testPrintRecordsWithCSVRecord() throws IOException { final String[] values = { "A", "B", "C" }; final String rowData = StringUtils.join(values, ','); final CharArrayWriter charArrayWriter = new CharArrayWriter(0); @@ -1719,7 +1770,7 @@ public void testPrintRecordsWithCSVRecord() throws IOException { } @Test - public void testPrintRecordsWithEmptyVector() throws IOException { + void testPrintRecordsWithEmptyVector() throws IOException { final PrintStream out = System.out; try { System.setOut(new PrintStream(NullOutputStream.INSTANCE)); @@ -1737,7 +1788,7 @@ public void testPrintRecordsWithEmptyVector() throws IOException { } @Test - public void testPrintRecordsWithObjectArray() throws IOException { + void testPrintRecordsWithObjectArray() throws IOException { final CharArrayWriter charArrayWriter = new CharArrayWriter(0); final Object[] objectArray = new Object[6]; try (CSVPrinter printer = CSVFormat.INFORMIX_UNLOAD.print(charArrayWriter)) { @@ -1751,7 +1802,7 @@ public void testPrintRecordsWithObjectArray() throws IOException { } @Test - public void testPrintRecordsWithResultSetOneRow() throws IOException, SQLException { + void testPrintRecordsWithResultSetOneRow() throws IOException, SQLException { try (CSVPrinter printer = CSVFormat.MYSQL.printer()) { try (ResultSet resultSet = new SimpleResultSet()) { assertInitialState(printer); @@ -1763,7 +1814,7 @@ public void testPrintRecordsWithResultSetOneRow() throws IOException, SQLExcepti } @Test - public void testPrintToFileWithCharsetUtf16Be() throws IOException { + void testPrintToFileWithCharsetUtf16Be() throws IOException { final File file = createTempFile(); try (CSVPrinter printer = CSVFormat.DEFAULT.print(file, StandardCharsets.UTF_16BE)) { printer.printRecord("a", "b\\c"); @@ -1772,7 +1823,7 @@ public void testPrintToFileWithCharsetUtf16Be() throws IOException { } @Test - public void testPrintToFileWithDefaultCharset() throws IOException { + void testPrintToFileWithDefaultCharset() throws IOException { final File file = createTempFile(); try (CSVPrinter printer = CSVFormat.DEFAULT.print(file, Charset.defaultCharset())) { printer.printRecord("a", "b\\c"); @@ -1781,7 +1832,7 @@ public void testPrintToFileWithDefaultCharset() throws IOException { } @Test - public void testPrintToPathWithDefaultCharset() throws IOException { + void testPrintToPathWithDefaultCharset() throws IOException { final Path file = createTempPath(); try (CSVPrinter printer = CSVFormat.DEFAULT.print(file, Charset.defaultCharset())) { printer.printRecord("a", "b\\c"); @@ -1790,7 +1841,7 @@ public void testPrintToPathWithDefaultCharset() throws IOException { } @Test - public void testQuoteAll() throws IOException { + void testQuoteAll() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuoteMode(QuoteMode.ALL))) { printer.printRecord("a", "b\nc", "d"); @@ -1799,7 +1850,29 @@ public void testQuoteAll() throws IOException { } @Test - public void testQuoteCommaFirstChar() throws IOException { + void testQuoteCharEscapedWithQuoteModeNone() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setQuote('"').setEscape('?').setQuoteMode(QuoteMode.NONE).get(); + final StringWriter sw = new StringWriter(); + final String col1 = "\"abc"; + final String col2 = "x\"y"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + printer.printRecord(col1, col2); + printer.printRecord(new StringReader(col1), new StringReader(col2)); + } + assertEquals("?\"abc,x?\"y" + RECORD_SEPARATOR + "?\"abc,x?\"y" + RECORD_SEPARATOR, sw.toString()); + // The emitted records must read back as the original values. + try (CSVParser parser = CSVParser.parse(sw.toString(), format)) { + final List records = parser.getRecords(); + assertEquals(2, records.size()); + for (final CSVRecord record : records) { + assertEquals(col1, record.get(0)); + assertEquals(col2, record.get(1)); + } + } + } + + @Test + void testQuoteCommaFirstChar() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.RFC4180)) { printer.printRecord(","); @@ -1808,7 +1881,35 @@ public void testQuoteCommaFirstChar() throws IOException { } @Test - public void testQuoteNonNumeric() throws IOException { + void testQuoteCommentMarkerFirstChar() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setCommentMarker(';').get(); + final StringWriter sw = new StringWriter(); + final String col1 = ";comment-like"; + try (CSVPrinter printer = new CSVPrinter(sw, format)) { + // A real comment is written with the marker, unquoted. + printer.printComment("a real comment"); + // A value starting with the marker is quoted, so it does not read back as a comment. + printer.printRecord(col1, "b"); + // The marker past the first character does not start a comment, so only the leading-marker value is quoted. + printer.printRecord("a;b", ";c"); + } + final String string = sw.toString(); + assertEquals("; a real comment" + RECORD_SEPARATOR + + "\";comment-like\",b" + RECORD_SEPARATOR + + "a;b,\";c\"" + RECORD_SEPARATOR, string); + // The comment is dropped on read; both data records survive intact. + try (CSVParser parser = CSVParser.parse(string, format)) { + final List records = parser.getRecords(); + assertEquals(2, records.size()); + assertEquals(col1, records.get(0).get(0)); + assertEquals("b", records.get(0).get(1)); + assertEquals("a;b", records.get(1).get(0)); + assertEquals(";c", records.get(1).get(1)); + } + } + + @Test + void testQuoteNonNumeric() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuoteMode(QuoteMode.NON_NUMERIC))) { printer.printRecord("a", "b\nc", Integer.valueOf(1)); @@ -1817,55 +1918,55 @@ public void testQuoteNonNumeric() throws IOException { } @Test - public void testRandomDefault() throws Exception { + void testRandomDefault() throws Exception { doRandom(CSVFormat.DEFAULT, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testRandomExcel() throws Exception { + void testRandomExcel() throws Exception { doRandom(CSVFormat.EXCEL, ITERATIONS_FOR_RANDOM_TEST); } @Test @Disabled - public void testRandomMongoDbCsv() throws Exception { + void testRandomMongoDbCsv() throws Exception { doRandom(CSVFormat.MONGODB_CSV, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testRandomMySql() throws Exception { + void testRandomMySql() throws Exception { doRandom(CSVFormat.MYSQL, ITERATIONS_FOR_RANDOM_TEST); } @Test @Disabled - public void testRandomOracle() throws Exception { + void testRandomOracle() throws Exception { doRandom(CSVFormat.ORACLE, ITERATIONS_FOR_RANDOM_TEST); } @Test @Disabled - public void testRandomPostgreSqlCsv() throws Exception { + void testRandomPostgreSqlCsv() throws Exception { doRandom(CSVFormat.POSTGRESQL_CSV, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testRandomPostgreSqlText() throws Exception { + void testRandomPostgreSqlText() throws Exception { doRandom(CSVFormat.POSTGRESQL_TEXT, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testRandomRfc4180() throws Exception { + void testRandomRfc4180() throws Exception { doRandom(CSVFormat.RFC4180, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testRandomTdf() throws Exception { + void testRandomTdf() throws Exception { doRandom(CSVFormat.TDF, ITERATIONS_FOR_RANDOM_TEST); } @Test - public void testSingleLineComment() throws IOException { + void testSingleLineComment() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withCommentMarker('#'))) { printer.printComment("This is a comment"); @@ -1875,7 +1976,7 @@ public void testSingleLineComment() throws IOException { } @Test - public void testSingleQuoteQuoted() throws IOException { + void testSingleQuoteQuoted() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote('\''))) { printer.print("a'b'c"); @@ -1885,7 +1986,7 @@ public void testSingleQuoteQuoted() throws IOException { } @Test - public void testSkipHeaderRecordFalse() throws IOException { + void testSkipHeaderRecordFalse() throws IOException { // functionally identical to testHeader, used to test CSV-153 final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3").withSkipHeaderRecord(false))) { @@ -1896,7 +1997,7 @@ public void testSkipHeaderRecordFalse() throws IOException { } @Test - public void testSkipHeaderRecordTrue() throws IOException { + void testSkipHeaderRecordTrue() throws IOException { // functionally identical to testHeaderNotSet, used to test CSV-153 final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withQuote(null).withHeader("C1", "C2", "C3").withSkipHeaderRecord(true))) { @@ -1907,7 +2008,7 @@ public void testSkipHeaderRecordTrue() throws IOException { } @Test - public void testTrailingDelimiterOnTwoColumns() throws IOException { + void testTrailingDelimiterOnTwoColumns() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrailingDelimiter())) { printer.printRecord("A", "B"); @@ -1916,7 +2017,7 @@ public void testTrailingDelimiterOnTwoColumns() throws IOException { } @Test - public void testTrimOffOneColumn() throws IOException { + void testTrimOffOneColumn() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrim(false))) { printer.print(" A "); @@ -1925,7 +2026,7 @@ public void testTrimOffOneColumn() throws IOException { } @Test - public void testTrimOnOneColumn() throws IOException { + void testTrimOnOneColumn() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrim())) { printer.print(" A "); @@ -1934,7 +2035,7 @@ public void testTrimOnOneColumn() throws IOException { } @Test - public void testTrimOnTwoColumns() throws IOException { + void testTrimOnTwoColumns() throws IOException { final StringWriter sw = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(sw, CSVFormat.DEFAULT.withTrim())) { printer.print(" A "); diff --git a/src/test/java/org/apache/commons/csv/CSVRecordTest.java b/src/test/java/org/apache/commons/csv/CSVRecordTest.java index f9fedc4b98..94060d62b2 100644 --- a/src/test/java/org/apache/commons/csv/CSVRecordTest.java +++ b/src/test/java/org/apache/commons/csv/CSVRecordTest.java @@ -45,7 +45,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -public class CSVRecordTest { +class CSVRecordTest { private enum EnumFixture { UNKNOWN_COLUMN @@ -86,7 +86,7 @@ record = parser.iterator().next(); } @Test - public void testCSVRecordNULLValues() throws IOException { + void testCSVRecordNULLValues() throws IOException { try (CSVParser parser = CSVParser.parse("A,B\r\nONE,TWO", CSVFormat.DEFAULT.withHeader())) { final CSVRecord csvRecord = new CSVRecord(parser, null, null, 0L, 0L, 0L); assertEquals(0, csvRecord.size()); @@ -95,7 +95,7 @@ public void testCSVRecordNULLValues() throws IOException { } @Test - public void testDuplicateHeaderGet() throws IOException { + void testDuplicateHeaderGet() throws IOException { final String csv = "A,A,B,B\n1,2,5,6\n"; final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get(); @@ -110,7 +110,7 @@ public void testDuplicateHeaderGet() throws IOException { } @Test - public void testDuplicateHeaderToMap() throws IOException { + void testDuplicateHeaderToMap() throws IOException { final String csv = "A,A,B,B\n1,2,5,6\n"; final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get(); @@ -126,64 +126,64 @@ public void testDuplicateHeaderToMap() throws IOException { } @Test - public void testGetInt() { + void testGetInt() { assertEquals(values[0], record.get(0)); assertEquals(values[1], record.get(1)); assertEquals(values[2], record.get(2)); } @Test - public void testGetNullEnum() { + void testGetNullEnum() { assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get((Enum) null)); } @Test - public void testGetString() { + void testGetString() { assertEquals(values[0], recordWithHeader.get(EnumHeader.FIRST.name())); assertEquals(values[1], recordWithHeader.get(EnumHeader.SECOND.name())); assertEquals(values[2], recordWithHeader.get(EnumHeader.THIRD.name())); } @Test - public void testGetStringInconsistentRecord() { + void testGetStringInconsistentRecord() { headerMap.put("fourth", Integer.valueOf(4)); assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get("fourth")); } @Test - public void testGetStringNoHeader() { + void testGetStringNoHeader() { assertThrows(IllegalStateException.class, () -> record.get("first")); } @Test - public void testGetUnmappedEnum() { + void testGetUnmappedEnum() { assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get(EnumFixture.UNKNOWN_COLUMN)); } @Test - public void testGetUnmappedName() { + void testGetUnmappedName() { assertThrows(IllegalArgumentException.class, () -> assertNull(recordWithHeader.get("fourth"))); } @Test - public void testGetUnmappedNegativeInt() { + void testGetUnmappedNegativeInt() { assertThrows(ArrayIndexOutOfBoundsException.class, () -> recordWithHeader.get(Integer.MIN_VALUE)); } @Test - public void testGetUnmappedPositiveInt() { + void testGetUnmappedPositiveInt() { assertThrows(ArrayIndexOutOfBoundsException.class, () -> recordWithHeader.get(Integer.MAX_VALUE)); } @Test - public void testGetWithEnum() { + void testGetWithEnum() { assertEquals(recordWithHeader.get("FIRST"), recordWithHeader.get(EnumHeader.FIRST)); assertEquals(recordWithHeader.get("SECOND"), recordWithHeader.get(EnumHeader.SECOND)); assertThrows(IllegalArgumentException.class, () -> recordWithHeader.get(EnumFixture.UNKNOWN_COLUMN)); } @Test - public void testIsConsistent() { + void testIsConsistent() { assertTrue(record.isConsistent()); assertTrue(recordWithHeader.isConsistent()); final Map map = recordWithHeader.getParser().getHeaderMap(); @@ -193,7 +193,7 @@ public void testIsConsistent() { } @Test - public void testIsInconsistent() throws IOException { + void testIsInconsistent() throws IOException { final String[] headers = { "first", "second", "third" }; final String rowData = StringUtils.join(values, ','); try (CSVParser parser = CSVFormat.DEFAULT.withHeader(headers).parse(new StringReader(rowData))) { @@ -205,14 +205,14 @@ public void testIsInconsistent() throws IOException { } @Test - public void testIsMapped() { + void testIsMapped() { assertFalse(record.isMapped("first")); assertTrue(recordWithHeader.isMapped(EnumHeader.FIRST.name())); assertFalse(recordWithHeader.isMapped("fourth")); } @Test - public void testIsSetInt() { + void testIsSetInt() { assertFalse(record.isSet(-1)); assertTrue(record.isSet(0)); assertTrue(record.isSet(2)); @@ -222,14 +222,14 @@ public void testIsSetInt() { } @Test - public void testIsSetString() { + void testIsSetString() { assertFalse(record.isSet("first")); assertTrue(recordWithHeader.isSet(EnumHeader.FIRST.name())); assertFalse(recordWithHeader.isSet("DOES NOT EXIST")); } @Test - public void testIterator() { + void testIterator() { int i = 0; for (final String value : record) { assertEquals(values[i], value); @@ -238,7 +238,7 @@ public void testIterator() { } @Test - public void testPutInMap() { + void testPutInMap() { final Map map = new ConcurrentHashMap<>(); this.recordWithHeader.putIn(map); validateMap(map, false); @@ -248,7 +248,7 @@ public void testPutInMap() { } @Test - public void testRemoveAndAddColumns() throws IOException { + void testRemoveAndAddColumns() throws IOException { // do: try (CSVPrinter printer = new CSVPrinter(new StringBuilder(), CSVFormat.DEFAULT)) { final Map map = recordWithHeader.toMap(); @@ -263,7 +263,7 @@ public void testRemoveAndAddColumns() throws IOException { } @Test - public void testSerialization() throws IOException, ClassNotFoundException { + void testSerialization() throws IOException, ClassNotFoundException { final CSVRecord shortRec; try (CSVParser parser = CSVParser.parse("A,B\n#my comment\nOne,Two", CSVFormat.DEFAULT.withHeader().withCommentMarker('#'))) { shortRec = parser.iterator().next(); @@ -296,7 +296,7 @@ public void testSerialization() throws IOException, ClassNotFoundException { } @Test - public void testStream() { + void testStream() { final AtomicInteger i = new AtomicInteger(); record.stream().forEach(value -> { assertEquals(values[i.get()], value); @@ -305,7 +305,7 @@ public void testStream() { } @Test - public void testToListAdd() { + void testToListAdd() { final String[] expected = values.clone(); final List list = record.toList(); list.add("Last"); @@ -315,7 +315,7 @@ public void testToListAdd() { } @Test - public void testToListFor() { + void testToListFor() { int i = 0; for (final String value : record.toList()) { assertEquals(values[i], value); @@ -324,7 +324,7 @@ public void testToListFor() { } @Test - public void testToListForEach() { + void testToListForEach() { final AtomicInteger i = new AtomicInteger(); record.toList().forEach(e -> { assertEquals(values[i.getAndIncrement()], e); @@ -332,7 +332,7 @@ public void testToListForEach() { } @Test - public void testToListSet() { + void testToListSet() { final String[] expected = values.clone(); final List list = record.toList(); list.set(list.size() - 1, "Last"); @@ -342,13 +342,13 @@ public void testToListSet() { } @Test - public void testToMap() { + void testToMap() { final Map map = this.recordWithHeader.toMap(); validateMap(map, true); } @Test - public void testToMapWithNoHeader() throws Exception { + void testToMapWithNoHeader() throws Exception { try (CSVParser parser = CSVParser.parse("a,b", CSVFormat.newFormat(','))) { final CSVRecord shortRec = parser.iterator().next(); final Map map = shortRec.toMap(); @@ -358,7 +358,7 @@ public void testToMapWithNoHeader() throws Exception { } @Test - public void testToMapWithShortRecord() throws Exception { + void testToMapWithShortRecord() throws Exception { try (CSVParser parser = CSVParser.parse("a,b", CSVFormat.DEFAULT.withHeader("A", "B", "C"))) { final CSVRecord shortRec = parser.iterator().next(); shortRec.toMap(); @@ -366,7 +366,7 @@ public void testToMapWithShortRecord() throws Exception { } @Test - public void testToString() { + void testToString() { assertNotNull(recordWithHeader.toString()); assertTrue(recordWithHeader.toString().contains("comment=")); assertTrue(recordWithHeader.toString().contains("recordNumber=")); diff --git a/src/test/java/org/apache/commons/csv/CsvAssertions.java b/src/test/java/org/apache/commons/csv/CsvAssertions.java new file mode 100644 index 0000000000..b6c2b5d9cd --- /dev/null +++ b/src/test/java/org/apache/commons/csv/CsvAssertions.java @@ -0,0 +1,29 @@ +/* + * 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.csv; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class CsvAssertions { + + public static void assertValuesEquals(final String[] expected, final CSVRecord actual) { + assertArrayEquals(expected, actual.values()); + } +} diff --git a/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java b/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java index b7db39f529..b8d9b9f198 100644 --- a/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java +++ b/src/test/java/org/apache/commons/csv/ExtendedBufferedReaderTest.java @@ -26,13 +26,14 @@ import static org.junit.jupiter.api.Assertions.assertNull; import java.io.StringReader; +import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; /** * Test {@link ExtendedBufferedReader}. */ -public class ExtendedBufferedReaderTest { +class ExtendedBufferedReaderTest { static final String LF = "\n"; static final String CR = "\r"; @@ -44,7 +45,7 @@ private ExtendedBufferedReader createBufferedReader(final String s) { } @Test - public void testEmptyInput() throws Exception { + void testEmptyInput() throws Exception { try (ExtendedBufferedReader br = createBufferedReader("")) { assertEquals(EOF, br.read()); assertEquals(EOF, br.peek()); @@ -58,7 +59,7 @@ public void testEmptyInput() throws Exception { * Test to illustrate https://issues.apache.org/jira/browse/CSV-75 */ @Test - public void testReadChar() throws Exception { + void testReadChar() throws Exception { final String test = "a" + LF + "b" + CR + "c" + LF + LF + "d" + CR + CR + "e" + LFCR + "f " + CRLF; // EOL eol EOL EOL eol eol EOL+CR EOL final int eolCount = 9; @@ -94,7 +95,7 @@ public void testReadChar() throws Exception { } @Test - public void testReadingInDifferentBuffer() throws Exception { + void testReadingInDifferentBuffer() throws Exception { final char[] tmp1 = new char[2]; final char[] tmp2 = new char[4]; try (ExtendedBufferedReader reader = createBufferedReader("1\r\n2\r\n")) { @@ -105,7 +106,20 @@ public void testReadingInDifferentBuffer() throws Exception { } @Test - public void testReadLine() throws Exception { + void testReadingSupplementaryCharacterTracksBytes() throws Exception { + final String input = "😀"; + final char[] buffer = new char[input.length()]; + try (ExtendedBufferedReader reader = new ExtendedBufferedReader(new StringReader(input), StandardCharsets.UTF_8, true)) { + assertEquals(input.length(), reader.read(buffer, 0, buffer.length)); + assertArrayEquals(input.toCharArray(), buffer); + assertEquals(input.getBytes(StandardCharsets.UTF_8).length, reader.getBytesRead()); + assertEquals(input.length(), reader.getPosition()); + assertEquals(input.charAt(input.length() - 1), reader.getLastChar()); + } + } + + @Test + void testReadLine() throws Exception { try (ExtendedBufferedReader br = createBufferedReader("")) { assertNull(br.readLine()); } @@ -148,7 +162,7 @@ public void testReadLine() throws Exception { } @Test - public void testReadLookahead1() throws Exception { + void testReadLookahead1() throws Exception { try (ExtendedBufferedReader br = createBufferedReader("1\n2\r3\n")) { assertEquals(0, br.getLineNumber()); assertEquals('1', br.peek()); @@ -207,7 +221,7 @@ public void testReadLookahead1() throws Exception { } @Test - public void testReadLookahead2() throws Exception { + void testReadLookahead2() throws Exception { final char[] ref = new char[5]; final char[] res = new char[5]; diff --git a/src/test/java/org/apache/commons/csv/JiraCsv196Test.java b/src/test/java/org/apache/commons/csv/JiraCsv196Test.java index cff3a77294..aaf8e206b3 100644 --- a/src/test/java/org/apache/commons/csv/JiraCsv196Test.java +++ b/src/test/java/org/apache/commons/csv/JiraCsv196Test.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package org.apache.commons.csv; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -27,34 +28,48 @@ import org.junit.jupiter.api.Test; -public class JiraCsv196Test { +class JiraCsv196Test { private Reader getTestInput(final String path) { return new InputStreamReader(ClassLoader.getSystemClassLoader().getResourceAsStream(path)); } @Test - public void testParseFourBytes() throws IOException { + void testParseFourBytes() throws IOException { final CSVFormat format = CSVFormat.Builder.create().setDelimiter(',').setQuote('\'').get(); - try (CSVParser parser = new CSVParser.Builder().setFormat(format).setReader(getTestInput("org/apache/commons/csv/CSV-196/emoji.csv")) - .setCharset(StandardCharsets.UTF_8).setTrackBytes(true).get()) { + // @formatter:off + try (@SuppressWarnings("resource") // parser closes the reader. + CSVParser parser = new CSVParser.Builder() + .setFormat(format) + .setReader(getTestInput("org/apache/commons/csv/CSV-196/emoji.csv")) + .setCharset(StandardCharsets.UTF_8) + .setTrackBytes(true) + .get()) { + // @formatter:on final long[] charByteKey = { 0, 84, 701, 1318, 1935 }; int idx = 0; - for (CSVRecord record : parser) { - assertEquals(charByteKey[idx++], record.getBytePosition(), "index " + idx); + for (final CSVRecord record : parser) { + assertEquals(charByteKey[idx++], record.getBytePosition(), "At index " + idx); } } } @Test - public void testParseThreeBytes() throws IOException { + void testParseThreeBytes() throws IOException { final CSVFormat format = CSVFormat.Builder.create().setDelimiter(',').setQuote('\'').get(); - try (CSVParser parser = new CSVParser.Builder().setFormat(format).setReader(getTestInput("org/apache/commons/csv/CSV-196/japanese.csv")) - .setCharset(StandardCharsets.UTF_8).setTrackBytes(true).get()) { + // @formatter:off + try (@SuppressWarnings("resource") // parser closes the reader. + CSVParser parser = new CSVParser.Builder() + .setFormat(format) + .setReader(getTestInput("org/apache/commons/csv/CSV-196/japanese.csv")) + .setCharset(StandardCharsets.UTF_8) + .setTrackBytes(true) + .get()) { + // @formatter:on final long[] charByteKey = { 0, 89, 242, 395 }; int idx = 0; - for (CSVRecord record : parser) { - assertEquals(charByteKey[idx++], record.getBytePosition(), "index " + idx); + for (final CSVRecord record : parser) { + assertEquals(charByteKey[idx++], record.getBytePosition(), "At index " + idx); } } } diff --git a/src/test/java/org/apache/commons/csv/JiraCsv318Test.java b/src/test/java/org/apache/commons/csv/JiraCsv318Test.java index 1424809cc7..984509e87d 100644 --- a/src/test/java/org/apache/commons/csv/JiraCsv318Test.java +++ b/src/test/java/org/apache/commons/csv/JiraCsv318Test.java @@ -38,7 +38,7 @@ * * @see CSVPrinter */ -public class JiraCsv318Test { +class JiraCsv318Test { private void checkOutput(final ByteArrayOutputStream baos) { checkOutput(baos.toString()); diff --git a/src/test/java/org/apache/commons/csv/LexerTest.java b/src/test/java/org/apache/commons/csv/LexerTest.java index 38ab125530..a76f6e513b 100644 --- a/src/test/java/org/apache/commons/csv/LexerTest.java +++ b/src/test/java/org/apache/commons/csv/LexerTest.java @@ -41,7 +41,7 @@ /** */ -public class LexerTest { +class LexerTest { private static void assertContent(final String expectedContent, final Token actualToken) { assertEquals(expectedContent, actualToken.content.toString()); @@ -71,7 +71,7 @@ public void setUp() { // simple token with escaping enabled @Test - public void testBackslashWithEscaping() throws IOException { + void testBackslashWithEscaping() throws IOException { /* * file: a,\,,b \,, */ @@ -91,7 +91,7 @@ public void testBackslashWithEscaping() throws IOException { // simple token with escaping not enabled @Test - public void testBackslashWithoutEscaping() throws IOException { + void testBackslashWithoutEscaping() throws IOException { /* * file: a,\,,b \,, */ @@ -113,16 +113,22 @@ public void testBackslashWithoutEscaping() throws IOException { } @Test - public void testBackspace() throws Exception { + void testBackspace() throws Exception { try (Lexer lexer = createLexer("character" + BACKSPACE + "NotEscaped", formatWithEscaping)) { assertNextToken("character" + BACKSPACE + "NotEscaped", lexer); } } @Test - public void testComments() throws IOException { - final String code = "first,line,\n" + "second,line,tokenWith#no-comment\n" + "# comment line \n" + - "third,line,#no-comment\n" + "# penultimate comment\n" + "# Final comment\n"; + void testComments() throws IOException { + // @formatter:off + final String code = "first,line,\n" + + "second,line,tokenWith#no-comment\n" + + "# comment line \n" + + "third,line,#no-comment\n" + + "# penultimate comment\n" + + "# Final comment\n"; + // @formatter:on final CSVFormat format = CSVFormat.DEFAULT.withCommentMarker('#'); try (Lexer lexer = createLexer(code, format)) { assertNextToken(TOKEN, "first", lexer); @@ -143,7 +149,7 @@ public void testComments() throws IOException { } @Test - public void testCommentsAndEmptyLines() throws IOException { + void testCommentsAndEmptyLines() throws IOException { final String code = "1,2,3,\n" + // 1 "\n" + // 1b "\n" + // 1c @@ -189,7 +195,7 @@ public void testCommentsAndEmptyLines() throws IOException { } @Test - public void testCR() throws Exception { + void testCR() throws Exception { try (Lexer lexer = createLexer("character" + CR + "NotEscaped", formatWithEscaping)) { assertNextToken("character", lexer); assertNextToken("NotEscaped", lexer); @@ -198,7 +204,7 @@ public void testCR() throws Exception { // From CSV-1 @Test - public void testDelimiterIsWhitespace() throws IOException { + void testDelimiterIsWhitespace() throws IOException { final String code = "one\ttwo\t\tfour \t five\t six"; try (Lexer lexer = createLexer(code, CSVFormat.TDF)) { assertNextToken(TOKEN, "one", lexer); @@ -210,8 +216,27 @@ public void testDelimiterIsWhitespace() throws IOException { } } + /** + * With {@code ignoreSurroundingSpaces} enabled and a multi-character delimiter whose first character is whitespace, + * the side-effecting {@link Lexer#isDelimiter(int)} must only be evaluated once per character, otherwise the + * delimiter is consumed in the whitespace-skip loop and the empty field at the boundary is dropped. + */ @Test - public void testEOFWithoutClosingQuote() throws Exception { + void testEmptyTokenBeforeWhitespacePrefixedMultiCharacterDelimiter() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter(" |").setIgnoreSurroundingSpaces(true).get(); + try (Lexer lexer = createLexer(" |a", format)) { + assertNextToken(TOKEN, "", lexer); + assertNextToken(EOF, "a", lexer); + } + try (Lexer lexer = createLexer("a | |b", format)) { + assertNextToken(TOKEN, "a", lexer); + assertNextToken(TOKEN, "", lexer); + assertNextToken(EOF, "b", lexer); + } + } + + @Test + void testEOFWithoutClosingQuote() throws Exception { final String code = "a,\"b"; try (Lexer lexer = createLexer(code, CSVFormat.Builder.create().setLenientEof(true).get())) { assertNextToken(TOKEN, "a", lexer); @@ -224,21 +249,21 @@ public void testEOFWithoutClosingQuote() throws Exception { } @Test // TODO is this correct? Do we expect BACKSPACE to be unescaped? - public void testEscapedBackspace() throws Exception { + void testEscapedBackspace() throws Exception { try (Lexer lexer = createLexer("character\\" + BACKSPACE + "Escaped", formatWithEscaping)) { assertNextToken("character" + BACKSPACE + "Escaped", lexer); } } @Test - public void testEscapedCharacter() throws Exception { + void testEscapedCharacter() throws Exception { try (Lexer lexer = createLexer("character\\aEscaped", formatWithEscaping)) { assertNextToken("character\\aEscaped", lexer); } } @Test - public void testEscapedControlCharacter() throws Exception { + void testEscapedControlCharacter() throws Exception { // we are explicitly using an escape different from \ here try (Lexer lexer = createLexer("character!rEscaped", CSVFormat.DEFAULT.withEscape('!'))) { assertNextToken("character" + CR + "Escaped", lexer); @@ -246,35 +271,35 @@ public void testEscapedControlCharacter() throws Exception { } @Test - public void testEscapedControlCharacter2() throws Exception { + void testEscapedControlCharacter2() throws Exception { try (Lexer lexer = createLexer("character\\rEscaped", CSVFormat.DEFAULT.withEscape('\\'))) { assertNextToken("character" + CR + "Escaped", lexer); } } @Test - public void testEscapedCR() throws Exception { + void testEscapedCR() throws Exception { try (Lexer lexer = createLexer("character\\" + CR + "Escaped", formatWithEscaping)) { assertNextToken("character" + CR + "Escaped", lexer); } } @Test // TODO is this correct? Do we expect FF to be unescaped? - public void testEscapedFF() throws Exception { + void testEscapedFF() throws Exception { try (Lexer lexer = createLexer("character\\" + FF + "Escaped", formatWithEscaping)) { assertNextToken("character" + FF + "Escaped", lexer); } } @Test - public void testEscapedLF() throws Exception { + void testEscapedLF() throws Exception { try (Lexer lexer = createLexer("character\\" + LF + "Escaped", formatWithEscaping)) { assertNextToken("character" + LF + "Escaped", lexer); } } @Test - public void testEscapedMySqlNullValue() throws Exception { + void testEscapedMySqlNullValue() throws Exception { // MySQL uses \N to symbolize null values. We have to restore this try (Lexer lexer = createLexer("character\\NEscaped", formatWithEscaping)) { assertNextToken("character\\NEscaped", lexer); @@ -282,7 +307,7 @@ public void testEscapedMySqlNullValue() throws Exception { } @Test // TODO is this correct? Do we expect TAB to be unescaped? - public void testEscapedTab() throws Exception { + void testEscapedTab() throws Exception { try (Lexer lexer = createLexer("character\\" + TAB + "Escaped", formatWithEscaping)) { assertNextToken("character" + TAB + "Escaped", lexer); } @@ -290,7 +315,7 @@ public void testEscapedTab() throws Exception { } @Test - public void testEscapingAtEOF() throws Exception { + void testEscapingAtEOF() throws Exception { final String code = "escaping at EOF is evil\\"; try (Lexer lexer = createLexer(code, formatWithEscaping)) { assertThrows(IOException.class, () -> lexer.nextToken(new Token())); @@ -298,16 +323,29 @@ public void testEscapingAtEOF() throws Exception { } @Test - public void testFF() throws Exception { + void testFF() throws Exception { try (Lexer lexer = createLexer("character" + FF + "NotEscaped", formatWithEscaping)) { assertNextToken("character" + FF + "NotEscaped", lexer); } } @Test - public void testIgnoreEmptyLines() throws IOException { - final String code = "first,line,\n" + "\n" + "\n" + "second,line\n" + "\n" + "\n" + "third line \n" + "\n" + - "\n" + "last, line \n" + "\n" + "\n" + "\n"; + void testIgnoreEmptyLines() throws IOException { + // @formatter:off + final String code = "first,line,\n" + + "\n" + + "\n" + + "second,line\n" + + "\n" + + "\n" + + "third line \n" + + "\n" + + "\n" + + "last, line \n" + + "\n" + + "\n" + + "\n"; + // @formatter:on final CSVFormat format = CSVFormat.DEFAULT.withIgnoreEmptyLines(); try (Lexer lexer = createLexer(code, format)) { assertNextToken(TOKEN, "first", lexer); @@ -324,7 +362,7 @@ public void testIgnoreEmptyLines() throws IOException { } @Test - public void testIsMetaCharCommentStart() throws IOException { + void testIsMetaCharCommentStart() throws IOException { try (Lexer lexer = createLexer("#", CSVFormat.DEFAULT.withCommentMarker('#'))) { final int ch = lexer.readEscape(); assertEquals('#', ch); @@ -332,7 +370,7 @@ public void testIsMetaCharCommentStart() throws IOException { } @Test - public void testLF() throws Exception { + void testLF() throws Exception { try (Lexer lexer = createLexer("character" + LF + "NotEscaped", formatWithEscaping)) { assertNextToken("character", lexer); assertNextToken("NotEscaped", lexer); @@ -341,7 +379,7 @@ public void testLF() throws Exception { // encapsulator tokenizer (single line) @Test - public void testNextToken4() throws IOException { + void testNextToken4() throws IOException { /* * file: a,"foo",b a, " foo",b a,"foo " ,b // whitespace after closing encapsulator a, " foo " ,b */ @@ -365,7 +403,7 @@ public void testNextToken4() throws IOException { // encapsulator tokenizer (multi line, delimiter in string) @Test - public void testNextToken5() throws IOException { + void testNextToken5() throws IOException { final String code = "a,\"foo\n\",b\n\"foo\n baar ,,,\"\n\"\n\t \n\""; try (Lexer lexer = createLexer(code, CSVFormat.DEFAULT)) { assertNextToken(TOKEN, "a", lexer); @@ -378,7 +416,7 @@ public void testNextToken5() throws IOException { // change delimiters, comment, encapsulater @Test - public void testNextToken6() throws IOException { + void testNextToken6() throws IOException { /* * file: a;'b and \' more ' !comment;;;; ;; */ @@ -390,8 +428,46 @@ public void testNextToken6() throws IOException { } } + /** + * A truncated escaped multi-character delimiter at EOF must not be accepted by reusing the previous escape delimiter + * look-ahead in {@link Lexer#isEscapeDelimiter()}. + */ + @Test + void testPartialEscapedMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").setEscape('!').get(); + try (Lexer lexer = createLexer("x![!|!]y![!|", format)) { + assertNextToken(EOF, "x[|]y![!|", lexer); + } + } + + /** + * Tests CSV-324. + */ + @Test + void testPartialMultiCharacterDelimiterAtEOF() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + try (Lexer lexer = createLexer("a[|]b[|", format)) { + assertNextToken(TOKEN, "a", lexer); + assertNextToken(EOF, "b[|", lexer); + } + } + + /** + * A truncated multi-character delimiter at EOF must not be accepted by reusing the look-ahead buffer left dirty by an + * earlier non-matching peek in the same token (CSV-324 only cleared the buffer once per token). + */ + @Test + void testPartialMultiCharacterDelimiterAtEOFAfterMismatch() throws IOException { + final CSVFormat format = CSVFormat.DEFAULT.builder().setDelimiter("[|]").get(); + // The "[a]" peek leaves ']' in the look-ahead buffer; the trailing "[|" must not match "[|]". + final String recordString = "x[a][|"; + try (Lexer lexer = createLexer(recordString, format)) { + assertNextToken(EOF, recordString, lexer); + } + } + @Test - public void testReadEscapeBackspace() throws IOException { + void testReadEscapeBackspace() throws IOException { try (Lexer lexer = createLexer("b", CSVFormat.DEFAULT.withEscape('\b'))) { final int ch = lexer.readEscape(); assertEquals(BACKSPACE, ch); @@ -399,7 +475,7 @@ public void testReadEscapeBackspace() throws IOException { } @Test - public void testReadEscapeFF() throws IOException { + void testReadEscapeFF() throws IOException { try (Lexer lexer = createLexer("f", CSVFormat.DEFAULT.withEscape('\f'))) { final int ch = lexer.readEscape(); assertEquals(FF, ch); @@ -407,7 +483,7 @@ public void testReadEscapeFF() throws IOException { } @Test - public void testReadEscapeTab() throws IOException { + void testReadEscapeTab() throws IOException { try (Lexer lexer = createLexer("t", CSVFormat.DEFAULT.withEscape('\t'))) { final int ch = lexer.readEscape(); assertNextToken(EOF, "", lexer); @@ -416,7 +492,7 @@ public void testReadEscapeTab() throws IOException { } @Test - public void testSurroundingSpacesAreDeleted() throws IOException { + void testSurroundingSpacesAreDeleted() throws IOException { final String code = "noSpaces, leadingSpaces,trailingSpaces , surroundingSpaces , ,,"; try (Lexer lexer = createLexer(code, CSVFormat.DEFAULT.withIgnoreSurroundingSpaces())) { assertNextToken(TOKEN, "noSpaces", lexer); @@ -430,7 +506,7 @@ public void testSurroundingSpacesAreDeleted() throws IOException { } @Test - public void testSurroundingTabsAreDeleted() throws IOException { + void testSurroundingTabsAreDeleted() throws IOException { final String code = "noTabs,\tleadingTab,trailingTab\t,\tsurroundingTabs\t,\t\t,,"; try (Lexer lexer = createLexer(code, CSVFormat.DEFAULT.withIgnoreSurroundingSpaces())) { assertNextToken(TOKEN, "noTabs", lexer); @@ -444,14 +520,14 @@ public void testSurroundingTabsAreDeleted() throws IOException { } @Test - public void testTab() throws Exception { + void testTab() throws Exception { try (Lexer lexer = createLexer("character" + TAB + "NotEscaped", formatWithEscaping)) { assertNextToken("character" + TAB + "NotEscaped", lexer); } } @Test - public void testTrailingTextAfterQuote() throws Exception { + void testTrailingTextAfterQuote() throws Exception { final String code = "\"a\" b,\"a\" \" b,\"a\" b \"\""; try (Lexer lexer = createLexer(code, CSVFormat.Builder.create().setTrailingData(true).get())) { assertNextToken(TOKEN, "a b", lexer); @@ -464,7 +540,7 @@ public void testTrailingTextAfterQuote() throws Exception { } @Test - public void testTrimTrailingSpacesZeroLength() throws Exception { + void testTrimTrailingSpacesZeroLength() throws Exception { final StringBuilder buffer = new StringBuilder(""); try (Lexer lexer = createLexer(buffer.toString(), CSVFormat.DEFAULT)) { lexer.trimTrailingSpaces(buffer); diff --git a/src/test/java/org/apache/commons/csv/PerformanceTest.java b/src/test/java/org/apache/commons/csv/PerformanceTest.java index bf0d483897..9284828e6c 100644 --- a/src/test/java/org/apache/commons/csv/PerformanceTest.java +++ b/src/test/java/org/apache/commons/csv/PerformanceTest.java @@ -44,7 +44,7 @@ * Basic test harness. */ @SuppressWarnings("boxing") -public class PerformanceTest { +class PerformanceTest { @FunctionalInterface private interface CSVParserFactory { diff --git a/src/test/java/org/apache/commons/csv/TokenTest.java b/src/test/java/org/apache/commons/csv/TokenTest.java index 0f7f2f1eed..075c1b1d9c 100644 --- a/src/test/java/org/apache/commons/csv/TokenTest.java +++ b/src/test/java/org/apache/commons/csv/TokenTest.java @@ -28,11 +28,11 @@ /** * Tests {@link Token}. */ -public class TokenTest { +class TokenTest { @ParameterizedTest @EnumSource(Token.Type.class) - public void testToString(final Token.Type type) { + void testToString(final Token.Type type) { // Should never blow up final Token token = new Token(); final String resetName = Token.Type.INVALID.name(); diff --git a/src/test/java/org/apache/commons/csv/UserGuideTest.java b/src/test/java/org/apache/commons/csv/UserGuideTest.java index 6b97ccded9..6cd8c72d7f 100644 --- a/src/test/java/org/apache/commons/csv/UserGuideTest.java +++ b/src/test/java/org/apache/commons/csv/UserGuideTest.java @@ -35,7 +35,7 @@ /** * Tests for the user guide. */ -public class UserGuideTest { +class UserGuideTest { @TempDir Path tempDir; @@ -54,7 +54,7 @@ public InputStreamReader newReader(final Path path) throws IOException { } @Test - public void testBomFull() throws UnsupportedEncodingException, IOException { + void testBomFull() throws UnsupportedEncodingException, IOException { final Path path = tempDir.resolve("test1.csv"); Files.copy(Utils.createUtf8Input("ColumnA, ColumnB, ColumnC\r\nA, B, C\r\n".getBytes(StandardCharsets.UTF_8), true), path); // @formatter:off @@ -74,7 +74,7 @@ public void testBomFull() throws UnsupportedEncodingException, IOException { } @Test - public void testBomUtil() throws UnsupportedEncodingException, IOException { + void testBomUtil() throws UnsupportedEncodingException, IOException { final Path path = tempDir.resolve("test2.csv"); Files.copy(Utils.createUtf8Input("ColumnA, ColumnB, ColumnC\r\nA, B, C\r\n".getBytes(StandardCharsets.UTF_8), true), path); try (Reader reader = newReader(path); diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv148Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv148Test.java index 71b056834c..67f1b785d5 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv148Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv148Test.java @@ -24,10 +24,10 @@ import org.apache.commons.csv.QuoteMode; import org.junit.jupiter.api.Test; -public class JiraCsv148Test { +class JiraCsv148Test { @Test - public void testWithIgnoreSurroundingSpacesEmpty() { + void testWithIgnoreSurroundingSpacesEmpty() { // @formatter:off final CSVFormat format = CSVFormat.DEFAULT.builder() .setQuoteMode(QuoteMode.ALL) @@ -47,7 +47,7 @@ public void testWithIgnoreSurroundingSpacesEmpty() { * quotation marks, while withIgnoreSurroundingSpace() cannot The same point: you can remove the leading and trailing spaces, tabs and other symbols. */ @Test - public void testWithTrimEmpty() { + void testWithTrimEmpty() { // @formatter:off final CSVFormat format = CSVFormat.DEFAULT.builder() .setQuoteMode(QuoteMode.ALL) @@ -55,7 +55,7 @@ public void testWithTrimEmpty() { .get(); // @formatter:on assertEquals( - "\"\",\"\",\"Single space on the left\",\"Single space on the right\"," + "\"Single spaces on both sides\",\"Multiple spaces on the left\"," + + "\"\",\"\",\"Single space on the left\",\"Single space on the right\",\"Single spaces on both sides\",\"Multiple spaces on the left\"," + "\"Multiple spaces on the right\",\"Multiple spaces on both sides\"", format.format("", " ", " Single space on the left", "Single space on the right ", " Single spaces on both sides ", " Multiple spaces on the left", "Multiple spaces on the right ", " Multiple spaces on both sides ")); diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv149Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv149Test.java index 5b3d40c9f0..b32e965665 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv149Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv149Test.java @@ -29,12 +29,12 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv149Test { +class JiraCsv149Test { private static final String CR_LF = "\r\n"; @Test - public void testJiraCsv149EndWithEOL() throws IOException { + void testJiraCsv149EndWithEOL() throws IOException { testJiraCsv149EndWithEolAtEof(true); } @@ -61,7 +61,7 @@ private void testJiraCsv149EndWithEolAtEof(final boolean eolAtEof) throws IOExce } @Test - public void testJiraCsv149EndWithoutEOL() throws IOException { + void testJiraCsv149EndWithoutEOL() throws IOException { testJiraCsv149EndWithEolAtEof(false); } } diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv150Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv150Test.java index f10477292c..eec91d52d0 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv150Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv150Test.java @@ -27,7 +27,7 @@ import org.apache.commons.csv.CSVParser; import org.junit.jupiter.api.Test; -public class JiraCsv150Test { +class JiraCsv150Test { private void testDisable(final CSVFormat format, final StringReader reader) throws IOException { try (CSVParser csvParser = CSVParser.builder().setReader(reader).setFormat(format).get()) { @@ -36,19 +36,19 @@ private void testDisable(final CSVFormat format, final StringReader reader) thro } @Test - public void testDisableComment() throws IOException { + void testDisableComment() throws IOException { final StringReader stringReader = new StringReader("\"66\u2441\",,\"\",\"DeutscheBK\ufffe\",\"000\"\r\n"); testDisable(CSVFormat.DEFAULT.builder().setCommentMarker(null).get(), stringReader); } @Test - public void testDisableEncapsulation() throws IOException { + void testDisableEncapsulation() throws IOException { final StringReader stringReader = new StringReader("66\u2441,,\"\",\ufffeDeutscheBK,\"000\"\r\n"); testDisable(CSVFormat.DEFAULT.builder().setQuote(null).get(), stringReader); } @Test - public void testDisableEscaping() throws IOException { + void testDisableEscaping() throws IOException { final StringReader stringReader = new StringReader("\"66\u2441\",,\"\",\"DeutscheBK\ufffe\",\"000\"\r\n"); testDisable(CSVFormat.DEFAULT.builder().setEscape(null).get(), stringReader); } diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv154Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv154Test.java index c045cdd269..90d657fcd1 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv154Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv154Test.java @@ -26,10 +26,10 @@ import org.apache.commons.csv.CSVPrinter; import org.junit.jupiter.api.Test; -public class JiraCsv154Test { +class JiraCsv154Test { @Test - public void testJiraCsv154_withCommentMarker() throws IOException { + void testJiraCsv154_withCommentMarker() throws IOException { final String comment = "This is a header comment"; // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() @@ -48,7 +48,7 @@ public void testJiraCsv154_withCommentMarker() throws IOException { } @Test - public void testJiraCsv154_withHeaderComments() throws IOException { + void testJiraCsv154_withHeaderComments() throws IOException { final String comment = "This is a header comment"; // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv167Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv167Test.java index b7e3bae858..607d0cf2a3 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv167Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv167Test.java @@ -31,7 +31,7 @@ import org.apache.commons.csv.QuoteMode; import org.junit.jupiter.api.Test; -public class JiraCsv167Test { +class JiraCsv167Test { private Reader getTestReader() { return new InputStreamReader( @@ -39,7 +39,7 @@ private Reader getTestReader() { } @Test - public void testParse() throws IOException { + void testParse() throws IOException { int totcomment = 0; int totrecs = 0; try (Reader reader = getTestReader(); BufferedReader br = new BufferedReader(reader)) { @@ -77,7 +77,7 @@ public void testParse() throws IOException { int comments = 0; int records = 0; try (Reader reader = getTestReader(); CSVParser parser = format.parse(reader)) { - for (CSVRecord csvRecord : parser) { + for (final CSVRecord csvRecord : parser) { records++; if (csvRecord.hasComment()) { comments++; diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv198Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv198Test.java index 641797fe88..1117c12ac9 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv198Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv198Test.java @@ -31,7 +31,7 @@ import org.apache.commons.csv.CSVParser; import org.junit.jupiter.api.Test; -public class JiraCsv198Test { +class JiraCsv198Test { // @formatter:off private static final CSVFormat CSV_FORMAT = CSVFormat.EXCEL.builder() @@ -42,7 +42,7 @@ public class JiraCsv198Test { // @formatter:on @Test - public void test() throws UnsupportedEncodingException, IOException { + void test() throws UnsupportedEncodingException, IOException { final InputStream pointsOfReference = getClass().getResourceAsStream("/org/apache/commons/csv/CSV-198/optd_por_public.csv"); assertNotNull(pointsOfReference); try (@SuppressWarnings("resource") diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv203Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv203Test.java index 8eee041560..2c9226506c 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv203Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv203Test.java @@ -29,10 +29,10 @@ * JIRA: withNullString value is printed without quotes when * QuoteMode.ALL is specified */ -public class JiraCsv203Test { +class JiraCsv203Test { @Test - public void testQuoteModeAll() throws Exception { + void testQuoteModeAll() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() .setNullString("N/A") @@ -48,7 +48,7 @@ public void testQuoteModeAll() throws Exception { } @Test - public void testQuoteModeAllNonNull() throws Exception { + void testQuoteModeAllNonNull() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() .setNullString("N/A") @@ -64,7 +64,7 @@ public void testQuoteModeAllNonNull() throws Exception { } @Test - public void testQuoteModeMinimal() throws Exception { + void testQuoteModeMinimal() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() .setNullString("N/A") @@ -80,7 +80,7 @@ public void testQuoteModeMinimal() throws Exception { } @Test - public void testQuoteModeNonNumeric() throws Exception { + void testQuoteModeNonNumeric() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() .setNullString("N/A") @@ -96,7 +96,7 @@ public void testQuoteModeNonNumeric() throws Exception { } @Test - public void testWithEmptyValues() throws Exception { + void testWithEmptyValues() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() .setNullString("N/A") @@ -113,7 +113,7 @@ public void testWithEmptyValues() throws Exception { } @Test - public void testWithoutNullString() throws Exception { + void testWithoutNullString() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() //.setNullString("N/A") @@ -129,7 +129,7 @@ public void testWithoutNullString() throws Exception { } @Test - public void testWithoutQuoteMode() throws Exception { + void testWithoutQuoteMode() throws Exception { // @formatter:off final CSVFormat format = CSVFormat.EXCEL.builder() .setNullString("N/A") diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv206Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv206Test.java index 3b69b173ba..2fecd10f16 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv206Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv206Test.java @@ -30,10 +30,10 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv206Test { +class JiraCsv206Test { @Test - public void testJiraCsv206MultipleCharacterDelimiter() throws IOException { + void testJiraCsv206MultipleCharacterDelimiter() throws IOException { // Read with multiple character delimiter final String source = "FirstName[|]LastName[|]Address\r\nJohn[|]Smith[|]123 Main St."; final StringReader reader = new StringReader(source); diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv211Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv211Test.java index a4e3960c96..28b559d1e1 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv211Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv211Test.java @@ -28,10 +28,10 @@ import org.apache.commons.csv.CSVParser; import org.junit.jupiter.api.Test; -public class JiraCsv211Test { +class JiraCsv211Test { @Test - public void testJiraCsv211Format() throws IOException { + void testJiraCsv211Format() throws IOException { // @formatter:off final CSVFormat printFormat = CSVFormat.DEFAULT.builder() .setDelimiter('\t') diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv213Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv213Test.java index d700843f47..90f5da4c5a 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv213Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv213Test.java @@ -38,7 +38,7 @@ * you want, you need to open a new CSVParser. *

    */ -public class JiraCsv213Test { +class JiraCsv213Test { private void createEndChannel(final File csvFile) { // @formatter:off @@ -64,7 +64,7 @@ private void createEndChannel(final File csvFile) { } @Test - public void test() { + void test() { createEndChannel(new File("src/test/resources/org/apache/commons/csv/CSV-213/999751170.patch.csv")); } } diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv227Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv227Test.java new file mode 100644 index 0000000000..2b9e335a8f --- /dev/null +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv227Test.java @@ -0,0 +1,49 @@ +/* + * 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.csv.issues; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVPrinter; +import org.apache.commons.csv.QuoteMode; +import org.junit.jupiter.api.Test; + +/** + * Tests https://issues.apache.org/jira/browse/CSV-227 + */ +class JiraCsv227Test { + + @Test + public void test() throws IOException { + final StringBuilder out = new StringBuilder(); + try (CSVPrinter printer = new CSVPrinter(out, CSVFormat.DEFAULT.withQuoteMode(QuoteMode.MINIMAL))) { + printer.printRecord("ㅁㅎㄷㄹ", "ㅁㅎㄷㄹ", "", "test2"); + printer.printRecord("한글3", "hello3", "3한글3", "test3"); + printer.printRecord("", "hello4", "", "test4"); + } + // ㅁㅎㄷㄹ,ㅁㅎㄷㄹ,,test2 + // 한글3,hello3,3한글3,test3 + // "",hello4,,test4 + assertEquals("ㅁㅎㄷㄹ,ㅁㅎㄷㄹ,,test2\r\n한글3,hello3,3한글3,test3\r\n\"\",hello4,,test4\r\n", out.toString()); + } +} diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv247Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv247Test.java index 85d9676fde..c2d9ac5910 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv247Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv247Test.java @@ -34,10 +34,10 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv247Test { +class JiraCsv247Test { @Test - public void testHeadersMissingOneColumnWhenAllowingMissingColumnNames() throws Exception { + void testHeadersMissingOneColumnWhenAllowingMissingColumnNames() throws Exception { final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().setAllowMissingColumnNames(true).get(); assertTrue(format.getAllowMissingColumnNames(), "We should allow missing column names"); @@ -63,7 +63,7 @@ record = iterator.next(); } @Test - public void testHeadersMissingThrowsWhenNotAllowingMissingColumnNames() { + void testHeadersMissingThrowsWhenNotAllowingMissingColumnNames() { final CSVFormat format = CSVFormat.DEFAULT.builder().setHeader().get(); assertFalse(format.getAllowMissingColumnNames(), "By default we should not allow missing column names"); diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv248Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv248Test.java index 08cfe13fa6..480a9dffa9 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv248Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv248Test.java @@ -33,7 +33,7 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv248Test { +class JiraCsv248Test { private static InputStream getTestInput() { return ClassLoader.getSystemClassLoader().getResourceAsStream("org/apache/commons/csv/CSV-248/csvRecord.bin"); @@ -50,7 +50,7 @@ private static InputStream getTestInput() { * @throws ClassNotFoundException If the CSVRecord cannot be deserialized */ @Test - public void testJiraCsv248() throws IOException, ClassNotFoundException { + void testJiraCsv248() throws IOException, ClassNotFoundException { // Record was originally created using CSV version 1.6 with the following code: // try (CSVParser parser = CSVParser.parse("A,B\n#my comment\nOne,Two", // CSVFormat.DEFAULT.builder().setHeader().setCommentMarker('#'))) { diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv249Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv249Test.java index f5fe47ca6a..4034b04bd7 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv249Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv249Test.java @@ -32,10 +32,10 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv249Test { +class JiraCsv249Test { @Test - public void testJiraCsv249() throws IOException { + void testJiraCsv249() throws IOException { final CSVFormat format = CSVFormat.DEFAULT.builder().setEscape('\\').get(); final StringWriter stringWriter = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(stringWriter, format)) { diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv253Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv253Test.java index 5b266a3e3a..13bb6a8270 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv253Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv253Test.java @@ -18,7 +18,7 @@ */ package org.apache.commons.csv.issues; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.apache.commons.csv.CsvAssertions.assertValuesEquals; import java.io.IOException; import java.io.StringReader; @@ -33,24 +33,24 @@ /** * Setting QuoteMode:ALL_NON_NULL or NON_NUMERIC can distinguish between empty string columns and absent value columns. */ -public class JiraCsv253Test { - - private void assertArrayEqual(final String[] expected, final CSVRecord actual) { - for (int i = 0; i < expected.length; i++) { - assertEquals(expected[i], actual.get(i)); - } - } +class JiraCsv253Test { @Test - public void testHandleAbsentValues() throws IOException { - final String source = "\"John\",,\"Doe\"\n" + ",\"AA\",123\n" + "\"John\",90,\n" + "\"\",,90"; + void testHandleAbsentValues() throws IOException { + // @formatter:off + final String source = + "\"John\",,\"Doe\"\n" + + ",\"AA\",123\n" + + "\"John\",90,\n" + + "\"\",,90"; + // @formatter:on final CSVFormat csvFormat = CSVFormat.DEFAULT.builder().setQuoteMode(QuoteMode.NON_NUMERIC).get(); try (CSVParser parser = csvFormat.parse(new StringReader(source))) { final Iterator csvRecords = parser.iterator(); - assertArrayEqual(new String[] {"John", null, "Doe"}, csvRecords.next()); - assertArrayEqual(new String[] {null, "AA", "123"}, csvRecords.next()); - assertArrayEqual(new String[] {"John", "90", null}, csvRecords.next()); - assertArrayEqual(new String[] {"", null, "90"}, csvRecords.next()); + assertValuesEquals(new String[] {"John", null, "Doe"}, csvRecords.next()); + assertValuesEquals(new String[] {null, "AA", "123"}, csvRecords.next()); + assertValuesEquals(new String[] {"John", "90", null}, csvRecords.next()); + assertValuesEquals(new String[] {"", null, "90"}, csvRecords.next()); } } } diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv254Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv254Test.java new file mode 100644 index 0000000000..629b42ee6b --- /dev/null +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv254Test.java @@ -0,0 +1,52 @@ +/* + * 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.csv.issues; + +import static org.apache.commons.csv.CsvAssertions.assertValuesEquals; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Iterator; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.apache.commons.csv.CSVRecord; +import org.junit.jupiter.api.Test; + +/** + * Tests https://issues.apache.org/jira/browse/CSV-254. + */ +class JiraCsv254Test { + + @Test + void test() throws IOException { + final CSVFormat csvFormat = CSVFormat.POSTGRESQL_CSV; + try (BufferedReader reader = Files.newBufferedReader(Paths.get("src/test/resources/org/apache/commons/csv/CSV-254/csv-254.csv"), + StandardCharsets.UTF_8); CSVParser parser = csvFormat.parse(reader)) { + final Iterator csvRecords = parser.iterator(); + assertValuesEquals(new String[] { "AA", "33", null }, csvRecords.next()); + assertValuesEquals(new String[] { "AA", null, "" }, csvRecords.next()); + assertValuesEquals(new String[] { null, "33", "CC" }, csvRecords.next()); + } + } +} diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv257Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv257Test.java new file mode 100644 index 0000000000..4234a7a0fa --- /dev/null +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv257Test.java @@ -0,0 +1,84 @@ +/* + * 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.csv.issues; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.StringReader; + +import org.apache.commons.csv.CSVFormat; +import org.apache.commons.csv.CSVParser; +import org.junit.jupiter.api.Test; + +/** + * Tests https://issues.apache.org/jira/browse/CSV-257 + */ +class JiraCsv257Test { + + private static final String INPUT = ","; + + @Test + void testHeaderBuilder() throws IOException { + // @formatter:off + final CSVFormat format = CSVFormat.RFC4180.builder() + .setDelimiter(INPUT.charAt(0)) + .setHeader() + .setSkipHeaderRecord(true) + .setIgnoreSurroundingSpaces(true) + .get(); + // @formatter:on + // Document the current behavior: Throw a IllegalArgumentException is a header name is missing. + assertThrows(IllegalArgumentException.class, () -> { + try (CSVParser parser = CSVParser.parse(INPUT, format)) { + // empty + } + }); + } + + @Test + void testHeaderDepreacted() throws IOException { + // @formatter:off + final CSVFormat format = CSVFormat.RFC4180 + .withDelimiter(INPUT.charAt(0)) + .withFirstRecordAsHeader() + .withIgnoreSurroundingSpaces(); + // @formatter:on + // Document the current behavior: Throw a IllegalArgumentException is a header name is missing. + assertThrows(IllegalArgumentException.class, () -> { + try (CSVParser parser = new CSVParser(new StringReader(INPUT), format)) { + // empty + } + }); + } + + @Test + void testNoHeaderBuilder() throws IOException { + // @formatter:off + final CSVFormat format = CSVFormat.RFC4180.builder() + .setDelimiter(INPUT.charAt(0)) + .setIgnoreSurroundingSpaces(true) + .get(); + // @formatter:on + try (CSVParser parser = CSVParser.parse(INPUT, format)) { + // empty + } + } +} diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv263Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv263Test.java index abae0ea2f5..18bb9580a3 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv263Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv263Test.java @@ -32,10 +32,10 @@ /** * Tests [CSV-263] Print from Reader with embedded quotes generates incorrect output. */ -public class JiraCsv263Test { +class JiraCsv263Test { @Test - public void testPrintFromReaderWithQuotes() throws IOException { + void testPrintFromReaderWithQuotes() throws IOException { // @formatter:off final CSVFormat format = CSVFormat.RFC4180.builder() .setDelimiter(',') diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv264Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv264Test.java index d910ef5828..857e42cb8f 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv264Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv264Test.java @@ -35,7 +35,7 @@ * * @see Jira Ticker */ -public class JiraCsv264Test { +class JiraCsv264Test { private static final String CSV_STRING = "\"\",\"B\",\"\"\n" + "\"1\",\"2\",\"3\"\n" + @@ -49,7 +49,7 @@ public class JiraCsv264Test { "\"6\",\"7\",\"\",\"\",\"10\""; @Test - public void testJiraCsv264() { + void testJiraCsv264() { final CSVFormat csvFormat = CSVFormat.DEFAULT .builder() .setHeader() @@ -62,7 +62,7 @@ public void testJiraCsv264() { } @Test - public void testJiraCsv264WithGapAllowEmpty() throws IOException { + void testJiraCsv264WithGapAllowEmpty() throws IOException { final CSVFormat csvFormat = CSVFormat.DEFAULT .builder() .setHeader() @@ -75,7 +75,7 @@ public void testJiraCsv264WithGapAllowEmpty() throws IOException { } @Test - public void testJiraCsv264WithGapDisallow() { + void testJiraCsv264WithGapDisallow() { final CSVFormat csvFormat = CSVFormat.DEFAULT .builder() .setHeader() diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv265Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv265Test.java index 4853672dc0..1bccad702f 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv265Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv265Test.java @@ -33,10 +33,10 @@ /** * Tests [CSV-265] {@link CSVRecord#getCharacterPosition()} returns the correct position after encountering a comment. */ -public class JiraCsv265Test { +class JiraCsv265Test { @Test - public void testCharacterPositionWithComments() throws IOException { + void testCharacterPositionWithComments() throws IOException { // @formatter:off final String csv = "# Comment1\n" + @@ -62,7 +62,7 @@ public void testCharacterPositionWithComments() throws IOException { } @Test - public void testCharacterPositionWithCommentsSpanningMultipleLines() throws IOException { + void testCharacterPositionWithCommentsSpanningMultipleLines() throws IOException { // @formatter:off final String csv = "# Comment1\n" + diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv271Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv271Test.java index 5ee5e0a01e..0269dec5d1 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv271Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv271Test.java @@ -29,10 +29,10 @@ import org.apache.commons.csv.CSVPrinter; import org.junit.jupiter.api.Test; -public class JiraCsv271Test { +class JiraCsv271Test { @Test - public void testJiraCsv271_withArray() throws IOException { + void testJiraCsv271_withArray() throws IOException { final CSVFormat csvFormat = CSVFormat.DEFAULT; final StringWriter stringWriter = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(stringWriter, csvFormat)) { @@ -43,7 +43,7 @@ public void testJiraCsv271_withArray() throws IOException { } @Test - public void testJiraCsv271_withList() throws IOException { + void testJiraCsv271_withList() throws IOException { final CSVFormat csvFormat = CSVFormat.DEFAULT; final StringWriter stringWriter = new StringWriter(); try (CSVPrinter printer = new CSVPrinter(stringWriter, csvFormat)) { diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv288Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv288Test.java index 0be6a52f81..065ee6bb37 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv288Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv288Test.java @@ -31,7 +31,7 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv288Test { +class JiraCsv288Test { private void print(final CSVRecord csvRecord, final CSVPrinter csvPrinter) throws IOException { for (final String value : csvRecord) { @@ -42,7 +42,7 @@ private void print(final CSVRecord csvRecord, final CSVPrinter csvPrinter) throw @Test // Before fix: // expected: but was: - public void testParseWithABADelimiter() throws Exception { + void testParseWithABADelimiter() throws Exception { final Reader in = new StringReader("a|~|b|~|c|~|d|~||~|f"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -57,7 +57,7 @@ public void testParseWithABADelimiter() throws Exception { @Test // Before fix: // expected: but was: - public void testParseWithDoublePipeDelimiter() throws Exception { + void testParseWithDoublePipeDelimiter() throws Exception { final Reader in = new StringReader("a||b||c||d||||f"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -72,7 +72,7 @@ public void testParseWithDoublePipeDelimiter() throws Exception { @Test // Regression, already passed before fix - public void testParseWithDoublePipeDelimiterDoubleCharValue() throws Exception { + void testParseWithDoublePipeDelimiterDoubleCharValue() throws Exception { final Reader in = new StringReader("a||bb||cc||dd||f"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -87,7 +87,7 @@ public void testParseWithDoublePipeDelimiterDoubleCharValue() throws Exception { @Test // Before fix: // expected: but was: - public void testParseWithDoublePipeDelimiterEndsWithDelimiter() throws Exception { + void testParseWithDoublePipeDelimiterEndsWithDelimiter() throws Exception { final Reader in = new StringReader("a||b||c||d||||f||"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -102,7 +102,7 @@ public void testParseWithDoublePipeDelimiterEndsWithDelimiter() throws Exception @Test // Before fix: // expected: but was: - public void testParseWithDoublePipeDelimiterQuoted() throws Exception { + void testParseWithDoublePipeDelimiterQuoted() throws Exception { final Reader in = new StringReader("a||\"b||c\"||d||||f"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -116,7 +116,7 @@ public void testParseWithDoublePipeDelimiterQuoted() throws Exception { @Test // Regression, already passed before fix - public void testParseWithSinglePipeDelimiterEndsWithDelimiter() throws Exception { + void testParseWithSinglePipeDelimiterEndsWithDelimiter() throws Exception { final Reader in = new StringReader("a|b|c|d||f|"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -131,7 +131,7 @@ public void testParseWithSinglePipeDelimiterEndsWithDelimiter() throws Exception @Test // Before fix: // expected: but was: - public void testParseWithTriplePipeDelimiter() throws Exception { + void testParseWithTriplePipeDelimiter() throws Exception { final Reader in = new StringReader("a|||b|||c|||d||||||f"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -145,7 +145,7 @@ public void testParseWithTriplePipeDelimiter() throws Exception { @Test // Regression, already passed before fix - public void testParseWithTwoCharDelimiter1() throws Exception { + void testParseWithTwoCharDelimiter1() throws Exception { final Reader in = new StringReader("a~|b~|c~|d~|~|f"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -159,7 +159,7 @@ public void testParseWithTwoCharDelimiter1() throws Exception { @Test // Regression, already passed before fix - public void testParseWithTwoCharDelimiter2() throws Exception { + void testParseWithTwoCharDelimiter2() throws Exception { final Reader in = new StringReader("a~|b~|c~|d~|~|f~"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -173,7 +173,7 @@ public void testParseWithTwoCharDelimiter2() throws Exception { @Test // Regression, already passed before fix - public void testParseWithTwoCharDelimiter3() throws Exception { + void testParseWithTwoCharDelimiter3() throws Exception { final Reader in = new StringReader("a~|b~|c~|d~|~|f|"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -187,7 +187,7 @@ public void testParseWithTwoCharDelimiter3() throws Exception { @Test // Regression, already passed before fix - public void testParseWithTwoCharDelimiter4() throws Exception { + void testParseWithTwoCharDelimiter4() throws Exception { final Reader in = new StringReader("a~|b~|c~|d~|~|f~~||g"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); @@ -202,7 +202,7 @@ public void testParseWithTwoCharDelimiter4() throws Exception { @Test // Before fix: // expected: but was: - public void testParseWithTwoCharDelimiterEndsWithDelimiter() throws Exception { + void testParseWithTwoCharDelimiterEndsWithDelimiter() throws Exception { final Reader in = new StringReader("a~|b~|c~|d~|~|f~|"); final StringBuilder stringBuilder = new StringBuilder(); try (CSVPrinter csvPrinter = new CSVPrinter(stringBuilder, CSVFormat.EXCEL); diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv290Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv290Test.java index f9dd6e9530..f251eeb7a5 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv290Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv290Test.java @@ -58,7 +58,7 @@ // 2 xyz \\b:\b \\t:\t \\n:\n \\r:\r a b // 3 a b,c,d "quoted" e // -public class JiraCsv290Test { +class JiraCsv290Test { private void testHelper(final String fileName, final CSVFormat format) throws Exception { List> content = new ArrayList<>(); @@ -84,17 +84,17 @@ private void testHelper(final String fileName, final CSVFormat format) throws Ex } @Test - public void testPostgresqlCsv() throws Exception { + void testPostgresqlCsv() throws Exception { testHelper("psql.csv", CSVFormat.POSTGRESQL_CSV); } @Test - public void testPostgresqlText() throws Exception { + void testPostgresqlText() throws Exception { testHelper("psql.tsv", CSVFormat.POSTGRESQL_TEXT); } @Test - public void testWriteThenRead() throws Exception { + void testWriteThenRead() throws Exception { final StringWriter sw = new StringWriter(); final CSVFormat format = CSVFormat.POSTGRESQL_CSV.builder().setHeader().setSkipHeaderRecord(true).get(); try (CSVPrinter printer = new CSVPrinter(sw, format)) { diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv294Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv294Test.java index 2cc62628e7..0e5de0751b 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv294Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv294Test.java @@ -36,7 +36,7 @@ import org.apache.commons.csv.CSVRecord; import org.junit.jupiter.api.Test; -public class JiraCsv294Test { +class JiraCsv294Test { private static void testInternal(final CSVFormat format, final String expectedSubstring) throws IOException { final ByteArrayOutputStream bos = new ByteArrayOutputStream(); @@ -58,24 +58,24 @@ private static void testInternal(final CSVFormat format, final String expectedSu } @Test - public void testDefaultCsvFormatWithBackslashEscapeWorks() throws IOException { + void testDefaultCsvFormatWithBackslashEscapeWorks() throws IOException { testInternal(CSVFormat.Builder.create().setEscape('\\').get(), ",\"b \\\"\\\"\","); } @Test - public void testDefaultCsvFormatWithNullEscapeWorks() throws IOException { + void testDefaultCsvFormatWithNullEscapeWorks() throws IOException { testInternal(CSVFormat.Builder.create().setEscape(null).get(), ",\"b \"\"\"\"\","); } @Test - public void testDefaultCsvFormatWithQuoteEscapeWorks() throws IOException { + void testDefaultCsvFormatWithQuoteEscapeWorks() throws IOException { // this one doesn't actually work but should behave like setEscape(null) // Printer is writing the expected content but Parser is unable to consume it testInternal(CSVFormat.Builder.create().setEscape('"').get(), ",\"b \"\"\"\"\","); } @Test - public void testDefaultCsvFormatWorks() throws IOException { + void testDefaultCsvFormatWorks() throws IOException { testInternal(CSVFormat.Builder.create().get(), ",\"b \"\"\"\"\","); } } diff --git a/src/test/java/org/apache/commons/csv/issues/JiraCsv93Test.java b/src/test/java/org/apache/commons/csv/issues/JiraCsv93Test.java index e34a8b02f6..7816412265 100644 --- a/src/test/java/org/apache/commons/csv/issues/JiraCsv93Test.java +++ b/src/test/java/org/apache/commons/csv/issues/JiraCsv93Test.java @@ -43,7 +43,7 @@ * Jira CSV-253 to a certain extent. *

    */ -public class JiraCsv93Test { +class JiraCsv93Test { private static Object[] objects1 = {"abc", "", null, "a,b,c", 123}; private static Object[] objects2 = {"abc", "NULL", null, "a,b,c", 123}; @@ -61,7 +61,7 @@ private void every(final CSVFormat csvFormat, final Object[] objects, final Stri } @Test - public void testWithNotSetNullString() throws IOException { + void testWithNotSetNullString() throws IOException { // @formatter:off every(CSVFormat.DEFAULT, objects1, @@ -91,7 +91,7 @@ public void testWithNotSetNullString() throws IOException { } @Test - public void testWithSetNullStringEmptyString() throws IOException { + void testWithSetNullStringEmptyString() throws IOException { // @formatter:off every(CSVFormat.DEFAULT.builder().setNullString("").get(), objects1, @@ -121,7 +121,7 @@ public void testWithSetNullStringEmptyString() throws IOException { } @Test - public void testWithSetNullStringNULL() throws IOException { + void testWithSetNullStringNULL() throws IOException { // @formatter:off every(CSVFormat.DEFAULT.builder().setNullString("NULL").get(), objects2, diff --git a/src/test/java/org/apache/commons/csv/perf/PerformanceTest.java b/src/test/java/org/apache/commons/csv/perf/PerformanceTest.java index efdb8f0418..bead12378d 100644 --- a/src/test/java/org/apache/commons/csv/perf/PerformanceTest.java +++ b/src/test/java/org/apache/commons/csv/perf/PerformanceTest.java @@ -43,7 +43,7 @@ * * To run this test, use: mvn test -Dtest=PerformanceTest */ -public class PerformanceTest { +class PerformanceTest { private static final String TEST_RESRC = "org/apache/commons/csv/perf/worldcitiespop.txt.gz"; @@ -110,7 +110,7 @@ public long testParseBigFile(final boolean traverseColumns) throws Exception { } @Test - public void testParseBigFileRepeat() throws Exception { + void testParseBigFileRepeat() throws Exception { long bestTime = Long.MAX_VALUE; for (int i = 0; i < this.max; i++) { bestTime = Math.min(testParseBigFile(false), bestTime); @@ -119,7 +119,7 @@ public void testParseBigFileRepeat() throws Exception { } @Test - public void testReadBigFile() throws Exception { + void testReadBigFile() throws Exception { long bestTime = Long.MAX_VALUE; long count; for (int i = 0; i < this.max; i++) { diff --git a/src/test/resources/org/apache/commons/csv/CSV-254/csv-254.csv b/src/test/resources/org/apache/commons/csv/CSV-254/csv-254.csv new file mode 100644 index 0000000000..e7d2972c5a --- /dev/null +++ b/src/test/resources/org/apache/commons/csv/CSV-254/csv-254.csv @@ -0,0 +1,3 @@ +AA,33, +AA,,"" +,33,CC