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<Annotation> {
static class Handler implements InvocationHandler, AnnotationInfo {
private final Class<? extends Annotation> annotationType;
private final String annotationTypeName;
private final Map<String, Object> values;
Handler(Class<? extends Annotation> annotationType, Map<String, Object> values) {
this.annotationType = annotationType;
this.annotationTypeName = annotationType.getName();
this.values = values;
}
Handler(String annotationTypeName, Map<String, Object> values) {
this.annotationType = UnknownAnnotation.class;
this.annotationTypeName = annotationTypeName;
this.values = values;
}
@Override
public String getAnnotationTypeName() {
return annotationTypeName;
}
@Override
public Map<String, Object> 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<Object> 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<String, Object> extractValues(Annotation obj) {
Map<String, Object> compareTo = sortedMapForIteration();
// Support for UnknownAnnotation
if (obj instanceof AnnotationInfo) {
AnnotationInfo info = (AnnotationInfo) obj;
for (Map.Entry<String, Object> 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<String, Object> values = sortedMapForIteration();
Class<? extends Annotation> 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<String, JsonElement> 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<String, Object> entry : info.getAnnotationValues().entrySet()) {
writer.name(entry.getKey());
dynamicCodex.write(entry.getValue(), context);
}
} else {
// The usual case
Class<? extends Annotation> 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();
}
}