/*
* #%L
* FlatPack serialization code
* %%
* Copyright (C) 2012 Perka Inc.
* %%
* 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.
* #L%
*/
package com.getperka.flatpack.codexes;
import static com.getperka.flatpack.util.FlatPackTypes.erase;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import javax.inject.Inject;
import javax.inject.Provider;
import com.getperka.flatpack.FlatPackVisitor;
import com.getperka.flatpack.HasUuid;
import com.getperka.flatpack.PostUnpack;
import com.getperka.flatpack.PreUnpack;
import com.getperka.flatpack.ext.Codex;
import com.getperka.flatpack.ext.DeserializationContext;
import com.getperka.flatpack.ext.DeserializationContext.EntitySource;
import com.getperka.flatpack.ext.EntityResolver;
import com.getperka.flatpack.ext.JsonKind;
import com.getperka.flatpack.ext.Property;
import com.getperka.flatpack.ext.SerializationContext;
import com.getperka.flatpack.ext.Type;
import com.getperka.flatpack.ext.TypeContext;
import com.getperka.flatpack.ext.UpdatingCodex;
import com.getperka.flatpack.ext.VisitorContext;
import com.getperka.flatpack.ext.Walker;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonWriter;
import com.google.inject.TypeLiteral;
/**
* Support for reading and writing entities that are known by {@link TypeContext}.
*
* @param <T> the type of entity to encode
*/
public class EntityCodex<T extends HasUuid> extends Codex<T> {
class PropertyWalker implements Walker<Property> {
private final T entity;
PropertyWalker(T entity) {
this.entity = entity;
}
@Override
public void walk(FlatPackVisitor visitor, Property prop, VisitorContext<Property> context) {
if (visitor.visit(prop, context)) {
@SuppressWarnings("unchecked")
Codex<Object> codex = (Codex<Object>) prop.getCodex();
Object value = getProperty(prop, entity);
Object newValue = context.walkProperty(prop, codex).accept(visitor, value);
// Object comparison intentional
if (value != newValue) {
setProperty(prop, entity, newValue);
}
}
visitor.endVisit(prop, context);
}
}
private Class<T> clazz;
@Inject
private EntityResolver entityResolver;
@com.google.inject.Inject(optional = true)
private Provider<T> provider;
private List<Method> preUnpackMethods;
private List<Method> postUnpackMethods;
@Inject
private TypeContext typeContext;
protected EntityCodex() {}
@Override
public void acceptNotNull(FlatPackVisitor visitor, T entity, VisitorContext<T> context) {
// See if there's a mare specific codex type that should be used instead
@SuppressWarnings("unchecked")
Codex<T> maybeSubtype = (Codex<T>) typeContext.getCodex(entity.getClass());
if (this != maybeSubtype) {
maybeSubtype.acceptNotNull(visitor, entity, context);
return;
}
// Call visitValue first
if (visitor.visitValue(entity, this, context)) {
if (visitor.visit(entity, this, context)) {
// Traverse all properties
PropertyWalker walker = new PropertyWalker(entity);
for (Property prop : typeContext.describe(clazz).getProperties()) {
context.walkImmutable(walker).accept(visitor, prop);
}
}
visitor.endVisit(entity, this, context);
}
visitor.endVisitValue(entity, this, context);
}
/**
* Performs a minimal amount of work to create an empty stub object to fill in later.
*
* @param element a JsonObject containing a {@code uuid} property.
* @param context this method will call {@link DeserializationContext#putEntity} to store the
* newly-allocated entity
*/
public T allocate(JsonElement element, DeserializationContext context) {
JsonElement uuidElement = element.getAsJsonObject().get("uuid");
if (uuidElement == null) {
context.fail(new IllegalArgumentException("Data entry missing uuid:\n"
+ element.toString()));
}
UUID uuid = UUID.fromString(uuidElement.getAsString());
return allocate(uuid, element, context, true);
}
public T allocateEmbedded(JsonElement element, DeserializationContext context) {
return allocate(UUID.randomUUID(), element, context, false);
}
@Override
public Type describe() {
return new Type.Builder()
.withJsonKind(JsonKind.STRING)
.withName(typeContext.describe(clazz).getTypeName())
.build();
}
public List<Method> getPostUnpackMethods() {
return postUnpackMethods;
}
public List<Method> getPreUnpackMethods() {
return preUnpackMethods;
}
@Override
public String getPropertySuffix() {
return "Uuid";
}
@Override
public T readNotNull(JsonElement element, DeserializationContext context) {
UUID uuid = UUID.fromString(element.getAsString());
HasUuid entity = context.getEntity(uuid);
/*
* If the UUID is a reference to an entity that isn't in the data section, delegate to the
* allocate() method. The entity will either be provided by an EntityResolver or a blank entity
* will be created if possible.
*/
if (entity == null) {
entity = allocate(uuid, element, context, true);
}
try {
return clazz.cast(entity);
} catch (ClassCastException e) {
throw new ClassCastException("Cannot cast a " + entity.getClass().getName()
+ " to a " + clazz.getName() + ". Duplicate UUID in data payload?");
}
}
/**
* For debugging use only.
*/
@Override
public String toString() {
return clazz.getCanonicalName();
}
@Override
public void writeNotNull(T object, SerializationContext context) throws IOException {
JsonWriter writer = context.getWriter();
writer.value(object.getUuid().toString());
}
/**
* A hook point for custom subtypes to synthesize property values. The default implementation
* invokes the method returned from {@link Property#getGetter()}.
*
* @param property the property being read
* @param target the object from which the property is being read
* @return the property value
* @throws Exception subclasses may delegate error handling to EntityCodex
*/
protected Object getProperty(Property property, HasUuid target) {
try {
return property.getGetter() == null ? null : property.getGetter().invoke(target);
} catch (Exception e) {
throw new RuntimeException("Could not retrieve property value", e);
}
}
/**
* A hook point for custom subtypes to synthesize property values. The default implementation
* invokes the method returned from {@link Property#getSetter()}.
*
* @param property the property being read
* @param target the object from which the property is being read
* @param value the new property value
* @throws Exception subclasses may delegate error handling to EntityCodex
*/
protected void setProperty(Property property, T target, Object value) {
if (property.getSetter() != null) {
try {
// Allow some properties (e.g. collections) to be updated in-place
@SuppressWarnings("unchecked")
Codex<Object> codex = (Codex<Object>) property.getCodex();
if (codex instanceof UpdatingCodex && property.getGetter() != null) {
Object oldValue = property.getGetter().invoke(target);
if (oldValue != null && value != null) {
value = ((UpdatingCodex<Object>) codex).replacementValue(oldValue, value);
}
}
property.getSetter().invoke(target, value);
} catch (Exception e) {
throw new RuntimeException("Could not set property value", e);
}
}
}
@Inject
void inject(TypeLiteral<T> clazz) {
this.clazz = erase(clazz.getType());
List<Method> pre = new ArrayList<Method>();
List<Method> post = new ArrayList<Method>();
// Iterate over all methods in the type and then its supertypes
for (Class<?> lookAt = this.clazz; lookAt != null; lookAt = lookAt.getSuperclass()) {
for (Method m : lookAt.getDeclaredMethods()) {
Class<?>[] params = m.getParameterTypes();
switch (params.length) {
case 0:
if (m.isAnnotationPresent(PreUnpack.class)) {
m.setAccessible(true);
pre.add(m);
}
if (m.isAnnotationPresent(PostUnpack.class)) {
m.setAccessible(true);
post.add(m);
}
break;
case 1:
if (m.isAnnotationPresent(PreUnpack.class) && params[0].equals(JsonObject.class)) {
m.setAccessible(true);
pre.add(m);
}
break;
}
}
}
// Reverse the list to call supertype methods first
Collections.reverse(pre);
Collections.reverse(post);
preUnpackMethods = pre.isEmpty() ? Collections.<Method> emptyList() :
Collections.unmodifiableList(pre);
postUnpackMethods = post.isEmpty() ? Collections.<Method> emptyList() :
Collections.unmodifiableList(post);
}
private T allocate(UUID uuid, JsonElement element, DeserializationContext context,
boolean useResolvers) {
T toReturn = null;
// Possibly delegate to injected resolvers
if (useResolvers) {
try {
toReturn = entityResolver.resolve(clazz, uuid);
if (toReturn != null) {
context.putEntity(uuid, toReturn, EntitySource.RESOLVED);
}
} catch (Exception e) {
context.fail(e);
}
}
// Otherwise try to construct a new instance
if (toReturn == null && provider != null) {
toReturn = provider.get();
toReturn.setUuid(uuid);
context.putEntity(uuid, toReturn, EntitySource.CREATED);
}
return toReturn;
}
}