diff --git a/src/main/java/com/redislabs/modules/rejson/JReJSON.java b/src/main/java/com/redislabs/modules/rejson/JReJSON.java index a3a5f8b..1a73e1a 100644 --- a/src/main/java/com/redislabs/modules/rejson/JReJSON.java +++ b/src/main/java/com/redislabs/modules/rejson/JReJSON.java @@ -28,6 +28,12 @@ package com.redislabs.modules.rejson; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import com.google.gson.Gson; import redis.clients.jedis.Jedis; @@ -36,9 +42,6 @@ import redis.clients.jedis.util.Pool; import redis.clients.jedis.util.SafeEncoder; -import java.util.ArrayList; -import java.util.List; - /** * JReJSON is the main ReJSON client class, wrapping connection management and all ReJSON commands */ @@ -49,6 +52,7 @@ public class JReJSON { private enum Command implements ProtocolCommand { DEL("JSON.DEL"), GET("JSON.GET"), + MGET("JSON.MGET"), SET("JSON.SET"), TYPE("JSON.TYPE"); private final byte[] raw; @@ -217,6 +221,46 @@ public T get(String key, Class clazz, Path... paths) { return gson.fromJson(rep, clazz); } + /** + * Returns the documents from multiple keys. Non-existing keys are reported as + * null. + * + * @param target class to serialize results + * @param clazz target class to serialize results + * @param keys keys for the JSON documents + * @return a List of documents rooted at path + */ + public List mget(Class clazz, String... keys) { + return mget(Path.ROOT_PATH, clazz, keys); + } + + /** + * Returns the values at path from multiple keys. Non-existing keys and + * non-existing paths are reported as null. + * + * @param path common path across all documents to root the results on + * @param target class to serialize results + * @param clazz target class to serialize results + * @param keys keys for the JSON documents + * @return a List of documents rooted at path + */ + public List mget(Path path, Class clazz, String... keys) { + String[] args = Stream // + .of(keys, new String[] { path.toString() }) // + .flatMap(Stream::of) // + .toArray(String[]::new); + + List rep; + try (Jedis conn = getConnection()) { + conn.getClient().sendCommand(Command.MGET, args); + rep = conn.getClient().getMultiBulkReply(); + } + + return rep.stream() // + .map(r -> gson.fromJson(r, clazz)) // + .collect(Collectors.toList()); + } + /** * Sets an object at the root path * @param key the key name diff --git a/src/test/java/com/redislabs/modules/rejson/ClientTest.java b/src/test/java/com/redislabs/modules/rejson/ClientTest.java index 639cf71..c432520 100644 --- a/src/test/java/com/redislabs/modules/rejson/ClientTest.java +++ b/src/test/java/com/redislabs/modules/rejson/ClientTest.java @@ -34,9 +34,10 @@ import static junit.framework.TestCase.assertSame; import static junit.framework.TestCase.assertTrue; import static junit.framework.TestCase.fail; -import static org.junit.Assert.assertThrows; +import java.util.Collections; import java.util.List; +import java.util.Objects; import org.junit.Before; import org.junit.Test; @@ -44,7 +45,6 @@ import com.google.gson.Gson; import redis.clients.jedis.Jedis; -import redis.clients.jedis.exceptions.JedisDataException; public class ClientTest { @@ -77,6 +77,63 @@ public FooBarObject() { } } + private static class Baz { + private String quuz; + private String grault; + private String waldo; + + public Baz(final String quuz, final String grault, final String waldo) { + this.quuz = quuz; + this.grault = grault; + this.waldo = waldo; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (getClass() != o.getClass()) + return false; + Baz other = (Baz) o; + + return Objects.equals(quuz, other.quuz) && // + Objects.equals(grault, other.grault) && // + Objects.equals(waldo, other.waldo); + } + } + + private static class Qux { + private String quux; + private String corge; + private String garply; + private Baz baz; + + public Qux(final String quux, final String corge, final String garply, final Baz baz) { + this.quux = quux; + this.corge = corge; + this.garply = garply; + this.baz = baz; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null) + return false; + if (getClass() != o.getClass()) + return false; + Qux other = (Qux) o; + + return Objects.equals(quux, other.quux) && // + Objects.equals(corge, other.corge) && // + Objects.equals(garply, other.garply) && // + Objects.equals(baz, other.baz); + } + } + private final Gson g = new Gson(); private final JReJSON client = new JReJSON("localhost",6379); private final Jedis jedis = new Jedis("localhost",6379); @@ -223,6 +280,81 @@ public void typeException() throws Exception { public void type1Exception() throws Exception { client.set( "foobar", new FooBarObject(), Path.ROOT_PATH); client.type( "foobar", new Path(".foo[1]")); - } + } + @Test + public void testMultipleGetAtRootPathAllKeysExist() throws Exception { + Baz baz1 = new Baz("quuz1", "grault1", "waldo1"); + Baz baz2 = new Baz("quuz2", "grault2", "waldo2"); + Qux qux1 = new Qux("quux1", "corge1", "garply1", baz1); + Qux qux2 = new Qux("quux2", "corge2", "garply2", baz2); + + client.set("qux1", qux1); + client.set("qux2", qux2); + + List oneQux = client.mget(Qux.class, "qux1"); + List allQux = client.mget(Qux.class, "qux1", "qux2"); + + assertEquals(1, oneQux.size()); + assertEquals(2, allQux.size()); + + assertEquals(qux1, oneQux.get(0)); + + Qux testQux1 = allQux.stream() // + .filter(q -> q.quux.equals("quux1")) // + .findFirst() // + .orElseThrow(() -> new NullPointerException("")); + Qux testQux2 = allQux.stream() // + .filter(q -> q.quux.equals("quux2")) // + .findFirst() // + .orElseThrow(() -> new NullPointerException("")); + + assertEquals(qux1, testQux1); + assertEquals(qux2, testQux2); + } + + @Test + public void testMultipleGetAtRootPathWithMissingKeys() throws Exception { + Baz baz1 = new Baz("quuz1", "grault1", "waldo1"); + Baz baz2 = new Baz("quuz2", "grault2", "waldo2"); + Qux qux1 = new Qux("quux1", "corge1", "garply1", baz1); + Qux qux2 = new Qux("quux2", "corge2", "garply2", baz2); + + client.set("qux1", qux1); + client.set("qux2", qux2); + + List allQux = client.mget(Qux.class, "qux1", "qux2", "qux3"); + + assertEquals(3, allQux.size()); + assertNull(allQux.get(2)); + allQux.removeAll(Collections.singleton(null)); + assertEquals(2, allQux.size()); + } + + @Test + public void testMultipleGetWithPathPathAllKeysExist() throws Exception { + Baz baz1 = new Baz("quuz1", "grault1", "waldo1"); + Baz baz2 = new Baz("quuz2", "grault2", "waldo2"); + Qux qux1 = new Qux("quux1", "corge1", "garply1", baz1); + Qux qux2 = new Qux("quux2", "corge2", "garply2", baz2); + + client.set("qux1", qux1); + client.set("qux2", qux2); + + List allBaz = client.mget(new Path("baz"), Baz.class, "qux1", "qux2"); + + assertEquals(2, allBaz.size()); + + Baz testBaz1 = allBaz.stream() // + .filter(b -> b.quuz.equals("quuz1")) // + .findFirst() // + .orElseThrow(() -> new NullPointerException("")); + Baz testBaz2 = allBaz.stream() // + .filter(q -> q.quuz.equals("quuz2")) // + .findFirst() // + .orElseThrow(() -> new NullPointerException("")); + + assertEquals(baz1, testBaz1); + assertEquals(baz2, testBaz2); + } }