diff --git a/build.gradle b/build.gradle index ba95b65..6b05b21 100644 --- a/build.gradle +++ b/build.gradle @@ -16,12 +16,6 @@ * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt */ -group = "com.github.fge"; -version = "0.2.1-SNAPSHOT"; -description = "A library for handling large text files as CharSequences"; -sourceCompatibility = "1.7"; -targetCompatibility = "1.7"; // defaults to sourceCompatibility - /* * This has to be here... It will not be taken into account in common.gradle! * @@ -31,7 +25,7 @@ targetCompatibility = "1.7"; // defaults to sourceCompatibility */ buildscript { repositories { - maven { url "http://repo.spring.io/plugins-release" }; + maven { url "https://repo.spring.io/plugins-release" }; mavenCentral(); } dependencies { @@ -46,7 +40,6 @@ buildscript { apply(plugin: "java"); apply(plugin: "maven"); apply(plugin: "signing"); -apply(plugin: "osgi"); apply(plugin: "idea"); apply(plugin: "eclipse"); apply(plugin: "propdeps"); @@ -55,6 +48,12 @@ apply(plugin: "propdeps-idea"); apply(plugin: "propdeps-eclipse"); apply(plugin: "info.solidsoft.pitest"); +group = "com.github.fge"; +version = "0.2.1-SNAPSHOT"; +description = "A library for handling large text files as CharSequences"; +sourceCompatibility = "1.7"; +targetCompatibility = "1.7"; // defaults to sourceCompatibility + /* * Repositories to use */ @@ -68,14 +67,15 @@ repositories { dependencies { provided(group: "com.google.code.findbugs", name: "jsr305", version: "3.0.0"); - compile(group: "com.google.guava", name: "guava", version: "18.0"); + compile(group: "com.google.guava", name: "guava", version: "30.1-android"); testCompile(group: "org.testng", name: "testng", version: "6.8.13") { exclude(group: "junit", module: "junit"); exclude(group: "org.beanshell", module: "bsh"); exclude(group: "org.yaml", module: "snakeyaml"); }; testCompile(group: "org.mockito", name: "mockito-core", version: "1.10.17"); - testCompile(group: "org.assertj", name: "assertj-core", version: "1.7.0"); + testCompile(group: "org.assertj", name: "assertj-core", version: "2.9.1"); + testCompile(group: "org.apache.commons", name: "commons-lang3", version: "3.11") } test { @@ -99,6 +99,10 @@ task sourcesJar(type: Jar, dependsOn: classes) { javadoc { options.overview = "src/main/java/overview.html"; + // https://github.com/gradle/gradle/issues/11182 + if (JavaVersion.current().compareTo(JavaVersion.VERSION_1_7) > 0) { + options.addStringOption("-release", "7") + } } task javadocJar(type: Jar, dependsOn: javadoc) { @@ -106,9 +110,9 @@ task javadocJar(type: Jar, dependsOn: javadoc) { from javadoc.destinationDir; } -javadoc.options.links("http://docs.oracle.com/javase/7/docs/api/"); -javadoc.options.links("http://jsr-305.googlecode.com/svn/trunk/javadoc/"); -javadoc.options.links("http://docs.guava-libraries.googlecode.com/git-history/v17.0/javadoc/"); +javadoc.options.links("https://docs.oracle.com/javase/7/docs/api/"); +javadoc.options.links("https://static.javadoc.io/com.google.code.findbugs/jsr305/3.0.2/"); +javadoc.options.links("https://static.javadoc.io/com.google.guava/guava/17.0/"); artifacts { archives jar; @@ -116,13 +120,10 @@ artifacts { archives javadocJar; } -task wrapper(type: Wrapper) { - gradleVersion = "2.2.1"; - distributionUrl = "http://services.gradle.org/distributions/gradle-${gradleVersion}-all.zip"; -} - -task pom << { - pom {}.writeTo("${projectDir}/pom.xml"); +task pom { + doLast { + pom {}.writeTo("${projectDir}/pom.xml"); + } } /* @@ -137,18 +138,20 @@ project.ext { sonatypeSnapshots = "https://oss.sonatype.org/content/repositories/snapshots/"; }; -task checkSigningRequirements << { - def requiredProperties = [ "sonatypeUsername", "sonatypePassword" ]; - def noDice = false; - requiredProperties.each { - if (project.properties[it] == null) { - noDice = true; - System.err.printf("property \"%s\" is not defined!") +task checkSigningRequirements { + doLast { + def requiredProperties = [ "sonatypeUsername", "sonatypePassword" ]; + def noDice = false; + requiredProperties.each { + if (project.properties[it] == null) { + noDice = true; + System.err.printf("property \"%s\" is not defined!") + } } + if (noDice) + throw new IllegalStateException("missing required properties for " + + "upload"); } - if (noDice) - throw new IllegalStateException("missing required properties for " + - "upload"); } uploadArchives { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 3c7abdf..e708b1c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a6f592b..4d9ca16 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Fri Feb 28 09:30:03 CET 2014 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip diff --git a/gradlew b/gradlew index 91a7e26..4f906e0 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed 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. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$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" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,33 +75,14 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -90,7 +106,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +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 @@ -110,11 +126,13 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# 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="" @@ -138,27 +156,30 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` 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" ;; + 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 -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=`save "$@"` + +# 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" -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,20 +24,23 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe 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. @@ -35,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% @@ -45,34 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_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=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - :execute @rem Setup the command line 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/com/github/fge/largetext/ClosableCharSequence.java b/src/main/java/com/github/fge/largetext/ClosableCharSequence.java new file mode 100644 index 0000000..f53786e --- /dev/null +++ b/src/main/java/com/github/fge/largetext/ClosableCharSequence.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of both licenses is available under the src/resources/ directory of + * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively). + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ + +package com.github.fge.largetext; + +import java.io.Closeable; + +/** + * {@link CharSequence} that has underlying resource that should be closed + * (thus {@link Closeable#close()} should be implemented). + */ +public interface ClosableCharSequence + extends CharSequence, Closeable +{ +} diff --git a/src/main/java/com/github/fge/largetext/LargeText.java b/src/main/java/com/github/fge/largetext/LargeText.java index 957bc4e..89492dd 100644 --- a/src/main/java/com/github/fge/largetext/LargeText.java +++ b/src/main/java/com/github/fge/largetext/LargeText.java @@ -24,6 +24,7 @@ import com.github.fge.largetext.range.IntRange; import com.github.fge.largetext.sequence.CharSequenceFactory; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; import javax.annotation.Nonnull; import javax.annotation.ParametersAreNonnullByDefault; @@ -31,7 +32,7 @@ import java.io.IOException; import java.nio.CharBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.util.List; import java.util.Map; import java.util.logging.Logger; @@ -86,7 +87,7 @@ */ @ParametersAreNonnullByDefault public abstract class LargeText - implements CharSequence, Closeable + implements ClosableCharSequence { private static final Logger LOGGER = Logger.getLogger(LargeText.class.getCanonicalName()); @@ -102,22 +103,22 @@ public abstract class LargeText * The only protected constructor * * @param channel the {@link FileChannel} to the (hopefully text) file - * @param charset the character encoding to use + * @param decoderSupplier the character decoder to use * @param quantity the quantity of size units * @param sizeUnit the size unit * @throws IOException failed to build a decoder */ - protected LargeText(final FileChannel channel, final Charset charset, + protected LargeText(final FileChannel channel, final Supplier decoderSupplier, final int quantity, final SizeUnit sizeUnit) throws IOException { this.channel = Preconditions.checkNotNull(channel, "file channel cannot be null"); - Preconditions.checkNotNull(charset, "charset cannot be null"); + Preconditions.checkNotNull(decoderSupplier, "charset decoder supplier cannot be null"); Preconditions.checkNotNull(sizeUnit, "size unit cannot be null"); final long windowSize = sizeUnit.sizeInBytes(quantity); - decoder = new TextDecoder(channel, charset, windowSize); - loader = new TextCache(channel, charset); + decoder = new TextDecoder(channel, decoderSupplier, windowSize); + loader = new TextCache(channel, decoderSupplier); factory = new CharSequenceFactory(decoder, loader); } diff --git a/src/main/java/com/github/fge/largetext/LargeTextFactory.java b/src/main/java/com/github/fge/largetext/LargeTextFactory.java index d1fb3e8..11484ce 100644 --- a/src/main/java/com/github/fge/largetext/LargeTextFactory.java +++ b/src/main/java/com/github/fge/largetext/LargeTextFactory.java @@ -19,6 +19,7 @@ package com.github.fge.largetext; import com.google.common.base.Preconditions; +import com.google.common.base.Supplier; import javax.annotation.Nonnull; import javax.annotation.concurrent.Immutable; @@ -26,6 +27,7 @@ import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.OpenOption; import java.nio.file.Path; @@ -70,7 +72,7 @@ @Immutable public final class LargeTextFactory { - private final Charset charset; + private final Supplier decoderSupplier; private final SizeUnit sizeUnit; private final int quantity; @@ -96,7 +98,7 @@ public static LargeTextFactory defaultFactory() private LargeTextFactory(final Builder builder) { - charset = builder.charset; + decoderSupplier = builder.decoderSupplier; sizeUnit = builder.sizeUnit; quantity = builder.quantity; } @@ -121,7 +123,7 @@ public LargeText fromPath(@Nonnull final Path path) Preconditions.checkNotNull(path, "path must not be null"); final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ); - return new NotThreadSafeLargeText(channel, charset, quantity, sizeUnit); + return new NotThreadSafeLargeText(channel, decoderSupplier, quantity, sizeUnit); } /** @@ -141,7 +143,7 @@ public LargeText load(@Nonnull final Path path) Preconditions.checkNotNull(path, "path must not be null"); final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ); - return new NotThreadSafeLargeText(channel, charset, quantity, sizeUnit); + return new NotThreadSafeLargeText(channel, decoderSupplier, quantity, sizeUnit); } /** @@ -160,7 +162,7 @@ public LargeText loadThreadSafe(@Nonnull final Path path) Preconditions.checkNotNull(path, "path must not be null"); final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ); - return new ThreadSafeLargeText(channel, charset, quantity, sizeUnit); + return new ThreadSafeLargeText(channel, decoderSupplier, quantity, sizeUnit); } /** @@ -172,7 +174,8 @@ public static final class Builder private static final long MIN_WINDOW_SIZE = 1024L; private static final long MAX_WINDOW_SIZE = (long) Integer.MAX_VALUE; - private Charset charset = StandardCharsets.UTF_8; + private Supplier decoderSupplier = + defaultDecoder(StandardCharsets.UTF_8); private SizeUnit sizeUnit = SizeUnit.MiB; private int quantity = 2; @@ -189,8 +192,9 @@ private Builder() */ public Builder setCharset(@Nonnull final Charset charset) { - this.charset = Preconditions.checkNotNull(charset, - "charset cannot be null"); + this.decoderSupplier = defaultDecoder(Preconditions.checkNotNull( + charset, "charset cannot be null" + )); return this; } @@ -210,6 +214,13 @@ public Builder setCharsetByName(@Nonnull final String charsetByName) return setCharset(c); } + public Builder setCharsetDecoder(@Nonnull final Supplier supplier) + { + this.decoderSupplier = Preconditions.checkNotNull(supplier, + "charset decoder supplier cannot be null"); + return this; + } + /** * Set the window size for this factory * @@ -248,4 +259,16 @@ public LargeTextFactory build() return new LargeTextFactory(this); } } + + private static Supplier defaultDecoder(@Nonnull final Charset charset) + { + return new Supplier() + { + @Override + public CharsetDecoder get() + { + return charset.newDecoder(); + } + }; + } } diff --git a/src/main/java/com/github/fge/largetext/NotThreadSafeLargeText.java b/src/main/java/com/github/fge/largetext/NotThreadSafeLargeText.java index 4df328d..857eb7f 100644 --- a/src/main/java/com/github/fge/largetext/NotThreadSafeLargeText.java +++ b/src/main/java/com/github/fge/largetext/NotThreadSafeLargeText.java @@ -20,13 +20,14 @@ import com.github.fge.largetext.load.TextRange; import com.github.fge.largetext.range.IntRange; +import com.google.common.base.Supplier; import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.concurrent.NotThreadSafe; import java.io.IOException; import java.nio.CharBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.nio.file.Path; /** @@ -48,11 +49,11 @@ public final class NotThreadSafeLargeText private IntRange range = EMPTY_RANGE; private CharBuffer buffer = EMPTY_BUFFER; - NotThreadSafeLargeText(final FileChannel channel, final Charset charset, + NotThreadSafeLargeText(final FileChannel channel, final Supplier decoderSupplier, final int quantity, final SizeUnit sizeUnit) throws IOException { - super(channel, charset, quantity, sizeUnit); + super(channel, decoderSupplier, quantity, sizeUnit); } @Override diff --git a/src/main/java/com/github/fge/largetext/ThreadSafeLargeText.java b/src/main/java/com/github/fge/largetext/ThreadSafeLargeText.java index 6bd3002..2e9f1fe 100644 --- a/src/main/java/com/github/fge/largetext/ThreadSafeLargeText.java +++ b/src/main/java/com/github/fge/largetext/ThreadSafeLargeText.java @@ -20,13 +20,14 @@ import com.github.fge.largetext.load.TextRange; import com.github.fge.largetext.range.IntRange; +import com.google.common.base.Supplier; import javax.annotation.ParametersAreNonnullByDefault; import javax.annotation.concurrent.ThreadSafe; import java.io.IOException; import java.nio.CharBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.nio.file.Path; /** @@ -59,11 +60,11 @@ protected CurrentBuffer initialValue() } }; - ThreadSafeLargeText(final FileChannel channel, final Charset charset, + ThreadSafeLargeText(final FileChannel channel, final Supplier decoderSupplier, final int quantity, final SizeUnit sizeUnit) throws IOException { - super(channel, charset, quantity, sizeUnit); + super(channel, decoderSupplier, quantity, sizeUnit); } @Override diff --git a/src/main/java/com/github/fge/largetext/load/TextCache.java b/src/main/java/com/github/fge/largetext/load/TextCache.java index d4f53c3..b1aa5aa 100644 --- a/src/main/java/com/github/fge/largetext/load/TextCache.java +++ b/src/main/java/com/github/fge/largetext/load/TextCache.java @@ -20,6 +20,7 @@ import com.github.fge.largetext.LargeTextException; import com.github.fge.largetext.range.LongRange; +import com.google.common.base.Supplier; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; @@ -30,7 +31,6 @@ import java.nio.CharBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -54,7 +54,7 @@ public final class TextCache { private final FileChannel channel; - private final Charset charset; + private final Supplier decoderSupplier; /* * This is why we need Guava: we want cache expiry, and it has this builtin. @@ -63,10 +63,10 @@ public final class TextCache */ private final LoadingCache cache; - public TextCache(final FileChannel channel, final Charset charset) + public TextCache(final FileChannel channel, final Supplier decoderSupplier) { this.channel = channel; - this.charset = charset; + this.decoderSupplier = decoderSupplier; cache = CacheBuilder.newBuilder() .expireAfterAccess(30L, TimeUnit.SECONDS) .recordStats().build(loader()); @@ -120,7 +120,7 @@ public CharBuffer load(@Nonnull final TextRange key) final long size = byteRange.getUpperBound() - start; final MappedByteBuffer buffer = channel.map(MapMode.READ_ONLY, start, size); - final CharsetDecoder decoder = charset.newDecoder(); + final CharsetDecoder decoder = decoderSupplier.get(); return decoder.decode(buffer).asReadOnlyBuffer(); } }; diff --git a/src/main/java/com/github/fge/largetext/load/TextDecoder.java b/src/main/java/com/github/fge/largetext/load/TextDecoder.java index b31b806..294d13b 100644 --- a/src/main/java/com/github/fge/largetext/load/TextDecoder.java +++ b/src/main/java/com/github/fge/largetext/load/TextDecoder.java @@ -22,6 +22,7 @@ import com.github.fge.largetext.LargeTextException; import com.github.fge.largetext.LargeTextFactory; import com.github.fge.largetext.range.IntRange; +import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.RangeMap; import com.google.common.collect.TreeRangeMap; @@ -34,11 +35,8 @@ import java.nio.CharBuffer; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; -import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; -import java.nio.charset.CodingErrorAction; -import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -87,7 +85,7 @@ public final class TextDecoder private final RangeMap ranges = TreeRangeMap.create(); private final FileChannel channel; - private final Charset charset; + private final Supplier decoderSupplier; private final long fileSize; private final long targetMapSize; @@ -95,18 +93,18 @@ public final class TextDecoder * Constructor; don't use directly! * * @param channel the {@link FileChannel} to the target file - * @param charset the character encoding to use + * @param decoderSupplier the character decoder to use * @param targetMapSize the target byte mapping size * @throws IOException error obtaining information on the channel */ - public TextDecoder(final FileChannel channel, final Charset charset, + public TextDecoder(final FileChannel channel, final Supplier decoderSupplier, final long targetMapSize) throws IOException { this.channel = channel; fileSize = channel.size(); this.targetMapSize = targetMapSize; - this.charset = charset; + this.decoderSupplier = decoderSupplier; executor.submit(decodingTask()); } @@ -151,12 +149,11 @@ public List getRanges(final IntRange range) Thread.currentThread().interrupt(); throw new LargeTextException("Interrupted", e); } - final Collection ret; synchronized (ranges) { - ret = ranges.subRangeMap(range.asGuavaRange()) - .asMapOfRanges().values(); + return ImmutableList.copyOf( + ranges.subRangeMap(range.asGuavaRange()).asMapOfRanges().values() + ); } - return ImmutableList.copyOf(ret); } /** @@ -199,9 +196,7 @@ private Runnable decodingTask() @Override public void run() { - final CharsetDecoder decoder = charset.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT); + final CharsetDecoder decoder = decoderSupplier.get(); final CharBuffer charMap = CharBuffer.allocate((int) targetMapSize); @@ -228,11 +223,11 @@ public void run() } byteOffset = textRange.getByteRange().getUpperBound(); charOffset = textRange.getCharRange().getUpperBound(); - status.setNrChars(charOffset); synchronized (ranges) { ranges.put(textRange.getCharRange().asGuavaRange(), textRange); } + status.setNrChars(charOffset); } status.setFinished(charOffset); } @@ -244,7 +239,13 @@ private TextRange nextRange(final long byteOffset, final int charOffset, final CharsetDecoder decoder, final CharBuffer charMap) throws IOException { - long nrBytes = Math.min(targetMapSize, fileSize - byteOffset); + long nrBytes = fileSize - byteOffset; + boolean endOfInput = true; + + if (nrBytes > targetMapSize) { + nrBytes = targetMapSize; + endOfInput = false; + } final MappedByteBuffer byteMap = channel.map(FileChannel.MapMode.READ_ONLY, byteOffset, nrBytes); @@ -252,7 +253,7 @@ private TextRange nextRange(final long byteOffset, final int charOffset, charMap.rewind(); decoder.reset(); - final CoderResult result = decoder.decode(byteMap, charMap, true); + final CoderResult result = decoder.decode(byteMap, charMap, endOfInput); /* * Unmappable character... It _can_ happen even with a decoder, see @@ -265,8 +266,8 @@ private TextRange nextRange(final long byteOffset, final int charOffset, * Incomplete byte sequence: in this case, the mapping position reflects * what was actually read; change the mapping size */ - if (result.isMalformed()) - nrBytes = (long) byteMap.position(); + if (result.isUnderflow() || result.isMalformed()) + nrBytes = byteMap.position(); return new TextRange(byteOffset, nrBytes, charOffset, charMap.position()); diff --git a/src/main/java/com/github/fge/largetext/sequence/EmptyCharSequence.java b/src/main/java/com/github/fge/largetext/sequence/EmptyCharSequence.java index 176e36f..f388ea4 100644 --- a/src/main/java/com/github/fge/largetext/sequence/EmptyCharSequence.java +++ b/src/main/java/com/github/fge/largetext/sequence/EmptyCharSequence.java @@ -51,4 +51,8 @@ public CharSequence subSequence(final int start, final int end) return this; } + @Override + public String toString() { + return ""; + } } diff --git a/src/test/java/com/github/fge/largetext/NotThreadSafeLargeTextTest.java b/src/test/java/com/github/fge/largetext/NotThreadSafeLargeTextTest.java index 44cfe0d..610fb6b 100644 --- a/src/test/java/com/github/fge/largetext/NotThreadSafeLargeTextTest.java +++ b/src/test/java/com/github/fge/largetext/NotThreadSafeLargeTextTest.java @@ -19,6 +19,9 @@ package com.github.fge.largetext; import com.google.common.base.Strings; +import com.google.common.base.Supplier; +import org.apache.commons.lang3.RandomStringUtils; +import org.assertj.core.api.ThrowableAssert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; @@ -27,30 +30,62 @@ import java.io.BufferedWriter; import java.io.IOException; import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Random; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public final class NotThreadSafeLargeTextTest { - private final String testString = Strings.repeat("abcdefghij", 5000); - private final int len = testString.length(); + private static final SizeUnit windowSizeUnit = SizeUnit.KiB; + private static final int windowSize = 16; + // safe to cast as max window size supported by LargeText is Integer.MAX_VALUE + private static final int len = (int) windowSizeUnit.sizeInBytes(windowSize); + private static final Charset charset = StandardCharsets.UTF_8; + private static final LargeTextFactory defaultFactory; + private static final LargeTextFactory replaceFactory; + private static final LargeTextFactory ignoreFactory; + private final String testString = RandomStringUtils.random(len); private final Random random = new Random(System.nanoTime()); + static { + final LargeTextFactory.Builder builder = LargeTextFactory.newBuilder() + .setWindowSize(windowSize, windowSizeUnit); + + defaultFactory = builder.setCharset(charset).build(); + replaceFactory = builder.setCharsetDecoder(new Supplier() + { + @Override + public CharsetDecoder get() + { + return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPLACE); + } + }).build(); + ignoreFactory = builder.setCharsetDecoder(new Supplier() + { + @Override + public CharsetDecoder get() + { + return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.IGNORE); + } + }).build(); + } + private Path testFile; - private LargeText largeText; @BeforeClass public void createFile() throws IOException { - final Charset charset = StandardCharsets.UTF_8; testFile = Files.createTempFile("foo", "bar"); try ( final BufferedWriter writer = Files.newBufferedWriter(testFile, @@ -59,42 +94,172 @@ public void createFile() writer.write(testString); writer.flush(); } - - final LargeTextFactory factory = LargeTextFactory.newBuilder() - .setWindowSize(16, SizeUnit.KiB) - .build(); - largeText = factory.load(testFile); } @DataProvider - public Iterator getIndices() + public Iterator getFactoriesAndIndices() { final List list = new ArrayList<>(); - for (int i = 0; i < 200; i++) - list.add(new Object[] { random.nextInt(len) }); + for (final LargeTextFactory factory : Arrays.asList(defaultFactory, replaceFactory, ignoreFactory)) + for (int i = 0; i < 2_000; i++) + list.add(new Object[] { factory, random.nextInt(len) }); return list.iterator(); } + @Test(dataProvider = "getFactoriesAndIndices") + public void nonThreadSafeLargeTextWorks(final LargeTextFactory factory, final int index) + throws IOException + { + try (LargeText text = factory.load(testFile)) { + final char actual = text.charAt(index); + final char expected = text.charAt(index); + assertThat(actual).overridingErrorMessage( + "Wrong character picked up! Was '%s', expected '%s'", + actual, expected + ).isEqualTo(expected); + } + } + + @Test + public void reportsMalformedCharacterByDefault() + throws IOException + { + try (final TemporaryFile tmp = new TemporaryFile()) { + final Path path = tmp.getPath(); + Files.write(path, new byte[]{ + (byte) 0x48, // H + (byte) 0x65, // e + (byte) 0x6C, // l + (byte) 0xD0, // ??? + (byte) 0x6F // o + }); + + try (final LargeText text = defaultFactory.load(path)) { + assertThatThrownBy(new ThrowableAssert.ThrowingCallable() + { + @Override + public void call() + { + text.charAt(4); + } + }).isInstanceOf(LargeTextException.class).hasMessage("decoding error"); + } + } + } + + @Test + public void replacesMalformedCharacterWhenDecoderIsConfiguredToReplace() + throws IOException + { + try (final TemporaryFile tmp = new TemporaryFile()) { + final Path path = tmp.getPath(); + Files.write(path, new byte[]{ + (byte) 0x48, // H + (byte) 0x65, // e + (byte) 0x6C, // l + (byte) 0xD0, // ??? + (byte) 0x6F // o + }); + + try (final LargeText text = replaceFactory.load(path)) { + assertThat(text.charAt(3)).isEqualTo('�'); + assertThat(text.charAt(4)).isEqualTo('o'); + assertThat(text.toString()).isEqualTo("Hel�o"); + } + } + } + + @Test + public void replacesMalformedCharacterOnUpperBoundWhenDecoderIsConfiguredToReplace() + throws IOException + { + int size = (int) windowSizeUnit.sizeInBytes(windowSize); + final byte[] content = new byte[size + 2]; + Arrays.fill(content, (byte) 0x61); // a + content[size] = (byte) 0xD0; // ??? + + try (final TemporaryFile tmp = new TemporaryFile()) { + final Path path = tmp.getPath(); + Files.write(path, content); + + try (final LargeText text = replaceFactory.load(path)) { + assertThat(text.charAt(size - 1)).isEqualTo('a'); + assertThat(text.charAt(size)).isEqualTo('�'); + assertThat(text.charAt(size + 1)).isEqualTo('a'); + } + } + } + + @Test + public void ignoresMalformedCharacterWhenDecoderIsConfiguredToIgnore() + throws IOException + { + try (final TemporaryFile tmp = new TemporaryFile()) { + final Path path = tmp.getPath(); + Files.write(path, new byte[]{ + (byte) 0x48, // H + (byte) 0x65, // e + (byte) 0x6C, // l + (byte) 0xD0, // ??? + (byte) 0x6F // o + }); + + try (final LargeText text = ignoreFactory.load(path)) { + assertThat(text.charAt(3)).isEqualTo('o'); + assertThat(text.toString()).isEqualTo("Helo"); + } + } + } - @Test(dataProvider = "getIndices") - public void nonThreadSafeLargeTextWorks(final int index) + // Bug was found when NotThreadSafeLargeTextTest was updated to use multiple LargeTexts + // instead of single created in set up method. + // + // Issue is: status.setNrChars(charOffset) unblocks thread that awaits for character but + // at the same time it does not guarantee that TextRange will be put to ranges map. + // NullPointerException happens in this order: + // + // 1. main thread invokes charAt with index that is not yet available + // 2. text decoder in main thread adds CharWaiter and blocks in await() + // 3. text-decoder thread travels through file until it reaches window that + // includes requested index + // 4. status.setNrChars(charOffset) at TextDecoder:226 unblocks main + // main thread as required index is reached + // 5. main thread wins race and synchronizes on ranges to get range. Range is not + // written yet, method returns null, NullPointerException is generated at + // NotThreadSafeLargeText:64 + // 6. text-decoder thread synchronizes on ranges and writes range. + // + // Set breakpoints (one that suspend single thread, not all threads) at: + // * TextDecoder:130 (synchronization on ranges) + // * TextDecoder:227 (synchronization on ranges) + // + // To reproduce, execute this test in debug mode. + // + // * if breakpoint hits text-decoder thread - switch to main and step over + // * if breakpoint hits main thread - step over + @Test + public void reproduceCharAtNullPointerExceptionCausedByStatusReportedBeforeRangesUpdate() + throws IOException { - final char actual = largeText.charAt(index); - final char expected = testString.charAt(index); - assertThat(actual).overridingErrorMessage( - "Wrong character picked up! Was '%s', expected '%s'", - actual, expected - ).isEqualTo(expected); + final String string = Strings.repeat("abcde", 5_000); + try (final TemporaryFile tmp = new TemporaryFile()) { + final Path path = tmp.getPath(); + + Files.write(path, string.getBytes(StandardCharsets.UTF_8)); + + try (final LargeText text = LargeTextFactory.defaultFactory().load(path)) { + text.charAt(500); + } + } } @AfterClass - public void closeEverything() + public void deleteFile() throws IOException { - largeText.close(); Files.delete(testFile); } -} \ No newline at end of file +} diff --git a/src/test/java/com/github/fge/largetext/TemporaryFile.java b/src/test/java/com/github/fge/largetext/TemporaryFile.java new file mode 100644 index 0000000..f4e1746 --- /dev/null +++ b/src/test/java/com/github/fge/largetext/TemporaryFile.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of both licenses is available under the src/resources/ directory of + * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively). + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ + +package com.github.fge.largetext; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; + +/** + * Thin wrapper of {@link Files#createTempFile(String, String, FileAttribute[])} that + * removes temporary file on {@link #close()}. + */ +public final class TemporaryFile implements AutoCloseable +{ + private final Path path; + + /** + * @throws IOException if fails to create temporary file + */ + public TemporaryFile() + throws IOException + { + this(null, null); + } + + /** + * @param prefix the prefix string to be used in generated file's name + * @param suffix the suffix string to be used in generated file's name + * @throws IOException if fails to create temporary file + */ + public TemporaryFile(String suffix, String prefix) + throws IOException + { + this.path = Files.createTempFile(suffix, prefix); + } + + public Path getPath() + { + return path; + } + + @Override + public void close() + throws IOException + { + Files.delete(path); + } +} diff --git a/src/test/java/com/github/fge/largetext/ThreadSafeLargeTextTest.java b/src/test/java/com/github/fge/largetext/ThreadSafeLargeTextTest.java index d45b459..c28493e 100644 --- a/src/test/java/com/github/fge/largetext/ThreadSafeLargeTextTest.java +++ b/src/test/java/com/github/fge/largetext/ThreadSafeLargeTextTest.java @@ -18,7 +18,7 @@ package com.github.fge.largetext; -import com.google.common.base.Strings; +import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -35,7 +35,7 @@ public final class ThreadSafeLargeTextTest { - private final String testString = Strings.repeat("abcdefghij", 5000); + private final String testString = RandomStringUtils.random(5_000); private final int len = testString.length(); private final Random random = new Random(System.nanoTime()); @@ -81,4 +81,4 @@ public void closeEverything() largeText.close(); Files.delete(testFile); } -} \ No newline at end of file +} diff --git a/src/test/java/com/github/fge/largetext/load/TextDecoderTest.java b/src/test/java/com/github/fge/largetext/load/TextDecoderTest.java new file mode 100644 index 0000000..0bf1c67 --- /dev/null +++ b/src/test/java/com/github/fge/largetext/load/TextDecoderTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of both licenses is available under the src/resources/ directory of + * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively). + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ + +package com.github.fge.largetext.load; + +import com.github.fge.largetext.LargeText; +import com.github.fge.largetext.LargeTextFactory; +import com.github.fge.largetext.SizeUnit; +import com.github.fge.largetext.TemporaryFile; +import com.google.common.base.Strings; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TextDecoderTest +{ + // ConcurrentModificationException is thrown when view of ranges is + // copied to ImmutableList and decoding task hasn't finished yet ( + // writes to ranges map). + // + // It is hard to reproduce ConcurrentModificationException without + // modification of TextDecoder. It is doable in debug mode. + // + // Set breakpoints (one that suspend single thread, not all threads) at: + // * TreeMap:1696 (SubMapIterator#nextEntry start) + // * TextDecoder:233 (synchronized block in decodingTask) + // + // Execute this test in debug mode. + // + // * resume until breakpoint in nextEntry becomes available + // (usually takes a hit or two) + // * switch to "text-decoder" thread + // * step through synchronized block + // * switch back to nextEntry thread + // * step through until m.modCount != expectedModCount because put + // happened + @Test + public void reproduceGetRangesConcurrentModificationException() + throws IOException + { + final String string = Strings.repeat("abcde", 5_000); + try (final TemporaryFile tmp = new TemporaryFile()) { + final Path path = tmp.getPath(); + + Files.write(path, string.getBytes(StandardCharsets.UTF_8)); + + final LargeTextFactory factory = LargeTextFactory.newBuilder() + .setCharset(StandardCharsets.US_ASCII) + .setWindowSize(1024, SizeUnit.B) + .build(); + + try (final LargeText text = factory.load(path)) { + text.subSequence(0, 450); + } + } + } +} diff --git a/src/test/java/com/github/fge/largetext/sequence/EmptyCharSequenceTest.java b/src/test/java/com/github/fge/largetext/sequence/EmptyCharSequenceTest.java new file mode 100644 index 0000000..ffe798d --- /dev/null +++ b/src/test/java/com/github/fge/largetext/sequence/EmptyCharSequenceTest.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014, Francis Galiegue (fgaliegue@gmail.com) + * + * This software is dual-licensed under: + * + * - the Lesser General Public License (LGPL) version 3.0 or, at your option, any + * later version; + * - the Apache Software License (ASL) version 2.0. + * + * The text of both licenses is available under the src/resources/ directory of + * this project (under the names LGPL-3.0.txt and ASL-2.0.txt respectively). + * + * Direct link to the sources: + * + * - LGPL 3.0: https://www.gnu.org/licenses/lgpl-3.0.txt + * - ASL 2.0: http://www.apache.org/licenses/LICENSE-2.0.txt + */ + +package com.github.fge.largetext.sequence; + +import org.testng.annotations.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EmptyCharSequenceTest +{ + @Test + public void toStringReturnsEmptyString() { + assertThat(EmptyCharSequence.INSTANCE.toString()).isEmpty(); + } +} diff --git a/src/test/java/com/github/fge/largetext/sequence/TextRangeCharSequenceTest.java b/src/test/java/com/github/fge/largetext/sequence/TextRangeCharSequenceTest.java index 254f301..fa43a9e 100644 --- a/src/test/java/com/github/fge/largetext/sequence/TextRangeCharSequenceTest.java +++ b/src/test/java/com/github/fge/largetext/sequence/TextRangeCharSequenceTest.java @@ -21,8 +21,9 @@ import com.github.fge.largetext.load.TextCache; import com.github.fge.largetext.load.TextDecoder; import com.github.fge.largetext.range.IntRange; -import com.google.common.base.Strings; +import com.google.common.base.Supplier; import com.google.common.collect.Lists; +import org.apache.commons.lang3.RandomStringUtils; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; @@ -33,6 +34,7 @@ import java.nio.CharBuffer; import java.nio.channels.FileChannel; import java.nio.charset.Charset; +import java.nio.charset.CharsetDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -48,13 +50,21 @@ public final class TextRangeCharSequenceTest private Path testFile; private FileChannel channel; private CharSequenceFactory factory; - private final String testString = Strings.repeat("abcdefghij", 5000); + private final String testString = RandomStringUtils.random(5_000); @BeforeClass public void createFile() throws IOException { final Charset charset = StandardCharsets.UTF_8; + final Supplier decoderSupplier = new Supplier() + { + @Override + public CharsetDecoder get() + { + return charset.newDecoder(); + } + }; testFile = Files.createTempFile("foo", "bar"); try ( final BufferedWriter writer = Files.newBufferedWriter(testFile, @@ -64,8 +74,8 @@ public void createFile() writer.flush(); } channel = FileChannel.open(testFile, StandardOpenOption.READ); - final TextDecoder decoder = new TextDecoder(channel, charset, 1000L); - final TextCache loader = new TextCache(channel, charset); + final TextDecoder decoder = new TextDecoder(channel, decoderSupplier, 1000L); + final TextCache loader = new TextCache(channel, decoderSupplier); factory = new CharSequenceFactory(decoder, loader); } @@ -74,7 +84,7 @@ public Iterator getClassTests() { final List list = Lists.newArrayList(); - list.add(new Object[] { 0, 1000, CharBuffer.class }); + list.add(new Object[] { 0, 250 /* 1000 / max 4 bytes per character */, CharBuffer.class }); list.add(new Object[] { 500, 1049, MultiRangeCharSequence.class }); return list.iterator();