From 47008268748562e26513a42c271cf710d808c6ad Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 9 May 2022 10:05:01 -0400 Subject: [PATCH 1/4] Delete rcv.iml This file contains local configuration generated by Intellij; it is already included in .gitignore. --- rcv.iml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 rcv.iml diff --git a/rcv.iml b/rcv.iml deleted file mode 100644 index a5a744bb8..000000000 --- a/rcv.iml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file From 6fc4e5ce82874d88fb5668d66ba5db156528ffc1 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 2 May 2022 13:20:29 -0400 Subject: [PATCH 2/4] Update dependencies Remove Xlint:unchecked and Xlint:deprecation; it is unclear why these were present. Remove always-clean configuration; relying on clean for build correctness is an anti-pattern. --- .idea/misc.xml | 2 +- build.gradle | 27 ++- gradle/wrapper/gradle-wrapper.jar | Bin 58910 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 257 ++++++++++++++--------- gradlew.bat | 21 +- src/main/java/module-info.java | 2 +- 7 files changed, 172 insertions(+), 139 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index d1bacf819..67cba9600 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -6,7 +6,7 @@ - + diff --git a/build.gradle b/build.gradle index 4b301454c..8b29f746a 100644 --- a/build.gradle +++ b/build.gradle @@ -3,8 +3,8 @@ plugins { id "checkstyle" id "idea" id "java-library" - id "org.openjfx.javafxplugin" version "0.0.11" id "org.beryx.jlink" version "2.20.0" + id "org.openjfx.javafxplugin" version "0.0.11" } repositories { @@ -12,14 +12,14 @@ repositories { } dependencies { - implementation "org.apache.commons:commons-csv:1.8" - implementation "org.apache.poi:poi-ooxml:4.1.2" - implementation "com.fasterxml.jackson.core:jackson-core:2.11.1" - implementation "com.fasterxml.jackson.core:jackson-annotations:2.11.1" - implementation "com.fasterxml.jackson.core:jackson-databind:2.11.1" - implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.11.1" - testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.6.2" + implementation 'org.apache.commons:commons-csv:1.9.0' + implementation 'org.apache.poi:poi-ooxml:5.2.2' + implementation 'com.fasterxml.jackson.core:jackson-core:2.13.2' + implementation 'com.fasterxml.jackson.core:jackson-annotations:2.13.2' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.2.2' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.13.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' } // ### Application plugin settings @@ -52,11 +52,10 @@ idea { } // ### Java plugin settings -sourceCompatibility = JavaVersion.VERSION_17 - -compileJava.dependsOn(clean) -compileJava { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + } } test { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053550b91381bbd28b1afc82d634bf73a8a..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18328 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 Y(TzS~Xdo8@nTUZrK}a{fL*HNQe=4o%4FCWD delta 17568 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index a9f778a7a..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index c733df485..015f2c777 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,7 +10,7 @@ requires javafx.fxml; requires javafx.graphics; requires java.xml; - requires poi.ooxml; + requires org.apache.poi.ooxml; // enable reflexive calls from network.brightspots.rcv into javafx.fxml opens network.brightspots.rcv; // our main module From 1ed2679870190b43ea3c810f9a4cb9cd7a15b84f Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 2 May 2022 13:53:10 -0400 Subject: [PATCH 3/4] Run `check` in CI rather than build This also checks style. Update to checkstyle 10.2 while I'm here and clean up all existing style violations (mainly missing docs comments on public classes). --- .github/workflows/test.yml | 2 +- build.gradle | 6 +- config/checkstyle/google_checks.xml | 160 ++++++++++-------- .../brightspots/rcv/GuiApplication.java | 2 +- .../brightspots/rcv/GuiConfigController.java | 3 + .../rcv/GuiTiebreakerController.java | 3 + .../java/network/brightspots/rcv/Main.java | 3 + .../brightspots/rcv/RawContestConfig.java | 22 ++- 8 files changed, 118 insertions(+), 83 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 95d4e549a..b360421f2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,4 +16,4 @@ jobs: java-version: '17' distribution: 'temurin' - name: Test with Gradle - run: ./gradlew build + run: ./gradlew check diff --git a/build.gradle b/build.gradle index 8b29f746a..c54a605f5 100644 --- a/build.gradle +++ b/build.gradle @@ -35,11 +35,13 @@ application { // ### Checkstyle plugin settings checkstyle { - toolVersion = "8.36.2" + toolVersion = '10.2' // Keep the below file updated along with subsequent versions of Checkstyle (make sure to choose // the tag matching the toolVersion above): - // https://github.com/checkstyle/checkstyle/blob/checkstyle-8.36.2/src/main/resources/google_checks.xml + // https://github.com/checkstyle/checkstyle/blob/checkstyle-10.2/src/main/resources/google_checks.xml configFile = file("$projectDir/config/checkstyle/google_checks.xml") + maxWarnings = 0 + ignoreFailures = false } System.setProperty( "org.checkstyle.google.suppressionfilter.config", "$projectDir/config/checkstyle/checkstyle-suppressions.xml") diff --git a/config/checkstyle/google_checks.xml b/config/checkstyle/google_checks.xml index 651247499..b26ce4297 100644 --- a/config/checkstyle/google_checks.xml +++ b/config/checkstyle/google_checks.xml @@ -1,7 +1,7 @@ + "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" + "https://checkstyle.org/dtds/configuration_1_3.dtd"> - + + + + + + + + + + + + - + + + + + + + - + @@ -35,9 +51,9 @@ + value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/> + value="Consider using special escape sequence instead of octal value or Unicode escaped value."/> @@ -52,15 +68,15 @@ + value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/> + value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/> @@ -88,8 +104,9 @@ + value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE, LITERAL_RETURN, + LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, LITERAL_FINALLY, DO_WHILE, ELLIPSIS, + LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_CATCH, LAMBDA"/> @@ -99,7 +116,7 @@ + value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/> + value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/> @@ -120,7 +137,7 @@ @@ -136,13 +153,13 @@ - + - + @@ -155,78 +172,83 @@ + value="Package name ''{0}'' must match pattern ''{1}''."/> + value="Type name ''{0}'' must match pattern ''{1}''."/> + value="Member name ''{0}'' must match pattern ''{1}''."/> + value="Parameter name ''{0}'' must match pattern ''{1}''."/> + value="Lambda parameter name ''{0}'' must match pattern ''{1}''."/> + value="Catch parameter name ''{0}'' must match pattern ''{1}''."/> + value="Local variable name ''{0}'' must match pattern ''{1}''."/> - + + value="Class type name ''{0}'' must match pattern ''{1}''."/> - + + + value="Record component name ''{0}'' must match pattern ''{1}''."/> + + + + value="Method type name ''{0}'' must match pattern ''{1}''."/> + value="Interface type name ''{0}'' must match pattern ''{1}''."/> + value="GenericWhitespace ''{0}'' is followed by whitespace."/> + value="GenericWhitespace ''{0}'' is preceded with whitespace."/> + value="GenericWhitespace ''{0}'' should followed by whitespace."/> + value="GenericWhitespace ''{0}'' is not preceded with whitespace."/> - + @@ -236,10 +258,11 @@ + @@ -250,18 +273,18 @@ + value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR, + LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF, + TYPE_EXTENSION_AND "/> @@ -289,17 +313,17 @@ + value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/> + value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/> - + @@ -312,14 +336,19 @@ + + + + + - - - + value="Method name ''{0}'' must match pattern ''{1}''."/> + @@ -328,26 +357,9 @@ - + - - - - - - - - - - - - - - - - - diff --git a/src/main/java/network/brightspots/rcv/GuiApplication.java b/src/main/java/network/brightspots/rcv/GuiApplication.java index 6d923ea4c..2d78e4edb 100644 --- a/src/main/java/network/brightspots/rcv/GuiApplication.java +++ b/src/main/java/network/brightspots/rcv/GuiApplication.java @@ -27,7 +27,7 @@ import javafx.stage.Stage; @SuppressWarnings("WeakerAccess") -public class GuiApplication extends Application { +class GuiApplication extends Application { private static final int STAGE_HEIGHT = 1000; private static final int STAGE_WIDTH = 1200; diff --git a/src/main/java/network/brightspots/rcv/GuiConfigController.java b/src/main/java/network/brightspots/rcv/GuiConfigController.java index 0ee955bea..2185617dc 100644 --- a/src/main/java/network/brightspots/rcv/GuiConfigController.java +++ b/src/main/java/network/brightspots/rcv/GuiConfigController.java @@ -72,6 +72,9 @@ import network.brightspots.rcv.Tabulator.TiebreakMode; import network.brightspots.rcv.Tabulator.WinnerElectionMode; +/** + * View controller for config layout. + */ @SuppressWarnings({"WeakerAccess"}) public class GuiConfigController implements Initializable { diff --git a/src/main/java/network/brightspots/rcv/GuiTiebreakerController.java b/src/main/java/network/brightspots/rcv/GuiTiebreakerController.java index b5c4b10b7..42affe55c 100644 --- a/src/main/java/network/brightspots/rcv/GuiTiebreakerController.java +++ b/src/main/java/network/brightspots/rcv/GuiTiebreakerController.java @@ -26,6 +26,9 @@ import javafx.scene.control.ListView; import javafx.stage.Stage; +/** + * View controller for tiebreaker layout. + */ @SuppressWarnings("WeakerAccess") public class GuiTiebreakerController { diff --git a/src/main/java/network/brightspots/rcv/Main.java b/src/main/java/network/brightspots/rcv/Main.java index 94691a717..7291fcf77 100644 --- a/src/main/java/network/brightspots/rcv/Main.java +++ b/src/main/java/network/brightspots/rcv/Main.java @@ -25,6 +25,9 @@ import java.util.ArrayList; import java.util.List; +/** + * Main entry point to the RCV tabulator program. + */ @SuppressWarnings("WeakerAccess") public class Main extends GuiApplication { diff --git a/src/main/java/network/brightspots/rcv/RawContestConfig.java b/src/main/java/network/brightspots/rcv/RawContestConfig.java index 57003fb23..ceddffd6f 100644 --- a/src/main/java/network/brightspots/rcv/RawContestConfig.java +++ b/src/main/java/network/brightspots/rcv/RawContestConfig.java @@ -27,6 +27,9 @@ import com.fasterxml.jackson.annotation.JsonInclude; import java.util.List; +/** + * Contest configuration that can be serialized and deserialized. + */ @SuppressWarnings("WeakerAccess") @JsonIgnoreProperties(ignoreUnknown = true) public class RawContestConfig { @@ -40,7 +43,9 @@ public class RawContestConfig { RawContestConfig() { } - // OutputSettings: encapsulates the output settings + /** + * Output settings that can be serialized and deserialized. + */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public static class OutputSettings { @@ -54,8 +59,11 @@ public static class OutputSettings { public boolean generateCdfJson; } - // CvrSource: encapsulates a source cast vote record file - // all indexes are 1-based + /** + * Source cast vote record file that can be serialized and deserialized. + * + *

All indexes are 1-based.

+ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public static class CvrSource { @@ -200,7 +208,9 @@ public void setTreatBlankAsUndeclaredWriteIn(boolean treatBlankAsUndeclaredWrite } } - // Candidate encapsulates data for each candidate in a contest + /** + * Contest candidate data that can be serialized and deserialized. + */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public static class Candidate { @@ -243,7 +253,9 @@ public void setExcluded(boolean excluded) { } } - // ContestRules: encapsulates the set of rules required to perform contest tabulation + /** + * Contest rules necessary for tabulation that can be serialized and deserialized. + */ @SuppressWarnings({"unused", "RedundantSuppression"}) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) From 7e33c79cd97573aa633af92a907eaed0566dec26 Mon Sep 17 00:00:00 2001 From: Tamir Duberstein Date: Mon, 2 May 2022 16:49:26 -0400 Subject: [PATCH 4/4] Add spotbugs and correct found problems > Task :spotbugsTest M D NP: Possible null pointer dereference in network.brightspots.rcv.TabulatorTests.runTabulationTest(String) due to return value of called method Dereferenced at TabulatorTests.java:[line 152] M B OS: network.brightspots.rcv.TabulatorTests.fileCompare(String, String) may fail to close stream At TabulatorTests.java:[line 58] H I Dm: Found reliance on default encoding in network.brightspots.rcv.TabulatorTests.fileCompare(String, String): new java.io.FileReader(String) At TabulatorTests.java:[line 57] > Task :spotbugsTest FAILED > Task :spotbugsMain M X OBL: network.brightspots.rcv.JsonParser.readFromFile(String, Class, boolean) may fail to clean up java.io.Reader Obligation to clean up resource created at JsonParser.java:[line 42] is not discharged H I Dm: Found reliance on default encoding in network.brightspots.rcv.JsonParser.readFromFile(String, Class, boolean): new java.io.FileReader(String) At JsonParser.java:[line 42] M P WMI: network.brightspots.rcv.ResultsWriter.getCandidatesWithRanksList(Map) makes inefficient use of keySet iterator instead of entrySet iterator At ResultsWriter.java:[line 168] M P WMI: network.brightspots.rcv.ResultsWriter.setCandidatesToRoundEliminated(Map) makes inefficient use of keySet iterator instead of entrySet iterator At ResultsWriter.java:[line 245] M P WMI: network.brightspots.rcv.ResultsWriter.setWinnerToRound(Map) makes inefficient use of keySet iterator instead of entrySet iterator At ResultsWriter.java:[line 256] M P WMI: network.brightspots.rcv.ResultsWriter.generatePrecinctSummaryFiles(Map, Map, Map) makes inefficient use of keySet iterator instead of entrySet iterator At ResultsWriter.java:[line 289] M P WMI: network.brightspots.rcv.ResultsWriter.updateCandidateNamesInTally(Map) makes inefficient use of keySet iterator instead of entrySet iterator At ResultsWriter.java:[line 880] M P WMI: network.brightspots.rcv.ResultsWriter.addActionObjects(String, List, int, ArrayList, TallyTransfers) makes inefficient use of keySet iterator instead of entrySet iterator At ResultsWriter.java:[line 921] M P WMI: network.brightspots.rcv.ClearBallotCvrReader.readCastVoteRecords(List, String) makes inefficient use of keySet iterator instead of entrySet iterator At ClearBallotCvrReader.java:[line 109] H I Dm: Found reliance on default encoding in network.brightspots.rcv.ClearBallotCvrReader.readCastVoteRecords(List, String): new java.io.FileReader(String) At ClearBallotCvrReader.java:[line 51] M D REC: Exception is caught when Exception is not thrown in network.brightspots.rcv.HartCvrReader.readCastVoteRecord(List, Path) At HartCvrReader.java:[line 128] M X OBL: network.brightspots.rcv.HartCvrReader.readCastVoteRecord(List, Path) may fail to clean up java.io.InputStream Obligation to clean up resource created at HartCvrReader.java:[line 80] is not discharged M X OBL: network.brightspots.rcv.CommonDataFormatReader.parseXml(List) may fail to clean up java.io.InputStream Obligation to clean up resource created at CommonDataFormatReader.java:[line 132] is not discharged M P WMI: network.brightspots.rcv.Tabulator.updatePastWinnerTallies() makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 319] M P WMI: network.brightspots.rcv.Tabulator.updatePastWinnerTallies() makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 273] M P WMI: network.brightspots.rcv.Tabulator.updatePastWinnerTallies() makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 297] M P WMI: network.brightspots.rcv.Tabulator.identifyWinners(Map, SortedMap) makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 481] M P WMI: network.brightspots.rcv.Tabulator.dropCandidatesBelowThreshold(SortedMap) makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 567] M P WMI: network.brightspots.rcv.Tabulator.runBatchElimination(SortedMap) makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 740] M P WMI: network.brightspots.rcv.Tabulator.computeTalliesForRound(int) makes inefficient use of keySet iterator instead of entrySet iterator At Tabulator.java:[line 1006] M C RCN: Nullcheck of config at line 143 of value previously dereferenced in network.brightspots.rcv.TabulatorSession.tabulate() At TabulatorSession.java:[line 143] H I Dm: Found reliance on default encoding in network.brightspots.rcv.TabulatorSession.tabulate(): new java.io.FileReader(String) At TabulatorSession.java:[line 152] M B OS: network.brightspots.rcv.GuiConfigController.loadTxtFileIntoString(String) may fail to close stream At GuiConfigController.java:[line 272] H I Dm: Found reliance on default encoding in network.brightspots.rcv.GuiConfigController.loadTxtFileIntoString(String): new java.io.InputStreamReader(InputStream) At GuiConfigController.java:[line 275] H I Dm: Found reliance on default encoding in network.brightspots.rcv.Tiebreak.doInteractiveCli(List): new java.util.Scanner(InputStream) At Tiebreak.java:[line 169] > Task :spotbugsMain FAILED --- build.gradle | 8 +- config/spotbugs/exclude.xml | 19 ++ .../brightspots/rcv/ClearBallotCvrReader.java | 10 +- .../rcv/CommonDataFormatReader.java | 273 +++++++++--------- .../brightspots/rcv/GuiConfigController.java | 15 +- .../brightspots/rcv/HartCvrReader.java | 11 +- .../network/brightspots/rcv/JsonParser.java | 20 +- .../brightspots/rcv/ResultsWriter.java | 73 ++--- .../network/brightspots/rcv/Tabulator.java | 63 ++-- .../brightspots/rcv/TabulatorSession.java | 11 +- .../network/brightspots/rcv/Tiebreak.java | 3 +- .../brightspots/rcv/TabulatorTests.java | 29 +- 12 files changed, 269 insertions(+), 266 deletions(-) create mode 100644 config/spotbugs/exclude.xml diff --git a/build.gradle b/build.gradle index c54a605f5..3bcf49ee5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ plugins { id "application" id "checkstyle" + id "com.github.spotbugs" version "5.0.6" id "idea" id "java-library" id "org.beryx.jlink" version "2.20.0" @@ -43,9 +44,14 @@ checkstyle { maxWarnings = 0 ignoreFailures = false } -System.setProperty( "org.checkstyle.google.suppressionfilter.config", +System.setProperty("org.checkstyle.google.suppressionfilter.config", "$projectDir/config/checkstyle/checkstyle-suppressions.xml") +spotbugs { + toolVersion = '4.6.0' + excludeFilter = file("config/spotbugs/exclude.xml") +} + // ### Idea plugin settings idea { module { diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml new file mode 100644 index 000000000..7a6a92166 --- /dev/null +++ b/config/spotbugs/exclude.xml @@ -0,0 +1,19 @@ + + + xsi:schemaLocation="https://raw.githubusercontent.com/spotbugs/spotbugs/4.6.0/spotbugs/etc/findbugsfilter.xsd"> + + + + + + + + + + + + + + diff --git a/src/main/java/network/brightspots/rcv/ClearBallotCvrReader.java b/src/main/java/network/brightspots/rcv/ClearBallotCvrReader.java index 31caeed03..ca47ae095 100644 --- a/src/main/java/network/brightspots/rcv/ClearBallotCvrReader.java +++ b/src/main/java/network/brightspots/rcv/ClearBallotCvrReader.java @@ -20,6 +20,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -48,7 +49,7 @@ void readCastVoteRecords(List castVoteRecords, String contestId) // map for tracking unrecognized candidates during parsing Map unrecognizedCandidateCounts = new HashMap<>(); try { - csvReader = new BufferedReader(new FileReader(this.cvrPath)); + csvReader = new BufferedReader(new FileReader(this.cvrPath, StandardCharsets.UTF_8)); // each "choice column" in the input Csv corresponds to a unique ranking: candidate+rank pair // we parse these rankings from the header row into a map for lookup during CVR parsing String firstRow = csvReader.readLine(); @@ -103,11 +104,10 @@ void readCastVoteRecords(List castVoteRecords, String contestId) // parse rankings String[] cvrData = row.split(","); ArrayList> rankings = new ArrayList<>(); - for (int columnIndex : columnIndexToRanking.keySet()) { - if (Integer.parseInt(cvrData[columnIndex]) == 1) { + for (var entry : columnIndexToRanking.entrySet()) { + if (Integer.parseInt(cvrData[entry.getKey()]) == 1) { // user marked this column - Pair ranking = columnIndexToRanking.get(columnIndex); - rankings.add(ranking); + rankings.add(entry.getValue()); } } // create the cast vote record diff --git a/src/main/java/network/brightspots/rcv/CommonDataFormatReader.java b/src/main/java/network/brightspots/rcv/CommonDataFormatReader.java index a638b6827..eac1e9472 100644 --- a/src/main/java/network/brightspots/rcv/CommonDataFormatReader.java +++ b/src/main/java/network/brightspots/rcv/CommonDataFormatReader.java @@ -129,165 +129,168 @@ void parseXml(List castVoteRecords) // load XML XmlMapper xmlMapper = new XmlMapper(); xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - FileInputStream inputStream = new FileInputStream(new File(filePath)); - CastVoteRecordReport cvrReport = xmlMapper.readValue(inputStream, CastVoteRecordReport.class); - - // Parse static election data: - - // Find the Contest we are tabulating: - // Note: Contest is different from CVRContest objects which appear in CVRSnapshots - Contest contestToTabulate = null; - for (Election election : cvrReport.Election) { - for (Contest contest : election.Contest) { - if (contest.Name.equals(this.contestId)) { - contestToTabulate = contest; - break; + try (FileInputStream inputStream = new FileInputStream(filePath)) { + CastVoteRecordReport cvrReport = xmlMapper.readValue(inputStream, CastVoteRecordReport.class); + inputStream.close(); + + // Parse static election data: + + // Find the Contest we are tabulating: + // Note: Contest is different from CVRContest objects which appear in CVRSnapshots + Contest contestToTabulate = null; + for (Election election : cvrReport.Election) { + for (Contest contest : election.Contest) { + if (contest.Name.equals(this.contestId)) { + contestToTabulate = contest; + break; + } } } - } - - if (contestToTabulate == null) { - Logger.severe("Contest \"%s\" from config file not found!", this.contestId); - throw new CvrParseException(); - } - // build a map of Candidates - HashMap candidateById = new HashMap<>(); - for (Election election : cvrReport.Election) { - for (Candidate candidate : election.Candidate) { - candidateById.put(candidate.ObjectId, candidate); + if (contestToTabulate == null) { + Logger.severe("Contest \"%s\" from config file not found!", this.contestId); + throw new CvrParseException(); } - } - // ContestSelections - HashMap contestSelectionById = new HashMap<>(); - for (ContestSelection contestSelection : contestToTabulate.ContestSelection) { - contestSelectionById.put(contestSelection.ObjectId, contestSelection); - } + // build a map of Candidates + HashMap candidateById = new HashMap<>(); + for (Election election : cvrReport.Election) { + for (Candidate candidate : election.Candidate) { + candidateById.put(candidate.ObjectId, candidate); + } + } - // build a map of GpUnits (aka precinct or district) - HashMap gpUnitById = new HashMap<>(); - for (GpUnit gpUnit : cvrReport.GpUnit) { - gpUnitById.put(gpUnit.ObjectId, gpUnit); - } + // ContestSelections + HashMap contestSelectionById = new HashMap<>(); + for (ContestSelection contestSelection : contestToTabulate.ContestSelection) { + contestSelectionById.put(contestSelection.ObjectId, contestSelection); + } - // process the Cvrs - int cvrIndex = 0; - String fileName = new File(filePath).getName(); - for (CVR cvr : cvrReport.CVR) { - CVRContest contest = getCvrContestXml(cvr, contestToTabulate); - if (contest == null) { - // the CVR does not contain any votes for this contest - continue; + // build a map of GpUnits (aka precinct or district) + HashMap gpUnitById = new HashMap<>(); + for (GpUnit gpUnit : cvrReport.GpUnit) { + gpUnitById.put(gpUnit.ObjectId, gpUnit); } - List> rankings = new ArrayList<>(); - // parse CVRContestSelections into rankings - // they will be null for an undervote - if (contest.CVRContestSelection != null) { - for (CVRContestSelection cvrContestSelection : contest.CVRContestSelection) { - if (cvrContestSelection.Status != null - && cvrContestSelection.Status.equals("needs-adjudication")) { - Logger.info("Contest Selection needs adjudication. Skipping."); - continue; - } - String contestSelectionId = cvrContestSelection.ContestSelectionId; - ContestSelection contestSelection = contestSelectionById.get(contestSelectionId); - if (contestSelection == null) { - Logger.severe("ContestSelection \"%s\" from CVR not found!", contestSelectionId); - throw new CvrParseException(); - } - String candidateId; - // check for declared write-in: - if (contestSelection.IsWriteIn != null - && contestSelection.IsWriteIn.equals(BOOLEAN_TRUE)) { - candidateId = Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL; - } else { - // validate candidate Ids: - // CDF allows multiple candidate Ids to support party ticket voting options - // but in practice this is always a single candidate id - if (contestSelection.CandidateIds == null - || contestSelection.CandidateIds.length == 0) { - Logger.severe( - "CandidateSelection \"%s\" has no CandidateIds!", contestSelection.ObjectId); - throw new CvrParseException(); - } - if (contestSelection.CandidateIds.length > 1) { - Logger.warning( - "CandidateSelection \"%s\" has multiple CandidateIds. " - + "Only the first one will be processed.", - contestSelection.ObjectId); - } - Candidate candidate = candidateById.get(contestSelection.CandidateIds[0]); - if (candidate == null) { - Logger.severe( - "CandidateId \"%s\" from ContestSelectionId \"%s\" not found!", - contestSelection.CandidateIds[0], contestSelection.ObjectId); - throw new CvrParseException(); + // process the Cvrs + int cvrIndex = 0; + String fileName = new File(filePath).getName(); + for (CVR cvr : cvrReport.CVR) { + CVRContest contest = getCvrContestXml(cvr, contestToTabulate); + if (contest == null) { + // the CVR does not contain any votes for this contest + continue; + } + List> rankings = new ArrayList<>(); + // parse CVRContestSelections into rankings + // they will be null for an undervote + if (contest.CVRContestSelection != null) { + for (CVRContestSelection cvrContestSelection : contest.CVRContestSelection) { + if (cvrContestSelection.Status != null + && cvrContestSelection.Status.equals("needs-adjudication")) { + Logger.info("Contest Selection needs adjudication. Skipping."); + continue; } - candidateId = candidate.Name; - if (candidateId.equals(overvoteLabel)) { - candidateId = Tabulator.EXPLICIT_OVERVOTE_LABEL; - } else if (!config.getCandidateCodeList().contains(candidateId)) { - Logger.severe("Unrecognized candidate found in CVR: %s", candidateId); - unrecognizedCandidateCounts.merge(candidateId, 1, Integer::sum); + String contestSelectionId = cvrContestSelection.ContestSelectionId; + ContestSelection contestSelection = contestSelectionById.get(contestSelectionId); + if (contestSelection == null) { + Logger.severe("ContestSelection \"%s\" from CVR not found!", contestSelectionId); + throw new CvrParseException(); } - } - - if (cvrContestSelection.Rank == null) { - for (SelectionPosition selectionPosition : cvrContestSelection.SelectionPosition) { - if (selectionPosition.CVRWriteIn != null) { - candidateId = Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL; - } - // ignore if no indication is present (NIST 1500-103 section 3.4.2) - if (selectionPosition.HasIndication != null - && selectionPosition.HasIndication.equals(STATUS_NO)) { - continue; + String candidateId; + // check for declared write-in: + if (contestSelection.IsWriteIn != null + && contestSelection.IsWriteIn.equals(BOOLEAN_TRUE)) { + candidateId = Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL; + } else { + // validate candidate Ids: + // CDF allows multiple candidate Ids to support party ticket voting options + // but in practice this is always a single candidate id + if (contestSelection.CandidateIds == null + || contestSelection.CandidateIds.length == 0) { + Logger.severe( + "CandidateSelection \"%s\" has no CandidateIds!", contestSelection.ObjectId); + throw new CvrParseException(); } - // skip if not allocable - if (selectionPosition.IsAllocable.equals(STATUS_NO)) { - continue; + if (contestSelection.CandidateIds.length > 1) { + Logger.warning( + "CandidateSelection \"%s\" has multiple CandidateIds. " + + "Only the first one will be processed.", + contestSelection.ObjectId); } - if (selectionPosition.Rank == null) { + + Candidate candidate = candidateById.get(contestSelection.CandidateIds[0]); + if (candidate == null) { Logger.severe( - "No Rank found on CVR \"%s\" Contest \"%s\"!", cvr.UniqueId, contest.ContestId); + "CandidateId \"%s\" from ContestSelectionId \"%s\" not found!", + contestSelection.CandidateIds[0], contestSelection.ObjectId); throw new CvrParseException(); } - Integer rank = Integer.parseInt(selectionPosition.Rank); + candidateId = candidate.Name; + if (candidateId.equals(overvoteLabel)) { + candidateId = Tabulator.EXPLICIT_OVERVOTE_LABEL; + } else if (!config.getCandidateCodeList().contains(candidateId)) { + Logger.severe("Unrecognized candidate found in CVR: %s", candidateId); + unrecognizedCandidateCounts.merge(candidateId, 1, Integer::sum); + } + } + + if (cvrContestSelection.Rank == null) { + for (SelectionPosition selectionPosition : cvrContestSelection.SelectionPosition) { + if (selectionPosition.CVRWriteIn != null) { + candidateId = Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL; + } + // ignore if no indication is present (NIST 1500-103 section 3.4.2) + if (selectionPosition.HasIndication != null + && selectionPosition.HasIndication.equals(STATUS_NO)) { + continue; + } + // skip if not allocable + if (selectionPosition.IsAllocable.equals(STATUS_NO)) { + continue; + } + if (selectionPosition.Rank == null) { + Logger.severe( + "No Rank found on CVR \"%s\" Contest \"%s\"!", cvr.UniqueId, + contest.ContestId); + throw new CvrParseException(); + } + Integer rank = Integer.parseInt(selectionPosition.Rank); + rankings.add(new Pair<>(rank, candidateId)); + } + } else { + Integer rank = Integer.parseInt(cvrContestSelection.Rank); rankings.add(new Pair<>(rank, candidateId)); } - } else { - Integer rank = Integer.parseInt(cvrContestSelection.Rank); - rankings.add(new Pair<>(rank, candidateId)); } } - } - // Extract GPUnit if provided - String precinctId = null; - if (cvr.BallotStyleUnitId != null) { - GpUnit unit = gpUnitById.get(cvr.BallotStyleUnitId); - if (unit == null) { - Logger.severe( - "GpUnit \"%s\" for CVR \"%s\" not found!", cvr.BallotStyleUnitId, cvr.UniqueId); - throw new CvrParseException(); + // Extract GPUnit if provided + String precinctId = null; + if (cvr.BallotStyleUnitId != null) { + GpUnit unit = gpUnitById.get(cvr.BallotStyleUnitId); + if (unit == null) { + Logger.severe( + "GpUnit \"%s\" for CVR \"%s\" not found!", cvr.BallotStyleUnitId, cvr.UniqueId); + throw new CvrParseException(); + } + precinctId = unit.Name; } - precinctId = unit.Name; - } - String computedCastVoteRecordId = String.format("%s(%d)", fileName, ++cvrIndex); - // create the new CastVoteRecord - CastVoteRecord newRecord = - new CastVoteRecord(computedCastVoteRecordId, cvr.UniqueId, precinctId, null, rankings); - castVoteRecords.add(newRecord); + String computedCastVoteRecordId = String.format("%s(%d)", fileName, ++cvrIndex); + // create the new CastVoteRecord + CastVoteRecord newRecord = + new CastVoteRecord(computedCastVoteRecordId, cvr.UniqueId, precinctId, null, rankings); + castVoteRecords.add(newRecord); - // provide some user feedback on the CVR count - if (castVoteRecords.size() % 50000 == 0) { - Logger.info("Parsed %d cast vote records.", castVoteRecords.size()); + // provide some user feedback on the CVR count + if (castVoteRecords.size() % 50000 == 0) { + Logger.info("Parsed %d cast vote records.", castVoteRecords.size()); + } + } + if (unrecognizedCandidateCounts.size() > 0) { + throw new UnrecognizedCandidatesException(unrecognizedCandidateCounts); } - } - if (unrecognizedCandidateCounts.size() > 0) { - throw new UnrecognizedCandidatesException(unrecognizedCandidateCounts); } } @@ -461,7 +464,7 @@ void parseJson(List castVoteRecords) // The following classes are based on the NIST 1500-103 UML structure. // Many of the elements represented here will not be present on any particular implementation of // a Cdf Cvr report. Many of these elements are also irrelevant for tabulation purposes. - // However they are included here for completeness and to aid in interpreting the UML. + // However, they are included here for completeness and to aid in interpreting the UML. // Note that fields identified as "boolean-like" can be (yes, no, 1, 0, or null) static class ContestSelection { diff --git a/src/main/java/network/brightspots/rcv/GuiConfigController.java b/src/main/java/network/brightspots/rcv/GuiConfigController.java index 2185617dc..f6e45ee0a 100644 --- a/src/main/java/network/brightspots/rcv/GuiConfigController.java +++ b/src/main/java/network/brightspots/rcv/GuiConfigController.java @@ -25,6 +25,7 @@ import java.io.File; import java.io.InputStreamReader; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -271,14 +272,10 @@ private static String getTextOrEmptyString(TextField textField) { private static String loadTxtFileIntoString(String configFileDocumentationFilename) { String text; - try { - text = - new BufferedReader( - new InputStreamReader( - Objects.requireNonNull( - ClassLoader.getSystemResourceAsStream(configFileDocumentationFilename)))) - .lines() - .collect(Collectors.joining("\n")); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(Objects.requireNonNull( + ClassLoader.getSystemResourceAsStream(configFileDocumentationFilename)), + StandardCharsets.UTF_8))) { + text = reader.lines().collect(Collectors.joining("\n")); } catch (Exception exception) { Logger.severe( "Error loading text file: %s\n%s", @@ -1000,7 +997,7 @@ public LocalDate fromString(String string) { textFieldNumberOfWinners.setDisable(false); } case MULTI_SEAT_BOTTOMS_UP_UNTIL_N_WINNERS, MULTI_SEAT_SEQUENTIAL_WINNER_TAKES_ALL -> - textFieldNumberOfWinners.setDisable(false); + textFieldNumberOfWinners.setDisable(false); case MULTI_SEAT_BOTTOMS_UP_USING_PERCENTAGE_THRESHOLD -> { textFieldNumberOfWinners.setText("0"); textFieldMultiSeatBottomsUpPercentageThreshold.setDisable(false); diff --git a/src/main/java/network/brightspots/rcv/HartCvrReader.java b/src/main/java/network/brightspots/rcv/HartCvrReader.java index 066914168..4fda9cab2 100644 --- a/src/main/java/network/brightspots/rcv/HartCvrReader.java +++ b/src/main/java/network/brightspots/rcv/HartCvrReader.java @@ -72,12 +72,11 @@ void readCastVoteRecordsFromFolder(List castVoteRecords) // parse Cvr xml file into CastVoteRecord objects and add them to the input List void readCastVoteRecord(List castVoteRecords, Path path) throws IOException { - try { - Logger.info("Reading Hart cast vote record file: %s...", path.getFileName()); + Logger.info("Reading Hart cast vote record file: %s...", path.getFileName()); - XmlMapper xmlMapper = new XmlMapper(); - xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - FileInputStream inputStream = new FileInputStream(path.toFile()); + XmlMapper xmlMapper = new XmlMapper(); + xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + try (FileInputStream inputStream = new FileInputStream(path.toFile())) { HartCvrXml xmlCvr = xmlMapper.readValue(inputStream, HartCvrXml.class); for (Contest contest : xmlCvr.Contests) { @@ -125,7 +124,7 @@ void readCastVoteRecord(List castVoteRecords, Path path) throws Logger.info("Parsed %d cast vote records.", castVoteRecords.size()); } } - } catch (Exception exception) { + } catch (IOException exception) { Logger.severe("Error parsing cast vote record:\n%s", exception); throw exception; } diff --git a/src/main/java/network/brightspots/rcv/JsonParser.java b/src/main/java/network/brightspots/rcv/JsonParser.java index 952f9ca22..92253b655 100644 --- a/src/main/java/network/brightspots/rcv/JsonParser.java +++ b/src/main/java/network/brightspots/rcv/JsonParser.java @@ -27,6 +27,7 @@ import java.io.File; import java.io.FileReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; @SuppressWarnings("SameParameterValue") class JsonParser { @@ -38,25 +39,28 @@ class JsonParser { // returns: instance of the object parsed from json or null if there was a problem private static T readFromFile(String jsonFilePath, Class valueType, boolean logsEnabled) { T createdObject; - try { - FileReader fileReader = new FileReader(jsonFilePath); + try (FileReader fileReader = new FileReader(jsonFilePath, StandardCharsets.UTF_8)) { ObjectMapper objectMapper = new ObjectMapper(); createdObject = objectMapper.readValue(fileReader, valueType); } catch (JsonParseException | JsonMappingException exception) { if (logsEnabled) { Logger.severe( - "Error parsing JSON file: %s\n%s\n" - + "Check file formatting and values and make sure they are correct!\n" - + "It might help to try surrounding values causing problems with quotes (e.g. " - + " \"value\").\nSee config_file_documentation.txt for more details.", + """ + Error parsing JSON file: %s + %s + Check file formatting and values and make sure they are correct! + It might help to try surrounding values causing problems with quotes (e.g. "value"). + See config_file_documentation.txt for more details.""", jsonFilePath, exception); } createdObject = null; } catch (IOException exception) { if (logsEnabled) { Logger.severe( - "Error opening file: %s\n%s\n" - + "Check file path and permissions and make sure they are correct!", + """ + Error opening file: %s + %s + Check file path and permissions and make sure they are correct!""", jsonFilePath, exception); } createdObject = null; diff --git a/src/main/java/network/brightspots/rcv/ResultsWriter.java b/src/main/java/network/brightspots/rcv/ResultsWriter.java index 1dc123423..05d3a121f 100644 --- a/src/main/java/network/brightspots/rcv/ResultsWriter.java +++ b/src/main/java/network/brightspots/rcv/ResultsWriter.java @@ -18,7 +18,7 @@ * Purpose: * Helper class takes tabulation results data as input and generates summary files which * contains results summary information. - * Currently we support a CSV summary file and a JSON summary file. + * Currently, we support a CSV summary file and a JSON summary file. */ package network.brightspots.rcv; @@ -137,37 +137,29 @@ private static String generateCvrSnapshotId(String cvrId, Integer round) { // generates an internal ContestSelectionId based on a candidate code private static String getCdfContestSelectionIdForCandidateCode(String code) { - String id = cdfCandidateCodeToContestSelectionId.get(code); - if (id == null) { - id = String.format("cs-%s", sanitizeStringForOutput(code).toLowerCase()); - cdfCandidateCodeToContestSelectionId.put(code, id); - } - return id; + return cdfCandidateCodeToContestSelectionId.computeIfAbsent(code, + c -> String.format("cs-%s", sanitizeStringForOutput(c).toLowerCase())); } // generates an internal CandidateId based on a candidate code private static String getCdfCandidateIdForCandidateCode(String code) { - String id = cdfCandidateCodeToCandidateId.get(code); - if (id == null) { - id = String.format("c-%s", sanitizeStringForOutput(code).toLowerCase()); - cdfCandidateCodeToCandidateId.put(code, id); - } - return id; + return cdfCandidateCodeToCandidateId.computeIfAbsent(code, + c -> String.format("c-%s", sanitizeStringForOutput(c).toLowerCase())); } // Instead of a map from rank to list of candidates, we need a sorted list of candidates // with the ranks they were given. (Ordinarily a candidate will have only a single rank, but they // could have multiple ranks if the ballot duplicates the candidate, i.e. assigns them multiple - // ranks. + // ranks.) // We sort by the lowest (best) rank, then alphabetically by name. private static List>> getCandidatesWithRanksList( Map> rankToCandidateIds) { Map> candidateIdToRanks = new HashMap<>(); // first group the ranks by candidate - for (int rank : rankToCandidateIds.keySet()) { - for (String candidateId : rankToCandidateIds.get(rank)) { + for (var entry : rankToCandidateIds.entrySet()) { + for (String candidateId : entry.getValue()) { candidateIdToRanks.computeIfAbsent(candidateId, k -> new LinkedList<>()); - candidateIdToRanks.get(candidateId).add(rank); + candidateIdToRanks.get(candidateId).add(entry.getKey()); } } // we want the ranks for a given candidate in ascending order @@ -238,13 +230,12 @@ ResultsWriter setPrecinctIds(Set precinctIds) { } ResultsWriter setCandidatesToRoundEliminated(Map candidatesToRoundEliminated) { - // roundToEliminatedCandidates is the inverse of candidatesToRoundEliminated map + // roundToEliminatedCandidates is the inverse of candidatesToRoundEliminated map, // so we can look up who got eliminated for each round roundToEliminatedCandidates = new HashMap<>(); - for (String candidate : candidatesToRoundEliminated.keySet()) { - int round = candidatesToRoundEliminated.get(candidate); - roundToEliminatedCandidates.computeIfAbsent(round, k -> new LinkedList<>()); - roundToEliminatedCandidates.get(round).add(candidate); + for (var entry : candidatesToRoundEliminated.entrySet()) { + roundToEliminatedCandidates.computeIfAbsent(entry.getValue(), k -> new LinkedList<>()); + roundToEliminatedCandidates.get(entry.getValue()).add(entry.getKey()); } return this; } @@ -252,10 +243,9 @@ ResultsWriter setCandidatesToRoundEliminated(Map candidatesToRo ResultsWriter setWinnerToRound(Map winnerToRound) { // very similar to the logic in setCandidatesToRoundEliminated above roundToWinningCandidates = new HashMap<>(); - for (String candidate : winnerToRound.keySet()) { - int round = winnerToRound.get(candidate); - roundToWinningCandidates.computeIfAbsent(round, k -> new LinkedList<>()); - roundToWinningCandidates.get(round).add(candidate); + for (var entry : winnerToRound.entrySet()) { + roundToWinningCandidates.computeIfAbsent(entry.getValue(), k -> new LinkedList<>()); + roundToWinningCandidates.get(entry.getValue()).add(entry.getKey()); } return this; } @@ -280,18 +270,15 @@ void generatePrecinctSummaryFiles( Map numBallotsByPrecinct) throws IOException { Set filenames = new HashSet<>(); - for (String precinct : precinctRoundTallies.keySet()) { + for (var entry : precinctRoundTallies.entrySet()) { + String precinct = entry.getKey(); + Map> roundTallies = entry.getValue(); String precinctFileString = getPrecinctFileString(precinct, filenames); String outputPath = getOutputFilePathFromInstance(String.format("%s_precinct_summary", precinctFileString)); int numBallots = numBallotsByPrecinct.get(precinct); - generateSummarySpreadsheet( - precinctRoundTallies.get(precinct), numBallots, precinct, outputPath); - generateSummaryJson( - precinctRoundTallies.get(precinct), - precinctTallyTransfers.get(precinct), - precinct, - outputPath); + generateSummarySpreadsheet(roundTallies, numBallots, precinct, outputPath); + generateSummaryJson(roundTallies, precinctTallyTransfers.get(precinct), precinct, outputPath); } } @@ -463,7 +450,7 @@ private void addHeaderRows(CSVPrinter csvPrinter, String precinct) throws IOExce csvPrinter.println(); } - // return a list of all input candidates sorted from highest tally to lowest + // return a list of all input candidates sorted from the highest tally to lowest private List sortCandidatesByTally(Map tally) { List> entries = new ArrayList<>(tally.entrySet()); entries.sort( @@ -479,7 +466,7 @@ private List sortCandidatesByTally(Map tally) { return ret; }); List sortedCandidates = new LinkedList<>(); - for (Map.Entry entry : entries) { + for (var entry : entries) { sortedCandidates.add(entry.getKey()); } return sortedCandidates; @@ -596,7 +583,7 @@ List writeGenericCvrCsv( // create NIST Common Data Format CVR json void generateCdfJson(List castVoteRecords) throws IOException, RoundSnapshotDataMissingException { - // generate GpUnitIds for precincts "geo-political units" (can be a precinct or jurisdiction) + // generate GpUnitIds for precincts "geopolitical units" (can be a precinct or jurisdiction) gpUnitIds = generateGpUnitIds(); String outputPath = getOutputFilePathFromInstance("cvr_cdf") + ".json"; @@ -876,8 +863,8 @@ private void generateSummaryJson( private Map updateCandidateNamesInTally(Map tally) { Map newTally = new HashMap<>(); - for (String key : tally.keySet()) { - newTally.put(config.getNameForCandidateCode(key), tally.get(key)); + for (var entry : tally.entrySet()) { + newTally.put(config.getNameForCandidateCode(entry.getKey()), entry.getValue()); } return newTally; } @@ -913,12 +900,12 @@ private void addActionObjects( if (transfersFromCandidate != null) { // We want to replace candidate IDs with names here, too. Map translatedTransfers = new HashMap<>(); - for (String candidateId : transfersFromCandidate.keySet()) { + for (var entry : transfersFromCandidate.entrySet()) { // candidateName will be null for special values like "exhausted" - String candidateName = config.getNameForCandidateCode(candidateId); + String candidateName = config.getNameForCandidateCode(entry.getKey()); translatedTransfers.put( - candidateName != null ? candidateName : candidateId, - transfersFromCandidate.get(candidateId)); + candidateName != null ? candidateName : entry.getKey(), + entry.getValue()); } action.put("transfers", translatedTransfers); } diff --git a/src/main/java/network/brightspots/rcv/Tabulator.java b/src/main/java/network/brightspots/rcv/Tabulator.java index e57a9fbd5..a64c3ffbe 100644 --- a/src/main/java/network/brightspots/rcv/Tabulator.java +++ b/src/main/java/network/brightspots/rcv/Tabulator.java @@ -136,9 +136,9 @@ Set tabulate() throws TabulationCancelledException { // - If winnerElectionMode is "Bottoms-up using percentage threshold", we loop until all // remaining candidates have vote shares that meet or exceed that threshold. // - // At each iteration, we'll either a) identify one or more + // At each iteration, we'll either a. identify one or more // winners and transfer their votes to the remaining candidates (if we still need to find more - // winners), or b) eliminate one or more candidates and gradually transfer votes to the + // winners), or b. eliminate one or more candidates and gradually transfer votes to the // remaining candidates. while (shouldContinueTabulating()) { currentRound++; @@ -268,15 +268,14 @@ private void updatePastWinnerTallies() { Map previousRoundTally = roundTallies.get(currentRound - 1); List winnersToProcess = new LinkedList<>(); Set winnersRequiringComputation = new HashSet<>(); - for (String winner : winnerToRound.keySet()) { + for (var entry : winnerToRound.entrySet()) { // skip someone who won in the current round (we only care about previous round winners) - int winningRound = winnerToRound.get(winner); - if (winningRound == currentRound) { + if (entry.getValue() == currentRound) { continue; } - winnersToProcess.add(winner); - if (winningRound == currentRound - 1) { - winnersRequiringComputation.add(winner); + winnersToProcess.add(entry.getKey()); + if (entry.getValue() == currentRound - 1) { + winnersRequiringComputation.add(entry.getKey()); } } @@ -291,10 +290,8 @@ private void updatePastWinnerTallies() { // initialize or populate precinct tallies if (config.isTabulateByPrecinctEnabled()) { - for (String precinct : precinctRoundTallies.keySet()) { - // this is all the tallies for the given precinct - Map> roundTalliesForPrecinct = - precinctRoundTallies.get(precinct); + // this is all the tallies for the given precinct + for (var roundTalliesForPrecinct : precinctRoundTallies.values()) { // and this is the tally for the current round for the precinct Map roundTallyForPrecinct = roundTalliesForPrecinct.get(currentRound); for (String winner : winnersToProcess) { @@ -307,16 +304,18 @@ private void updatePastWinnerTallies() { } } - // process all the CVRs if needed (if we have any winners from the previous round to process) + // process all the CVRs if needed (i.e. if we have any winners from the previous round to + // process) if (winnersRequiringComputation.size() > 0) { for (CastVoteRecord cvr : castVoteRecords) { // the record of winners who got partial votes from this CVR Map winnerToFractionalValue = cvr.getWinnerToFractionalValue(); - for (String winner : winnerToFractionalValue.keySet()) { + for (var entry : winnerToFractionalValue.entrySet()) { + String winner = entry.getKey(); if (!winnersRequiringComputation.contains(winner)) { continue; } - BigDecimal fractionalTransferValue = winnerToFractionalValue.get(winner); + BigDecimal fractionalTransferValue = entry.getValue(); incrementTally(roundTally, fractionalTransferValue, winner); if (config.isTabulateByPrecinctEnabled() && cvr.getPrecinct() != null) { @@ -366,7 +365,7 @@ private void setWinningThreshold(Map currentRoundCandidateTo ? config.getNumberOfWinners() : config.getNumberOfWinners() + 1); // If we use integers, we shouldn't use any decimal places. - // Otherwise we use the amount of decimal places specified by the user + // Otherwise, we use the amount of decimal places specified by the user. int decimals = config.isNonIntegerWinningThresholdEnabled() ? config.getDecimalPlacesForVoteArithmetic() @@ -418,7 +417,7 @@ private boolean shouldContinueTabulating() { } } - // This handles continued tabulation after a winner has been chosen when + // Handles continued tabulation after a winner has been chosen when // continueUntilTwoCandidatesRemain is true. private boolean isCandidateContinuing(String candidate) { CandidateStatus status = getCandidateStatus(candidate); @@ -475,11 +474,10 @@ private List identifyWinners( // case we just wait until there are numWinners candidates remaining and then declare all // of them as winners simultaneously). // tally indexes over all tallies to find any winners - for (BigDecimal tally : currentRoundTallyToCandidates.keySet()) { - if (tally.compareTo(winningThreshold) >= 0) { + for (var entry : currentRoundTallyToCandidates.entrySet()) { + if (entry.getKey().compareTo(winningThreshold) >= 0) { // we have winner(s) - List winningCandidates = currentRoundTallyToCandidates.get(tally); - for (String candidate : winningCandidates) { + for (String candidate : entry.getValue()) { // The undeclared write-in placeholder can't win! if (!candidate.equals(UNDECLARED_WRITE_IN_OUTPUT_LABEL)) { selectedWinners.add(candidate); @@ -490,7 +488,7 @@ private List identifyWinners( } } - // Edge case: if we've identified multiple winners in this round but we're only supposed to + // Edge case: if we've identified multiple winners in this round, but we're only supposed to // elect one winner per round, pick the top vote-getter and defer the others to subsequent // rounds. if (config.isMultiSeatAllowOnlyOneWinnerPerRoundEnabled() && selectedWinners.size() > 1) { @@ -562,14 +560,14 @@ private List dropCandidatesBelowThreshold( BigDecimal threshold = config.getMinimumVoteThreshold(); if (threshold.signum() == 1 && currentRoundTallyToCandidates.firstKey().compareTo(threshold) < 0) { - for (BigDecimal tally : currentRoundTallyToCandidates.keySet()) { - if (tally.compareTo(threshold) < 0) { - for (String candidate : currentRoundTallyToCandidates.get(tally)) { + for (var entry : currentRoundTallyToCandidates.entrySet()) { + if (entry.getKey().compareTo(threshold) < 0) { + for (String candidate : entry.getValue()) { eliminated.add(candidate); Logger.info( "Eliminated candidate \"%s\" in round %d because they only had %s vote(s), below " + "the minimum threshold of %s.", - candidate, currentRound, tally, threshold); + candidate, currentRound, entry.getKey(), threshold); } } else { break; @@ -721,7 +719,8 @@ private List runBatchElimination( // At each iteration, currentVoteTally is the next-lowest vote count received by one or more // candidate(s) in the current round. - for (BigDecimal currentVoteTally : currentRoundTallyToCandidates.keySet()) { + for (var entry : currentRoundTallyToCandidates.entrySet()) { + BigDecimal currentVoteTally = entry.getKey(); // a shallow copy is sufficient LinkedList newEliminations = new LinkedList<>(eliminations); // Test whether leapfrogging is possible. @@ -737,7 +736,7 @@ private List runBatchElimination( } // Add the candidates for the currentVoteTally to the seen list and accumulate their votes. // currentCandidates is all candidates receiving the current vote tally - List currentCandidates = currentRoundTallyToCandidates.get(currentVoteTally); + List currentCandidates = entry.getValue(); BigDecimal totalForThisRound = config.multiply(currentVoteTally, new BigDecimal(currentCandidates.size())); runningTotal = runningTotal.add(totalForThisRound); @@ -758,7 +757,7 @@ private List runBatchElimination( } // purpose: determine if any overvote has occurred for this ranking set (from a CVR) - // and if so return how to handle it based on the rules configuration in use + // and if so return how to handle it based on the rule configuration in use // param: candidateSet all candidates this CVR contains at a particular rank // return: an OvervoteDecision enum to be applied to the CVR under consideration private OvervoteDecision getOvervoteDecision(Set candidateSet) { @@ -1000,10 +999,10 @@ && isCandidateContinuing(cvr.getCurrentRecipientOfVote())) { // Take the tallies for this round for each precinct and merge them into the main map tracking // the tallies by precinct. if (config.isTabulateByPrecinctEnabled()) { - for (String precinct : roundTallyByPrecinct.keySet()) { + for (var entry : roundTallyByPrecinct.entrySet()) { Map> roundTalliesForPrecinct = - precinctRoundTallies.get(precinct); - roundTalliesForPrecinct.put(currentRound, roundTallyByPrecinct.get(precinct)); + precinctRoundTallies.get(entry.getKey()); + roundTalliesForPrecinct.put(currentRound, entry.getValue()); } } diff --git a/src/main/java/network/brightspots/rcv/TabulatorSession.java b/src/main/java/network/brightspots/rcv/TabulatorSession.java index a464870c1..e2e3dcf9f 100644 --- a/src/main/java/network/brightspots/rcv/TabulatorSession.java +++ b/src/main/java/network/brightspots/rcv/TabulatorSession.java @@ -32,6 +32,7 @@ import java.io.BufferedReader; import java.io.FileReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -142,14 +143,14 @@ void tabulate() { ContestConfig config = ContestConfig.loadContestConfig(configPath); checkConfigVersionMatchesApp(config); boolean tabulationSuccess = false; - //noinspection ConstantConditions - if (config != null && config.validate() && setUpLogging(config)) { + if (config.validate() && setUpLogging(config)) { Logger.info("Computer name: %s", Utils.getComputerName()); Logger.info("User name: %s", Utils.getUserName()); Logger.info("Config file: %s", configPath); try { Logger.fine("Begin config file contents:"); - BufferedReader reader = new BufferedReader(new FileReader(configPath)); + BufferedReader reader = new BufferedReader( + new FileReader(configPath, StandardCharsets.UTF_8)); String line = reader.readLine(); while (line != null) { Logger.fine(line); @@ -164,7 +165,7 @@ void tabulate() { if (config.isMultiSeatSequentialWinnerTakesAllEnabled()) { Logger.info("This is a multi-pass IRV contest."); int numWinners = config.getNumberOfWinners(); - // temporarily set config to single-seat so we can run sequential elections + // temporarily set config to single-seat so that we can run sequential elections config.setNumberOfWinners(1); while (config.getSequentialWinners().size() < numWinners) { Logger.info( @@ -324,7 +325,7 @@ private List parseCastVoteRecords(ContestConfig config, Set tiedCandidates) throws TabulationCa String selection = null; while (selection == null) { - Scanner sc = new Scanner(System.in); + Scanner sc = new Scanner(System.in, StandardCharsets.UTF_8); String userInput = sc.nextLine(); if (userInput.equals(CLI_CANCEL_COMMAND)) { System.out.println("Cancelling tabulation..."); diff --git a/src/test/java/network/brightspots/rcv/TabulatorTests.java b/src/test/java/network/brightspots/rcv/TabulatorTests.java index 4bb59187e..fa6085e53 100644 --- a/src/test/java/network/brightspots/rcv/TabulatorTests.java +++ b/src/test/java/network/brightspots/rcv/TabulatorTests.java @@ -33,6 +33,7 @@ import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Paths; import network.brightspots.rcv.ContestConfig.Provider; @@ -51,13 +52,10 @@ class TabulatorTests { // compare file contents line by line to identify differences private static boolean fileCompare(String path1, String path2) { boolean result = true; - FileReader fr1 = null; - FileReader fr2 = null; - try { - fr1 = new FileReader(path1); - BufferedReader br1 = new BufferedReader(fr1); - fr2 = new FileReader(path2); - BufferedReader br2 = new BufferedReader(fr2); + try ( + BufferedReader br1 = new BufferedReader(new FileReader(path1, StandardCharsets.UTF_8)); + BufferedReader br2 = new BufferedReader(new FileReader(path2, StandardCharsets.UTF_8)) + ) { int currentLine = 1; int errorCount = 0; @@ -91,17 +89,6 @@ private static boolean fileCompare(String path1, String path2) { } catch (IOException exception) { Logger.severe("Error reading file!\n%s", exception); result = false; - } finally { - try { - if (fr1 != null) { - fr1.close(); - } - if (fr2 != null) { - fr2.close(); - } - } catch (IOException exception) { - Logger.severe("Error closing file!\n%s", exception); - } } return result; } @@ -147,9 +134,9 @@ private static void runTabulationTest(String stem) { } // test passed so cleanup test output folder File outputFolder = new File(session.getOutputPath()); - if (outputFolder.listFiles() != null) { - //noinspection ConstantConditions - for (File file : outputFolder.listFiles()) { + File[] files = outputFolder.listFiles(); + if (files != null) { + for (File file : files) { if (!file.isDirectory()) { try { Files.delete(file.toPath());