/*
* Copyright 2014 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.model.internal.inspect;
import com.google.common.base.Joiner;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.google.common.util.concurrent.UncheckedExecutionException;
import net.jcip.annotations.ThreadSafe;
import org.gradle.api.Nullable;
import org.gradle.api.Transformer;
import org.gradle.internal.Cast;
import org.gradle.internal.Factory;
import org.gradle.internal.UncheckedException;
import org.gradle.internal.reflect.GroovyMethods;
import org.gradle.model.InvalidModelRuleDeclarationException;
import org.gradle.model.RuleInput;
import org.gradle.model.RuleSource;
import org.gradle.model.RuleTarget;
import org.gradle.model.internal.core.ModelAction;
import org.gradle.model.internal.core.ModelPath;
import org.gradle.model.internal.core.ModelReference;
import org.gradle.model.internal.core.ModelView;
import org.gradle.model.internal.core.MutableModelNode;
import org.gradle.model.internal.core.rule.describe.ModelRuleDescriptor;
import org.gradle.model.internal.manage.binding.StructBindings;
import org.gradle.model.internal.manage.binding.StructBindingsStore;
import org.gradle.model.internal.manage.instance.GeneratedViewState;
import org.gradle.model.internal.manage.instance.ManagedInstance;
import org.gradle.model.internal.manage.instance.ManagedProxyFactory;
import org.gradle.model.internal.manage.schema.ModelProperty;
import org.gradle.model.internal.manage.schema.ModelSchema;
import org.gradle.model.internal.manage.schema.ModelSchemaStore;
import org.gradle.model.internal.manage.schema.ScalarValueSchema;
import org.gradle.model.internal.manage.schema.StructSchema;
import org.gradle.model.internal.method.WeaklyTypeReferencingMethod;
import org.gradle.model.internal.registry.ModelRegistry;
import org.gradle.model.internal.registry.RuleContext;
import org.gradle.model.internal.type.ModelType;
import org.gradle.util.CollectionUtils;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;
@ThreadSafe
public class ModelRuleExtractor {
private final LoadingCache<Class<?>, CachedRuleSource> cache = CacheBuilder.newBuilder()
.weakKeys()
.build(new CacheLoader<Class<?>, CachedRuleSource>() {
public CachedRuleSource load(Class<?> source) {
return doExtract(source);
}
});
private final Iterable<MethodModelRuleExtractor> handlers;
private final ManagedProxyFactory proxyFactory;
private final ModelSchemaStore schemaStore;
private final StructBindingsStore structBindingsStore;
public ModelRuleExtractor(Iterable<MethodModelRuleExtractor> handlers, ManagedProxyFactory proxyFactory, ModelSchemaStore schemaStore, StructBindingsStore structBindingsStore) {
this.handlers = handlers;
this.proxyFactory = proxyFactory;
this.schemaStore = schemaStore;
this.structBindingsStore = structBindingsStore;
}
private String describeHandlers() {
String desc = Joiner.on(", ").join(CollectionUtils.collect(handlers, new Transformer<String, MethodModelRuleExtractor>() {
public String transform(MethodModelRuleExtractor original) {
return original.getDescription();
}
}));
return "[" + desc + "]";
}
/**
* Creates a new rule source instance to be applied to a model element.
*
* @throws InvalidModelRuleDeclarationException On badly formed rule source class.
*/
public <T> ExtractedRuleSource<T> extract(Class<T> source) throws InvalidModelRuleDeclarationException {
try {
return cache.get(source).newInstance(source);
} catch (ExecutionException e) {
throw UncheckedException.throwAsUncheckedException(e);
} catch (UncheckedExecutionException e) {
throw UncheckedException.throwAsUncheckedException(e.getCause());
}
}
private <T> CachedRuleSource doExtract(final Class<T> source) {
final ModelType<T> type = ModelType.of(source);
FormattingValidationProblemCollector problems = new FormattingValidationProblemCollector("rule source", type);
DefaultMethodModelRuleExtractionContext context = new DefaultMethodModelRuleExtractionContext(this, problems);
// TODO - exceptions thrown here should point to some extensive documentation on the concept of class rule sources
StructSchema<T> schema = getSchema(source, context);
if (schema == null) {
throw new InvalidModelRuleDeclarationException(problems.format());
}
// sort for determinism
Set<Method> methods = new TreeSet<Method>(Ordering.usingToString());
methods.addAll(Arrays.asList(source.getDeclaredMethods()));
ImmutableList.Builder<ModelProperty<?>> implicitInputs = ImmutableList.builder();
ModelProperty<?> target = null;
for (ModelProperty<?> property : schema.getProperties()) {
if (property.isAnnotationPresent(RuleTarget.class)) {
target = property;
} else if (property.isAnnotationPresent(RuleInput.class) && !(property.getSchema() instanceof ScalarValueSchema)) {
implicitInputs.add(property);
}
for (WeaklyTypeReferencingMethod<?, ?> method : property.getAccessors()) {
methods.remove(method.getMethod());
}
}
ImmutableList.Builder<ExtractedRuleDetails> rules = ImmutableList.builder();
for (Method method : methods) {
MethodRuleDefinition<?, ?> ruleDefinition = DefaultMethodRuleDefinition.create(source, method);
ExtractedModelRule rule = getMethodHandler(ruleDefinition, method, context);
if (rule != null) {
rules.add(new ExtractedRuleDetails(ruleDefinition, rule));
}
}
if (context.hasProblems()) {
throw new InvalidModelRuleDeclarationException(problems.format());
}
StructBindings<T> bindings = structBindingsStore.getBindings(schema.getType());
if (schema.getProperties().isEmpty()) {
return new StatelessRuleSource(rules.build(), Modifier.isAbstract(source.getModifiers()) ? new AbstractRuleSourceFactory<T>(schema, bindings, proxyFactory) : new ConcreteRuleSourceFactory<T>(type));
} else {
return new ParameterizedRuleSource(rules.build(), target, implicitInputs.build(), schema, bindings, proxyFactory);
}
}
private <T> StructSchema<T> getSchema(Class<T> source, RuleSourceValidationProblemCollector problems) {
if (!RuleSource.class.isAssignableFrom(source) || !source.getSuperclass().equals(RuleSource.class)) {
problems.add("Rule source classes must directly extend " + RuleSource.class.getName());
}
ModelSchema<T> schema = schemaStore.getSchema(source);
if (!(schema instanceof StructSchema)) {
return null;
}
validateClass(source, problems);
return (StructSchema<T>) schema;
}
@Nullable
private ExtractedModelRule getMethodHandler(MethodRuleDefinition<?, ?> ruleDefinition, Method method, MethodModelRuleExtractionContext context) {
MethodModelRuleExtractor handler = null;
for (MethodModelRuleExtractor candidateHandler : handlers) {
if (candidateHandler.isSatisfiedBy(ruleDefinition)) {
if (handler == null) {
handler = candidateHandler;
} else {
context.add(method, "Can only be one of " + describeHandlers());
validateRuleMethod(ruleDefinition, method, context);
return null;
}
}
}
if (handler != null) {
validateRuleMethod(ruleDefinition, method, context);
return handler.registration(ruleDefinition, context);
} else {
validateNonRuleMethod(method, context);
return null;
}
}
private void validateClass(Class<?> source, RuleSourceValidationProblemCollector problems) {
int modifiers = source.getModifiers();
if (Modifier.isInterface(modifiers)) {
problems.add("Must be a class, not an interface");
}
if (source.getEnclosingClass() != null) {
if (Modifier.isStatic(modifiers)) {
if (Modifier.isPrivate(modifiers)) {
problems.add("Class cannot be private");
}
} else {
problems.add("Enclosed classes must be static and non private");
}
}
Constructor<?>[] constructors = source.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
if (constructor.getParameterTypes().length > 0) {
problems.add("Cannot declare a constructor that takes arguments");
break;
}
}
Field[] fields = source.getDeclaredFields();
for (Field field : fields) {
int fieldModifiers = field.getModifiers();
if (!field.isSynthetic() && !(Modifier.isStatic(fieldModifiers) && Modifier.isFinal(fieldModifiers))) {
problems.add(field, "Fields must be static final.");
}
}
}
private void validateRuleMethod(MethodRuleDefinition<?, ?> ruleDefinition, Method ruleMethod, RuleSourceValidationProblemCollector problems) {
if (Modifier.isPrivate(ruleMethod.getModifiers())) {
problems.add(ruleMethod, "A rule method cannot be private");
}
if (Modifier.isAbstract(ruleMethod.getModifiers())) {
problems.add(ruleMethod, "A rule method cannot be abstract");
}
if (ruleMethod.getTypeParameters().length > 0) {
problems.add(ruleMethod, "Cannot have type variables (i.e. cannot be a generic method)");
}
// TODO validations on method: synthetic, bridge methods, varargs, abstract, native
ModelType<?> returnType = ModelType.returnType(ruleMethod);
if (returnType.isRawClassOfParameterizedType()) {
problems.add(ruleMethod, "Raw type " + returnType + " used for return type (all type parameters must be specified of parameterized type)");
}
for (int i = 0; i < ruleDefinition.getReferences().size(); i++) {
ModelReference<?> reference = ruleDefinition.getReferences().get(i);
if (reference.getType().isRawClassOfParameterizedType()) {
problems.add(ruleMethod, "Raw type " + reference.getType() + " used for parameter " + (i + 1) + " (all type parameters must be specified of parameterized type)");
}
if (reference.getPath() != null) {
try {
ModelPath.validatePath(reference.getPath().getPath());
} catch (Exception e) {
problems.add(ruleDefinition, "The declared model element path '" + reference.getPath().getPath() + "' used for parameter " + (i + 1) + " is not a valid path", e);
}
}
}
}
private void validateNonRuleMethod(Method method, RuleSourceValidationProblemCollector problems) {
if (!Modifier.isPrivate(method.getModifiers()) && !Modifier.isStatic(method.getModifiers()) && !method.isSynthetic() && !GroovyMethods.isObjectMethod(method)) {
problems.add(method, "A method that is not annotated as a rule must be private");
}
}
private static class ConcreteRuleSourceFactory<T> implements Factory<T> {
// Reference class via `ModelType` to avoid strong reference
private final ModelType<T> type;
public ConcreteRuleSourceFactory(ModelType<T> type) {
this.type = type;
}
@Override
public T create() {
Class<T> concreteClass = type.getConcreteClass();
try {
Constructor<T> declaredConstructor = concreteClass.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
return declaredConstructor.newInstance();
} catch (InvocationTargetException e) {
throw UncheckedException.throwAsUncheckedException(e.getTargetException());
} catch (Exception e) {
throw UncheckedException.throwAsUncheckedException(e);
}
}
}
private static class AbstractRuleSourceFactory<T> implements Factory<T>, GeneratedViewState {
private final StructSchema<T> schema;
private final StructBindings<?> bindings;
private final ManagedProxyFactory proxyFactory;
public AbstractRuleSourceFactory(StructSchema<T> schema, StructBindings<?> bindings, ManagedProxyFactory proxyFactory) {
this.schema = schema;
this.bindings = bindings;
this.proxyFactory = proxyFactory;
}
@Override
public String getDisplayName() {
return "rule source " + schema.getType().getDisplayName();
}
@Override
public Object get(String name) {
throw new UnsupportedOperationException();
}
@Override
public void set(String name, Object value) {
throw new UnsupportedOperationException();
}
@Override
public T create() {
return proxyFactory.createProxy(this, schema, bindings);
}
}
interface CachedRuleSource {
<T> ExtractedRuleSource<T> newInstance(Class<T> source);
}
private static class StatelessRuleSource implements CachedRuleSource {
private final DefaultExtractedRuleSource<?> ruleSource;
public <T> StatelessRuleSource(List<ExtractedRuleDetails> rules, Factory<T> factory) {
this.ruleSource = new StatelessExtractedRuleSource<T>(rules, factory);
}
@Override
public <T> ExtractedRuleSource<T> newInstance(Class<T> source) {
return Cast.uncheckedCast(ruleSource);
}
}
private static class ParameterizedRuleSource implements CachedRuleSource {
private final List<ExtractedRuleDetails> rules;
@Nullable
private final ModelProperty<?> target;
private final List<ModelProperty<?>> implicitInputs;
private final StructSchema<?> schema;
private final StructBindings<?> bindings;
private final ManagedProxyFactory proxyFactory;
public <T> ParameterizedRuleSource(List<ExtractedRuleDetails> rules, @Nullable ModelProperty<?> target, List<ModelProperty<?>> implicitInputs, StructSchema<T> schema, StructBindings<?> bindings, ManagedProxyFactory proxyFactory) {
this.rules = rules;
this.target = target;
this.implicitInputs = implicitInputs;
this.schema = schema;
this.bindings = bindings;
this.proxyFactory = proxyFactory;
}
@Override
public <T> ExtractedRuleSource<T> newInstance(Class<T> source) {
StructSchema<T> schema = Cast.uncheckedCast(this.schema);
return new ParameterizedExtractedRuleSource<T>(rules, target, implicitInputs, schema, bindings, proxyFactory);
}
}
private static class ExtractedRuleDetails {
final MethodRuleDefinition<?, ?> method;
final ExtractedModelRule rule;
public ExtractedRuleDetails(MethodRuleDefinition<?, ?> method, ExtractedModelRule rule) {
this.method = method;
this.rule = rule;
}
}
private static abstract class DefaultExtractedRuleSource<T> implements ExtractedRuleSource<T> {
private final List<ExtractedRuleDetails> rules;
public DefaultExtractedRuleSource(List<ExtractedRuleDetails> rules) {
this.rules = rules;
}
public List<ExtractedModelRule> getRules() {
return CollectionUtils.collect(rules, new Transformer<ExtractedModelRule, ExtractedRuleDetails>() {
@Override
public ExtractedModelRule transform(ExtractedRuleDetails extractedRuleDetails) {
return extractedRuleDetails.rule;
}
});
}
@Override
public void apply(final ModelRegistry modelRegistry, MutableModelNode target) {
final ModelPath targetPath = calculateTarget(target);
for (final ExtractedRuleDetails details : rules) {
details.rule.apply(new MethodModelRuleApplicationContext() {
@Override
public ModelRegistry getRegistry() {
return modelRegistry;
}
@Override
public ModelPath getScope() {
return targetPath;
}
@Override
public ModelAction contextualize(final MethodRuleAction action) {
final List<ModelReference<?>> inputs = withImplicitInputs(action.getInputs());
final ModelReference<?> mappedSubject = mapSubject(action.getSubject(), targetPath);
mapInputs(inputs.subList(0, action.getInputs().size()), targetPath);
final MethodRuleDefinition<?, ?> methodRuleDefinition = details.method;
final Factory<? extends T> factory = getFactory();
return new ContextualizedModelAction<T>(methodRuleDefinition, mappedSubject, inputs, action, factory);
}
}, target);
}
}
protected ModelPath calculateTarget(MutableModelNode target) {
return target.getPath();
}
private void mapInputs(List<ModelReference<?>> inputs, ModelPath targetPath) {
for (int i = 0; i < inputs.size(); i++) {
ModelReference<?> input = inputs.get(i);
if (input.getPath() != null) {
inputs.set(i, input.withPath(targetPath.descendant(input.getPath())));
} else {
inputs.set(i, input.inScope(ModelPath.ROOT));
}
}
}
private ModelReference<?> mapSubject(ModelReference<?> subject, ModelPath targetPath) {
if (subject.getPath() == null) {
return subject.inScope(targetPath);
} else {
return subject.withPath(targetPath.descendant(subject.getPath()));
}
}
protected List<ModelReference<?>> withImplicitInputs(List<? extends ModelReference<?>> inputs) {
return new ArrayList<ModelReference<?>>(inputs);
}
@Override
public List<? extends Class<?>> getRequiredPlugins() {
List<Class<?>> plugins = new ArrayList<Class<?>>();
for (ExtractedRuleDetails details : rules) {
plugins.addAll(details.rule.getRuleDependencies());
}
return plugins;
}
@Override
public void assertNoPlugins() {
for (ExtractedRuleDetails details : rules) {
if (!details.rule.getRuleDependencies().isEmpty()) {
StringBuilder message = new StringBuilder();
details.method.getDescriptor().describeTo(message);
message.append(" has dependencies on plugins: ");
message.append(details.rule.getRuleDependencies());
message.append(". Plugin dependencies are not supported in this context.");
throw new UnsupportedOperationException(message.toString());
}
}
}
private static class ContextualizedModelAction<T> implements ModelAction {
private final MethodRuleDefinition<?, ?> methodRuleDefinition;
private final ModelReference<?> mappedSubject;
private final List<ModelReference<?>> inputs;
private final MethodRuleAction action;
private final Factory<? extends T> factory;
public ContextualizedModelAction(MethodRuleDefinition<?, ?> methodRuleDefinition, ModelReference<?> mappedSubject, List<ModelReference<?>> inputs, MethodRuleAction action, Factory<? extends T> factory) {
this.methodRuleDefinition = methodRuleDefinition;
this.mappedSubject = mappedSubject;
this.inputs = inputs;
this.action = action;
this.factory = factory;
}
@Override
public ModelRuleDescriptor getDescriptor() {
return methodRuleDefinition.getDescriptor();
}
@Override
public ModelReference<?> getSubject() {
return mappedSubject;
}
@Override
public List<? extends ModelReference<?>> getInputs() {
return inputs;
}
@Override
public void execute(MutableModelNode modelNode, List<ModelView<?>> inputs) {
WeaklyTypeReferencingMethod<Object, Object> method = Cast.uncheckedCast(methodRuleDefinition.getMethod());
ModelRuleInvoker<Object> invoker = new DefaultModelRuleInvoker<Object, Object>(method, factory);
action.execute(invoker, modelNode, inputs.subList(0, action.getInputs().size()));
}
}
}
private static class StatelessExtractedRuleSource<T> extends DefaultExtractedRuleSource<T> {
private final Factory<T> factory;
public StatelessExtractedRuleSource(List<ExtractedRuleDetails> rules, Factory<T> factory) {
super(rules);
this.factory = factory;
}
@Override
public Factory<? extends T> getFactory() {
return factory;
}
}
private static class ParameterizedExtractedRuleSource<T> extends DefaultExtractedRuleSource<T> implements GeneratedViewState, Factory<T> {
@Nullable
private final ModelProperty<?> targetProperty;
private final List<ModelProperty<?>> implicitInputs;
private final StructSchema<T> schema;
private final StructBindings<?> bindings;
private final ManagedProxyFactory proxyFactory;
private final Map<String, Object> values = new HashMap<String, Object>();
public ParameterizedExtractedRuleSource(List<ExtractedRuleDetails> rules, @Nullable ModelProperty<?> targetProperty, List<ModelProperty<?>> implicitInputs, StructSchema<T> schema, StructBindings<?> bindings, ManagedProxyFactory proxyFactory) {
super(rules);
this.targetProperty = targetProperty;
this.implicitInputs = implicitInputs;
this.schema = schema;
this.bindings = bindings;
this.proxyFactory = proxyFactory;
}
@Override
public Factory<? extends T> getFactory() {
return this;
}
@Override
public T create() {
return proxyFactory.createProxy(this, schema, bindings);
}
@Override
public String getDisplayName() {
return "rule source " + schema.getType().getDisplayName();
}
@Override
public Object get(String name) {
ModelProperty<?> property = schema.getProperty(name);
if (property.getSchema() instanceof ScalarValueSchema) {
return values.get(name);
}
MutableModelNode node = (MutableModelNode) values.get(name);
if (node == null) {
return null;
}
return node.asImmutable(property.getType(), RuleContext.get()).getInstance();
}
@Override
public void set(String name, Object value) {
ModelProperty<?> property = schema.getProperty(name);
if (property.getSchema() instanceof ScalarValueSchema) {
values.put(name, value);
} else {
MutableModelNode backingNode = ((ManagedInstance) value).getBackingNode();
values.put(name, backingNode);
}
}
@Override
protected ModelPath calculateTarget(MutableModelNode target) {
if (targetProperty != null) {
return ((MutableModelNode)values.get(targetProperty.getName())).getPath();
}
return target.getPath();
}
@Override
protected List<ModelReference<?>> withImplicitInputs(List<? extends ModelReference<?>> inputs) {
List<ModelReference<?>> allInputs = new ArrayList<ModelReference<?>>(inputs.size() + implicitInputs.size());
allInputs.addAll(inputs);
for (ModelProperty<?> property : implicitInputs) {
MutableModelNode backingNode = (MutableModelNode) values.get(property.getName());
allInputs.add(ModelReference.of(backingNode.getPath()));
}
return allInputs;
}
}
}