org.hibernate.metamodel.source.annotations.JandexHelper Maven / Gradle / Ivy
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2011, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.metamodel.source.annotations;
import java.beans.Introspector;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hibernate.AssertionFailure;
import org.hibernate.HibernateException;
import org.hibernate.boot.registry.classloading.spi.ClassLoaderService;
import org.hibernate.internal.util.type.PrimitiveWrapperHelper;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.Index;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
/**
* Utility methods for working with the jandex annotation index.
*
* @author Hardy Ferentschik
*/
public class JandexHelper {
private static final Map DEFAULT_VALUES_BY_ELEMENT = new HashMap();
private JandexHelper() {
}
/**
* Retrieves a jandex annotation element value. If the value is {@code null}, the default value specified in the
* annotation class is retrieved instead.
*
* There are two special cases. {@code Class} parameters should be retrieved as strings (and then can later be
* loaded) and enumerated values should be retrieved via {@link #getEnumValue(AnnotationInstance, String, Class)}.
*
*
* @param annotation the annotation containing the element with the supplied name
* @param element the name of the element value to be retrieve
* @param type the type of element to retrieve. The following types are supported:
*
* - Byte
* - Short
* - Integer
* - Character
* - Float
* - Double
* - Long
* - Boolean
* - String
* - AnnotationInstance
*
* @return the value if not {@code null}, else the default value if not
* {@code null}, else {@code null}.
*
* @throws AssertionFailure in case the specified {@code type} is a class instance or the specified type causes a {@code ClassCastException}
* when retrieving the value.
*/
@SuppressWarnings("unchecked")
public static T getValue(AnnotationInstance annotation, String element, Class type) throws AssertionFailure {
if ( Class.class.equals( type ) ) {
throw new AssertionFailure(
"Annotation parameters of type Class should be retrieved as strings (fully qualified class names)"
);
}
if ( type.isPrimitive() ) {
type = PrimitiveWrapperHelper.getDescriptorByPrimitiveType( type ).getWrapperClass();
}
// try getting the untyped value from Jandex
AnnotationValue annotationValue = annotation.value( element );
try {
if ( annotationValue != null ) {
return explicitAnnotationParameter( annotationValue, type );
}
else {
return defaultAnnotationParameter( getDefaultValue( annotation, element ), type );
}
}
catch ( ClassCastException e ) {
throw new AssertionFailure(
String.format(
"the annotation property %s of annotation %s is not of type %s",
element,
annotation.name(),
type.getName()
),
e
);
}
}
// THIS IS FOR 4.3.x AND SHOULD BE CONSIDERED TEMPORARY. HHH-8118 corrected CL use in JandexHelper by adding
// CLS as method arguments. But that was done in the metamodel branch only before I knew that master added a few
// uses. HHH-8316 needs it for 4.3. DO NOT LET THIS GET MERGED INTO METAMODEL!
public static T getValue(AnnotationInstance annotation, String element, Class type,
ClassLoaderService classLoaderService) throws AssertionFailure {
if ( Class.class.equals( type ) ) {
throw new AssertionFailure(
"Annotation parameters of type Class should be retrieved as strings (fully qualified class names)"
);
}
if ( type.isPrimitive() ) {
type = PrimitiveWrapperHelper.getDescriptorByPrimitiveType( type ).getWrapperClass();
}
// try getting the untyped value from Jandex
AnnotationValue annotationValue = annotation.value( element );
try {
if ( annotationValue != null ) {
return explicitAnnotationParameter( annotationValue, type );
}
else {
return defaultAnnotationParameter( getDefaultValue( annotation, element, classLoaderService ), type );
}
}
catch ( ClassCastException e ) {
throw new AssertionFailure(
String.format(
"the annotation property %s of annotation %s is not of type %s",
element,
annotation.name(),
type.getName()
),
e
);
}
}
/**
* Retrieves a jandex annotation element value, converting it to the supplied enumerated type. If the value is
* null
, the default value specified in the annotation class is retrieved instead.
*
* @param an enumerated type
* @param annotation the annotation containing the enumerated element with the supplied name
* @param element the name of the enumerated element value to be retrieve
* @param type the type to which to convert the value before being returned
*
* @return the value converted to the supplied enumerated type if the value is not null
, else the default value if
* not null
, else null
.
*
* @see #getValue(AnnotationInstance, String, Class)
*/
@SuppressWarnings("unchecked")
public static > T getEnumValue(AnnotationInstance annotation, String element, Class type) {
AnnotationValue val = annotation.value( element );
if ( val == null ) {
return (T) getDefaultValue( annotation, element );
}
return Enum.valueOf( type, val.asEnum() );
}
/**
* Expects a method or field annotation target and returns the property name for this target
*
* @param target the annotation target
*
* @return the property name of the target. For a field it is the field name and for a method name it is
* the method name stripped of 'is', 'has' or 'get'
*/
public static String getPropertyName(AnnotationTarget target) {
if ( !( target instanceof MethodInfo || target instanceof FieldInfo ) ) {
throw new AssertionFailure( "Unexpected annotation target " + target.toString() );
}
if ( target instanceof FieldInfo ) {
return ( (FieldInfo) target ).name();
}
else {
final String methodName = ( (MethodInfo) target ).name();
String propertyName;
if ( methodName.startsWith( "is" ) ) {
propertyName = Introspector.decapitalize( methodName.substring( 2 ) );
}
else if ( methodName.startsWith( "has" ) ) {
propertyName = Introspector.decapitalize( methodName.substring( 3 ) );
}
else if ( methodName.startsWith( "get" ) ) {
propertyName = Introspector.decapitalize( methodName.substring( 3 ) );
}
else {
throw new AssertionFailure( "Expected a method following the Java Bean notation" );
}
return propertyName;
}
}
/**
* @param classInfo the class info from which to retrieve the annotation instance
* @param annotationName the annotation to retrieve from the class info
*
* @return the single annotation defined on the class or {@code null} in case the annotation is not specified at all
*
* @throws org.hibernate.AssertionFailure in case there is there is more than one annotation of this type.
*/
public static AnnotationInstance getSingleAnnotation(ClassInfo classInfo, DotName annotationName)
throws AssertionFailure {
return getSingleAnnotation( classInfo.annotations(), annotationName );
}
/**
* @param annotations List of annotation instances keyed against their dot name.
* @param annotationName the annotation to retrieve from map
*
* @return the single annotation of the specified dot name or {@code null} in case the annotation is not specified at all
*
* @throws org.hibernate.AssertionFailure in case there is there is more than one annotation of this type.
*/
public static AnnotationInstance getSingleAnnotation(Map> annotations, DotName annotationName)
throws AssertionFailure {
List annotationList = annotations.get( annotationName );
if ( annotationList == null ) {
return null;
}
else if ( annotationList.size() == 1 ) {
return annotationList.get( 0 );
}
else {
throw new AssertionFailure(
"Found more than one instance of the annotation "
+ annotationList.get( 0 ).name().toString()
+ ". Expected was one."
);
}
}
/**
* @param annotations List of annotation instances keyed against their dot name.
* @param annotationName the annotation to check
*
* @return returns {@code true} if the map contains only a single instance of specified annotation or {@code false} otherwise.
*
* @throws org.hibernate.AssertionFailure in case there is there is more than one annotation of this type.
*/
public static boolean containsSingleAnnotations(Map> annotations, DotName annotationName)
throws AssertionFailure {
return getSingleAnnotation( annotations, annotationName ) != null;
}
/**
* Creates a jandex index for the specified classes
*
* @param classLoaderService class loader service
* @param classes the classes to index
*
* @return an annotation repository w/ all the annotation discovered in the specified classes
*/
public static Index indexForClass(ClassLoaderService classLoaderService, Class... classes) {
Indexer indexer = new Indexer();
for ( Class clazz : classes ) {
InputStream stream = classLoaderService.locateResourceStream(
clazz.getName().replace( '.', '/' ) + ".class"
);
try {
indexer.index( stream );
}
catch ( IOException e ) {
StringBuilder builder = new StringBuilder();
builder.append( "[" );
int count = 0;
for ( Class c : classes ) {
builder.append( c.getName() );
if ( count < classes.length - 1 ) {
builder.append( "," );
}
count++;
}
builder.append( "]" );
throw new HibernateException( "Unable to create annotation index for " + builder.toString() );
}
}
return indexer.complete();
}
public static Map> getMemberAnnotations(ClassInfo classInfo, String name) {
if ( classInfo == null ) {
throw new IllegalArgumentException( "classInfo cannot be null" );
}
if ( name == null ) {
throw new IllegalArgumentException( "name cannot be null" );
}
Map> annotations = new HashMap>();
for ( List annotationList : classInfo.annotations().values() ) {
for ( AnnotationInstance instance : annotationList ) {
String targetName = null;
if ( instance.target() instanceof FieldInfo ) {
targetName = ( (FieldInfo) instance.target() ).name();
}
else if ( instance.target() instanceof MethodInfo ) {
targetName = ( (MethodInfo) instance.target() ).name();
}
if ( targetName != null && name.equals( targetName ) ) {
addAnnotationToMap( instance, annotations );
}
}
}
return annotations;
}
private static void addAnnotationToMap(AnnotationInstance instance, Map> annotations) {
DotName dotName = instance.name();
List list;
if ( annotations.containsKey( dotName ) ) {
list = annotations.get( dotName );
}
else {
list = new ArrayList();
annotations.put( dotName, list );
}
list.add( instance );
}
private static Object getDefaultValue(AnnotationInstance annotation, String element) {
String name = annotation.name().toString();
String fqElement = name + '.' + element;
Object val = DEFAULT_VALUES_BY_ELEMENT.get( fqElement );
if ( val != null ) {
return val;
}
try {
val = Index.class.getClassLoader().loadClass( name ).getMethod( element ).getDefaultValue();
DEFAULT_VALUES_BY_ELEMENT.put( fqElement, val );
return val == null ? null : val;
}
catch ( RuntimeException error ) {
throw error;
}
catch ( Exception error ) {
throw new AssertionFailure(
String.format( "The annotation %s does not define a parameter '%s'", name, element ),
error
);
}
}
// THIS IS FOR 4.3.x AND SHOULD BE CONSIDERED TEMPORARY. HHH-8118 corrected CL use in JandexHelper by adding
// CLS as method arguments. But that was done in the metamodel branch only before I knew that master added a few
// uses. HHH-8316 needs it for 4.3. DO NOT LET THIS GET MERGED INTO METAMODEL!
private static Object getDefaultValue(AnnotationInstance annotation, String element,
ClassLoaderService classLoaderService) {
String name = annotation.name().toString();
String fqElement = name + '.' + element;
Object val = DEFAULT_VALUES_BY_ELEMENT.get( fqElement );
if ( val != null ) {
return val;
}
try {
val = classLoaderService.classForName( name ).getMethod( element ).getDefaultValue();
if ( val != null ) {
// Annotation parameters of type Class are handled using Strings
if ( val instanceof Class ) {
val = ( ( Class ) val ).getName();
}
}
DEFAULT_VALUES_BY_ELEMENT.put( fqElement, val );
return val;
}
catch ( RuntimeException error ) {
throw error;
}
catch ( Exception error ) {
throw new AssertionFailure(
String.format( "The annotation %s does not define a parameter '%s'", name, element ),
error
);
}
}
private static T defaultAnnotationParameter(Object defaultValue, Class type) {
Object returnValue = defaultValue;
// resolve some mismatches between what's stored in jandex and what the defaults are for annotations
// in case of nested annotation arrays, jandex returns arrays of AnnotationInstances, hence we return
// an empty array of this type here
if ( defaultValue.getClass().isArray() && defaultValue.getClass().getComponentType().isAnnotation() ) {
returnValue = new AnnotationInstance[0];
}
return type.cast( returnValue );
}
private static T explicitAnnotationParameter(AnnotationValue annotationValue, Class type) {
Object returnValue = annotationValue.value();
// if the jandex return type is Type we actually try to retrieve a class parameter
// for our purposes we just return the fqcn of the class
if ( returnValue instanceof Type ) {
returnValue = ( (Type) returnValue ).name().toString();
}
// arrays we have to handle explicitly
if ( type.isArray() ) {
AnnotationValue[] values = (AnnotationValue[]) returnValue;
Class componentType = type.getComponentType();
Object[] arr = (Object[]) Array.newInstance( componentType, values.length );
for ( int i = 0; i < values.length; i++ ) {
arr[i] = componentType.cast( values[i].value() );
}
returnValue = arr;
}
return type.cast( returnValue );
}
}