org.gradle.model.internal.inspect.ModelRuleExtractor Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of gradle-api Show documentation
Show all versions of gradle-api Show documentation
Gradle 6.9.1 API redistribution.
/*
* 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, CachedRuleSource> cache = CacheBuilder.newBuilder()
.weakKeys()
.build(new CacheLoader, CachedRuleSource>() {
public CachedRuleSource load(Class> source) {
return doExtract(source);
}
});
private final Iterable handlers;
private final ManagedProxyFactory proxyFactory;
private final ModelSchemaStore schemaStore;
private final StructBindingsStore structBindingsStore;
public ModelRuleExtractor(Iterable 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() {
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 ExtractedRuleSource extract(Class 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 CachedRuleSource doExtract(final Class source) {
final ModelType 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 schema = getSchema(source, context);
if (schema == null) {
throw new InvalidModelRuleDeclarationException(problems.format());
}
// sort for determinism
Set methods = new TreeSet(Ordering.usingToString());
methods.addAll(Arrays.asList(source.getDeclaredMethods()));
ImmutableList.Builder> 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 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 bindings = structBindingsStore.getBindings(schema.getType());
if (schema.getProperties().isEmpty()) {
return new StatelessRuleSource(rules.build(), Modifier.isAbstract(source.getModifiers()) ? new AbstractRuleSourceFactory(schema, bindings, proxyFactory) : new ConcreteRuleSourceFactory(type));
} else {
return new ParameterizedRuleSource(rules.build(), target, implicitInputs.build(), schema, bindings, proxyFactory);
}
}
private StructSchema getSchema(Class 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 schema = schemaStore.getSchema(source);
if (!(schema instanceof StructSchema)) {
return null;
}
validateClass(source, problems);
return (StructSchema) 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 implements Factory {
// Reference class via `ModelType` to avoid strong reference
private final ModelType type;
public ConcreteRuleSourceFactory(ModelType type) {
this.type = type;
}
@Override
public T create() {
Class concreteClass = type.getConcreteClass();
try {
Constructor 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 implements Factory, GeneratedViewState {
private final StructSchema schema;
private final StructBindings> bindings;
private final ManagedProxyFactory proxyFactory;
public AbstractRuleSourceFactory(StructSchema 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 {
ExtractedRuleSource newInstance(Class source);
}
private static class StatelessRuleSource implements CachedRuleSource {
private final DefaultExtractedRuleSource> ruleSource;
public StatelessRuleSource(List rules, Factory factory) {
this.ruleSource = new StatelessExtractedRuleSource(rules, factory);
}
@Override
public ExtractedRuleSource newInstance(Class source) {
return Cast.uncheckedCast(ruleSource);
}
}
private static class ParameterizedRuleSource implements CachedRuleSource {
private final List rules;
@Nullable
private final ModelProperty> target;
private final List> implicitInputs;
private final StructSchema> schema;
private final StructBindings> bindings;
private final ManagedProxyFactory proxyFactory;
public ParameterizedRuleSource(List rules, @Nullable ModelProperty> target, List> implicitInputs, StructSchema 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 ExtractedRuleSource newInstance(Class source) {
StructSchema schema = Cast.uncheckedCast(this.schema);
return new ParameterizedExtractedRuleSource(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 implements ExtractedRuleSource {
private final List rules;
public DefaultExtractedRuleSource(List rules) {
this.rules = rules;
}
public List getRules() {
return CollectionUtils.collect(rules, new Transformer() {
@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> 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(methodRuleDefinition, mappedSubject, inputs, action, factory);
}
}, target);
}
}
protected ModelPath calculateTarget(MutableModelNode target) {
return target.getPath();
}
private void mapInputs(List> 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> withImplicitInputs(List extends ModelReference>> inputs) {
return new ArrayList>(inputs);
}
@Override
public List extends Class>> getRequiredPlugins() {
List> plugins = new ArrayList>();
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 implements ModelAction {
private final MethodRuleDefinition, ?> methodRuleDefinition;
private final ModelReference> mappedSubject;
private final List> inputs;
private final MethodRuleAction action;
private final Factory extends T> factory;
public ContextualizedModelAction(MethodRuleDefinition, ?> methodRuleDefinition, ModelReference> mappedSubject, List> 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> inputs) {
WeaklyTypeReferencingMethod
© 2015 - 2025 Weber Informatics LLC | Privacy Policy