DATACASS-164: dyn proxies now implement Serializable, don't require MapId extension

This commit is contained in:
Matthew Adams
2014-09-19 08:05:21 -05:00
parent dfa4b34a10
commit e3630f6917
7 changed files with 514 additions and 87 deletions

View File

@@ -0,0 +1,29 @@
package org.springframework.data.cassandra.repository.support;
import java.lang.reflect.Method;
public class IdInterfaceException extends RuntimeException {
private static final long serialVersionUID = -1635695314254522703L;
String idInterfaceName;
String method;
public IdInterfaceException(Class<?> idInterface, Method method, String message) {
this(idInterface.getClass().getName(), method == null ? null : method.toString(), message);
}
public IdInterfaceException(String idInterfaceName, String method, String message) {
super(message);
this.idInterfaceName = idInterfaceName;
this.method = method;
}
public String getIdInterfaceName() {
return idInterfaceName;
}
public String getMethod() {
return method;
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2013-2014 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
*
* http://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 org.springframework.data.cassandra.repository.support;
import java.util.Collection;
import java.util.LinkedList;
/**
* Aggregator of multiple violations for convenience when verifying id interfaces. This allows the framework to
* communicate all errors at once, rather than one at a time.
*
* @author Matthew T. Adams
*/
@SuppressWarnings("serial")
public class IdInterfaceExceptions extends RuntimeException {
Collection<IdInterfaceException> exceptions = new LinkedList<IdInterfaceException>();
String idInterfaceName;
public IdInterfaceExceptions(Class<?> idInterface) {
this.idInterfaceName = idInterface.getClass().getName();
}
public void add(IdInterfaceException e) {
exceptions.add(e);
}
/**
* Returns a list of the {@link IdInterfaceException}s aggregated within.
*/
public Collection<IdInterfaceException> getExceptions() {
return exceptions;
}
/**
* Returns a list of the {@link IdInterfaceException} messages aggregated within.
*/
public Collection<String> getMessages() {
Collection<String> messages = new LinkedList<String>();
for (IdInterfaceException e : exceptions) {
messages.add(e.getMessage());
}
return messages;
}
/**
* Returns the number of exceptions aggregated in this exception.
*/
public int getCount() {
return exceptions.size();
}
@Override
public String getMessage() {
StringBuilder builder = new StringBuilder(idInterfaceName).append(":\n");
for (IdInterfaceException e : exceptions) {
builder.append(e.getMessage()).append("\n");
}
return builder.toString();
}
public String getIdInterfaceName() {
return idInterfaceName;
}
}

View File

@@ -0,0 +1,100 @@
package org.springframework.data.cassandra.repository.support;
import java.io.Serializable;
import java.lang.reflect.Method;
import org.springframework.data.cassandra.repository.MapId;
public class IdInterfaceValidator {
/**
* Validates the form of the given id interface candidate type. If the interface violates the following restrictions,
* then an {@link IdInterfaceExceptions} is thrown containing all of the violations encountered, which can be obtained
* from {@link IdInterfaceExceptions#getExceptions()} or, as a convenience,
* {@link IdInterfaceExceptions#getMessages()}.
* <p/>
* Id interfaces are intended to have methods representing setters and getters. Getter methods take the form
* <ul>
* <li><code>PropertyType getPropertyName()</code> or</li>
* <li><code>PropertyType propertyName()</code>.</li>
* </ul>
* Setter methods take the form
* <ul>
* <li><code>void|IdType setPropertyName(PropertyType)</code>,</li>
* <li><code>void|IdType withPropertyName(PropertyType)</code>, or</li>
* <li><code>void|IdType propertyName(PropertyType)</code>;</li>
* </ul>
* setter methods may also declare that they return their id interface type to support method chaining.
* <p/>
* Id interfaces
* <ul>
* <li>must be an <code>interface</code>, not a <code>class</code>,</li>
* <li>may extend {@link Serializable},</li>
* <li>may extend {@link MapId},</li>
* <li>must have getter methods that only return a {@link Serializable} type,</li>
* <li>must have setter methods that only take a single {@link Serializable} type,</li>
* <li>must have setter methods that only return <code>void</code> or the id interface's type,</li>
* <li>must not define any getter methods with the literal name "get", and</li>
* <li>must not define any setter methods with the literal names "set" or "with".</li>
* </ul>
*
* @param id The candidate interface type
* @throws IdInterfaceExceptions
* @see IdInterfaceExceptions#getExceptions()
* @see {@link IdInterfaceException}
*/
public static void validate(Class<?> id) {
IdInterfaceExceptions x = new IdInterfaceExceptions(id);
if (!id.isInterface()) {
x.add(new IdInterfaceException(id, null, "id type must be an interface"));
}
Class<?>[] interfaces = id.getInterfaces();
if (interfaces.length > 2
|| ((interfaces.length == 1 && !(interfaces[0].equals(Serializable.class) || interfaces[0].equals(MapId.class))))) {
x.add(new IdInterfaceException(id, null, "id type may only extend Serializable and/or MapId"));
}
for (Method m : id.getDeclaredMethods()) {
Class<?>[] args = m.getParameterTypes();
String name = m.getName();
Class<?> ret = m.getReturnType();
switch (args.length) {
case 0: // then getter
if (name.startsWith("get") && name.length() == 3) {
x.add(new IdInterfaceException(id, m, "getter methods must have a property name following 'get' prefix"));
}
if (!Serializable.class.isAssignableFrom(ret)) {
x.add(new IdInterfaceException(id, m, "getter methods must return Serializable types"));
}
break;
case 1: // then setter
if (name.startsWith("set") && name.length() == 3) {
x.add(new IdInterfaceException(id, m, "setter methods must have a property name following 'set' prefix"));
}
if (name.startsWith("with") && name.length() == 4) {
x.add(new IdInterfaceException(id, m, "setter methods must have a property name following 'with' prefix"));
}
if (!void.class.equals(ret) && !id.equals(ret)) {
x.add(new IdInterfaceException(id, m,
"setter methods not returning void may only return the same type as their id interface"));
}
Class<?> arg = args[0];
if (!Serializable.class.isAssignableFrom(arg)) {
x.add(new IdInterfaceException(id, m, "setter methods must take exactly one Serializable type"));
}
break;
default:
x.add(new IdInterfaceException(id, m,
"id interface methods may only take zero parameters for a getter or one parameter for a setter; found "
+ args.length));
}
}
if (x.getCount() > 0) {
throw x;
}
}
}

View File

@@ -1,19 +1,63 @@
package org.springframework.data.cassandra.repository.support;
import java.io.Serializable;
import java.lang.reflect.Proxy;
import org.springframework.data.cassandra.repository.MapId;
import org.springframework.util.Assert;
/**
* Factory class for producing implementations of given id interfaces. For restrictions on id interfaces definitions,
* see {@link IdInterfaceValidator#validate(Class)}.
*
* @see IdInterfaceValidator#validate(Class)
* @author Matthew T. Adams
*/
@SuppressWarnings("unchecked")
public class MapIdFactory {
public static <T extends MapId> T id(Class<T> idInterface) {
/**
* Produces an implementation of the given id interface type using the type's class loader. For restrictions on id
* interfaces definitions, see {@link IdInterfaceValidator#validate(Class)}. Returns an implementation of the given
* interface that also implements {@link MapId} and {@link Serializable}, so it can be cast as such if necessary.
*
* @param idInterface The type of the id interface.
* @return An implementation of the given interface that also implements {@link MapId} and {@link Serializable}.
* @see IdInterfaceValidator#validate(Class)
*/
public static <T> T id(Class<T> idInterface) {
Assert.notNull(idInterface);
return id(idInterface, idInterface.getClassLoader());
}
public static <T extends MapId> T id(Class<T> idInterface, ClassLoader loader) {
return (T) Proxy.newProxyInstance(loader, new Class<?>[] { idInterface }, new MapIdProxyDelegate(idInterface));
/**
* Produces an implementation of the given class loader. For restrictions on id interfaces definitions, see
* {@link IdInterfaceValidator#validate(Class)}. Returns an implementation of the given interface that also implements
* {@link MapId} and {@link Serializable}, so it can be cast as such if necessary.
*
* @param idInterface The type of the id interface.
* @return An implementation of the given interface that also implements {@link MapId} and {@link Serializable}.
* @see IdInterfaceValidator#validate(Class)
*/
public static <T> T id(Class<T> idInterface, ClassLoader loader) {
if (MapId.class.equals(idInterface)) {
return (T) new BasicMapId();
}
IdInterfaceValidator.validate(idInterface);
Class<?>[] interfaces = idInterface.getInterfaces();
switch (interfaces.length) {
case 0:
interfaces = new Class<?>[] { idInterface, MapId.class, Serializable.class };
break;
case 1:
Class<?> other = interfaces[0].equals(Serializable.class) ? MapId.class : Serializable.class;
interfaces = new Class<?>[] { idInterface, interfaces[0], other };
break;
default:
interfaces = new Class<?>[] { idInterface, interfaces[0], interfaces[1] };
}
return (T) Proxy.newProxyInstance(loader, interfaces, new MapIdProxyDelegate(idInterface));
}
}

View File

@@ -10,77 +10,15 @@ import java.util.Map;
import org.springframework.data.cassandra.repository.MapId;
import org.springframework.util.StringUtils;
/**
* Delegate class for dynamic proxies of id interfaces; delegates to {@link BasicMapId}.
*
* @see MapIdFactory#id(Class)
* @see MapIdFactory#id(Class, ClassLoader)
* @author Matthew T. Adams
*/
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 String.format("%s %s(%s)", returnType, name, Arrays.toString(argTypes));
}
@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() {
int hash = 37 ^ name.hashCode();
if (argTypes != null) {
for (Class<?> c : argTypes) {
hash ^= c.hashCode();
}
}
if (returnType != null) {
hash ^= returnType.hashCode();
}
return hash;
}
}
private static final Map<Signature, Signature> MAP_ID_SIGNATURES;
static {
@@ -92,10 +30,10 @@ class MapIdProxyDelegate implements InvocationHandler {
}
}
MapId delegate = new BasicMapId();
Class<?> idInterface;
private MapId delegate = new BasicMapId();
private Class<?> idInterface;
MapIdProxyDelegate(Class<?> idInterface) {
public MapIdProxyDelegate(Class<?> idInterface) {
this.idInterface = idInterface;
}
@@ -119,11 +57,11 @@ class MapIdProxyDelegate implements InvocationHandler {
return invokeGetter(method);
}
private boolean isMapIdMethod(Method method) {
public boolean isMapIdMethod(Method method) {
return MAP_ID_SIGNATURES.containsKey(new Signature(method, true));
}
private Serializable invokeGetter(Method method) {
public Serializable invokeGetter(Method method) {
String name = method.getName();
if (name.startsWith("get")) {
if (name.length() == 3) {
@@ -136,7 +74,7 @@ class MapIdProxyDelegate implements InvocationHandler {
return delegate.get(name);
}
private void invokeSetter(Method method, Object value) {
public void invokeSetter(Method method, Object value) {
String name = method.getName();
int minLength = 1;
boolean isSet = name.startsWith("set");
@@ -164,3 +102,72 @@ class MapIdProxyDelegate implements InvocationHandler {
delegate.put(name, (Serializable) value);
}
}
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 String.format("%s %s(%s)", returnType, name, Arrays.toString(argTypes));
}
@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() {
int hash = 37 ^ name.hashCode();
if (argTypes != null) {
for (Class<?> c : argTypes) {
hash ^= c.hashCode();
}
}
if (returnType != null) {
hash ^= returnType.hashCode();
}
return hash;
}
}

View File

@@ -75,7 +75,7 @@ public class CassandraTemplateMapIdProxyDelegateIntegrationTest extends
assertNull(t.selectOneById(SinglePkc.class, id));
}
public interface SinglePkcId extends MapId {
public interface SinglePkcId {
SinglePkcId key(String key);
String key();
@@ -149,7 +149,7 @@ public class CassandraTemplateMapIdProxyDelegateIntegrationTest extends
assertNull(t.selectOneById(MultiPkc.class, id));
}
public interface MultiPkcId extends MapId {
public interface MultiPkcId {
MultiPkcId key0(String key0);
String key0();

View File

@@ -3,27 +3,32 @@ 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.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.springframework.data.cassandra.repository.support.IdInterfaceValidator.validate;
import static org.springframework.data.cassandra.repository.support.MapIdFactory.id;
import java.io.Serializable;
import java.util.Random;
import org.junit.Test;
import org.springframework.data.cassandra.repository.MapId;
import org.springframework.data.cassandra.repository.support.IdInterfaceExceptions;
public class MapIdFactoryTest {
static interface MyId extends MapId {
MyId string(String s);
interface HappyExtendingMapIdAndSerializable extends MapId, Serializable {
HappyExtendingMapIdAndSerializable string(String s);
void setString(String s);
MyId withString(String s);
HappyExtendingMapIdAndSerializable withString(String s);
String string();
String getString();
MyId number(Integer i);
HappyExtendingMapIdAndSerializable number(Integer i);
void setNumber(Integer i);
@@ -33,12 +38,12 @@ public class MapIdFactoryTest {
}
@Test
public void test() {
public void testHappyExtendingMapId() {
Random r = new Random();
String s = "" + r.nextInt();
Integer i = new Integer(r.nextInt());
MyId id = id(MyId.class);
HappyExtendingMapIdAndSerializable id = id(HappyExtendingMapIdAndSerializable.class);
assertNull(id.string());
assertNull(id.number());
@@ -50,7 +55,7 @@ public class MapIdFactoryTest {
assertEquals(i, id.number());
assertEquals(i, id.get("number"));
MyId returned = null;
HappyExtendingMapIdAndSerializable returned = null;
returned = id.number(i = r.nextInt());
assertSame(returned, id);
@@ -95,4 +100,168 @@ public class MapIdFactoryTest {
assertNull(id.number());
assertNull(id.get("number"));
}
interface HappyExtendingNothing {
HappyExtendingNothing string(String s);
void setString(String s);
HappyExtendingNothing withString(String s);
String string();
String getString();
HappyExtendingNothing number(Integer i);
void setNumber(Integer i);
Integer number();
Integer getNumber();
}
@Test
public void testHappyExtendingNothing() {
Random r = new Random();
String s = "" + r.nextInt();
Integer i = new Integer(r.nextInt());
HappyExtendingNothing id = id(HappyExtendingNothing.class);
assertTrue(id instanceof Serializable);
assertTrue(id instanceof MapId);
MapId mapid = (MapId) id;
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, mapid.get("number"));
HappyExtendingNothing returned = null;
returned = id.number(i = r.nextInt());
assertSame(returned, id);
assertEquals(i, id.getNumber());
assertEquals(i, id.number());
assertEquals(i, mapid.get("number"));
mapid.put("number", i = r.nextInt());
assertEquals(i, id.getNumber());
assertEquals(i, id.number());
assertEquals(i, mapid.get("number"));
id.setString(s);
assertEquals(s, id.getString());
assertEquals(s, id.string());
assertEquals(s, mapid.get("string"));
returned = id.string(s = "" + r.nextInt());
assertSame(returned, id);
assertEquals(s, id.getString());
assertEquals(s, id.string());
assertEquals(s, mapid.get("string"));
returned = id.withString(s = "" + r.nextInt());
assertSame(returned, id);
assertEquals(s, id.getString());
assertEquals(s, id.string());
assertEquals(s, mapid.get("string"));
mapid.put("string", s = "" + r.nextInt());
assertEquals(s, id.getString());
assertEquals(s, id.string());
assertEquals(s, mapid.get("string"));
id.setString(null);
assertNull(id.getString());
assertNull(id.string());
assertNull(mapid.get("string"));
id.setNumber(null);
assertNull(id.getNumber());
assertNull(id.number());
assertNull(mapid.get("number"));
}
class IdClass {}
interface Foo {}
interface IdExtendingNotMapId extends Foo {}
interface LiteralGet {
String get();
}
interface GetterReturningVoid {
void getString();
}
interface GetReturningVoid {
void string();
}
interface GetReturningNonSerializable {
Object getFoo();
}
interface MethodWithMoreThanOneArgument {
void foo(Object a, Object b);
}
interface LiteralSet {
void set(String s);
}
interface LiteralWith {
void with(String s);
}
interface SetterMethodNotReturningVoidOrThis {
String string(String s);
}
interface SetMethodNotReturningVoidOrThis {
String setString(String s);
}
interface WithMethodNotReturningVoidOrThis {
String withString(String s);
}
interface SetterMethodTakingNonSerializable {
void string(Object o);
}
interface SetMethodTakingNonSerializable {
void string(Object o);
}
interface WithMethodTakingNonSerializable {
void string(Object o);
}
@Test
public void testUnhappies() {
Class<?>[] interfaces = new Class<?>[] { IdClass.class, IdExtendingNotMapId.class, LiteralGet.class,
GetterReturningVoid.class, GetReturningVoid.class, GetReturningNonSerializable.class,
MethodWithMoreThanOneArgument.class, LiteralSet.class, LiteralWith.class,
SetterMethodNotReturningVoidOrThis.class, SetMethodNotReturningVoidOrThis.class,
WithMethodNotReturningVoidOrThis.class, SetterMethodTakingNonSerializable.class,
SetMethodTakingNonSerializable.class, WithMethodTakingNonSerializable.class };
for (Class<?> i : interfaces) {
try {
validate(i);
fail("should've caught IdInterfaceException validating interface " + i);
} catch (IdInterfaceExceptions e) {
assertEquals(1, e.getCount());
}
}
}
}