/*
* Copyright 2016 the original author or authors.
*
* 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.gradle.api.internal.project.taskfactory;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import groovy.lang.GroovyObject;
import org.gradle.api.DefaultTask;
import org.gradle.api.Task;
import org.gradle.api.internal.AbstractTask;
import org.gradle.api.internal.ConventionTask;
import org.gradle.api.internal.tasks.options.OptionValues;
import org.gradle.api.tasks.CacheableTask;
import org.gradle.api.tasks.Console;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.internal.reflect.GroovyMethods;
import org.gradle.internal.reflect.PropertyAccessorType;
import org.gradle.internal.reflect.Types;
import javax.inject.Inject;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
public class DefaultTaskClassValidatorExtractor implements TaskClassValidatorExtractor {
// Avoid reflecting on classes we know we don't need to look at
private static final Collection<Class<?>> IGNORED_SUPER_CLASSES = ImmutableSet.<Class<?>>of(
ConventionTask.class, DefaultTask.class, AbstractTask.class, Task.class, Object.class, GroovyObject.class
);
private final static List<? extends PropertyAnnotationHandler> HANDLERS = Arrays.asList(
new InputFilePropertyAnnotationHandler(),
new InputDirectoryPropertyAnnotationHandler(),
new InputFilesPropertyAnnotationHandler(),
new OutputFilePropertyAnnotationHandler(),
new OutputFilesPropertyAnnotationHandler(),
new OutputDirectoryPropertyAnnotationHandler(),
new OutputDirectoriesPropertyAnnotationHandler(),
new InputPropertyAnnotationHandler(),
new DestroysPropertyAnnotationHandler(),
new NestedBeanPropertyAnnotationHandler(),
new NoOpPropertyAnnotationHandler(Inject.class),
new NoOpPropertyAnnotationHandler(Console.class),
new NoOpPropertyAnnotationHandler(Internal.class),
new NoOpPropertyAnnotationHandler(OptionValues.class)
);
private final Map<Class<? extends Annotation>, PropertyAnnotationHandler> annotationHandlers;
private final Multimap<Class<? extends Annotation>, Class<? extends Annotation>> annotationOverrides;
private final Set<Class<? extends Annotation>> relevantAnnotationTypes;
public DefaultTaskClassValidatorExtractor(PropertyAnnotationHandler... customAnnotationHandlers) {
this(Arrays.asList(customAnnotationHandlers));
}
public DefaultTaskClassValidatorExtractor(Iterable<? extends PropertyAnnotationHandler> customAnnotationHandlers) {
Iterable<PropertyAnnotationHandler> allAnnotationHandlers = Iterables.concat(HANDLERS, customAnnotationHandlers);
Map<Class<? extends Annotation>, PropertyAnnotationHandler> annotationsHandlers = Maps.uniqueIndex(allAnnotationHandlers, new Function<PropertyAnnotationHandler, Class<? extends Annotation>>() {
@Override
public Class<? extends Annotation> apply(PropertyAnnotationHandler handler) {
return handler.getAnnotationType();
}
});
this.annotationHandlers = annotationsHandlers;
this.annotationOverrides = collectAnnotationOverrides(allAnnotationHandlers);
this.relevantAnnotationTypes = collectRelevantAnnotationTypes(annotationsHandlers.keySet());
}
private static Multimap<Class<? extends Annotation>, Class<? extends Annotation>> collectAnnotationOverrides(Iterable<PropertyAnnotationHandler> allAnnotationHandlers) {
ImmutableSetMultimap.Builder<Class<? extends Annotation>, Class<? extends Annotation>> builder = ImmutableSetMultimap.builder();
for (PropertyAnnotationHandler handler : allAnnotationHandlers) {
if (handler instanceof OverridingPropertyAnnotationHandler) {
builder.put(((OverridingPropertyAnnotationHandler) handler).getOverriddenAnnotationType(), handler.getAnnotationType());
}
}
return builder.build();
}
private static Set<Class<? extends Annotation>> collectRelevantAnnotationTypes(Set<Class<? extends Annotation>> propertyTypeAnnotations) {
return ImmutableSet.<Class<? extends Annotation>>builder()
.addAll(propertyTypeAnnotations)
.add(Optional.class)
.add(SkipWhenEmpty.class)
.add(PathSensitive.class)
.build();
}
@Override
public TaskClassValidator extractValidator(Class<? extends Task> type) {
boolean cacheable = type.isAnnotationPresent(CacheableTask.class);
ImmutableSortedSet.Builder<TaskPropertyInfo> annotatedPropertiesBuilder = ImmutableSortedSet.naturalOrder();
ImmutableList.Builder<TaskClassValidationMessage> validationMessages = ImmutableList.builder();
Queue<TypeEntry> queue = new ArrayDeque<TypeEntry>();
queue.add(new TypeEntry(null, type));
while (!queue.isEmpty()) {
TypeEntry entry = queue.remove();
parseProperties(entry.parent, entry.type, annotatedPropertiesBuilder, validationMessages, cacheable, queue);
}
return new TaskClassValidator(annotatedPropertiesBuilder.build(), validationMessages.build(), cacheable);
}
private <T> void parseProperties(final TaskPropertyInfo parent, Class<T> type, ImmutableSet.Builder<TaskPropertyInfo> annotatedProperties, final ImmutableCollection.Builder<TaskClassValidationMessage> validationMessages, final boolean cacheable, Queue<TypeEntry> queue) {
final Set<Class<? extends Annotation>> propertyTypeAnnotations = annotationHandlers.keySet();
final Map<String, DefaultTaskPropertyActionContext> propertyContexts = Maps.newLinkedHashMap();
Types.walkTypeHierarchy(type, IGNORED_SUPER_CLASSES, new Types.TypeVisitor<T>() {
@Override
public void visitType(Class<? super T> type) {
Map<String, Field> fields = getFields(type);
List<Getter> getters = getGetters(type);
for (Getter getter : getters) {
Method method = getter.getMethod();
String fieldName = getter.getName();
Field field = fields.get(fieldName);
String propertyName = parent != null ? parent.getName() + '.' + fieldName : fieldName;
DefaultTaskPropertyActionContext propertyContext = propertyContexts.get(propertyName);
if (propertyContext == null) {
propertyContext = new DefaultTaskPropertyActionContext(propertyTypeAnnotations, parent, propertyName, method, cacheable, validationMessages);
propertyContexts.put(propertyName, propertyContext);
}
if (field != null) {
propertyContext.setInstanceVariableField(field);
}
Iterable<Annotation> declaredAnnotations = mergeDeclaredAnnotations(propertyContext, method, field);
// Discard overridden property type annotations when an overriding annotation is also present
Iterable<Annotation> overriddenAnnotations = filterOverridingAnnotations(declaredAnnotations, propertyTypeAnnotations);
recordAnnotations(propertyContext, overriddenAnnotations, propertyTypeAnnotations);
}
}
});
for (DefaultTaskPropertyActionContext propertyContext : propertyContexts.values()) {
TaskPropertyInfo property = createProperty(propertyContext);
if (property != null) {
annotatedProperties.add(property);
Class<?> nestedType = propertyContext.getNestedType();
if (nestedType != null) {
queue.add(new TypeEntry(property, nestedType));
}
}
}
}
private Iterable<Annotation> mergeDeclaredAnnotations(TaskPropertyActionContext propertyContext, Method method, Field field) {
Collection<Annotation> methodAnnotations = collectRelevantAnnotations(method.getDeclaredAnnotations());
if (field == null) {
return methodAnnotations;
}
Collection<Annotation> fieldAnnotations = collectRelevantAnnotations(field.getDeclaredAnnotations());
if (fieldAnnotations.isEmpty()) {
return methodAnnotations;
}
if (methodAnnotations.isEmpty()) {
return fieldAnnotations;
}
for (Annotation methodAnnotation : methodAnnotations) {
Iterator<Annotation> iFieldAnnotation = fieldAnnotations.iterator();
while (iFieldAnnotation.hasNext()) {
Annotation fieldAnnotation = iFieldAnnotation.next();
if (methodAnnotation.annotationType().equals(fieldAnnotation.annotationType())) {
propertyContext.validationMessage("has both a getter and field declared with annotation @" + methodAnnotation.annotationType().getSimpleName());
iFieldAnnotation.remove();
}
}
}
return Iterables.concat(methodAnnotations, fieldAnnotations);
}
private Collection<Annotation> collectRelevantAnnotations(Annotation[] annotations) {
List<Annotation> relevantAnnotations = Lists.newArrayListWithCapacity(annotations.length);
for (Annotation annotation : annotations) {
if (relevantAnnotationTypes.contains(annotation.annotationType())) {
relevantAnnotations.add(annotation);
}
}
return relevantAnnotations;
}
private Iterable<Annotation> filterOverridingAnnotations(final Iterable<Annotation> declaredAnnotations, final Set<Class<? extends Annotation>> propertyTypeAnnotations) {
return Iterables.filter(declaredAnnotations, new Predicate<Annotation>() {
@Override
public boolean apply(Annotation input) {
Class<? extends Annotation> annotationType = input.annotationType();
if (!propertyTypeAnnotations.contains(annotationType)) {
return true;
}
for (Class<? extends Annotation> overridingAnnotation : annotationOverrides.get(annotationType)) {
for (Annotation declaredAnnotation : declaredAnnotations) {
if (declaredAnnotation.annotationType().equals(overridingAnnotation)) {
return false;
}
}
}
return true;
}
});
}
private void recordAnnotations(TaskPropertyActionContext propertyContext, Iterable<Annotation> annotations, Set<Class<? extends Annotation>> propertyTypeAnnotations) {
Set<Class<? extends Annotation>> declaredPropertyTypes = Sets.newLinkedHashSet();
for (Annotation annotation : annotations) {
if (propertyTypeAnnotations.contains(annotation.annotationType())) {
declaredPropertyTypes.add(annotation.annotationType());
}
propertyContext.addAnnotation(annotation);
}
if (declaredPropertyTypes.size() > 1) {
propertyContext.validationMessage("has conflicting property types declared: "
+ Joiner.on(", ").join(Iterables.transform(declaredPropertyTypes, new Function<Class<? extends Annotation>, String>() {
@Override
public String apply(Class<? extends Annotation> annotationType) {
return "@" + annotationType.getSimpleName();
}
}))
);
}
}
private static class TypeEntry {
private final TaskPropertyInfo parent;
private final Class<?> type;
public TypeEntry(TaskPropertyInfo parent, Class<?> type) {
this.parent = parent;
this.type = type;
}
}
private TaskPropertyInfo createProperty(DefaultTaskPropertyActionContext propertyContext) {
Class<? extends Annotation> propertyType = propertyContext.getPropertyType();
if (propertyType != null) {
if (propertyContext.isAnnotationPresent(Optional.class)) {
propertyContext.setOptional(true);
}
PropertyAnnotationHandler handler = annotationHandlers.get(propertyType);
handler.attachActions(propertyContext);
return propertyContext.createProperty();
} else {
propertyContext.validationMessage("is not annotated with an input or output annotation");
return null;
}
}
private static Map<String, Field> getFields(Class<?> type) {
Map<String, Field> fields = Maps.newHashMap();
for (Field field : type.getDeclaredFields()) {
fields.put(field.getName(), field);
}
return fields;
}
private static List<Getter> getGetters(Class<?> type) {
Method[] methods = type.getDeclaredMethods();
List<Getter> getters = Lists.newArrayListWithCapacity(methods.length);
for (Method method : methods) {
PropertyAccessorType accessorType = PropertyAccessorType.of(method);
// We only care about getters
if (accessorType == null || accessorType == PropertyAccessorType.SETTER) {
continue;
}
// We only care about actual methods the user added
if (method.isBridge() || GroovyMethods.isObjectMethod(method)) {
continue;
}
getters.add(new DefaultTaskClassValidatorExtractor.Getter(method, accessorType.propertyNameFor(method)));
}
Collections.sort(getters);
return getters;
}
private static class Getter implements Comparable<Getter> {
private final Method method;
private final String name;
public Getter(Method method, String name) {
this.method = method;
this.name = name;
}
public String getName() {
return name;
}
public Method getMethod() {
return method;
}
@Override
public int compareTo(Getter o) {
// Sort "is"-getters before "get"-getters when both are available
return method.getName().compareTo(o.method.getName());
}
}
private static class DefaultTaskPropertyActionContext implements TaskPropertyActionContext {
private final Set<Class<? extends Annotation>> propertyTypeAnnotations;
private final TaskPropertyInfo parent;
private final String name;
private final Method method;
private final List<Annotation> annotations = Lists.newArrayList();
private final boolean cacheable;
private final ImmutableCollection.Builder<TaskClassValidationMessage> validationMessages;
private Field instanceVariableField;
private ValidationAction validationAction;
private UpdateAction configureAction;
private boolean optional;
private Class<?> nestedType;
private Class<? extends Annotation> propertyType;
public DefaultTaskPropertyActionContext(Set<Class<? extends Annotation>> propertyTypeAnnotations, TaskPropertyInfo parent, String name, Method method, boolean cacheable, ImmutableCollection.Builder<TaskClassValidationMessage> validationMessages) {
this.propertyTypeAnnotations = propertyTypeAnnotations;
this.parent = parent;
this.name = name;
this.method = method;
this.cacheable = cacheable;
this.validationMessages = validationMessages;
}
@Override
public String getName() {
return name;
}
@Override
public Class<? extends Annotation> getPropertyType() {
return propertyType;
}
@Override
public Class<?> getValueType() {
return instanceVariableField != null
? instanceVariableField.getType()
: method.getReturnType();
}
@Override
public void addAnnotation(Annotation annotation) {
Class<? extends Annotation> annotationType = annotation.annotationType();
// Record the most specific property type annotation only
if (propertyType == null && isPropertyTypeAnnotation(annotationType)) {
propertyType = annotationType;
}
// Record the most specific annotation only
if (!isAnnotationPresent(annotation.annotationType())) {
annotations.add(annotation);
}
}
private boolean isPropertyTypeAnnotation(Class<? extends Annotation> annotationType) {
return propertyTypeAnnotations.contains(annotationType);
}
@Override
public <A extends Annotation> A getAnnotation(Class<A> annotationType) {
for (Annotation annotation : annotations) {
if (annotationType.equals(annotation.annotationType())) {
return annotationType.cast(annotation);
}
}
return null;
}
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annotationType) {
return getAnnotation(annotationType) != null;
}
@Override
public void setInstanceVariableField(Field instanceVariableField) {
if (this.instanceVariableField == null && instanceVariableField != null) {
this.instanceVariableField = instanceVariableField;
}
}
@Override
public boolean isOptional() {
return optional;
}
@Override
public boolean isCacheable() {
return cacheable;
}
@Override
public void setOptional(boolean optional) {
this.optional = optional;
}
@Override
public void setValidationAction(ValidationAction action) {
this.validationAction = action;
}
@Override
public void setConfigureAction(UpdateAction action) {
this.configureAction = action;
}
public Class<?> getNestedType() {
return nestedType;
}
@Override
public void setNestedType(Class<?> nestedType) {
this.nestedType = nestedType;
}
public TaskPropertyInfo createProperty() {
if (configureAction == null && validationAction == null) {
return null;
}
return new TaskPropertyInfo(parent, name, propertyType, method, validationAction, configureAction, optional);
}
@Override
public void validationMessage(String message) {
validationMessages.add(TaskClassValidationMessage.property(name, message));
}
}
}