Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 28 additions & 28 deletions src/main/java/com/github/rschmitt/dynamicobject/DynamicObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,35 +21,35 @@
public interface DynamicObject<D extends DynamicObject<D>> extends Map {
/**
* @return the underlying Clojure map backing this instance. Downcasting the return value of this method to any
* particular Java type (e.g. IPersistentMap) is not guaranteed to work with future versions of Clojure.
* particular Java type (e.g. IPersistentMap) is not guaranteed to work with future versions of Clojure.
*/
Map getMap();

/**
* @return the apparent type of this instance. Note that {@code getClass} will return the class of the interface
* proxy and not the interface itself.
* proxy and not the interface itself.
*/
Class<D> getType();

/**
* Invokes clojure.pprint/pprint, which writes a pretty-printed representation of the object to the currently bound
* value of *out*, which defaults to System.out (stdout).
*/
void prettyPrint();

/**
* Like {@link DynamicObject#prettyPrint}, but returns the pretty-printed string instead of writing it to *out*.
*/
String toFormattedString();

/**
* Return a copy of this instance with {@code other}'s fields merged in (nulls don't count). If a given field is
* present in both instances, the fields in {@code other} will take precedence.
* <p/>
* Equivalent to: {@code (merge-with (fn [a b] (if (nil? b) a b)) this other)}
*/
D merge(D other);

/**
* Recursively compares this instance with {@code other}, returning a new instance containing all of the common
* elements of both {@code this} and {@code other}. Maps and lists are compared recursively; everything else,
Expand All @@ -58,22 +58,22 @@ public interface DynamicObject<D extends DynamicObject<D>> extends Map {
* Equivalent to: {@code (nth (clojure.data/diff this other) 2)}
*/
D intersect(D other);

/**
* Recursively compares this instance with {@code other}, similar to {@link #intersect}, but returning the fields that
* are unique to {@code this}. Uses the same recursion strategy as {@code intersect}.
* <p/>
* Equivalent to: {@code (nth (clojure.data/diff this other) 0)}
*/
D subtract(D other);

/**
* Validate that all fields annotated with @Required are non-null, and that all present fields are of the correct
* type. Returns the validated instance unchanged, which allows the validate method to be called at the end of a
* fluent builder chain.
*/
D validate();

/**
* Post-deserialization hook. The intended use of this method is to facilitate format upgrades. For example, if a
* new field has been added to a DynamicObject schema, this method can be implemented to add that field (with
Expand All @@ -85,50 +85,50 @@ public interface DynamicObject<D extends DynamicObject<D>> extends Map {
*/
@Deprecated
D afterDeserialization();

/**
* Serialize the given object to Edn. Any {@code EdnTranslator}s that have been registered through
* {@link DynamicObject#registerType} will be invoked as needed.
*/
static String serialize(Object o) {
return EdnSerialization.serialize(o);
}

static void serialize(Object o, Writer w) {
EdnSerialization.serialize(o, w);
}

/**
* Deserializes a DynamicObject or registered type from a String.
*
* @param edn The Edn representation of the object.
* @param edn The Edn representation of the object.
* @param type The type of class to deserialize. Must be an interface that extends DynamicObject.
*/
static <T> T deserialize(String edn, Class<T> type) {
return EdnSerialization.deserialize(edn, type);
}

/**
* Lazily deserialize a stream of top-level Edn elements as the given type.
*/
static <T> Stream<T> deserializeStream(PushbackReader streamReader, Class<T> type) {
return EdnSerialization.deserializeStream(streamReader, type);
}

/**
* Serialize a single object {@code o} to binary Fressian data.
*/
static byte[] toFressianByteArray(Object o) {
return FressianSerialization.toFressianByteArray(o);
}

/**
* Deserialize and return the Fressian-encoded object in {@code bytes}.
*/
static <T> T fromFressianByteArray(byte[] bytes) {
return FressianSerialization.fromFressianByteArray(bytes);
}

/**
* Create a {@link FressianReader} instance to read from {@code is}. The reader will be created with support for all
* the basic Java and Clojure types, all DynamicObject types registered by calling {@link #registerTag(Class,
Expand All @@ -140,7 +140,7 @@ static <T> T fromFressianByteArray(byte[] bytes) {
static FressianReader createFressianReader(InputStream is, boolean validateChecksum) {
return FressianSerialization.createFressianReader(is, validateChecksum);
}

/**
* Create a {@link FressianWriter} instance to write to {@code os}. The writer will be created with support for all
* the basic Java and Clojure types, all DynamicObject types registered by calling {@link #registerTag(Class,
Expand All @@ -151,68 +151,68 @@ static FressianReader createFressianReader(InputStream is, boolean validateCheck
static FressianWriter createFressianWriter(OutputStream os) {
return FressianSerialization.createFressianWriter(os);
}

/**
* Lazily deserialize a stream of Fressian-encoded values as the given type. A Fressian footer, if encountered, will
* be validated.
*/
static <T> Stream<T> deserializeFressianStream(InputStream is, Class<T> type) {
return FressianSerialization.deserializeFressianStream(is, type);
}

/**
* Use the supplied {@code map} to back an instance of {@code type}.
*/
static <D extends DynamicObject<D>> D wrap(Map map, Class<D> type) {
return Instances.wrap(map, type);
}

/**
* Create a "blank" instance of {@code type}, backed by an empty Clojure map. All fields will be null.
*/
static <D extends DynamicObject<D>> D newInstance(Class<D> type) {
return Instances.newInstance(type);
}

/**
* Register an {@link EdnTranslator} to enable instances of {@code type} to be serialized to and deserialized from
* Edn using reader tags.
*/
static <T> void registerType(Class<T> type, EdnTranslator<T> translator) {
EdnSerialization.registerType(type, translator);
}

/**
* Register a {@link org.fressian.handlers.ReadHandler} and {@link org.fressian.handlers.WriteHandler} to enable
* instances of {@code type} to be serialized to and deserialized from Fressian data.
*/
static void registerType(Class type, String tag, ReadHandler readHandler, WriteHandler writeHandler) {
FressianSerialization.registerType(type, tag, readHandler, writeHandler);
}

/**
* Deregister the given {@code translator}. After this method is invoked, it will no longer be possible to read or
* write instances of {@code type} unless another translator is registered.
*/
static <T> void deregisterType(Class<T> type) {
Serialization.deregisterType(type);
}

/**
* Register a reader tag for a DynamicObject type. This is useful for reading Edn representations of Clojure
* records.
*/
static <D extends DynamicObject<D>> void registerTag(Class<D> type, String tag) {
Serialization.registerTag(type, tag);
}

/**
* Deregister the reader tag for the given DynamicObject type.
*/
static <D extends DynamicObject<D>> void deregisterTag(Class<D> type) {
Serialization.deregisterTag(type);
}

/**
* Specify a default reader, which is a function that will be called when any unknown reader tags are encountered.
* The function will be passed the reader tag (as a string) and the tagged Edn element, and can return whatever it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,39 +16,50 @@

public class InvokeDynamicInvocationHandler implements DynamicInvocationHandler {
private final Class dynamicObjectType;

public InvokeDynamicInvocationHandler(Class dynamicObjectType) {
this.dynamicObjectType = dynamicObjectType;
}

@Override
@SuppressWarnings("unchecked")
public CallSite handleInvocation(
MethodHandles.Lookup lookup,
String methodName,
MethodType methodType,
MethodHandle superMethod
) throws Throwable {
public CallSite handleInvocation(MethodHandles.Lookup lookup, String methodName, MethodType methodType, MethodHandle superMethod) throws Throwable {
Class proxyType = methodType.parameterArray()[0];
MethodHandle mh;
if (superMethod != null && !"validate".equals(methodName)) {
MethodHandle mh = null;
if (superMethod != null && !"validate".equals(methodName) && !"equals".equals(methodName) && !"hashCode".equals(methodName)) {
mh = superMethod.asType(methodType);
return new ConstantCallSite(mh);
}
if ("validate".equals(methodName)) {
mh = lookup.findSpecial(DynamicObjectInstance.class, "$$validate", methodType(Object.class, new Class[]{}), proxyType).asType(methodType);
if ("equals".equals(methodName)) {
try {
mh = lookup.findSpecial(dynamicObjectType, "isEqualTo", methodType(Boolean.class, new Class[] { Object.class }), proxyType).asType(methodType);
} catch (NoSuchMethodException ex) {
mh = superMethod.asType(methodType);
return new ConstantCallSite(mh);
}
} else if ("hashCode".equals(methodName)) {

try {
mh = lookup.findSpecial(dynamicObjectType, "getHashCode", methodType(Integer.class, new Class[] {}), proxyType).asType(methodType);
} catch (NoSuchMethodException ex) {
mh = superMethod.asType(methodType);
return new ConstantCallSite(mh);
}

} else if ("validate".equals(methodName)) {
mh = lookup.findSpecial(DynamicObjectInstance.class, "$$validate", methodType(Object.class, new Class[] {}), proxyType).asType(methodType);
} else if ("$$customValidate".equals(methodName)) {
try {
mh = lookup.findSpecial(dynamicObjectType, "validate", methodType(dynamicObjectType), proxyType);
} catch (NoSuchMethodException ex) {
mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[]{}), proxyType);
mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[] {}), proxyType);
}
mh = mh.asType(methodType);
} else if ("afterDeserialization".equals(methodName)) {
mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[]{}), proxyType).asType(methodType);
mh = lookup.findSpecial(DynamicObjectInstance.class, "$$noop", methodType(Object.class, new Class[] {}), proxyType).asType(methodType);
} else {
Method method = dynamicObjectType.getMethod(methodName, methodType.dropParameterTypes(0, 1).parameterArray());

if (isBuilderMethod(method)) {
Object key = Reflection.getKeyForBuilder(method);
if (Reflection.isMetadataBuilder(method)) {
Expand Down Expand Up @@ -77,7 +88,7 @@ public CallSite handleInvocation(
}
return new ConstantCallSite(mh);
}

private boolean isBuilderMethod(Method method) {
return method.getReturnType().equals(dynamicObjectType) && method.getParameterCount() == 1;
}
Expand Down
102 changes: 102 additions & 0 deletions src/test/java/com/github/rschmitt/dynamicobject/EqualityTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.github.rschmitt.dynamicobject;

import static com.github.rschmitt.dynamicobject.DynamicObject.deserialize;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import org.junit.Test;

public class EqualityTest {
@Test
public void customEqualityTest() {
String edn1 = "{:firstName \"Tom\",:lastName \"Brady\",:ssn \"123456789\" }";
String edn2 = "{:firstName \"Thomas\",:lastName \"Brady\",:ssn \"123456789\" }";
Person person1 = deserialize(edn1, Person.class);
Person person2 = deserialize(edn2, Person.class);
assertEquals(person1, person2);

Set<Person> people = new HashSet<>(Arrays.asList(person1, person2));
assertTrue(people.contains(person1));
}

@Test
public void regualarEqualityTest() {
String edn1 = "{:accountType \"Credit Card\",:name \"Visa\",:number \"123456789\" }";
String edn2 = "{:accountType \"Credit Card\",:name \"Master Card\",:number \"123456789\" }";
Account account1 = deserialize(edn1, Account.class);
Account account2 = deserialize(edn2, Account.class);
assertNotEquals(account1, account2);

String edn3 = "{:accountType \"Credit Card\",:name \"Visa\",:number \"123456789\" }";
String edn4 = "{:accountType \"Credit Card\",:name \"Visa\",:number \"123456789\" }";
Account account3 = deserialize(edn3, Account.class);
Account account4 = deserialize(edn4, Account.class);
assertEquals(account3, account4);

Set<Account> accounts = new HashSet<>(Arrays.asList(account1, account2));
assertTrue(accounts.contains(account1));
}

public interface Person extends DynamicObject<Person> {
@Key(":firstName")
String getFirstName();

@Key(":firstName")
Person withFirstName(String firstName);

@Key(":lastName")
String getLastName();

@Key(":lastName")
Person withLastName(String lastName);

@Key(":ssn")
String getSsn();

@Key(":ssn")
Person withSsn(String ssn);

default Boolean isEqualTo(Object other) {
if (other == this)
return true;
if (other == null)
return false;
if (other instanceof Person) {
Person otherPerson = (Person) other;
return this.getSsn().equals(otherPerson.getSsn());
} else {
return equals(other);
}
}

default Integer getHashCode() {
return getSsn().hashCode();
}

}

public interface Account extends DynamicObject<Account> {
@Key(":accountType")
String getAccountType();

@Key(":accountType")
Account withAccountType(String accountType);

@Key(":name")
String getName();

@Key(":name")
Account withName(String name);

@Key(":number")
String getAccountNumber();

@Key(":number")
Account withAccountNumber(String number);
}
}