/*
* Copyright 2010 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 org.apache.commons.lang.StringUtils;
import org.gradle.api.Action;
import org.gradle.api.GradleException;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.Task;
import org.gradle.api.internal.TaskInternal;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction;
import org.gradle.util.ReflectionUtil;
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 java.util.*;
import java.util.concurrent.Callable;
/**
* A {@link ITaskFactory} which determines task actions, inputs and outputs based on annotation attached to the task
* properties. Also provides some validation based on these annotations.
*/
public class AnnotationProcessingTaskFactory implements ITaskFactory {
private final ITaskFactory taskFactory;
private final Map<Class, List<Action<Task>>> actionsForType = new HashMap<Class, List<Action<Task>>>();
private final List<? extends PropertyAnnotationHandler> handlers = Arrays.asList(
new InputFilePropertyAnnotationHandler(),
new InputDirectoryPropertyAnnotationHandler(),
new InputFilesPropertyAnnotationHandler(),
new OutputFilePropertyAnnotationHandler(),
new OutputDirectoryPropertyAnnotationHandler(),
new InputPropertyAnnotationHandler(),
new NestedBeanPropertyAnnotationHandler());
private final ValidationAction notNullValidator = new ValidationAction() {
public void validate(String propertyName, Object value) throws InvalidUserDataException {
if (value == null) {
throw new InvalidUserDataException(String.format("No value has been specified for property '%s'.",
propertyName));
}
}
};
public AnnotationProcessingTaskFactory(ITaskFactory taskFactory) {
this.taskFactory = taskFactory;
}
public TaskInternal createTask(ProjectInternal project, Map<String, ?> args) {
TaskInternal task = taskFactory.createTask(project, args);
Class<? extends Task> type = task.getClass();
List<Action<Task>> actions = actionsForType.get(type);
if (actions == null) {
actions = createActionsForType(type);
actionsForType.put(type, actions);
}
for (Action<Task> action : actions) {
task.doFirst(action);
if (action instanceof Validator) {
Validator validator = (Validator) action;
validator.addInputsAndOutputs(task);
}
}
return task;
}
private List<Action<Task>> createActionsForType(Class<? extends Task> type) {
List<Action<Task>> actions = new ArrayList<Action<Task>>();
findTaskActions(type, actions);
findProperties(type, actions);
return actions;
}
private void findProperties(Class<? extends Task> type, List<Action<Task>> actions) {
Validator validator = new Validator();
validator.attachActions(null, type);
if (!validator.properties.isEmpty()) {
actions.add(validator);
}
}
private void findTaskActions(Class<? extends Task> type, List<Action<Task>> actions) {
Set<String> methods = new HashSet<String>();
for (Class current = type; current != null; current = current.getSuperclass()) {
for (Method method : current.getDeclaredMethods()) {
attachTaskAction(method, actions, methods);
}
}
}
private void attachTaskAction(final Method method, Collection<Action<Task>> actions, Collection<String> methods) {
if (method.getAnnotation(TaskAction.class) == null) {
return;
}
if (Modifier.isStatic(method.getModifiers())) {
throw new GradleException(String.format("Cannot use @TaskAction annotation on static method %s.%s().",
method.getDeclaringClass().getSimpleName(), method.getName()));
}
if (method.getParameterTypes().length > 0) {
throw new GradleException(String.format(
"Cannot use @TaskAction annotation on method %s.%s() as this method takes parameters.",
method.getDeclaringClass().getSimpleName(), method.getName()));
}
if (methods.contains(method.getName())) {
return;
}
methods.add(method.getName());
actions.add(new Action<Task>() {
public void execute(Task task) {
ReflectionUtil.invoke(task, method.getName(), new Object[0]);
}
});
}
private static boolean isGetter(Method method) {
return method.getName().startsWith("get") && method.getReturnType() != Void.TYPE
&& method.getParameterTypes().length == 0 && !Modifier.isStatic(method.getModifiers());
}
private class Validator implements Action<Task> {
private Set<PropertyInfo> properties = new LinkedHashSet<PropertyInfo>();
public void addInputsAndOutputs(final Task task) {
for (final PropertyInfo property : properties) {
Callable<Object> futureValue = new Callable<Object>() {
public Object call() throws Exception {
return property.getValue(task).getValue();
}
};
property.configureAction.update(task, futureValue);
}
}
public void execute(Task task) {
try {
List<PropertyValue> propertyValues = new ArrayList<PropertyValue>();
for (PropertyInfo property : properties) {
propertyValues.add(property.getValue(task));
}
for (PropertyValue propertyValue : propertyValues) {
propertyValue.checkNotNull();
}
for (PropertyValue propertyValue : propertyValues) {
propertyValue.checkSkip();
}
for (PropertyValue propertyValue : propertyValues) {
propertyValue.checkValid();
}
} catch (InvalidUserDataException e) {
throw new InvalidUserDataException(String.format("Error validating %s: %s", task, e.getMessage()), e);
}
}
public void attachActions(PropertyInfo parent, Class<?> type) {
if (type.getSuperclass() != null) {
attachActions(parent, type.getSuperclass());
}
for (Method method : type.getDeclaredMethods()) {
if (!isGetter(method)) {
continue;
}
String fieldName = StringUtils.uncapitalize(method.getName().substring(3));
String propertyName = fieldName;
if (parent != null) {
propertyName = parent.getName() + '.' + propertyName;
}
PropertyInfo propertyInfo = new PropertyInfo(this, parent, propertyName, method);
attachValidationActions(propertyInfo, fieldName);
if (propertyInfo.required) {
properties.add(propertyInfo);
}
}
}
private void attachValidationActions(PropertyInfo propertyInfo, String fieldName) {
for (PropertyAnnotationHandler handler : handlers) {
attachValidationAction(handler, propertyInfo, fieldName);
}
}
private void attachValidationAction(PropertyAnnotationHandler handler, PropertyInfo propertyInfo, String fieldName) {
final Method method = propertyInfo.method;
Class<? extends Annotation> annotationType = handler.getAnnotationType();
AnnotatedElement annotationTarget = null;
if (method.getAnnotation(annotationType) != null) {
annotationTarget = method;
} else {
try {
Field field = method.getDeclaringClass().getDeclaredField(fieldName);
if (field.getAnnotation(annotationType) != null) {
annotationTarget = field;
}
} catch (NoSuchFieldException e) {
// ok - ignore
}
}
if (annotationTarget == null) {
return;
}
Annotation optional = annotationTarget.getAnnotation(Optional.class);
if (optional == null) {
propertyInfo.setNotNullValidator(notNullValidator);
}
propertyInfo.attachActions(handler);
}
}
private interface PropertyValue {
Object getValue();
void checkNotNull();
void checkSkip();
void checkValid();
}
private static class PropertyInfo implements PropertyActionContext {
private static final ValidationAction NO_OP_VALIDATION_ACTION = new ValidationAction() {
public void validate(String propertyName, Object value) throws InvalidUserDataException {
}
};
private static final PropertyValue NO_OP_VALUE = new PropertyValue() {
public Object getValue() {
return null;
}
public void checkNotNull() {
}
public void checkSkip() {
}
public void checkValid() {
}
};
private static final UpdateAction NO_OP_CONFIGURATION_ACTION = new UpdateAction() {
public void update(Task task, Callable<Object> futureValue) {
}
};
private final Validator validator;
private final PropertyInfo parent;
private final String propertyName;
private final Method method;
private ValidationAction skipAction = NO_OP_VALIDATION_ACTION;
private ValidationAction validationAction = NO_OP_VALIDATION_ACTION;
private ValidationAction notNullValidator = NO_OP_VALIDATION_ACTION;
private UpdateAction configureAction = NO_OP_CONFIGURATION_ACTION;
public boolean required;
private PropertyInfo(Validator validator, PropertyInfo parent, String propertyName, Method method) {
this.validator = validator;
this.parent = parent;
this.propertyName = propertyName;
this.method = method;
}
@Override
public String toString() {
return propertyName;
}
public String getName() {
return propertyName;
}
public Class<?> getType() {
return method.getReturnType();
}
public AnnotatedElement getTarget() {
return method;
}
public void setSkipAction(ValidationAction action) {
skipAction = action;
}
public void setValidationAction(ValidationAction action) {
validationAction = action;
}
public void setConfigureAction(UpdateAction action) {
configureAction = action;
}
public void setNotNullValidator(ValidationAction notNullValidator) {
this.notNullValidator = notNullValidator;
}
public void attachActions(Class<?> type) {
validator.attachActions(this, type);
}
public PropertyValue getValue(Object rootObject) {
Object bean = rootObject;
if (parent != null) {
PropertyValue parentValue = parent.getValue(rootObject);
if (parentValue.getValue() == null) {
return NO_OP_VALUE;
}
bean = parentValue.getValue();
}
final Object value = ReflectionUtil.invoke(bean, method.getName(), new Object[0]);
return new PropertyValue() {
public Object getValue() {
return value;
}
public void checkNotNull() {
notNullValidator.validate(propertyName, value);
}
public void checkSkip() {
skipAction.validate(propertyName, value);
}
public void checkValid() {
if (value != null) {
validationAction.validate(propertyName, value);
}
}
};
}
public void attachActions(PropertyAnnotationHandler handler) {
handler.attachActions(this);
required = true;
}
}
}