/*
* Copyright 2008 Werner Guttmann
*
* 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.
*/
package org.castor.jdo.jpa.info;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.persistence.Entity;
import javax.persistence.MappedSuperclass;
import org.castor.core.annotationprocessing.AnnotationTargetException;
import org.castor.core.annotationprocessing.TargetAwareAnnotationProcessingService;
import org.castor.jdo.jpa.natures.JPAClassNature;
import org.castor.jdo.jpa.natures.JPAFieldNature;
import org.castor.jdo.jpa.processors.ReflectionsHelper;
import org.exolab.castor.mapping.MappingException;
/**
* Uses a JPA annotated {@link Class} to build a {@link ClassInfo} and
* {@link FieldInfo}s of it and parse the mapping information in them.
*
* @author Peter Schmidt
* @since 1.3
*/
public final class ClassInfoBuilder {
/**
* The {@link TargetAwareAnnotationProcessingService} for class related
* annotations.
*/
private static TargetAwareAnnotationProcessingService _classAnnotationProcessingService =
new JPAClassAnnotationProcessingService();
/**
* The {@link TargetAwareAnnotationProcessingService} for field related
* annotations.
*/
private static TargetAwareAnnotationProcessingService _fieldAnnotationProcessingService =
new JPAFieldAnnotationProcessingService();
/**
* Do not allow instances of utility classes.
*/
private ClassInfoBuilder() {
}
/**
* Builds a new {@link ClassInfo} describing the given Class. Annotations
* for the class and its fields are read using the
* {@link TargetAwareAnnotationProcessingService}s defined by
* {@link #setClassAnnotationProcessingService(TargetAwareAnnotationProcessingService)}
* and
* {@link #setFieldAnnotationProcessingService(TargetAwareAnnotationProcessingService)}
* . The information is stored in the {@link ClassInfo} and its related
* {@link FieldInfo}s.
*
* @param type
* The Class Object representing the Class that shall be
* described.
* @return a new {@link ClassInfo} describing the given Class or null if the
* given type was not describable.
* @throws MappingException
* if annotation placement is invalid (field and property access
* for the same field) or if composite keys are used!
*/
public static ClassInfo buildClassInfo(final Class<?> type)
throws MappingException {
if (type == null) {
throw new IllegalArgumentException("Argument type must not be null");
}
/*
* get and return classInfo from ClassInfoRegistry, if already generated
*/
ClassInfo classInfo = ClassInfoRegistry.getClassInfo(type);
if (classInfo != null) {
return classInfo;
}
if (!isDescribable(type)) {
return null;
}
/*
* create new ClassInfo and Nature
*/
classInfo = new ClassInfo(type);
classInfo.addNature(JPAClassNature.class.getName());
JPAClassNature jpaClassNature = new JPAClassNature(classInfo);
/*
* process class annotations
*/
try {
_classAnnotationProcessingService.processAnnotations(jpaClassNature,
type.getAnnotations(), type);
} catch (AnnotationTargetException e) {
throw new MappingException(
"Could not process class bound annotations for class "
+ type.getSimpleName(), e);
}
/*
* process annotations for all declared (not inherited) fields =>
* fieldAccess. This is not supported by castor and so a mapping
* exception is thrown
*/
for (Field field : type.getDeclaredFields()) {
if (field.getAnnotations().length != 0) {
if (hasJPAAnnotations(field)) {
throw new MappingException(
"Castor does not support field access, thus annotated fields are "
+ "not supported! Move annotations to the getter method of "
+ field.getName());
}
}
}
/*
* process annotations for all declared (not inherited) getter methods
*/
for (Method method : type.getDeclaredMethods()) {
if (ReflectionsHelper.isGetter(method)) {
if (isDescribable(type, method)) {
buildFieldInfo(classInfo, method);
} else {
throw new MappingException(
"Invalid method annotated, method is not describeable!");
}
}
}
if (classInfo.getKeyFieldCount() > 1) {
// Castor JPA does not support composite keys
throw new MappingException(
"Castor-JPA does not support composite keys (found in "
+ type.getName() + ")");
}
/*
* register ClassInfo in Registry
*/
ClassInfoRegistry.registerClassInfo(type, classInfo);
return classInfo;
}
/**
* Checks if the {@link AnnotatedElement} has any Annotations of the Package
* javax.persistence (JPA related annotations).
*
* @param annotatedElement
* The {@link AnnotatedElement} (Field, Method, Class) to be
* checked
* @return true if any JPA annotations are found.
*/
private static boolean hasJPAAnnotations(final AnnotatedElement annotatedElement) {
for (Annotation annotation : annotatedElement.getAnnotations()) {
Class<? extends Annotation> annotationClass = annotation.annotationType();
if (annotationClass.getPackage().equals(Package.getPackage("javax.persistence"))) {
return true;
}
}
return false;
}
/**
* Build a {@link FieldInfo} describing the field (accessed by the given
* {@link Method}) by processing its annotations and add the generated
* {@link FieldInfo} to the given {@link ClassInfo} (as field or key).
*
* @param classInfo
* the {@link ClassInfo} of the declaring class
* @param method
* the {@link Method} used to access (get) the underlying member.
* @throws MappingException
* if a FieldInfo with the same name already exists (usually
* when using field AND property access).
*/
private static void buildFieldInfo(final ClassInfo classInfo, final Method method)
throws MappingException {
if (classInfo == null) {
throw new IllegalArgumentException(
"Argument classInfo must not be null.");
}
if (method == null) {
throw new IllegalArgumentException(
"Argument method must not be null.");
}
String fieldName = ReflectionsHelper.getFieldnameFromGetter(method);
if (fieldName == null) {
throw new IllegalArgumentException(
"Can not resolve Fieldname from method name.");
}
Method setterMethod = null;
try {
setterMethod = ReflectionsHelper.getSetterMethodFromGetter(method);
} catch (SecurityException e) {
throw new MappingException("Setter method for field " + fieldName
+ " is not accessible!");
} catch (NoSuchMethodException e) {
throw new MappingException("Setter method for field " + fieldName
+ " does not exist!", e);
}
Class<?> fieldType = method.getReturnType();
FieldInfo fieldInfo = new FieldInfo(classInfo, fieldType, fieldName, method, setterMethod);
fieldInfo.addNature(JPAFieldNature.class.getName());
JPAFieldNature jpaFieldNature = new JPAFieldNature(fieldInfo);
try {
_fieldAnnotationProcessingService.processAnnotations(jpaFieldNature,
method.getAnnotations(), method);
} catch (AnnotationTargetException e) {
throw new MappingException(
"Could not process annotations for method "
+ method.getName(), e);
}
if (jpaFieldNature.isId()) {
classInfo.addKey(fieldInfo);
} else {
classInfo.addFieldInfo(fieldInfo);
}
}
/**
* Checks whether a class is describable or not. A class is describable if it
* is not Void, Object or Class and is annotated with the {@link Entity}
* annotation.
*
* @param type
* the class to check
* @return false if the given type is Void, Object or Class or is not
* annotated with the {@link Entity} annotation - all other Classes
* are describable.
*/
private static boolean isDescribable(final Class<?> type) {
if (Object.class.equals(type) || Void.class.equals(type)
|| Class.class.equals(type)) {
return false;
}
if (type.getAnnotation(Entity.class) == null &&
type.getAnnotation(MappedSuperclass.class) == null ) {
return false;
}
return true;
}
/**
* Checks whether a {@link Method} is describable or not. A method is NOT
* describable if:
* <ul>
* <li>The declaring Class of the method is NOT the given type (except for
* methods defined by interfaces)</li>
* <li>The method is synthetic</li>
* <li>The method is static</li>
* <li>The method is transient</li>
* </ul>
*
* @param type
* the Class the method belongs to
* @param method
* the {@link Method} we want to check
* @return true if the method is describable, false if not.
*/
private static boolean isDescribable(final Class<?> type, final Method method) {
boolean isDescribeable = true;
Class<?> declaringClass = method.getDeclaringClass();
if ((declaringClass != null) && (!type.equals(declaringClass))
&& (!declaringClass.isInterface())) {
isDescribeable = false;
}
if (method.isSynthetic()) {
isDescribeable &= false;
}
if (Modifier.isStatic(method.getModifiers())) {
isDescribeable &= false;
}
if (Modifier.isTransient(method.getModifiers())) {
isDescribeable &= false;
}
return isDescribeable;
}
}