/* * Copyright 2005 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.processors.fieldprocessors; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import javax.persistence.FetchType; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.castor.core.annotationprocessing.AnnotationTargetException; import org.castor.core.nature.BaseNature; import org.castor.jdo.jpa.info.FieldInfo; import org.castor.jdo.jpa.natures.JPAFieldNature; import org.castor.jdo.jpa.processors.BaseJPAAnnotationProcessor; import org.castor.jdo.jpa.processors.ReflectionsHelper; /** * Processes the JPA annotation {@link ManyToMany}. This is the most complex * processor. After this processor is done, we know, that we have 2 fields that * have a well defined relation to each other. This processor checks for target * entities, relationship owning and checks that there is at least one join * definition for this relation. * * After this processor is done, all 5 relation linked methods (and of cource * the Many2Many related ones) of {@link JPAFieldNature} will return valid * values. * * @author Peter Schmidt * @version 05.02.2009 * */ public class JPAManyToManyProcessor extends BaseJPAAnnotationProcessor { /** * This enumeration is used to determine the origin of JoinTable * information. * * @author Peter Schmidt * @version 12.02.2009 */ private enum JoinTableStrategy { /** * copy the information from the related property but invert the * JoinColumns. */ inverseCopy, /** * create default mappings - this property is the relations owner. */ defaultWeOwn, /** * create default mappings - the other property is the relations owner. */ defaultHeOwns, /** * do nothing - a {@link JoinTable} annotation exists that will be * processed by another processor. */ nothing } /** * The <a href="http://jakarta.apache.org/commons/logging/">Jakarta Commons * Logging</a> instance used for all logging. */ private static Log _log = LogFactory.getFactory().getInstance( JPAManyToManyProcessor.class); /** * The value of {@link ManyToMany#mappedBy()}. */ private String _mappedBy; /** * @param _fieldInfo * The {@link FieldInfo} of this field. */ private FieldInfo _fieldInfo; /** * The target of the annotation. */ private AnnotatedElement _target; /** * {@inheritDoc} * * @see org.castor.core.annotationprocessing.AnnotationProcessor#forAnnotationClass() */ public Class<? extends Annotation> forAnnotationClass() { return ManyToMany.class; } /** * {@inheritDoc} * * @see org.castor.core.annotationprocessing.TargetAwareAnnotationProcessor# * processAnnotation(BaseNature, Annotation, AnnotatedElement) */ public <I extends BaseNature, A extends Annotation> boolean processAnnotation( final I info, final A annotation, final AnnotatedElement target) throws AnnotationTargetException { if ((info instanceof JPAFieldNature) && (annotation instanceof ManyToMany) && ((target instanceof Method) || (target instanceof Field))) { _log.debug("processing field annotation " + annotation.toString()); JPAFieldNature jpaFieldNature = (JPAFieldNature) info; ManyToMany manyToMany = (ManyToMany) annotation; /* * try generating values */ this._mappedBy = manyToMany.mappedBy(); this._fieldInfo = jpaFieldNature.getFieldInfo(); this._target = target; // target Entity and collectionType Class<?> collectionType; try { collectionType = ReflectionsHelper.getCollectionType( this._target, true); } catch (AnnotationTargetException e) { _log.error(e.getMessage()); throw e; } Class<?> targetEntity = manyToMany.targetEntity(); if (void.class.equals(targetEntity)) { try { targetEntity = ReflectionsHelper .getTargetEntityFromGenerics(this._target); if (targetEntity == null) { // Error => no generics used! String className = ((Member) this._target) .getDeclaringClass().getName(); String targetName = ((Member) this._target).getName(); String message = "Target entity for ManyToMany relation on " + className + "#" + targetName + " not specified - use generics or specify targetEntity!"; throw new AnnotationTargetException(message); } } catch (AnnotationTargetException e) { _log.error(e.getMessage()); throw e; } } // related properties GET method Method otherPropertiesGetter; try { otherPropertiesGetter = this .getRelationOtherGetMethod(targetEntity); } catch (AnnotationTargetException e) { _log.error(e.getMessage()); throw e; } // joinTableStrategie => where to get JoinTable information JoinTableStrategy joinTableStrategy = this .getJoinTableStrategy(otherPropertiesGetter); switch (joinTableStrategy) { case defaultHeOwns: case defaultWeOwn: AnnotationTargetException e = new AnnotationTargetException( "Default values for ManyToMany relations are not supported by Castor!"); _log.error(e.getMessage()); throw e; case inverseCopy: jpaFieldNature.setManyToManyInverseCopy(true); break; default: break; } /* * all values are generated past this point */ /* * @ManyToMany.targetEntity */ jpaFieldNature.setRelationTargetEntity(targetEntity); jpaFieldNature.setRelationCollectionType(collectionType); /* * @ManyToMany.cascade */ if (manyToMany.cascade().length > 0) { jpaFieldNature.setCascadeTypes(manyToMany.cascade()); } /* * @ManyToMany.fetch */ jpaFieldNature.setRelationLazyFetch(false); if (manyToMany.fetch() == FetchType.LAZY) { jpaFieldNature.setRelationLazyFetch(true); } /* * @ManyToMany.mappedBy */ if (_mappedBy.length() != 0) { jpaFieldNature.setRelationMappedBy(_mappedBy); } jpaFieldNature.setManyToMany(true); return true; } return false; } /** * Get the getter method of the related property using reflection. * * @param otherClass * The Class object representing the target entity. * @return The getter {@link Method} accessing the related field or NULL if * this relation is unidirectional. * @throws AnnotationTargetException * If {@link ManyToMany#mappedBy()} refers to a field that is * not related with this field or the getter Method could not be * found (the property does not exist at all). */ private Method getRelationOtherGetMethod(final Class<?> otherClass) throws AnnotationTargetException { Class<?> describedClass = _fieldInfo.getDeclaringClassInfo() .getDescribedClass(); String fieldName = _fieldInfo.getFieldName(); if ((_mappedBy != null) && (_mappedBy.length() != 0)) { // we know which property is related to us // the other side is an owner => this is bidirectional String propertyName = _mappedBy.substring(0, 1).toUpperCase() + _mappedBy.substring(1); String methodName = "get" + propertyName; try { // check if our mappedBy can be a valid relational partner // does method exist? Method otherMethod = otherClass.getMethod(methodName, new Class<?>[0]); // does it have a ManyToMany with us as targetEntity? Class<?> targetEntityFromGenerics = ReflectionsHelper .getTargetEntityFromGenerics(otherMethod); if (describedClass.equals(targetEntityFromGenerics)) { return otherMethod; } throw new AnnotationTargetException("MappedBy '" + _mappedBy + "' in Class " + otherClass.getName() + " is not ManyToMany related with '" + describedClass.getName() + "' property '" + fieldName + "'!"); } catch (AnnotationTargetException e) { throw e; } catch (Exception e) { throw new AnnotationTargetException("MappedBy '" + _mappedBy + "' does not exist in Class " + otherClass.getName() + " (could not find method '" + methodName + "')!"); } } // we don't know which property is related to us // the other side is not an owner for (Method otherMethod : otherClass.getMethods()) { // search through all GET methods if (!otherMethod.getName().startsWith("get")) { continue; } if (checkMappedByToTarget(otherMethod, describedClass, fieldName)) { return otherMethod; } } // the relation is unidirectional! return null; } /** * Little helper to check if a {@link AnnotatedElement} ({@link Field} or * {@link Method}) has a {@link ManyToMany} annotation that is mapped by the * given field (Class and name). * * @param property * The property to check * @param targetClass * The Class that should be targetEntity of the given property * @param targetProperty * The name of the field (mapped by the given property) * @return true iff the given property has a {@link ManyToMany} annotation * with targetEntity referreing to targetClass and is mapped by the * given field name targetProperty. * @throws AnnotationTargetException * if property does not define a targetEntity and is not generic * or the generic definition is not sufficient */ private boolean checkMappedByToTarget(final AnnotatedElement property, final Class<?> targetClass, final String targetProperty) throws AnnotationTargetException { ManyToMany otherManyToMany = property.getAnnotation(ManyToMany.class); if (otherManyToMany != null) { // if property has manytomany relation, get its targetEntity Class<?> otherTargetEntity = otherManyToMany.targetEntity(); if (void.class.equals(otherTargetEntity)) { otherTargetEntity = ReflectionsHelper .getTargetEntityFromGenerics(property); if (otherTargetEntity == null) { // Error => no generics used! String className = ((Member) property).getDeclaringClass() .getName(); String targetName = ((Member) property).getName(); String message = "Target entity for ManyToMany relation on " + className + "#" + targetName + " not specified - use generics or specify targetEntity!"; throw new AnnotationTargetException(message); } } if (otherTargetEntity.equals(targetClass)) { // if our entity is the targetEntity if (targetProperty.equals(otherManyToMany.mappedBy())) { // if our field is the owning field return true; } } } return false; } /** * Analyse the relation and determine where to get JoinTable information * from. * * @param otherPropertiesGetter * The getter Method of the related field. * @return The {@link JoinTableStrategy} representing what to do. * @throws AnnotationTargetException * If a {@link JoinTable} was found on a non-owning side or both * side are owner and no {@link JoinTable} was found at all * (defaults can not be generated then). * * @see {@link JoinTableStrategy} */ private JoinTableStrategy getJoinTableStrategy( final Method otherPropertiesGetter) throws AnnotationTargetException { if (otherPropertiesGetter != null) { // bi-directional relation String otherFieldName = ReflectionsHelper .getFieldnameFromGetter(otherPropertiesGetter); String otherMappedBy = otherPropertiesGetter.getAnnotation( ManyToMany.class).mappedBy(); boolean weOwn = this._fieldInfo.getFieldName() .equals(otherMappedBy); boolean heOwns = otherFieldName.equals(_mappedBy); if (_target.getAnnotation(JoinTable.class) != null) { // we have a JoinTable definition => we MUST be an owner => // check that! if (weOwn) { // we are an owner! => JoinTable is valid here... do nothing return JoinTableStrategy.nothing; } // we are not an owner, but we have a JoinTable definition => // ERROR String message = "JoinTable definition on a non-owning side (" + this._fieldInfo.getFieldName() + ") is not valid!"; _log.error(message); throw new AnnotationTargetException(message); } // we have no JoinTable definition if (otherPropertiesGetter.getAnnotation(JoinTable.class) != null) { // other side has JoinTable definition => do nothing if (heOwns) { return JoinTableStrategy.inverseCopy; } // he is not an owner, but has a JoinTable definition => ERROR String message = "JoinTable definition on a non-owning side (" + _mappedBy + ") is not valid!"; _log.error(message); throw new AnnotationTargetException(message); } // there is no JoinTable definition at all! if (heOwns && !weOwn) { // other side is the only owner => we take his defaults return JoinTableStrategy.defaultHeOwns; } else if (!heOwns && weOwn) { // we are the only owner => we take our defaults return JoinTableStrategy.defaultWeOwn; } // 2 owners but no JoinTable => ERROR! String message = "Can not create default mapping if both entities ('" + _fieldInfo.getDeclaringClassInfo().getDescribedClass() .getName() + "' and '" + otherPropertiesGetter.getDeclaringClass().getName() + "')are owner!"; _log.error(message); throw new AnnotationTargetException(message); } // uni-directional relation // we are the only owner => we need @JoinTable OR MappingDefaults if (!(_target.getAnnotation(JoinTable.class) != null)) { // generate mapping defaults; we are owner return JoinTableStrategy.defaultWeOwn; } // we are owner and have a JoinTable definition... do nothing return JoinTableStrategy.nothing; } }