package com.mastfrog.giulius.mongojack;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Objects;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
/**
* Base class for objects that have some typed fields but can also support
* ad-hoc properties it does not define. This is used in order to write POJO
* classes which can be serialized/deserialized either from the web or from
* MongoDB which are allowed to contain properties not explicitly defined, and
* have a way of retaining them.
* <p/>
* Implementations should use final fields and a constructor annotated with
* @JsonCreator whose arguments use @JsonProperty to identify them -
* these annotations are also used to ensure the catch-all setter cannot be used
* to override property names or cause duplicate keys in the resulting JSON.
* <p/>
* The equals() contract of this class is that if a._id == b._id, the objects
* are equal; if either is null, all metadata which does not have the name _id
* is compared and if all are equal then the objects are equal.
*
* @author Tim Boudreau
*/
public abstract class ExtensibleJsonObject implements Iterable<Map.Entry<String, Object>> {
@JsonIgnore
private final Map<String, Object> metadata = new HashMap<>();
/**
* Gets properties that are not explicitly defined but which are present.
*
* @return The additional properties
*/
@JsonAnyGetter
public Map<String, Object> metadata() {
return metadata;
}
/**
* Setter for Jackson to use with ad-hoc properties.
*
* @param key The property name
* @param value The vaue
*/
@JsonAnySetter
public void put(String key, Object value) {
if (propertyNames().contains(key)) {
throw new IllegalArgumentException("Cannot replace property '" + key
+ "' with ad-hoc value '" + value + "'");
}
metadata.put(key, value);
}
/**
* Iterate non-standard key/value pairs.
*
* @return An iterator
*/
@Override
public Iterator<Map.Entry<String, Object>> iterator() {
return metadata.entrySet().iterator();
}
/**
* Get a property not defined on the child class but which was present at
* deserialization.
*
* @param <T> The type
* @param key The name of the property
* @param type The type
* @return A property or null
*/
public <T> T get(String key, Class<T> type) {
return metadata.containsKey(key) ? type.cast(metadata.get(key)) : null;
}
@Override
public int hashCode() {
return 7 * metadata.hashCode() + getClass().getName().hashCode();
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
} else if (o == null) {
return false;
}
if (o.getClass() == getClass()) {
ExtensibleJsonObject obj = (ExtensibleJsonObject) o;
return metadataEquals(obj.metadata, metadata);
}
return false;
}
public String toString() {
StringBuilder sb = new StringBuilder();
for (Iterator<Map.Entry<String, Object>> it = metadata.entrySet().iterator(); it.hasNext();) {
Map.Entry<String, Object> e = it.next();
sb.append(e.getKey()).append('=').append(e.getValue());
if (it.hasNext()) {
sb.append(",");
}
}
return sb.toString();
}
private static boolean metadataEquals(Map<String, Object> a, Map<String, Object> b) {
Object ida = a.get("_id");
Object idb = b.get("_id");
if (ida != null && idb != null) {
return ida.equals(idb);
} else {
Set<String> allKeys = new HashSet<String>(a.keySet());
allKeys.addAll(b.keySet());
boolean result = true;
for (String key : allKeys) {
if ("_id".equals(key)) {
continue;
}
Object ao = a.get(key);
Object bo = b.get(key);
result = Objects.equal(ao, bo);
if (!result) {
break;
}
}
return result;
}
}
/**
* Return a set of those property names which *are* defined on this class,
* and therefore should not be used with the catch-all setter. The default
* implementation looks at the constructor arguments for @JsonProperty
* annotations returns the set of all such property names.
*
* @return The set of property names this subclass defines.
*/
protected Set<String> propertyNames() {
Set<String> cached = cache.get(getClass());
if (cached != null) {
return cached;
}
Set<String> result = new HashSet<>();
for (Constructor c : getClass().getConstructors()) {
// int pc = c.getParameterCount(); // JDK 8
int pc = c.getParameterTypes().length;
Annotation[][] annos = c.getParameterAnnotations();
for (int i = 0; i < pc; i++) {
Annotation[] curr = annos[i];
for (Annotation a : curr) {
if (a instanceof JsonProperty) {
JsonProperty p = (JsonProperty) a;
result.add(p.value());
}
}
}
}
cache.put(getClass(), result);
return result;
}
private static final Map<Class<?>, Set<String>> cache = new HashMap<>();
}