/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package org.apache.deltaspike.core.util.metadata;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
/**
* <p>A small helper class to create an Annotation instance of the given annotation class
* via {@link java.lang.reflect.Proxy}. The annotation literal gets filled with the default values.</p>
* <p/>
* <p>This class can be used to dynamically create Annotations which can be usd in AnnotatedTyp.
* This is e.g. the case if you configure an annotation via properties or XML file. In those cases you
* cannot use {@link javax.enterprise.util.AnnotationLiteral} because the type is not known at compile time.</p>
* <p>usage:</p>
* <pre>
* String annotationClassName = ...;
* Class<? extends annotation> annotationClass =
* (Class<? extends Annotation>) ClassUtils.getClassLoader(null).loadClass(annotationClassName);
* Annotation a = AnnotationInstanceProvider.of(annotationClass)
* </pre>
*/
public class AnnotationInstanceProvider implements Annotation, InvocationHandler, Serializable
{
private static final long serialVersionUID = -2345068201195886173L;
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
private static final Class[] EMPTY_CLASS_ARRAY = new Class[0];
private final Class<? extends Annotation> annotationClass;
private final Map<String, ?> memberValues;
/**
* Required to use the result of the factory instead of a default implementation
* of {@link javax.enterprise.util.AnnotationLiteral}.
*
* @param annotationClass class of the target annotation
*/
private AnnotationInstanceProvider(Class<? extends Annotation> annotationClass, Map<String, ?> memberValues)
{
this.annotationClass = annotationClass;
this.memberValues = memberValues;
}
/**
* Creates an annotation instance for the given annotation class
*
* @param annotationClass type of the target annotation
* @param values A non-null map of the member values, keys being the name of the members
* @param <T> current type
* @return annotation instance for the given type
*/
@SuppressWarnings("unchecked")
public static <T extends Annotation> T of(Class<T> annotationClass, Map<String, ?> values)
{
if (values == null)
{
throw new IllegalArgumentException("Map of values must not be null");
}
String key = annotationClass.getName() + "_" + values.hashCode();
return (T) initAnnotation(key, annotationClass, values);
}
/**
* Creates an annotation instance for the given annotation class
*
* @param annotationClass type of the target annotation
* @param <T> current type
* @return annotation instance for the given type
*/
@SuppressWarnings("unchecked")
public static <T extends Annotation> T of(Class<T> annotationClass)
{
return (T) of(annotationClass, Collections.EMPTY_MAP);
}
private static synchronized <T extends Annotation> Annotation initAnnotation(String key,
Class<T> annotationClass,
Map<String, ?> values)
{
return (Annotation) Proxy.newProxyInstance(annotationClass.getClassLoader(),
new Class[]{annotationClass},
new AnnotationInstanceProvider(annotationClass, values));
}
/**
* {@inheritDoc}
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Exception
{
if ("hashCode".equals(method.getName()))
{
return hashCode();
}
else if ("equals".equals(method.getName()))
{
if (Proxy.isProxyClass(args[0].getClass()))
{
if (Proxy.getInvocationHandler(args[0]) instanceof AnnotationInstanceProvider)
{
return equals(Proxy.getInvocationHandler(args[0]));
}
}
return equals(args[0]);
}
else if ("annotationType".equals(method.getName()))
{
return annotationType();
}
else if ("toString".equals(method.getName()))
{
return toString();
}
else
{
if (memberValues.containsKey(method.getName()))
{
return memberValues.get(method.getName());
}
else // Default cause, probably won't ever happen, unless annotations get actual methods
{
return method.getDefaultValue();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public Class<? extends Annotation> annotationType()
{
return annotationClass;
}
/**
* Copied from Apache OWB (javax.enterprise.util.AnnotationLiteral#toString())
* with minor changes.
*
* @return the current state of the annotation as string
*/
@Override
public String toString()
{
Method[] methods = annotationClass.getDeclaredMethods();
StringBuilder sb = new StringBuilder("@" + annotationType().getName() + "(");
int length = methods.length;
for (int i = 0; i < length; i++)
{
// Member name
sb.append(methods[i].getName()).append("=");
// Member value
Object memberValue;
try
{
memberValue = invoke(this, methods[i], EMPTY_OBJECT_ARRAY);
}
catch (Exception e)
{
memberValue = "";
}
sb.append(memberValue);
if (i < length - 1)
{
sb.append(",");
}
}
sb.append(")");
return sb.toString();
}
@Override
public boolean equals(Object o)
{
if (this == o)
{
return true;
}
if (!(o instanceof AnnotationInstanceProvider))
{
if (annotationClass.isInstance(o))
{
for (Map.Entry<String, ?> entry : memberValues.entrySet())
{
try
{
Object oValue = annotationClass.getMethod(entry.getKey(), EMPTY_CLASS_ARRAY)
.invoke(o, EMPTY_OBJECT_ARRAY);
if (oValue != null && entry.getValue() != null)
{
if (!oValue.equals(entry.getValue()))
{
return false;
}
}
else // This may not actually ever happen, unless null is a default for a member
{
return false;
}
}
catch (IllegalAccessException e)
{
throw new RuntimeException(e);
}
catch (InvocationTargetException e)
{
throw new RuntimeException(e);
}
catch (NoSuchMethodException e)
{
throw new RuntimeException(e);
}
}
return true;
}
return false;
}
AnnotationInstanceProvider that = (AnnotationInstanceProvider) o;
if (!annotationClass.equals(that.annotationClass))
{
return false;
}
return memberValues.equals(that.memberValues);
}
@Override
public int hashCode()
{
int result = 0;
Class<? extends Annotation> type = annotationClass;
for (Method m : type.getDeclaredMethods())
{
try
{
Object value = invoke(this, m, EMPTY_OBJECT_ARRAY);
if (value == null)
{
throw new IllegalStateException(String.format("Annotation method %s returned null", m));
}
result += hashMember(m.getName(), value);
}
catch (RuntimeException ex)
{
throw ex;
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
}
return result;
}
//besides modularity, this has the advantage of autoboxing primitives:
/**
* Helper method for generating a hash code for a member of an annotation.
*
* @param name the name of the member
* @param value the value of the member
* @return a hash code for this member
*/
private int hashMember(String name, Object value)
{
int part1 = name.hashCode() * 127;
if (value.getClass().isArray())
{
return part1 ^ arrayMemberHash(value.getClass().getComponentType(), value);
}
if (value instanceof Annotation)
{
return part1 ^ hashCode((Annotation) value);
}
return part1 ^ value.hashCode();
}
/**
* Helper method for generating a hash code for an array.
*
* @param componentType the component type of the array
* @param o the array
* @return a hash code for the specified array
*/
private static int arrayMemberHash(Class<?> componentType, Object o)
{
if (componentType.equals(Byte.TYPE))
{
return Arrays.hashCode((byte[]) o);
}
if (componentType.equals(Short.TYPE))
{
return Arrays.hashCode((short[]) o);
}
if (componentType.equals(Integer.TYPE))
{
return Arrays.hashCode((int[]) o);
}
if (componentType.equals(Character.TYPE))
{
return Arrays.hashCode((char[]) o);
}
if (componentType.equals(Long.TYPE))
{
return Arrays.hashCode((long[]) o);
}
if (componentType.equals(Float.TYPE))
{
return Arrays.hashCode((float[]) o);
}
if (componentType.equals(Double.TYPE))
{
return Arrays.hashCode((double[]) o);
}
if (componentType.equals(Boolean.TYPE))
{
return Arrays.hashCode((boolean[]) o);
}
return Arrays.hashCode((Object[]) o);
}
/**
* <p>Generate a hash code for the given annotation using the algorithm
* presented in the {@link Annotation#hashCode()} API docs.</p>
*
* @param a the Annotation for a hash code calculation is desired, not
* {@code null}
* @return the calculated hash code
* @throws RuntimeException if an {@code Exception} is encountered during
* annotation member access
* @throws IllegalStateException if an annotation method invocation returns
* {@code null}
*/
private int hashCode(Annotation a)
{
int result = 0;
Class<? extends Annotation> type = a.annotationType();
for (Method m : type.getDeclaredMethods())
{
try
{
Object value = m.invoke(a);
if (value == null)
{
throw new IllegalStateException(String.format("Annotation method %s returned null", m));
}
result += hashMember(m.getName(), value);
}
catch (RuntimeException ex)
{
throw ex;
}
catch (Exception ex)
{
throw new RuntimeException(ex);
}
}
return result;
}
}