From fa29ceb12a15b335e158eab3427143a435878c64 Mon Sep 17 00:00:00 2001 From: dengliming Date: Sun, 3 May 2020 13:03:47 +0800 Subject: [PATCH] Add support for STRALGO LCS #1280 Original pull request: #1282. --- .../core/AbstractRedisAsyncCommands.java | 5 + .../core/AbstractRedisReactiveCommands.java | 5 + .../io/lettuce/core/RedisCommandBuilder.java | 8 + .../java/io/lettuce/core/StrAlgoArgs.java | 168 ++++++++++++++++++ .../io/lettuce/core/StringMatchResult.java | 118 ++++++++++++ .../api/async/RedisStringAsyncCommands.java | 11 ++ .../reactive/RedisStringReactiveCommands.java | 11 ++ .../core/api/sync/RedisStringCommands.java | 11 ++ .../NodeSelectionStringAsyncCommands.java | 11 ++ .../api/sync/NodeSelectionStringCommands.java | 11 ++ .../core/output/StringMatchResultOutput.java | 90 ++++++++++ .../io/lettuce/core/protocol/CommandType.java | 2 +- .../lettuce/core/api/RedisStringCommands.java | 9 + .../StringCommandIntegrationTests.java | 48 ++++- 14 files changed, 506 insertions(+), 2 deletions(-) create mode 100644 src/main/java/io/lettuce/core/StrAlgoArgs.java create mode 100644 src/main/java/io/lettuce/core/StringMatchResult.java create mode 100644 src/main/java/io/lettuce/core/output/StringMatchResultOutput.java diff --git a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java index b33facf462..cf69e2a4a2 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisAsyncCommands.java @@ -1427,6 +1427,11 @@ public RedisFuture strlen(K key) { return dispatch(commandBuilder.strlen(key)); } + @Override + public RedisFuture stralgoLcs(StrAlgoArgs args) { + return dispatch(commandBuilder.stralgoLcs(args)); + } + @Override public RedisFuture> sunion(K... keys) { return dispatch(commandBuilder.sunion(keys)); diff --git a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java index e54d93692b..bd4b66ee2f 100644 --- a/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java +++ b/src/main/java/io/lettuce/core/AbstractRedisReactiveCommands.java @@ -1503,6 +1503,11 @@ public Mono strlen(K key) { return createMono(() -> commandBuilder.strlen(key)); } + @Override + public Mono stralgoLcs(StrAlgoArgs strAlgoArgs) { + return createMono(() -> commandBuilder.stralgoLcs(strAlgoArgs)); + } + @Override public Flux sunion(K... keys) { return createDissolvingFlux(() -> commandBuilder.sunion(keys)); diff --git a/src/main/java/io/lettuce/core/RedisCommandBuilder.java b/src/main/java/io/lettuce/core/RedisCommandBuilder.java index 74c3808647..50a155aee4 100644 --- a/src/main/java/io/lettuce/core/RedisCommandBuilder.java +++ b/src/main/java/io/lettuce/core/RedisCommandBuilder.java @@ -1972,6 +1972,14 @@ Command strlen(K key) { return createCommand(STRLEN, new IntegerOutput<>(codec), key); } + Command stralgoLcs(StrAlgoArgs strAlgoArgs) { + LettuceAssert.notNull(strAlgoArgs, "StrAlgoArgs " + MUST_NOT_BE_NULL); + + CommandArgs args = new CommandArgs<>(codec); + strAlgoArgs.build(args); + return createCommand(STRALGO, new StringMatchResultOutput<>(codec, strAlgoArgs.isWithIdx()), args); + } + Command> sunion(K... keys) { notEmpty(keys); diff --git a/src/main/java/io/lettuce/core/StrAlgoArgs.java b/src/main/java/io/lettuce/core/StrAlgoArgs.java new file mode 100644 index 0000000000..ba993faf33 --- /dev/null +++ b/src/main/java/io/lettuce/core/StrAlgoArgs.java @@ -0,0 +1,168 @@ +/* + * Copyright 2011-2020 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. + */ +package io.lettuce.core; + +import io.lettuce.core.internal.LettuceAssert; +import io.lettuce.core.protocol.CommandArgs; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Argument list builder for the Redis STRALGO command. + * Static import the methods from {@link StrAlgoArgs.Builder} and call the methods: {@code block(…)} . + *

+ * {@link StrAlgoArgs} is a mutable object and instances should be used only once to avoid shared mutable state. + * + * @author dengliming + * @since 6.0 + */ +public class StrAlgoArgs implements CompositeArgument { + + private boolean justLen; + private int minMatchLen; + private boolean withMatchLen; + private boolean withIdx; + private By by = By.STRINGS; + private String[] keys; + private Charset charset = StandardCharsets.UTF_8; + + /** + * Builder entry points for {@link StrAlgoArgs}. + */ + public static class Builder { + + /** + * Utility constructor. + */ + private Builder() { + } + + /** + * Creates new {@link StrAlgoArgs} by keys. + * + * @return new {@link StrAlgoArgs} with {@literal By KEYS} set. + */ + public static StrAlgoArgs keys(String... keys) { + return new StrAlgoArgs().by(By.KEYS, keys); + } + + /** + * Creates new {@link StrAlgoArgs} by strings. + * + * @return new {@link StrAlgoArgs} with {@literal By STRINGS} set. + */ + public static StrAlgoArgs strings(String... strings) { + return new StrAlgoArgs().by(By.STRINGS, strings); + } + + /** + * Creates new {@link StrAlgoArgs} by strings and charset. + * + * @return new {@link StrAlgoArgs} with {@literal By STRINGS} set. + */ + public static StrAlgoArgs strings(Charset charset, String... strings) { + return new StrAlgoArgs().by(By.STRINGS, strings).charset(charset); + } + } + /** + * restrict the list of matches to the ones of a given minimal length. + * + * @return {@code this} {@link StrAlgoArgs}. + */ + public StrAlgoArgs minMatchLen(int minMatchLen) { + this.minMatchLen = minMatchLen; + return this; + } + + /** + * Request just the length of the match for results. + * + * @return {@code this} {@link StrAlgoArgs}. + */ + public StrAlgoArgs justLen() { + justLen = true; + return this; + } + + /** + * Request match len for results. + * + * @return {@code this} {@link StrAlgoArgs}. + */ + public StrAlgoArgs withMatchLen() { + withMatchLen = true; + return this; + } + + /** + * Request match position in each strings for results. + * + * @return {@code this} {@link StrAlgoArgs}. + */ + public StrAlgoArgs withIdx() { + withIdx = true; + return this; + } + + public StrAlgoArgs by(By by, String... keys) { + this.by = by; + this.keys = keys; + return this; + } + + public boolean isWithIdx() { + return withIdx; + } + + public StrAlgoArgs charset(Charset charset) { + this.charset = charset; + return this; + } + + public enum By { + STRINGS, KEYS + } + + public void build(CommandArgs args) { + LettuceAssert.notEmpty(keys, "strings or keys must be not empty"); + + args.add("LCS"); + args.add(by.name()); + for (String key : keys) { + if (by == By.STRINGS) { + args.add(key.getBytes(charset)); + } else { + args.add(key); + } + } + if (justLen) { + args.add("LEN"); + } + if (withIdx) { + args.add("IDX"); + } + + if (minMatchLen > 0) { + args.add("MINMATCHLEN"); + args.add(minMatchLen); + } + + if (withMatchLen) { + args.add("WITHMATCHLEN"); + } + } +} diff --git a/src/main/java/io/lettuce/core/StringMatchResult.java b/src/main/java/io/lettuce/core/StringMatchResult.java new file mode 100644 index 0000000000..eb401b93c1 --- /dev/null +++ b/src/main/java/io/lettuce/core/StringMatchResult.java @@ -0,0 +1,118 @@ +/* + * Copyright 2011-2020 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. + */ +package io.lettuce.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * Result for STRALGO command + * + * @author dengliming + */ +public class StringMatchResult { + + private String matchString; + private List matches = new ArrayList<>(); + private long len; + + public StringMatchResult matchString(String matchString) { + this.matchString = matchString; + return this; + } + + public StringMatchResult addMatch(MatchedPosition match) { + this.matches.add(match); + return this; + } + + public StringMatchResult len(long len) { + this.len = len; + return this; + } + + public String getMatchString() { + return matchString; + } + + public List getMatches() { + return matches; + } + + public long getLen() { + return len; + } + + /** + * match position in each strings + */ + public static class MatchedPosition { + private Position a; + private Position b; + private long matchLen; + + public MatchedPosition(Position a, Position b, long matchLen) { + this.a = a; + this.b = b; + this.matchLen = matchLen; + } + + public Position getA() { + return a; + } + + public void setA(Position a) { + this.a = a; + } + + public Position getB() { + return b; + } + + public void setB(Position b) { + this.b = b; + } + + public long getMatchLen() { + return matchLen; + } + + public void setMatchLen(long matchLen) { + this.matchLen = matchLen; + } + } + + /** + * position range + */ + public static class Position { + private final long start; + private final long end; + + public Position(long start, long end) { + this.start = start; + this.end = end; + } + + public long getStart() { + return start; + } + + public long getEnd() { + return end; + } + } +} diff --git a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java index 8daee16124..35b9d4a7c3 100644 --- a/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java +++ b/src/main/java/io/lettuce/core/api/async/RedisStringAsyncCommands.java @@ -23,6 +23,8 @@ import io.lettuce.core.RedisFuture; import io.lettuce.core.SetArgs; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; /** * Asynchronous executed commands for Strings. @@ -374,4 +376,13 @@ public interface RedisStringAsyncCommands { * @return Long integer-reply the length of the string at {@code key}, or {@code 0} when {@code key} does not exist. */ RedisFuture strlen(K key); + + /** + * The STRALGO implements complex algorithms that operate on strings. + * + * Right now the only algorithm implemented is the LCS algorithm (longest common substring). + * @param strAlgoArgs + * @return StringMatchResult + */ + RedisFuture stralgoLcs(StrAlgoArgs strAlgoArgs); } diff --git a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java index 718d12cee6..86282215bc 100644 --- a/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java +++ b/src/main/java/io/lettuce/core/api/reactive/RedisStringReactiveCommands.java @@ -24,6 +24,8 @@ import io.lettuce.core.SetArgs; import io.lettuce.core.Value; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; /** * Reactive executed commands for Strings. @@ -375,4 +377,13 @@ public interface RedisStringReactiveCommands { * @return Long integer-reply the length of the string at {@code key}, or {@code 0} when {@code key} does not exist. */ Mono strlen(K key); + + /** + * The STRALGO implements complex algorithms that operate on strings. + * + * Right now the only algorithm implemented is the LCS algorithm (longest common substring). + * @param strAlgoArgs + * @return StringMatchResult + */ + Mono stralgoLcs(StrAlgoArgs strAlgoArgs); } diff --git a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java index b176d07273..c96a624a6f 100644 --- a/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java +++ b/src/main/java/io/lettuce/core/api/sync/RedisStringCommands.java @@ -22,6 +22,8 @@ import io.lettuce.core.KeyValue; import io.lettuce.core.SetArgs; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; /** * Synchronous executed commands for Strings. @@ -373,4 +375,13 @@ public interface RedisStringCommands { * @return Long integer-reply the length of the string at {@code key}, or {@code 0} when {@code key} does not exist. */ Long strlen(K key); + + /** + * The STRALGO implements complex algorithms that operate on strings. + * + * Right now the only algorithm implemented is the LCS algorithm (longest common substring). + * @param strAlgoArgs + * @return StringMatchResult + */ + StringMatchResult stralgoLcs(StrAlgoArgs strAlgoArgs); } diff --git a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java index 00114bd641..7e25a57fdd 100644 --- a/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/async/NodeSelectionStringAsyncCommands.java @@ -22,6 +22,8 @@ import io.lettuce.core.KeyValue; import io.lettuce.core.SetArgs; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; /** * Asynchronous executed commands on a node selection for Strings. @@ -373,4 +375,13 @@ public interface NodeSelectionStringAsyncCommands { * @return Long integer-reply the length of the string at {@code key}, or {@code 0} when {@code key} does not exist. */ AsyncExecutions strlen(K key); + + /** + * The STRALGO implements complex algorithms that operate on strings. + * + * Right now the only algorithm implemented is the LCS algorithm (longest common substring). + * @param strAlgoArgs + * @return StringMatchResult + */ + AsyncExecutions stralgoLcs(StrAlgoArgs strAlgoArgs); } diff --git a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java index 3e0e9b437e..66b5ee4891 100644 --- a/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java +++ b/src/main/java/io/lettuce/core/cluster/api/sync/NodeSelectionStringCommands.java @@ -22,6 +22,8 @@ import io.lettuce.core.KeyValue; import io.lettuce.core.SetArgs; import io.lettuce.core.output.KeyValueStreamingChannel; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; /** * Synchronous executed commands on a node selection for Strings. @@ -373,4 +375,13 @@ public interface NodeSelectionStringCommands { * @return Long integer-reply the length of the string at {@code key}, or {@code 0} when {@code key} does not exist. */ Executions strlen(K key); + + /** + * The STRALGO implements complex algorithms that operate on strings. + * + * Right now the only algorithm implemented is the LCS algorithm (longest common substring). + * @param strAlgoArgs + * @return StringMatchResult + */ + Executions stralgoLcs(StrAlgoArgs strAlgoArgs); } diff --git a/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java b/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java new file mode 100644 index 0000000000..f221f61f14 --- /dev/null +++ b/src/main/java/io/lettuce/core/output/StringMatchResultOutput.java @@ -0,0 +1,90 @@ +/* + * Copyright 2018-2020 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. + */ +package io.lettuce.core.output; + +import io.lettuce.core.StringMatchResult; +import static io.lettuce.core.StringMatchResult.MatchedPosition; +import static io.lettuce.core.StringMatchResult.Position; +import io.lettuce.core.codec.RedisCodec; +import java.nio.ByteBuffer; +import java.util.*; + +/** + * {@link StringMatchResult}. + * + * @author dengliming + * @since 6.0.0 + */ +public class StringMatchResultOutput extends CommandOutput { + + private boolean withIdx; + private String matchString; + private int len; + private List positions; + + public StringMatchResultOutput(RedisCodec codec, boolean withIdx) { + super(codec, new StringMatchResult()); + this.withIdx = withIdx; + } + + @Override + public void set(ByteBuffer bytes) { + if (!withIdx && matchString == null) { + matchString = (String) codec.decodeKey(bytes); + output.matchString(matchString); + } + } + + @Override + public void set(long integer) { + this.len = (int) integer; + + if (positions == null) { + positions = new ArrayList<>(); + } + positions.add(integer); + } + + @Override + public void multi(int count) { + } + + @Override + public void complete(int depth) { + if (depth == 2) { + output.addMatch(buildMatchedString(positions)); + positions = null; + } + if (depth == 0) { + output.len(len); + } + } + + private MatchedPosition buildMatchedString(List positions) { + if (positions == null) { + return null; + } + + int size = positions.size(); + // not WITHMATCHLEN + long matchLen = size % 2 == 0 ? 0L : positions.get(size - 1); + return new MatchedPosition( + new Position(positions.get(0), positions.get(1)), + new Position(positions.get(2), positions.get(3)), + matchLen + ); + } +} diff --git a/src/main/java/io/lettuce/core/protocol/CommandType.java b/src/main/java/io/lettuce/core/protocol/CommandType.java index 7c747caf45..df3cb35b02 100644 --- a/src/main/java/io/lettuce/core/protocol/CommandType.java +++ b/src/main/java/io/lettuce/core/protocol/CommandType.java @@ -37,7 +37,7 @@ public enum CommandType implements ProtocolKeyword { // String - APPEND, GET, GETRANGE, GETSET, MGET, MSET, MSETNX, SET, SETEX, PSETEX, SETNX, SETRANGE, STRLEN, + APPEND, GET, GETRANGE, GETSET, MGET, MSET, MSETNX, SET, SETEX, PSETEX, SETNX, SETRANGE, STRLEN, STRALGO, // Numeric diff --git a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java index 72d22fe338..d76cf45f2f 100644 --- a/src/main/templates/io/lettuce/core/api/RedisStringCommands.java +++ b/src/main/templates/io/lettuce/core/api/RedisStringCommands.java @@ -374,4 +374,13 @@ public interface RedisStringCommands { * @return Long integer-reply the length of the string at {@code key}, or {@code 0} when {@code key} does not exist. */ Long strlen(K key); + + /** + * The STRALGO implements complex algorithms that operate on strings. + * + * Right now the only algorithm implemented is the LCS algorithm (longest common substring). + * @param strAlgoArgs + * @return StringMatchResult + */ + StringMatchResult stralgoLcs(StrAlgoArgs strAlgoArgs); } diff --git a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java index fd2020c67e..65f0fb38c8 100644 --- a/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java +++ b/src/test/java/io/lettuce/core/commands/StringCommandIntegrationTests.java @@ -34,6 +34,9 @@ import io.lettuce.core.RedisException; import io.lettuce.core.TestSupport; import io.lettuce.core.api.sync.RedisCommands; +import io.lettuce.core.StrAlgoArgs; +import io.lettuce.core.StringMatchResult; +import static io.lettuce.core.StringMatchResult.Position; import io.lettuce.test.KeyValueStreamingAdapter; import io.lettuce.test.LettuceExtension; import io.lettuce.test.condition.EnabledOnCommand; @@ -238,4 +241,47 @@ void time() { Long.parseLong(time.get(0)); Long.parseLong(time.get(1)); } -} + + @Test + void strAlgo() { + StringMatchResult matchResult = redis.stralgoLcs(StrAlgoArgs.Builder + .strings("ohmytext", "mynewtext")); + assertThat(matchResult.getMatchString()).isEqualTo("mytext"); + + // STRALGO LCS STRINGS a b + matchResult = redis.stralgoLcs(StrAlgoArgs.Builder + .strings("a", "b").minMatchLen(4).withIdx().withMatchLen()); + assertThat(matchResult.getMatchString()).isNullOrEmpty(); + assertThat(matchResult.getLen()).isEqualTo(0); + } + + @Test + void strAlgoJustLen() { + StringMatchResult matchResult = redis.stralgoLcs(StrAlgoArgs.Builder + .strings("ohmytext", "mynewtext").justLen()); + assertThat(matchResult.getLen()).isEqualTo(6); + } + + @Test + void strAlgoWithMinMatchLen() { + StringMatchResult matchResult = redis.stralgoLcs(StrAlgoArgs.Builder + .strings("ohmytext", "mynewtext").minMatchLen(4)); + assertThat(matchResult.getMatchString()).isEqualTo("mytext"); + } + + @Test + void strAlgoWithIdx() { + // STRALGO LCS STRINGS ohmytext mynewtext IDX MINMATCHLEN 4 WITHMATCHLEN + StringMatchResult matchResult = redis.stralgoLcs(StrAlgoArgs.Builder + .strings("ohmytext", "mynewtext").minMatchLen(4).withIdx().withMatchLen()); + assertThat(matchResult.getMatches()).hasSize(1); + assertThat(matchResult.getMatches().get(0).getMatchLen()).isEqualTo(4); + Position a = matchResult.getMatches().get(0).getA(); + Position b = matchResult.getMatches().get(0).getB(); + assertThat(a.getStart()).isEqualTo(4); + assertThat(a.getEnd()).isEqualTo(7); + assertThat(b.getStart()).isEqualTo(5); + assertThat(b.getEnd()).isEqualTo(8); + assertThat(matchResult.getLen()).isEqualTo(6); + } +} \ No newline at end of file