diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index ba1c91e019..81d0b01844 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -30,7 +30,6 @@ import java.nio.channels.WritableByteChannel; import java.nio.file.OpenOption; import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.concurrent.Callable; @@ -568,65 +567,290 @@ public abstract class DataBufferUtils { /** * Return a {@link Matcher} for the given delimiter. - * The matcher can be used to find the delimiters in data buffers. + * The matcher can be used to find the delimiters in a stream of data buffers. * @param delimiter the delimiter bytes to find * @return the matcher * @since 5.2 */ public static Matcher matcher(byte[] delimiter) { - Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty"); - return new KnuthMorrisPrattMatcher(delimiter); + return createMatcher(delimiter); } - /** Return a {@link Matcher} for the given delimiters. - * The matcher can be used to find the delimiters in data buffers. + /** + * Return a {@link Matcher} for the given delimiters. + * The matcher can be used to find the delimiters in a stream of data buffers. * @param delimiters the delimiters bytes to find * @return the matcher * @since 5.2 */ public static Matcher matcher(byte[]... delimiters) { Assert.isTrue(delimiters.length > 0, "Delimiters must not be empty"); - if (delimiters.length == 1) { - return matcher(delimiters[0]); - } - else { - Matcher[] matchers = new Matcher[delimiters.length]; - for (int i = 0; i < delimiters.length; i++) { - matchers[i] = matcher(delimiters[i]); - } - return new CompositeMatcher(matchers); + return (delimiters.length == 1 ? createMatcher(delimiters[0]) : new CompositeMatcher(delimiters)); + } + + private static NestedMatcher createMatcher(byte[] delimiter) { + Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty"); + switch (delimiter.length) { + case 1: + return (delimiter[0] == 10 ? SingleByteMatcher.NEWLINE_MATCHER : new SingleByteMatcher(delimiter)); + case 2: + return new TwoByteMatcher(delimiter); + default: + return new KnuthMorrisPrattMatcher(delimiter); } } /** - * Defines an object that matches a data buffer against a delimiter. + * Contract to find delimiter(s) against one or more data buffers that can + * be passed one at a time to the {@link #match(DataBuffer)} method. + * * @since 5.2 * @see #match(DataBuffer) */ public interface Matcher { /** - * Returns the position of the final matching delimiter byte that matches the given buffer, - * or {@code -1} if not found. - * @param dataBuffer the buffer in which to search for the delimiter - * @return the position of the final matching delimiter, or {@code -1} if not found. + * Find the first matching delimiter and return the index of the last + * byte of the delimiter, or {@code -1} if not found. */ int match(DataBuffer dataBuffer); /** - * Return the delimiter used for this matcher. - * @return the delimiter + * Return the delimiter from the last invocation of {@link #match(DataBuffer)}. */ byte[] delimiter(); /** - * Resets the state of this matcher. + * Reset the state of this matcher. */ void reset(); } + /** + * Matcher that supports searching for multiple delimiters. + */ + private static class CompositeMatcher implements Matcher { + + private static final byte[] NO_DELIMITER = new byte[0]; + + + private final NestedMatcher[] matchers; + + byte[] longestDelimiter = NO_DELIMITER; + + CompositeMatcher(byte[][] delimiters) { + this.matchers = initMatchers(delimiters); + } + + private static NestedMatcher[] initMatchers(byte[][] delimiters) { + NestedMatcher[] matchers = new NestedMatcher[delimiters.length]; + for (int i = 0; i < delimiters.length; i++) { + matchers[i] = createMatcher(delimiters[i]); + } + return matchers; + } + + @Override + public int match(DataBuffer dataBuffer) { + this.longestDelimiter = NO_DELIMITER; + + for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) { + byte b = dataBuffer.getByte(pos); + + for (NestedMatcher matcher : this.matchers) { + if (matcher.match(b) && matcher.delimiter().length > this.longestDelimiter.length) { + this.longestDelimiter = matcher.delimiter(); + } + } + + if (this.longestDelimiter != NO_DELIMITER) { + reset(); + return pos; + } + } + return -1; + } + + @Override + public byte[] delimiter() { + Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!"); + return this.longestDelimiter; + } + + @Override + public void reset() { + for (NestedMatcher matcher : this.matchers) { + matcher.reset(); + } + } + } + + + /** + * Matcher that can be nested within {@link CompositeMatcher} where multiple + * matchers advance together using the same index, one byte at a time. + */ + private interface NestedMatcher extends Matcher { + + /** + * Perform a match against the next byte of the stream and return true + * if the delimiter is fully matched. + */ + boolean match(byte b); + + } + + + /** + * Matcher for a single byte delimiter. + */ + private static class SingleByteMatcher implements NestedMatcher { + + static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10}); + + private final byte[] delimiter; + + SingleByteMatcher(byte[] delimiter) { + Assert.isTrue(delimiter.length == 1, "Expected a 1 byte delimiter"); + this.delimiter = delimiter; + } + + @Override + public int match(DataBuffer dataBuffer) { + for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) { + byte b = dataBuffer.getByte(pos); + if (match(b)) { + return pos; + } + } + return -1; + } + + @Override + public boolean match(byte b) { + return this.delimiter[0] == b; + } + + @Override + public byte[] delimiter() { + return this.delimiter; + } + + @Override + public void reset() { + } + } + + + /** + * Base class for a {@link NestedMatcher}. + */ + private static abstract class AbstractNestedMatcher implements NestedMatcher { + + private final byte[] delimiter; + + private int matches = 0; + + + protected AbstractNestedMatcher(byte[] delimiter) { + this.delimiter = delimiter; + } + + protected void setMatches(int index) { + this.matches = index; + } + + protected int getMatches() { + return this.matches; + } + + @Override + public int match(DataBuffer dataBuffer) { + for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) { + byte b = dataBuffer.getByte(pos); + if (match(b)) { + reset(); + return pos; + } + } + return -1; + } + + @Override + public boolean match(byte b) { + if (b == this.delimiter[this.matches]) { + this.matches++; + return (this.matches == delimiter().length); + } + return false; + } + + @Override + public byte[] delimiter() { + return this.delimiter; + } + + @Override + public void reset() { + this.matches = 0; + } + } + + + /** + * Matcher with a 2 byte delimiter that does not benefit from a + * Knuth-Morris-Pratt suffix-prefix table. + */ + private static class TwoByteMatcher extends AbstractNestedMatcher { + + protected TwoByteMatcher(byte[] delimiter) { + super(delimiter); + Assert.isTrue(delimiter.length == 2, "Expected a 2-byte delimiter"); + } + } + + + /** + * Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm. + * @see Knuth-Morris-Pratt string matching + */ + private static class KnuthMorrisPrattMatcher extends AbstractNestedMatcher { + + private final int[] table; + + public KnuthMorrisPrattMatcher(byte[] delimiter) { + super(delimiter); + this.table = longestSuffixPrefixTable(delimiter); + } + + private static int[] longestSuffixPrefixTable(byte[] delimiter) { + int[] result = new int[delimiter.length]; + result[0] = 0; + for (int i = 1; i < delimiter.length; i++) { + int j = result[i - 1]; + while (j > 0 && delimiter[i] != delimiter[j]) { + j = result[j - 1]; + } + if (delimiter[i] == delimiter[j]) { + j++; + } + result[i] = j; + } + return result; + } + + @Override + public boolean match(byte b) { + while (getMatches() > 0 && b != delimiter()[getMatches()]) { + setMatches(this.table[getMatches() - 1]); + } + return super.match(b); + } + } + + private static class ReadableByteChannelGenerator implements Consumer> { private final ReadableByteChannel channel; @@ -908,124 +1132,4 @@ public abstract class DataBufferUtils { } } - - /** - * Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm. - * @see Knuth-Morris-Pratt string matching - */ - private static class KnuthMorrisPrattMatcher implements Matcher { - - private final byte[] delimiter; - - private final int[] table; - - private int matches = 0; - - public KnuthMorrisPrattMatcher(byte[] delimiter) { - this.delimiter = Arrays.copyOf(delimiter, delimiter.length); - this.table = longestSuffixPrefixTable(delimiter); - } - - private static int[] longestSuffixPrefixTable(byte[] delimiter) { - int[] result = new int[delimiter.length]; - result[0] = 0; - for (int i = 1; i < delimiter.length; i++) { - int j = result[i - 1]; - while (j > 0 && delimiter[i] != delimiter[j]) { - j = result[j - 1]; - } - if (delimiter[i] == delimiter[j]) { - j++; - } - result[i] = j; - } - return result; - } - - @Override - public int match(DataBuffer dataBuffer) { - for (int i = dataBuffer.readPosition(); i < dataBuffer.writePosition(); i++) { - byte b = dataBuffer.getByte(i); - - while (this.matches > 0 && b != this.delimiter[this.matches]) { - this.matches = this.table[this.matches - 1]; - } - - if (b == this.delimiter[this.matches]) { - this.matches++; - if (this.matches == this.delimiter.length) { - reset(); - return i; - } - } - } - return -1; - } - - @Override - public byte[] delimiter() { - return Arrays.copyOf(this.delimiter, this.delimiter.length); - } - - @Override - public void reset() { - this.matches = 0; - } - } - - - /** - * Implementation of {@link Matcher} that wraps several other matchers. - */ - private static class CompositeMatcher implements Matcher { - - private static final byte[] NO_DELIMITER = new byte[0]; - - private final Matcher[] matchers; - - byte[] longestDelimiter = NO_DELIMITER; - - public CompositeMatcher(Matcher[] matchers) { - this.matchers = matchers; - } - - @Override - public int match(DataBuffer dataBuffer) { - this.longestDelimiter = NO_DELIMITER; - int bestEndIdx = Integer.MAX_VALUE; - - - for (Matcher matcher : this.matchers) { - int endIdx = matcher.match(dataBuffer); - if (endIdx != -1 && - endIdx <= bestEndIdx && - matcher.delimiter().length > this.longestDelimiter.length) { - bestEndIdx = endIdx; - this.longestDelimiter = matcher.delimiter(); - } - } - if (bestEndIdx == Integer.MAX_VALUE) { - this.longestDelimiter = NO_DELIMITER; - return -1; - } - else { - reset(); - return bestEndIdx; - } - } - - @Override - public byte[] delimiter() { - Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!"); - return this.longestDelimiter; - } - - @Override - public void reset() { - for (Matcher matcher : this.matchers) { - matcher.reset(); - } - } - } - } diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java index 2e34843b9c..7c71dc8b72 100644 --- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java @@ -886,18 +886,38 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests { void matcher2(String displayName, DataBufferFactory bufferFactory) { super.bufferFactory = bufferFactory; - DataBuffer foo = stringBuffer("fooobar"); + DataBuffer foo = stringBuffer("foooobar"); byte[] delims = "oo".getBytes(StandardCharsets.UTF_8); DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims); - int result = matcher.match(foo); - assertThat(result).isEqualTo(2); - foo.readPosition(2); - result = matcher.match(foo); - assertThat(result).isEqualTo(3); - foo.readPosition(3); - result = matcher.match(foo); - assertThat(result).isEqualTo(-1); + int endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(2); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(4); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(-1); + + release(foo); + } + + @ParameterizedDataBufferAllocatingTest + void matcher3(String displayName, DataBufferFactory bufferFactory) { + super.bufferFactory = bufferFactory; + + DataBuffer foo = stringBuffer("foooobar"); + + byte[] delims = "oo".getBytes(StandardCharsets.UTF_8); + DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims); + int endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(2); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(4); + foo.readPosition(endIndex + 1); + endIndex = matcher.match(foo); + assertThat(endIndex).isEqualTo(-1); release(foo); }