All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.getperka.flatpack.codexes.EntityCodex Maven / Gradle / Ivy

There is a newer version: 2.21.0
Show newest version
/*
 * #%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 java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

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.EntityResolver;
import com.getperka.flatpack.ext.JsonKind;
import com.getperka.flatpack.ext.PrincipalMapper;
import com.getperka.flatpack.ext.Property;
import com.getperka.flatpack.ext.PropertyPath;
import com.getperka.flatpack.ext.SerializationContext;
import com.getperka.flatpack.ext.Type;
import com.getperka.flatpack.ext.TypeContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonWriter;

public class EntityCodex extends Codex {
  public static class MyReceiver implements PropertyPath.Receiver {
    private final Principal lookFor;
    private final PrincipalMapper mapper;
    private boolean result;

    public MyReceiver(PrincipalMapper mapper, Principal lookFor) {
      this.lookFor = lookFor;
      this.mapper = mapper;
    }

    public boolean getResult() {
      return result;
    }

    @Override
    public boolean receive(Object value) {
      if (value instanceof HasUuid) {
        List principals = mapper.getPrincipals((HasUuid) value);
        if (principals != null && principals.contains(lookFor)) {
          result = true;
          return false;
        }
      }
      return true;
    }
  }

  private final Class clazz;
  private final List preUnpackMethods;
  private final List postUnpackMethods;

  public EntityCodex(Class clazz) {
    this.clazz = clazz;

    List pre = new ArrayList();
    List post = new ArrayList();
    // Iterate over all methods in the type and then its supertypes
    for (Class lookAt = 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. emptyList() :
        Collections.unmodifiableList(pre);
    postUnpackMethods = post.isEmpty() ? Collections. emptyList() :
        Collections.unmodifiableList(post);
  }

  /**
   * 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(TypeContext context) {
    return new Type.Builder()
        .withJsonKind(JsonKind.STRING)
        .withName(context.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 {
      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 roles = context.getRoles();
      for (Property prop : context.getTypeContext().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 codex = (EntityCodex) prop.getCodex();
            HasUuid embedded = codex.allocate(UUID.randomUUID(), context, false);
            codex.readProperties(embedded, element, context);
            value = embedded;
          } else {

            @SuppressWarnings("unchecked")
            Codex codex = (Codex) 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;
          }

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

          // Record the value as having been set
          context.addModified(object, prop);

          // Perhaps set the other side of a OneToMany relationship
          Property impliedPropery = prop.getImpliedPropery();
          if (impliedPropery != null && value != null) {
            context.addPostWork(new ImpliedPropertySetter(context, impliedPropery, value, object));
          }
        } 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 maybeSubtype = context.getTypeContext().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();
    }
  }

  private T allocate(UUID uuid, DeserializationContext context, boolean useResolvers) {
    try {
      T toReturn = null;
      boolean resolved = false;

      // Possibly delegate to injected resolvers
      if (useResolvers) {
        for (EntityResolver resolver : context.getConfiguration().getEntityResolvers()) {
          toReturn = resolver.resolve(clazz, uuid);
          if (toReturn != null) {
            resolved = true;
            break;
          }
        }
      }

      // Otherwise try to construct a new instance
      if (toReturn == null) {
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        toReturn = constructor.newInstance();
      }

      toReturn.setUuid(uuid);
      context.putEntity(uuid, toReturn, resolved);
      return toReturn;
    } catch (InvocationTargetException e) {
      // Report the causal exception instead
      context.fail(e.getCause());
    } catch (NoSuchMethodError e) {
      context.fail(new UnsupportedOperationException("The type " + clazz.getName()
        + " does not have a zero-arg constructor"));
    } catch (Exception e) {
      context.fail(e);
    }
    return null;
  }

  /**
   * 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;

    // Write all properties
    for (Property prop : context.getTypeContext().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;
      }
      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 codex = (Codex) prop.getCodex();

        if (prop.isEmbedded()) {
          @SuppressWarnings("unchecked")
          EntityCodex embeddedCodex = (EntityCodex) 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();
      }
    }
  }
}