/* * 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.openjpa.persistence.meta; import static javax.persistence.AccessType.FIELD; import static javax.persistence.AccessType.PROPERTY; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Map.Entry; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; import javax.persistence.Access; import javax.persistence.AccessType; import javax.persistence.Embeddable; import javax.persistence.Entity; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.MappedSuperclass; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import javax.persistence.Transient; import org.apache.openjpa.lib.util.Localizer; import org.apache.openjpa.meta.AccessCode; import org.apache.openjpa.util.UserException; /** * Extracts persistent metadata information by analyzing available annotation * in *.java source files. Requires JDK6 Annotation Processing environment * available. * * @author Pinaki Poddar * @since 2.0.0 */ public class SourceAnnotationHandler implements MetadataProcessor<TypeElement, Element> { private final ProcessingEnvironment processingEnv; private final Types typeUtility; private final CompileTimeLogger logger; /** * Set of Inclusion Filters based on member type, access type or transient * annotations. Used to determine the subset of available field/method that * are persistent. */ protected AccessFilter propertyAccessFilter = new AccessFilter(PROPERTY); protected AccessFilter fieldAccessFilter = new AccessFilter(FIELD); protected KindFilter fieldFilter = new KindFilter(ElementKind.FIELD); protected KindFilter methodFilter = new KindFilter(ElementKind.METHOD); protected TransientFilter nonTransientFilter = new TransientFilter(); protected AnnotatedFilter annotatedFilter = new AnnotatedFilter(); protected GetterFilter getterFilter = new GetterFilter(); protected SetterFilter setterFilter = new SetterFilter(); protected static List<Class<? extends Annotation>> mappingAnnos = new ArrayList<Class<? extends Annotation>>(); static { mappingAnnos.add(OneToOne.class); mappingAnnos.add(OneToMany.class); mappingAnnos.add(ManyToOne.class); mappingAnnos.add(ManyToMany.class); } private static Localizer _loc = Localizer.forPackage(SourceAnnotationHandler.class); /** * Construct with JDK6 annotation processing environment. * */ public SourceAnnotationHandler(ProcessingEnvironment processingEnv, CompileTimeLogger logger) { super(); this.processingEnv = processingEnv; this.typeUtility = processingEnv.getTypeUtils(); this.logger = logger; } public int determineTypeAccess(TypeElement type) { AccessType access = getExplicitAccessType(type); boolean isExplicit = access != null; return isExplicit ? access == AccessType.FIELD ? AccessCode.EXPLICIT | AccessCode.FIELD : AccessCode.EXPLICIT | AccessCode.PROPERTY : getImplicitAccessType(type); } public int determineMemberAccess(Element m) { return 0; } public List<Exception> validateAccess(TypeElement t) { return null; } public boolean isMixedAccess(TypeElement t) { return false; } /** * Gets the list of persistent fields and/or methods for the given type. * * Scans relevant @AccessType annotation and field/method as per JPA * specification to determine the candidate set of field/methods. */ public Set<Element> getPersistentMembers(TypeElement type) { int access = determineTypeAccess(type); if (AccessCode.isExplicit(access)) { return AccessCode.isField(access) ? getFieldAccessPersistentMembers(type) : getPropertyAccessPersistentMembers(type); } return getDefaultAccessPersistentMembers(type, access); } /** * Collect members for the given type which uses explicit field access. */ private Set<Element> getFieldAccessPersistentMembers(TypeElement type) { List<? extends Element> allMembers = type.getEnclosedElements(); Set<VariableElement> allFields = (Set<VariableElement>) filter(allMembers, fieldFilter, nonTransientFilter); Set<ExecutableElement> allMethods = (Set<ExecutableElement>) filter(allMembers, methodFilter, nonTransientFilter); Set<ExecutableElement> getters = filter(allMethods, getterFilter, propertyAccessFilter, annotatedFilter); Set<ExecutableElement> setters = filter(allMethods, setterFilter); getters = matchGetterAndSetter(getters, setters); return merge(getters, allFields); } /** * Collect members for the given type which uses explicit field access. */ private Set<Element> getPropertyAccessPersistentMembers(TypeElement type) { List<? extends Element> allMembers = type.getEnclosedElements(); Set<ExecutableElement> allMethods = (Set<ExecutableElement>) filter(allMembers, methodFilter, nonTransientFilter); Set<ExecutableElement> getters = filter(allMethods, getterFilter); Set<ExecutableElement> setters = filter(allMethods, setterFilter); getters = matchGetterAndSetter(getters, setters); return merge(filter(allMembers, fieldFilter, nonTransientFilter, fieldAccessFilter), getters); } private Set<Element> getDefaultAccessPersistentMembers(TypeElement type, int access) { Set<Element> result = new HashSet<Element>(); List<? extends Element> allMembers = type.getEnclosedElements(); if (AccessCode.isField(access)) { Set<VariableElement> allFields = (Set<VariableElement>) filter(allMembers, fieldFilter, nonTransientFilter); result.addAll(allFields); } else { Set<ExecutableElement> allMethods = (Set<ExecutableElement>) filter(allMembers, methodFilter, nonTransientFilter); Set<ExecutableElement> getters = filter(allMethods, getterFilter); Set<ExecutableElement> setters = filter(allMethods, setterFilter); getters = matchGetterAndSetter(getters, setters); result.addAll(getters); } return result; } private int getImplicitAccessType(TypeElement type) { List<? extends Element> allMembers = type.getEnclosedElements(); Set<VariableElement> allFields = (Set<VariableElement>) filter(allMembers, fieldFilter, nonTransientFilter); Set<ExecutableElement> allMethods = (Set<ExecutableElement>) filter(allMembers, methodFilter, nonTransientFilter); Set<VariableElement> annotatedFields = filter(allFields, annotatedFilter); Set<ExecutableElement> getters = filter(allMethods, getterFilter, annotatedFilter); Set<ExecutableElement> setters = filter(allMethods, setterFilter); getters = matchGetterAndSetter(getters, setters); boolean isFieldAccess = !annotatedFields.isEmpty(); boolean isPropertyAccess = !getters.isEmpty(); if (isFieldAccess && isPropertyAccess) { throw new UserException(_loc.get("access-mixed", type, toString(annotatedFields), toString(getters))); } if (isFieldAccess) { return AccessCode.FIELD; } else if (isPropertyAccess) { return AccessCode.PROPERTY; } else { TypeElement superType = getPersistentSupertype(type); return (superType == null) ? AccessCode.FIELD : determineTypeAccess(superType); } } Set<Element> merge(Set<? extends Element> a, Set<? extends Element> b) { Set<Element> result = new HashSet<Element>(); result.addAll(a); for (Element e1 : b) { boolean hide = false; String key = getPersistentMemberName(e1); for (Element e2 : a) { if (getPersistentMemberName(e2).equals(key)) { hide = true; break; } } if (!hide) { result.add(e1); } } return result; } /** * Matches the given getters with the given setters. Removes the getters * that do not have a corresponding setter. */ private Set<ExecutableElement> matchGetterAndSetter( Set<ExecutableElement> getters, Set<ExecutableElement> setters) { Collection<ExecutableElement> unmatched = new ArrayList<ExecutableElement>(); for (ExecutableElement getter : getters) { String getterName = getter.getSimpleName().toString(); TypeMirror getterReturnType = getter.getReturnType(); String expectedSetterName = "set" + getterName.substring( (isBooleanGetter(getter) ? "is" : "get").length()); boolean matched = false; for (ExecutableElement setter : setters) { TypeMirror setterArgType = setter.getParameters() .iterator().next().asType(); String actualSetterName = setter.getSimpleName().toString(); matched = actualSetterName.equals(expectedSetterName) && typeUtility.isSameType(setterArgType, getterReturnType); if (matched) break; } if (!matched) { logger.warn(_loc.get("getter-unmatched", getter, getter.getEnclosingElement())); unmatched.add(getter); } } getters.removeAll(unmatched); return getters; } // ======================================================================== // Selection Filters select specific elements from a collection. // ======================================================================== /** * Inclusive element filtering predicate. * */ private static interface InclusiveFilter<T extends Element> { /** * Return true to include the given element. */ boolean includes(T e); } /** * Filter the given collection with the conjunction of filters. The given * collection itself is not modified. */ <T extends Element> Set<T> filter(Collection<T> coll, InclusiveFilter... filters) { Set<T> result = new HashSet<T>(); for (T e : coll) { boolean include = true; for (InclusiveFilter f : filters) { if (!f.includes(e)) { include = false; break; } } if (include) result.add(e); } return result; } /** * Selects getter method. A getter method name starts with 'get', returns a * non-void type and has no argument. Or starts with 'is', returns a boolean * and has no argument. * */ static class GetterFilter implements InclusiveFilter<ExecutableElement> { public boolean includes(ExecutableElement method) { return isGetter(method); } } /** * Selects setter method. A setter method name starts with 'set', returns a * void and has single argument. * */ static class SetterFilter implements InclusiveFilter<ExecutableElement> { public boolean includes(ExecutableElement method) { return isSetter(method); } } /** * Selects elements which is annotated with @Access annotation and that * annotation has the given AccessType value. * */ static class AccessFilter implements InclusiveFilter<Element> { final AccessType target; public AccessFilter(AccessType target) { this.target = target; } public boolean includes(Element obj) { Object value = getAnnotationValue(obj, Access.class); return equalsByValue(target, value); } } /** * Selects elements of given kind. * */ static class KindFilter implements InclusiveFilter<Element> { final ElementKind target; public KindFilter(ElementKind target) { this.target = target; } public boolean includes(Element obj) { return obj.getKind() == target; } } /** * Selects all non-transient element. */ static class TransientFilter implements InclusiveFilter<Element> { public boolean includes(Element obj) { Set<Modifier> modifiers = obj.getModifiers(); boolean isTransient = isAnnotatedWith(obj, Transient.class) || modifiers.contains(Modifier.TRANSIENT); return !isTransient && !modifiers.contains(Modifier.STATIC); } } /** * Selects all annotated element. */ static class AnnotatedFilter implements InclusiveFilter<Element> { public boolean includes(Element obj) { return isAnnotated(obj); } } /** * Get access type of the given class, if specified explicitly. * null otherwise. * * @param type * @return FIELD or PROPERTY */ AccessType getExplicitAccessType(TypeElement type) { Object access = getAnnotationValue(type, Access.class); if (equalsByValue(AccessType.FIELD, access)) return AccessType.FIELD; if (equalsByValue(AccessType.PROPERTY, access)) return AccessType.PROPERTY; return null; } /** * Gets the value of the given annotation, if present, in the given * declaration. Otherwise, null. */ public static Object getAnnotationValue(Element decl, Class<? extends Annotation> anno) { return getAnnotationValue(decl, anno, "value"); } /** * Gets the value of the given attribute of the given annotation, if * present, in the given declaration. Otherwise, null. */ public static Object getAnnotationValue(Element e, Class<? extends Annotation> anno, String attr) { if (e == null || e.getAnnotation(anno) == null) return null; List<? extends AnnotationMirror> annos = e.getAnnotationMirrors(); for (AnnotationMirror mirror : annos) { if (mirror.getAnnotationType().toString().equals(anno.getName())) { Map<? extends ExecutableElement, ? extends AnnotationValue> values = mirror.getElementValues(); for (Entry<? extends ExecutableElement, ? extends AnnotationValue> entry : values.entrySet()) { if (entry.getKey().getSimpleName().toString().equals(attr)) { return entry.getValue().getValue(); } } } } return null; } public static String toString(Collection<? extends Element> elements) { StringBuilder tmp = new StringBuilder(); int i = 0; for (Element e : elements) { tmp.append(e.getSimpleName() + (++i == elements.size() ? "" : ",")); } return tmp.toString(); } String toDetails(Element e) { TypeMirror mirror = e.asType(); return new StringBuilder(e.getKind().toString()).append(" ") .append(e.toString()) .append("Mirror ") .append(mirror.getKind().toString()) .append(mirror.toString()).toString(); } String getPersistentMemberName(Element e) { return isMethod(e) ? extractFieldName((ExecutableElement)e) : e.getSimpleName().toString(); } public String extractFieldName(ExecutableElement method) { String name = method.getSimpleName().toString(); String head = isNormalGetter(method) ? "get" : "is"; name = name.substring(head.length()); return Character.toLowerCase(name.charAt(0)) + name.substring(1); } // ========================================================================= // Annotation processing utilities // ========================================================================= /** * Affirms if the given element is annotated with <em>any</em> * <code>javax.persistence.*</code> or <code>org.apache.openjpa.*</code> * annotation. */ public static boolean isAnnotated(Element e) { return isAnnotatedWith(e, (Set<String>)null); } /** * Affirms if the given declaration has the given annotation. */ boolean isAnnotatedAsEntity(Element e) { return isAnnotatedWith(e, Entity.class) || isAnnotatedWith(e, Embeddable.class) || isAnnotatedWith(e, MappedSuperclass.class); } /** * Affirms if the given declaration has the given annotation. */ public static boolean isAnnotatedWith(Element e, Class<? extends Annotation> anno) { return e != null && e.getAnnotation(anno) != null; } /** * Affirms if the given element is annotated with any of the given * annotations. * * @param annos null checks for any annotation that starts with * 'javax.persistence.' or 'openjpa.*'. * */ public static boolean isAnnotatedWith(Element e, Set<String> annos) { if (e == null) return false; List<? extends AnnotationMirror> mirrors = e.getAnnotationMirrors(); if (annos == null) { for (AnnotationMirror mirror : mirrors) { String name = mirror.getAnnotationType().toString(); if (startsWith(name, "javax.persistence.") || startsWith(name, "org.apache.openjpa.")) return true; } return false; } else { for (AnnotationMirror mirror : mirrors) { String name = mirror.getAnnotationType().toString(); if (annos.contains(name)) return true; } return false; } } TypeMirror getTargetEntityType(Element e) { for (Class<? extends Annotation> anno : mappingAnnos) { Object target = getAnnotationValue(e, anno, "targetEntity"); if (target != null) { return (TypeMirror)target; } }; return null; } String getDeclaredTypeName(TypeMirror mirror) { return getDeclaredTypeName(mirror, true); } String getDeclaredTypeName(TypeMirror mirror, boolean box) { return getDeclaredTypeName(mirror, box, false); } /** * Get the element name of the class the given mirror represents. If the * mirror is primitive then returns the corresponding boxed class name. * If the mirror is parameterized returns only the generic type i.e. * if the given declared type is * <code>java.util.Set<java.lang.String></code> this method will * return <code>java.util.Set</code>. */ String getDeclaredTypeName(TypeMirror mirror, boolean box, boolean persistentCollection) { if (mirror == null || mirror.getKind() == TypeKind.NULL || mirror.getKind() == TypeKind.WILDCARD) return "java.lang.Object"; if (mirror.getKind() == TypeKind.ARRAY) { if(persistentCollection) { TypeMirror comp = ((ArrayType)mirror).getComponentType(); return getDeclaredTypeName(comp, false); } else { return mirror.toString(); } } mirror = box ? box(mirror) : mirror; if (isPrimitive(mirror)) return ((PrimitiveType)mirror).toString(); Element elem = typeUtility.asElement(mirror); if (elem == null) throw new RuntimeException(_loc.get("mmg-no-type", mirror).getMessage()); return elem.toString(); } /** * Gets the declared type of the given member. For fields, returns the * declared type while for method returns the return type. * * @param e a field or method. * @exception if given member is neither a field nor a method. */ TypeMirror getDeclaredType(Element e) { TypeMirror result = null; switch (e.getKind()) { case FIELD: result = e.asType(); break; case METHOD: result = ((ExecutableElement) e).getReturnType(); break; default: throw new IllegalArgumentException(toDetails(e)); } return result; } /** * Affirms if the given type mirrors a primitive. */ private boolean isPrimitive(TypeMirror mirror) { TypeKind kind = mirror.getKind(); return kind == TypeKind.BOOLEAN || kind == TypeKind.BYTE || kind == TypeKind.CHAR || kind == TypeKind.DOUBLE || kind == TypeKind.FLOAT || kind == TypeKind.INT || kind == TypeKind.LONG || kind == TypeKind.SHORT; } public TypeMirror box(TypeMirror t) { if (isPrimitive(t)) return processingEnv.getTypeUtils() .boxedClass((PrimitiveType)t).asType(); return t; } /** * Gets the parameter type argument at the given index of the given type. * * @return if the given type represents a parameterized type, then the * indexed parameter type argument. Otherwise null. */ TypeMirror getTypeParameter(Element e, TypeMirror mirror, int index, boolean checkTarget) { if (mirror.getKind() == TypeKind.ARRAY) return ((ArrayType)mirror).getComponentType(); if (mirror.getKind() != TypeKind.DECLARED) return null; if (checkTarget) { TypeMirror target = getTargetEntityType(e); if (target != null) return target; } List<? extends TypeMirror> params = ((DeclaredType)mirror).getTypeArguments(); TypeMirror param = (params == null || params.size() < index+1) ? typeUtility.getNullType() : params.get(index); if (param.getKind() == TypeKind.NULL || param.getKind() == TypeKind.WILDCARD) { logger.warn(_loc.get("generic-type-param", e, getDeclaredType(e), e.getEnclosingElement())); } return param; } public TypeElement getPersistentSupertype(TypeElement cls) { if (cls == null) return null; TypeMirror sup = cls.getSuperclass(); if (sup == null || sup.getKind() == TypeKind.NONE || isRootObject(sup)) return null; TypeElement supe = (TypeElement) processingEnv.getTypeUtils().asElement(sup); if (isAnnotatedAsEntity(supe)) return supe; return getPersistentSupertype(supe); } // ======================================================================== // Utilities // ======================================================================== /** * Affirms if the given mirror represents a primitive or non-primitive * boolean. */ public static boolean isBoolean(TypeMirror type) { return (type != null && (type.getKind() == TypeKind.BOOLEAN || "java.lang.Boolean".equals(type.toString()))); } /** * Affirms if the given mirror represents a void. */ public static boolean isVoid(TypeMirror type) { return (type != null && type.getKind() == TypeKind.VOID); } /** * Affirms if the given element represents a method. */ public static boolean isMethod(Element e) { return e != null && ExecutableElement.class.isInstance(e) && e.getKind() == ElementKind.METHOD; } /** * Affirms if the given method matches the following signature * <code> public T getXXX() </code> * where T is any non-void type. */ public static boolean isNormalGetter(ExecutableElement method) { String methodName = method.getSimpleName().toString(); return method.getKind() == ElementKind.METHOD && startsWith(methodName, "get") && method.getParameters().isEmpty() && !isVoid(method.getReturnType()); } /** * Affirms if the given method matches the following signature * <code> public boolean isXyz() </code> * <code> public Boolean isXyz() </code> */ public static boolean isBooleanGetter(ExecutableElement method) { String methodName = method.getSimpleName().toString(); return method.getKind() == ElementKind.METHOD && startsWith(methodName, "is") && method.getParameters().isEmpty() && isBoolean(method.getReturnType()); } public static boolean isGetter(ExecutableElement method) { return isNormalGetter(method) || isBooleanGetter(method); } /** * Affirms if the given method matches the following signature * <code> public void setXXX(T t) </code> */ public static boolean isSetter(ExecutableElement method) { String methodName = method.getSimpleName().toString(); return method.getKind() == ElementKind.METHOD && startsWith(methodName, "set") && method.getParameters().size() == 1 && isVoid(method.getReturnType()); } /** * Affirms if the given mirror represents root java.lang.Object. */ public static boolean isRootObject(TypeMirror type) { return type != null && "java.lang.Object".equals(type.toString()); } /** * Affirms if the given full string starts with the given head. */ public static boolean startsWith(String full, String head) { return full != null && head != null && full.startsWith(head) && full.length() > head.length(); } /** * Affirms if the given enum equals the given value. */ public static boolean equalsByValue(Enum<?> e, Object v) { return e == v || (v != null && e != null && e.toString().equals(v.toString())); } }