Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ java.lang.NullPointerException: Cannot invoke
<action type="fix" dev="ggregory" due-to="Omkhar Arasaratnam, Gary Gregory">AtomicSafeInitializer.get() busy-spin without yield burns CPU during slow initialization (#1651).</action>
<action type="fix" dev="ggregory" due-to="Omkhar Arasaratnam, Gary Gregory">StrBuilder.readFrom(Readable) exposes stale internal buffer to Readable parameter (#1652).</action>
<action type="fix" dev="ggregory" due-to="Omkhar Arasaratnam, Gary Gregory">EqualsBuilder.reflectionEquals() array branch missing cycle guard causes stack overflow on self-referential Object arrays (#1653).</action>
<action type="fix" dev="pkarwasz" due-to="Piotr Karwasz">WordUtils.wrap() leaves separator characters in the output when the wrapOn regex match is longer than one character (#1655).</action>
<!-- ADD -->
<action type="add" dev="ggregory" due-to="Gary Gregory">Add JavaVersion.JAVA_27.</action>
<action type="add" dev="ggregory" due-to="Gary Gregory">Add SystemUtils.IS_JAVA_27.</action>
Expand Down
21 changes: 13 additions & 8 deletions src/main/java/org/apache/commons/lang3/text/WordUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -648,29 +648,31 @@ public static String wrap(final String str, int wrapLength, String newLineStr, f

while (offset < inputLineLength) {
int spaceToWrapAt = -1;
int endOfWrapAt = -1;
Matcher matcher = patternToWrapOn.matcher(
str.substring(offset, Math.min((int) Math.min(Integer.MAX_VALUE, offset + wrapLength + 1L), inputLineLength)));
if (matcher.find()) {
if (matcher.start() == 0) {
// If the match is zero-width, advance by at least 1 to avoid infinite loop.
offset += matcher.end() > 0 ? matcher.end() : 1;
spaceToWrapAt = matcher.start() + offset;
endOfWrapAt = matcher.end() + offset;
// Skip leading match, if it is not zero-width
if (spaceToWrapAt == offset && endOfWrapAt != offset) {
offset = endOfWrapAt;
continue;
}
spaceToWrapAt = matcher.start() + offset;
}
// only last line without leading spaces is left
if (inputLineLength - offset <= wrapLength) {
break;
}
while (matcher.find()) {
spaceToWrapAt = matcher.start() + offset;
endOfWrapAt = matcher.end() + offset;
}
if (spaceToWrapAt >= offset) {
if (endOfWrapAt > offset) {
// normal case
wrappedLine.append(str, offset, spaceToWrapAt);
wrappedLine.append(newLineStr);
offset = spaceToWrapAt + 1;

offset = endOfWrapAt;
} else // really long word or URL
if (wrapLongWords) {
// wrap really long word one line at a time
Expand All @@ -680,14 +682,17 @@ public static String wrap(final String str, int wrapLength, String newLineStr, f
} else {
// do not wrap really long word, just extend beyond limit
matcher = patternToWrapOn.matcher(str.substring(offset + wrapLength));
spaceToWrapAt = -1;
if (matcher.find()) {
spaceToWrapAt = matcher.start() + offset + wrapLength;
endOfWrapAt = matcher.end() + offset + wrapLength;
}

if (spaceToWrapAt >= 0) {
wrappedLine.append(str, offset, spaceToWrapAt);
wrappedLine.append(newLineStr);
offset = spaceToWrapAt + 1;
// at least offset + wrapLength >= offset + 1
offset = endOfWrapAt;
} else {
wrappedLine.append(str, offset, str.length());
offset = inputLineLength;
Expand Down
82 changes: 51 additions & 31 deletions src/test/java/org/apache/commons/lang3/text/WordUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,62 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.params.provider.Arguments.arguments;

import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.time.Duration;
import java.util.stream.Stream;

import org.apache.commons.lang3.AbstractLangTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

/**
* Tests for WordUtils class.
*/
@Deprecated
class WordUtilsTest extends AbstractLangTest {

static Stream<Arguments> testWrapStringIntStringBooleanString() {
return Stream.of(
// null passthrough
arguments(null, -1, false, "/", null),
// no changes test
arguments("flammable/inflammable", 30, false, "/", "flammable/inflammable"),
// wrap on / and small width
arguments("flammable/inflammable", 2, false, "/", "flammable\ninflammable"),
// wrap long words on / 1
arguments("flammable/inflammable", 9, true, "/", "flammable\ninflammab\nle"),
// wrap long words on / 2
arguments("flammable/inflammable", 15, true, "/", "flammable\ninflammable"),
// wrap long words on / 3
arguments("flammableinflammable", 15, true, "/", "flammableinflam\nmable"),
// default values
arguments("a/a/a/a", -1, false, "/", "a\na\na\na"),
arguments("a a a a", 1, false, null, "a\na\na\na"),
// strip leading / keep trailing
arguments("///abc///def///ghi", 3, false, "/", "abc\ndef\nghi"),
arguments("///abc///def///ghi", 4, false, "/", "abc/\ndef/\nghi"),
arguments("///abc///def///ghi", 5, false, "/", "abc//\ndef//\nghi"),
// keep only two trailing, wrap on third
arguments("///abc///def///ghi", 6, false, "/", "abc//\ndef//\nghi"),
// zero-width regex match must advance to avoid an infinite loop
arguments("abcabc", 3, false, "(?=a)", "abc\nabc"),
arguments("abcdefabcdef", 4, false, "(?=a)", "abcdef\nabcdef"),
arguments("abcdefabcdef", 4, true, "(?=a)", "abcd\nef\nabcd\nef"),
// width two regex
arguments("abc\\/abc", 3, false, "\\\\/", "abc\nabc"),
arguments("abcdef\\/abcdef", 4, false, "\\\\/", "abcdef\nabcdef"),
arguments("abcdef\\/abcdef", 4, true, "\\\\/", "abcd\nef\nabcd\nef"),
// variable-width regex
arguments(".abc.-def.--ghi", 5, false, "[.]-*", "abc\ndef\nghi")
);
}

@Test
void testCapitalize_String() {
assertNull(WordUtils.capitalize(null));
Expand Down Expand Up @@ -406,34 +446,14 @@ void testWrap_StringIntStringBoolean() {
assertEquals(expected, WordUtils.wrap(input, 20, "\n", true));
}

@Test
void testWrap_StringIntStringBooleanString() {

//no changes test
String input = "flammable/inflammable";
String expected = "flammable/inflammable";
assertEquals(expected, WordUtils.wrap(input, 30, "\n", false, "/"));

// wrap on / and small width
expected = "flammable\ninflammable";
assertEquals(expected, WordUtils.wrap(input, 2, "\n", false, "/"));

// wrap long words on / 1
expected = "flammable\ninflammab\nle";
assertEquals(expected, WordUtils.wrap(input, 9, "\n", true, "/"));

// wrap long words on / 2
expected = "flammable\ninflammable";
assertEquals(expected, WordUtils.wrap(input, 15, "\n", true, "/"));

// wrap long words on / 3
input = "flammableinflammable";
expected = "flammableinflam\nmable";
assertEquals(expected, WordUtils.wrap(input, 15, "\n", true, "/"));
}

@Test
void testZeroWidthWrapOnRegex() {
assertTimeout(Duration.ofSeconds(2), () -> assertNotNull(WordUtils.wrap("abcdef", 3, "\n", false, "(?=a)")));
@ParameterizedTest
@MethodSource
@Timeout(2)
void testWrapStringIntStringBooleanString(final String str, final int wrapLength, final boolean wrapLongWords, final String wrapOn, final String expected) {
assertEquals(expected, WordUtils.wrap(str, wrapLength, "\n", wrapLongWords, wrapOn));
final String sep = System.lineSeparator();
if (!sep.equals("\n")) {
assertEquals(expected != null ? expected.replace("\n", sep) : null, WordUtils.wrap(str, wrapLength, null, wrapLongWords, wrapOn));
}
}
}
Loading