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

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

There is a newer version: 2.21.0
Show newest version
package com.getperka.flatpack.codexes;

/*
 * #%L
 * FlatPack serialization code
 * %%
 * Copyright (C) 2012 - 2013 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%
 */

import static com.getperka.flatpack.util.FlatPackCollections.listForAny;
import static com.getperka.flatpack.util.FlatPackCollections.sortedMapForIteration;

import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.inject.Inject;

import org.slf4j.Logger;

import com.getperka.flatpack.ext.DeserializationContext;
import com.getperka.flatpack.ext.JsonKind;
import com.getperka.flatpack.ext.SerializationContext;
import com.getperka.flatpack.ext.Type;
import com.getperka.flatpack.ext.TypeContext;
import com.getperka.flatpack.ext.TypeHint;
import com.getperka.flatpack.inject.FlatPackLogger;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonWriter;

/**
 * Encodes Java annotations as simple datastructures. If an annotation being deserialized is not
 * available in the classpath, it will be replaced with an {@link UnknownAnnotation}. The annotation
 * instances produced by this codex will also implement the {@link AnnotationInfo} interface, which
 * allows map-based access to annotation properties.
 */
public class AnnotationCodex extends ValueCodex {
  static class Handler implements InvocationHandler, AnnotationInfo {
    private final Class annotationType;
    private final String annotationTypeName;
    private final Map values;

    Handler(Class annotationType, Map values) {
      this.annotationType = annotationType;
      this.annotationTypeName = annotationType.getName();
      this.values = values;
    }

    Handler(String annotationTypeName, Map values) {
      this.annotationType = UnknownAnnotation.class;
      this.annotationTypeName = annotationTypeName;
      this.values = values;
    }

    @Override
    public String getAnnotationTypeName() {
      return annotationTypeName;
    }

    @Override
    public Map getAnnotationValues() {
      return values;
    }

    @Override
    public int hashCode() {
      return values.hashCode();
    }

    @Override
    public Object invoke(Object instance, Method m, Object[] args) throws Throwable {
      if (Object.class.equals(m.getDeclaringClass())) {
        if (m.getName().equals("equals")) {
          return equals((Annotation) instance, args[0]);
        }
        return m.invoke(this, args);
      }
      if (AnnotationInfo.class.equals(m.getDeclaringClass())) {
        return m.invoke(this, args);
      }
      if (m.getName().equals("annotationType")) {
        return annotationType;
      }
      Object toReturn = values.get(m.getName());
      if (toReturn == null) {
        return m.getDefaultValue();
      }
      return toReturn;
    }

    @Override
    public String toString() {
      return values.toString();
    }

    private List asList(Object array) {
      List toReturn = listForAny();
      for (int i = 0, j = Array.getLength(array); i < j; i++) {
        toReturn.add(Array.get(array, i));
      }
      return toReturn;
    }

    private boolean equals(Annotation instance, Object obj) {
      // Ensure the incoming object is an annotation of the same type
      if (!(obj instanceof Annotation)
        || !annotationType.equals(((Annotation) obj).annotationType())) {
        return false;
      }

      if (obj instanceof AnnotationInfo) {
        AnnotationInfo info = (AnnotationInfo) obj;
        if (!annotationTypeName.equals(info.getAnnotationTypeName())) {
          return false;
        }
      }

      // Quick test for comparison to self
      if (Proxy.isProxyClass(obj.getClass()) && Proxy.getInvocationHandler(obj) instanceof Handler) {
        Handler handler = (Handler) Proxy.getInvocationHandler(obj);
        if (this == handler) {
          return true;
        }
      }
      return extractValues(instance).equals(extractValues((Annotation) obj));
    }

    private Map extractValues(Annotation obj) {
      Map compareTo = sortedMapForIteration();

      // Support for UnknownAnnotation
      if (obj instanceof AnnotationInfo) {
        AnnotationInfo info = (AnnotationInfo) obj;
        for (Map.Entry entry : info.getAnnotationValues().entrySet()) {
          Object value = entry.getValue();
          if (value.getClass().isArray()) {
            value = asList(value);
          }
          compareTo.put(entry.getKey(), value);
          System.out.println(entry.getKey() + " " + value.getClass().getName() + " " + value);
        }
        return compareTo;
      }

      for (Method m : annotationType.getDeclaredMethods()) {
        m.setAccessible(true);
        Throwable ex;
        try {
          Object value = m.invoke(obj);
          if (m.getReturnType().isArray()) {
            value = asList(value);
          }
          compareTo.put(m.getName(), value);
          continue;
        } catch (IllegalAccessException e) {
          // Unexpected, since interface methods are public
          ex = e;
        } catch (InvocationTargetException e) {
          ex = e.getCause();
        }
        throw new RuntimeException("Could not extract annotation value", ex);
      }
      return compareTo;
    }
  }

  private static final String TYPE_KEY = "@";

  private static final Type TYPE = new Type.Builder()
      .withJsonKind(JsonKind.ANY)
      .withTypeHint(TypeHint.create(Annotation.class))
      .build();

  @Inject
  private DynamicCodex dynamicCodex;
  @FlatPackLogger
  @Inject
  private Logger logger;
  @Inject
  private TypeContext typeContext;

  /**
   * Requires injection.
   */
  protected AnnotationCodex() {}

  @Override
  public Type describe() {
    return TYPE;
  }

  @Override
  public Annotation readNotNull(JsonElement element, DeserializationContext context)
      throws Exception {
    JsonObject obj = element.getAsJsonObject();
    if (!obj.has(TYPE_KEY)) {
      logger.error("Incoming annotation has no @ member");
      return null;
    }

    String typeName = obj.get(TYPE_KEY).getAsString();
    Map values = sortedMapForIteration();

    Class annotationType;
    Handler h;
    try {
      annotationType =
          Class.forName(typeName, false, Thread.currentThread().getContextClassLoader())
              .asSubclass(Annotation.class);

      for (Method m : annotationType.getDeclaredMethods()) {
        JsonElement elt = obj.get(m.getName());
        if (elt == null || elt.isJsonNull()) {
          continue;
        }
        Object value = typeContext.getCodex(m.getGenericReturnType()).read(elt, context);
        values.put(m.getName(), value);
      }
      h = new Handler(annotationType, Collections.unmodifiableMap(values));
    } catch (ClassCastException e) {
      logger.warn(
          "Attempting to decode an annotation type @{} which is not assignable to Annotation",
          typeName);
      return null;
    } catch (ClassNotFoundException e) {
      annotationType = UnknownAnnotation.class;

      for (Map.Entry entry : obj.entrySet()) {
        if (TYPE_KEY.equals(entry.getKey())) {
          continue;
        }
        Object value;
        if (entry.getValue().isJsonObject() && entry.getValue().getAsJsonObject().has(TYPE_KEY)) {
          // Try to decode nested annotations
          value = readNotNull(entry.getValue(), context);
        } else {
          // Guess at value types
          value = dynamicCodex.read(entry.getValue(), context);
        }
        values.put(entry.getKey(), value);
      }
      h = new Handler(typeName, Collections.unmodifiableMap(values));
    }

    Annotation a = annotationType.cast(
        Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
            new Class[] { annotationType, AnnotationInfo.class }, h));
    return a;
  }

  @Override
  public void writeNotNull(Annotation a, SerializationContext context) throws Exception {
    JsonWriter writer = context.getWriter().beginObject().name(TYPE_KEY);
    if (a instanceof AnnotationInfo) {
      // Support for UnknownAnnotation
      AnnotationInfo info = (AnnotationInfo) a;
      writer.value(info.getAnnotationTypeName());
      for (Map.Entry entry : info.getAnnotationValues().entrySet()) {
        writer.name(entry.getKey());
        dynamicCodex.write(entry.getValue(), context);
      }
    } else {
      // The usual case
      Class annotationType = a.annotationType();
      writer.value(annotationType.getName());
      for (Method m : annotationType.getDeclaredMethods()) {
        m.setAccessible(true);
        Object value = m.invoke(a);
        writer.name(m.getName());
        typeContext.getCodex(m.getReturnType()).write(value, context);
      }
    }
    writer.endObject();
  }
}