From 5f98a05d8bc66894d79621385f00d6c2cf1ff355 Mon Sep 17 00:00:00 2001 From: Matthew Adams Date: Wed, 17 Sep 2014 09:38:10 -0500 Subject: [PATCH] DATACASS-164: app & test code done --- .../repository/support/MapIdFactory.java | 19 ++ .../support/MapIdProxyDelegate.java | 160 +++++++++++++ ...lateMapIdProxyDelegateIntegrationTest.java | 210 ++++++++++++++++++ .../unit/mapidfactory/MapIdFactoryTest.java | 98 ++++++++ 4 files changed, 487 insertions(+) create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdFactory.java create mode 100644 spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdProxyDelegate.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/mapid/proxy/CassandraTemplateMapIdProxyDelegateIntegrationTest.java create mode 100644 spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/unit/mapidfactory/MapIdFactoryTest.java diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdFactory.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdFactory.java new file mode 100644 index 000000000..bb56034ec --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdFactory.java @@ -0,0 +1,19 @@ +package org.springframework.data.cassandra.repository.support; + +import java.lang.reflect.Proxy; + +import org.springframework.data.cassandra.repository.MapId; +import org.springframework.util.Assert; + +@SuppressWarnings("unchecked") +public class MapIdFactory { + + public static T id(Class idInterface) { + Assert.notNull(idInterface); + return id(idInterface, idInterface.getClassLoader()); + } + + public static T id(Class idInterface, ClassLoader loader) { + return (T) Proxy.newProxyInstance(loader, new Class[] { idInterface }, new MapIdProxyDelegate(idInterface)); + } +} diff --git a/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdProxyDelegate.java b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdProxyDelegate.java new file mode 100644 index 000000000..421924e1f --- /dev/null +++ b/spring-data-cassandra/src/main/java/org/springframework/data/cassandra/repository/support/MapIdProxyDelegate.java @@ -0,0 +1,160 @@ +package org.springframework.data.cassandra.repository.support; + +import java.io.Serializable; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Arrays; + +import org.springframework.data.cassandra.repository.MapId; +import org.springframework.util.StringUtils; + +class MapIdProxyDelegate implements InvocationHandler { + + static class Signature { + String name; + Class[] argTypes; + Class returnType; + + Signature(Method method, boolean includeReturnType) { + this(method.getName(), method.getParameterTypes(), includeReturnType ? method.getReturnType() : null); + } + + Signature(String name, Class[] argTypes, Class returnType) { + this.name = name; + this.argTypes = argTypes; + this.returnType = returnType; + } + + @Override + public String toString() { + return "Signature [name=" + name + ", argTypes=" + Arrays.toString(argTypes) + ", returnType=" + returnType + "]"; + } + + @Override + public boolean equals(Object that) { + if (that == null) { + return false; + } + if (this == that) { + return true; + } + if (!(that instanceof Signature)) { + return false; + } + Signature that_ = (Signature) that; + if (!this.name.equals(that_.name)) { + return false; + } + if ((this.argTypes == null && that_.argTypes != null) || (this.argTypes != null && that_.argTypes == null)) { + return false; + } + if (this.argTypes != null) { + if (this.argTypes.length != that_.argTypes.length) { + return false; + } + for (int i = 0; i < this.argTypes.length; i++) { + if (!this.argTypes[i].equals(that_.argTypes[i])) { + return false; + } + } + } + if (this.returnType == null) { + return that_.returnType == null; + } + return this.returnType.equals(that_.returnType); + } + + @Override + public int hashCode() { + return name.hashCode() ^ (argTypes != null ? argTypes.hashCode() : 0) + ^ (returnType != null ? returnType.hashCode() : 0); + } + } + + private static final Signature[] MAP_ID_SIGNATURES; + + static { + Method[] mapIdMethods = MapId.class.getMethods(); + MAP_ID_SIGNATURES = new Signature[mapIdMethods.length]; + int i = 0; + for (Method m : mapIdMethods) { + MAP_ID_SIGNATURES[i++] = new Signature(m, true); + } + } + + MapId delegate = new BasicMapId(); + Class idInterface; + + MapIdProxyDelegate(Class idInterface) { + this.idInterface = idInterface; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (isMapIdMethod(method)) { + return method.invoke(delegate, args); + } + + if (args != null && args.length > 1) { + throw new IllegalArgumentException(String.format("Method [%s] on interface [%s] must take zero or one argument", + method, idInterface)); + } + boolean isSetter = args != null && args.length == 1; + String name = method.getName(); + + if (isSetter) { + handleSetter(name, args[0]); + return void.class.equals(method.getReturnType()) ? null : proxy; + } + + return handleGetter(name); + } + + private boolean isMapIdMethod(Method method) { + for (Signature mapIdSignature : MAP_ID_SIGNATURES) { + if (mapIdSignature.equals(new Signature(method, true))) { + return true; + } + } + return false; + } + + private Serializable handleGetter(String name) { + if (name.startsWith("get")) { + if (name.length() == 3) { + throw new IllegalArgumentException(String.format("Method [%s] on interface [%s] must be of form " + + "' get()' or " + "' ()'", name, idInterface)); + } + name = StringUtils.uncapitalize(name.substring(3)); + } + + return delegate.get(name); + } + + private void handleSetter(String name, Object value) { + int minLength = 1; + boolean isSet = name.startsWith("set"); + boolean isWith = name.startsWith("with"); + minLength += isSet ? 3 : isWith ? 4 : 0; + int length = name.length(); + if (isSet || isWith) { + if (length < minLength) { + throw new IllegalArgumentException(String.format("Method [%s] on interface [%s] must be of form " + + "' set()', " + + "' with()' or " + + "' ()'", name, idInterface)); + } + name = StringUtils.uncapitalize(name.substring(minLength - 1)); + } + + if (value == null) { + delegate.put(name, null); + return; + } + if (!(value instanceof Serializable)) { + throw new IllegalArgumentException(String.format("Given object [%s] must implement %s", value, + Serializable.class.getName())); + } + delegate.put(name, (Serializable) value); + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/mapid/proxy/CassandraTemplateMapIdProxyDelegateIntegrationTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/mapid/proxy/CassandraTemplateMapIdProxyDelegateIntegrationTest.java new file mode 100644 index 000000000..f59a2c988 --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/integration/mapping/mapid/proxy/CassandraTemplateMapIdProxyDelegateIntegrationTest.java @@ -0,0 +1,210 @@ +package org.springframework.data.cassandra.test.integration.mapping.mapid.proxy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.springframework.data.cassandra.repository.support.MapIdFactory.id; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cassandra.core.PrimaryKeyType; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.mapping.Column; +import org.springframework.data.cassandra.mapping.PrimaryKeyColumn; +import org.springframework.data.cassandra.mapping.Table; +import org.springframework.data.cassandra.repository.MapId; +import org.springframework.data.cassandra.test.integration.support.AbstractSpringDataEmbeddedCassandraIntegrationTest; +import org.springframework.data.cassandra.test.integration.support.IntegrationTestConfig; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class CassandraTemplateMapIdProxyDelegateIntegrationTest extends + AbstractSpringDataEmbeddedCassandraIntegrationTest { + + @Configuration + public static class Config extends IntegrationTestConfig { + + @Override + public String[] getEntityBasePackages() { + return new String[] { SinglePkc.class.getPackage().getName() }; + } + } + + @Autowired + CassandraOperations t; + + @Before + public void before() { + assertNotNull(t); + } + + @Test + public void testSinglePkc() { + + // insert + SinglePkc inserted = new SinglePkc(uuid()); + inserted.setValue(uuid()); + SinglePkc saved = t.insert(inserted); + assertSame(saved, inserted); + + // select + SinglePkcId id = id(SinglePkcId.class).key(saved.getKey()); + SinglePkc selected = t.selectOneById(SinglePkc.class, id); + assertNotSame(selected, saved); + assertEquals(saved.getKey(), selected.getKey()); + assertEquals(saved.getValue(), selected.getValue()); + + // update + selected.setValue(uuid()); + SinglePkc updated = t.update(selected); + assertSame(updated, selected); + + selected = t.selectOneById(SinglePkc.class, id); + assertNotSame(selected, updated); + assertEquals(updated.getValue(), selected.getValue()); + + // delete + t.delete(selected); + assertNull(t.selectOneById(SinglePkc.class, id)); + } + + public interface SinglePkcId extends MapId { + SinglePkcId key(String key); + + String key(); + } + + @Table + public static class SinglePkc { + + @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) + String key; + + @Column + String value; + + /** + * @deprecated for persistence use only + */ + @Deprecated + @SuppressWarnings("unused") + private SinglePkc() {} + + public SinglePkc(String key) { + setKey(key); + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + @Test + public void testMultiPkc() { + + // insert + MultiPkc inserted = new MultiPkc(uuid(), uuid()); + inserted.setValue(uuid()); + MultiPkc saved = t.insert(inserted); + assertSame(saved, inserted); + + // select + MultiPkcId id = id(MultiPkcId.class).key0(saved.getKey0()).key1(saved.getKey1()); + MultiPkc selected = t.selectOneById(MultiPkc.class, id); + assertNotSame(selected, saved); + assertEquals(saved.getKey0(), selected.getKey0()); + assertEquals(saved.getKey1(), selected.getKey1()); + assertEquals(saved.getValue(), selected.getValue()); + + // update + selected.setValue(uuid()); + MultiPkc updated = t.update(selected); + assertSame(updated, selected); + + selected = t.selectOneById(MultiPkc.class, id); + assertNotSame(selected, updated); + assertEquals(updated.getValue(), selected.getValue()); + + // delete + t.delete(selected); + assertNull(t.selectOneById(MultiPkc.class, id)); + } + + public interface MultiPkcId extends MapId { + MultiPkcId key0(String key0); + + String key0(); + + MultiPkcId key1(String key1); + + String key1(); + } + + @Table + public static class MultiPkc { + + @PrimaryKeyColumn(ordinal = 0, type = PrimaryKeyType.PARTITIONED) + String key0; + + @PrimaryKeyColumn(ordinal = 1) + String key1; + + @Column + String value; + + /** + * @deprecated for persistence use only + */ + @Deprecated + @SuppressWarnings("unused") + private MultiPkc() {} + + public MultiPkc(String key0, String key1) { + setKey0(key0); + setKey1(key1); + } + + public String getKey0() { + return key0; + } + + public void setKey0(String key0) { + this.key0 = key0; + } + + public String getKey1() { + return key1; + } + + public void setKey1(String key1) { + this.key1 = key1; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } +} diff --git a/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/unit/mapidfactory/MapIdFactoryTest.java b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/unit/mapidfactory/MapIdFactoryTest.java new file mode 100644 index 000000000..0ef97f44b --- /dev/null +++ b/spring-data-cassandra/src/test/java/org/springframework/data/cassandra/test/unit/mapidfactory/MapIdFactoryTest.java @@ -0,0 +1,98 @@ +package org.springframework.data.cassandra.test.unit.mapidfactory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.springframework.data.cassandra.repository.support.MapIdFactory.id; + +import java.util.Random; + +import org.junit.Test; +import org.springframework.data.cassandra.repository.MapId; + +public class MapIdFactoryTest { + + static interface MyId extends MapId { + MyId string(String s); + + void setString(String s); + + MyId withString(String s); + + String string(); + + String getString(); + + MyId number(Integer i); + + void setNumber(Integer i); + + Integer number(); + + Integer getNumber(); + } + + @Test + public void test() { + Random r = new Random(); + String s = "" + r.nextInt(); + Integer i = new Integer(r.nextInt()); + + MyId id = id(MyId.class); + + assertNull(id.string()); + assertNull(id.number()); + assertNull(id.getString()); + assertNull(id.getNumber()); + + id.setNumber(i); + assertEquals(i, id.getNumber()); + assertEquals(i, id.number()); + assertEquals(i, id.get("number")); + + MyId returned = null; + + returned = id.number(i = r.nextInt()); + assertSame(returned, id); + assertEquals(i, id.getNumber()); + assertEquals(i, id.number()); + assertEquals(i, id.get("number")); + + id.put("number", i = r.nextInt()); + assertEquals(i, id.getNumber()); + assertEquals(i, id.number()); + assertEquals(i, id.get("number")); + + id.setString(s); + assertEquals(s, id.getString()); + assertEquals(s, id.string()); + assertEquals(s, id.get("string")); + + returned = id.string(s = "" + r.nextInt()); + assertSame(returned, id); + assertEquals(s, id.getString()); + assertEquals(s, id.string()); + assertEquals(s, id.get("string")); + + returned = id.withString(s = "" + r.nextInt()); + assertSame(returned, id); + assertEquals(s, id.getString()); + assertEquals(s, id.string()); + assertEquals(s, id.get("string")); + + id.put("string", s = "" + r.nextInt()); + assertEquals(s, id.getString()); + assertEquals(s, id.string()); + assertEquals(s, id.get("string")); + + id.setString(null); + assertNull(id.getString()); + assertNull(id.string()); + assertNull(id.get("string")); + + id.setNumber(null); + assertNull(id.getNumber()); + assertNull(id.number()); + assertNull(id.get("number")); + } +}