com.getperka.flatpack.codexes.EntityCodex.java Source code

Java tutorial

Introduction

Here is the source code for com.getperka.flatpack.codexes.EntityCodex.java

Source

/*
 * #%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.Set;
import java.util.UUID;

import javax.inject.Inject;
import javax.inject.Provider;

import com.getperka.flatpack.HasUuid;
import com.getperka.flatpack.PersistenceAware;
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.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.util.FlatPackCollections;
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> {

    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() {
    }

    /**
     * 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. If {@code null}, a
     *          randomly-generated UUID will be assigned to the allocated object
     * @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());
        T toReturn = allocate(uuid, context, true);

        // Register PostUnpack methods
        if (!postUnpackMethods.isEmpty()) {
            context.addPostWork(new PostUnpackInvoker(toReturn, postUnpackMethods));
        }
        return toReturn;
    }

    @Override
    public Type describe() {
        return new Type.Builder().withJsonKind(JsonKind.STRING).withName(typeContext.getPayloadName(clazz)).build();
    }

    @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, 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?");
        }
    }

    public void readProperties(T object, JsonObject element, DeserializationContext context) {
        context.pushPath("(EntityCodex.readProperties())" + object.getUuid());

        try {
            // Ignore incoming data with just a UUID value to avoid unnecessary warnings
            if (element.entrySet().size() == 1 && element.has("uuid")) {
                return;
            }

            if (!context.checkAccess(object)) {
                return;
            }

            // Allow the object to see the data that's about to be applied
            for (Method m : preUnpackMethods) {
                if (m.getParameterTypes().length == 0) {
                    m.invoke(object);
                } else {
                    m.invoke(object, element);
                }
            }

            List<String> roles = context.getRoles();
            for (Property prop : typeContext.extractProperties(clazz)) {
                if (!prop.maySet(roles)) {
                    continue;
                }

                String simplePropertyName = prop.getName();
                context.pushPath("." + simplePropertyName);
                try {
                    Object value;
                    if (prop.isEmbedded()) {
                        /*
                         * Embedded objects are never referred to by uuid in the payload, so an instance will
                         * need to be allocated before reading in the properties.
                         */
                        @SuppressWarnings("unchecked")
                        EntityCodex<HasUuid> codex = (EntityCodex<HasUuid>) prop.getCodex();
                        HasUuid embedded = codex.allocate(UUID.randomUUID(), context, false);
                        codex.readProperties(embedded, element, context);
                        value = embedded;
                    } else {

                        @SuppressWarnings("unchecked")
                        Codex<Object> codex = (Codex<Object>) prop.getCodex();

                        // merchant would become merchantUuid
                        String payloadPropertyName = simplePropertyName + codex.getPropertySuffix();

                        // Ignore undefined property values, while allowing explicit nullification
                        if (!element.has(payloadPropertyName)) {
                            continue;
                        }

                        value = codex.read(element.get(payloadPropertyName), context);
                    }

                    if (value == null && prop.getSetter().getParameterTypes()[0].isPrimitive()) {
                        // Don't try to pass a null to a primitive setter
                        continue;
                    }

                    // Perhaps set the other side of a OneToMany relationship
                    Property impliedPropery = prop.getImpliedProperty();
                    if (impliedPropery != null && value != null) {
                        // Ensure that any linked property is also mutable
                        if (!impliedPropery.maySet(roles) || !checkAccess(value, context)) {
                            context.addWarning(object,
                                    "Ignoring property %s because the inverse relationship (%s) may not be set",
                                    prop.getName(), impliedPropery.getName());
                            continue;
                        }
                        context.addPostWork(new ImpliedPropertySetter(context, impliedPropery, value, object));
                    }

                    // Set the value
                    prop.getSetter().invoke(object, value);

                    // Record the value as having been set
                    context.addModified(object, prop);
                } catch (Exception e) {
                    context.fail(e);
                } finally {
                    context.popPath();
                }
            }
        } catch (Exception e) {
            context.fail(e);
        } finally {
            context.popPath();
        }
    }

    @Override
    public void scanNotNull(T object, SerializationContext context) throws Exception {
        // Handle subtypes of the expected type by delegating to a more specific implementation
        Codex<HasUuid> maybeSubtype = typeContext.getCodex(object.getClass());
        if (this == maybeSubtype) {
            if (context.add(object)) {
                traverse(object, false, context, null);
            }
        } else {
            maybeSubtype.scanNotNull(object, context);
        }
    }

    /**
     * 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());
    }

    public void writeProperties(T object, SerializationContext context) {
        context.pushPath("(EntityCodex.writeProperties())" + object.getUuid());
        try {
            JsonWriter writer = context.getWriter();
            writer.beginObject();
            traverse(object, false, context, writer);
            writer.endObject();
        } catch (Exception e) {
            context.fail(e);
        } finally {
            context.popPath();
        }
    }

    @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, DeserializationContext context, boolean useResolvers) {
        T toReturn = null;
        boolean resolved = false;

        // Possibly delegate to injected resolvers
        if (useResolvers) {
            try {
                toReturn = entityResolver.resolve(clazz, uuid);
            } catch (Exception e) {
                context.fail(e);
            }
            if (toReturn != null) {
                resolved = true;
            }
        }

        // Otherwise try to construct a new instance
        if (toReturn == null && provider != null) {
            toReturn = provider.get();
        }

        toReturn.setUuid(uuid);
        context.putEntity(uuid, toReturn, resolved);
        return toReturn;
    }

    /**
     * A fan-out to to {@link DeserializationContext#checkAccess(HasUuid)} that will accept
     * collections.
     */
    private boolean checkAccess(Object object, DeserializationContext ctx) {
        if (object instanceof HasUuid) {
            return ctx.checkAccess((HasUuid) object);
        }
        if (object instanceof Iterable) {
            for (Object obj : ((Iterable<?>) object)) {
                if (!checkAccess(obj, ctx)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * A scuzzy method that either scans, writes, or reads based on whether or not {@code writer} is
     * null
     */
    private void traverse(T object, boolean isEmbedded, SerializationContext context, JsonWriter writer)
            throws Exception {

        if (object == null)
            return;

        Set<String> dirtyPropertyNames;
        if (object instanceof PersistenceAware) {
            dirtyPropertyNames = FlatPackCollections.setForIteration();
            // Always write out uuid
            dirtyPropertyNames.add("uuid");
            dirtyPropertyNames.addAll(((PersistenceAware) object).dirtyPropertyNames());
        } else {
            dirtyPropertyNames = null;
        }

        // Write all properties
        for (Property prop : typeContext.extractProperties(clazz)) {
            // Check access
            if (!prop.mayGet(context.getRoles())) {
                continue;
            }
            // Ignore OneToMany type properties unless specifically requested
            if (prop.isDeepTraversalOnly() && !context.getTraversalMode().writeAllProperties()) {
                continue;
            }
            // Don't emit a redundant uuid property
            if (isEmbedded && "uuid".equals(prop.getName())) {
                continue;
            }
            // Skip clean properties
            if (dirtyPropertyNames != null && !dirtyPropertyNames.contains(prop.getName())) {
                continue;
            }
            context.pushPath("." + prop.getName());
            try {
                // Extract the value
                prop.getGetter().setAccessible(true);
                Object value = prop.getGetter().invoke(object);

                // Figure out how to interpret the value
                @SuppressWarnings("unchecked")
                Codex<Object> codex = (Codex<Object>) prop.getCodex();

                if (prop.isEmbedded()) {
                    @SuppressWarnings("unchecked")
                    EntityCodex<HasUuid> embeddedCodex = (EntityCodex<HasUuid>) prop.getCodex();
                    embeddedCodex.traverse((HasUuid) value, true, context, writer);
                } else if (writer == null) {
                    // Either scan or write the property value
                    codex.scan(value, context);
                } else if (!(prop.isSuppressDefaultValue() && codex.isDefaultValue(value))) {
                    // Write the value of the property, optionally suppressing default values
                    writer.name(prop.getName() + codex.getPropertySuffix());
                    codex.write(value, context);
                }
            } catch (Exception e) {
                context.fail(e);
            } finally {
                context.popPath();
            }
        }
    }
}