/* * Copyright 2017 TNG Technology Consulting GmbH * * 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 com.tngtech.archunit.core.domain; import java.lang.annotation.Annotation; import java.util.Objects; import java.util.Set; import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.core.domain.properties.HasOwner; import com.tngtech.archunit.core.domain.properties.HasOwner.Functions.Get; import com.tngtech.archunit.core.domain.properties.HasParameterTypes; import com.tngtech.archunit.core.domain.properties.HasReturnType; import com.tngtech.archunit.core.importer.DomainBuilders.CodeUnitCallTargetBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.ConstructorCallTargetBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.FieldAccessTargetBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.MethodCallTargetBuilder; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.DescribedPredicate.equalTo; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME; public abstract class AccessTarget implements HasName.AndFullName, CanBeAnnotated, HasOwner<JavaClass> { private final String name; private final JavaClass owner; private final String fullName; AccessTarget(JavaClass owner, String name, String fullName) { this.name = name; this.owner = owner; this.fullName = fullName; } @Override public String getName() { return name; } @Override public JavaClass getOwner() { return owner; } @Override public String getFullName() { return fullName; } @Override public int hashCode() { return Objects.hash(fullName); } /** * Tries to resolve the targeted members (methods, fields or constructors). In most cases this will be a * single element, if the target was imported, or an empty set, if the target was not imported. However, * for {@link MethodCallTarget MethodCallTargets}, there can be multiple possible targets. * * @see MethodCallTarget#resolve() * @see FieldAccessTarget#resolve() * @see ConstructorCallTarget#resolve() * * @return Set of all members that match the call target */ @PublicAPI(usage = ACCESS) public abstract Set<? extends JavaMember> resolve(); @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final AccessTarget other = (AccessTarget) obj; return Objects.equals(this.fullName, other.fullName); } @Override public String toString() { return "target{" + fullName + '}'; } /** * Returns true, if one of the resolved targets is annotated with the given annotation type.<br> * NOTE: If the target was not imported, this method will always return false. * * @param annotationType The type of the annotation to check for * @return true if one of the resolved targets is annotated with the given type */ @Override public boolean isAnnotatedWith(Class<? extends Annotation> annotationType) { return isAnnotatedWith(annotationType.getName()); } /** * @see AccessTarget#isAnnotatedWith(Class) */ @Override public boolean isAnnotatedWith(final String annotationTypeName) { return anyMember(new Predicate<JavaMember>() { @Override public boolean apply(JavaMember input) { return input.isAnnotatedWith(annotationTypeName); } }); } /** * Returns true, if one of the resolved targets is annotated with an annotation matching the predicate.<br> * NOTE: If the target was not imported, this method will always return false. * * @param predicate Qualifies matching annotations * @return true if one of the resolved targets is annotated with an annotation matching the predicate */ @Override public boolean isAnnotatedWith(final DescribedPredicate<? super JavaAnnotation> predicate) { return anyMember(new Predicate<JavaMember>() { @Override public boolean apply(JavaMember input) { return input.isAnnotatedWith(predicate); } }); } private boolean anyMember(Predicate<JavaMember> predicate) { for (final JavaMember member : resolve()) { if (predicate.apply(member)) { return true; } } return false; } // NOTE: JDK 1.7 u80 seems to have a bug here, if we import HasType, the compile will fail??? public static final class FieldAccessTarget extends AccessTarget implements com.tngtech.archunit.core.domain.properties.HasType { private final JavaClass type; private final Supplier<Optional<JavaField>> field; FieldAccessTarget(FieldAccessTargetBuilder builder) { super(builder.getOwner(), builder.getName(), builder.getFullName()); this.type = builder.getType(); this.field = Suppliers.memoize(builder.getField()); } @Override public JavaClass getType() { return type; } /** * @return A field that matches this target, or {@link Optional#absent()} if no matching field was imported. */ @PublicAPI(usage = ACCESS) public Optional<JavaField> resolveField() { return field.get(); } /** * @return Fields that match the target, this will always be either one field, or no field * @see #resolveField() */ @Override public Set<JavaField> resolve() { return resolveField().asSet(); } } public abstract static class CodeUnitCallTarget extends AccessTarget implements HasParameterTypes, HasReturnType { private final ImmutableList<JavaClass> parameters; private final JavaClass returnType; CodeUnitCallTarget(CodeUnitCallTargetBuilder builder) { super(builder.getOwner(), builder.getName(), builder.getFullName()); this.parameters = ImmutableList.copyOf(builder.getParameters()); this.returnType = builder.getReturnType(); } @Override public JavaClassList getParameters() { return DomainObjectCreationContext.createJavaClassList(parameters); } @Override public JavaClass getReturnType() { return returnType; } /** * Tries to resolve the targeted method or constructor. * * @see ConstructorCallTarget#resolveConstructor() * @see MethodCallTarget#resolve() */ @Override public abstract Set<? extends JavaCodeUnit> resolve(); } public static final class ConstructorCallTarget extends CodeUnitCallTarget { private final Supplier<Optional<JavaConstructor>> constructor; ConstructorCallTarget(ConstructorCallTargetBuilder builder) { super(builder); constructor = builder.getConstructor(); } /** * @return A constructor that matches this target, or {@link Optional#absent()} if no matching constructor * was imported. */ @PublicAPI(usage = ACCESS) public Optional<JavaConstructor> resolveConstructor() { return constructor.get(); } /** * @return constructors that match the target, this will always be either one constructor, or no constructor * @see #resolveConstructor() */ @Override public Set<JavaConstructor> resolve() { return resolveConstructor().asSet(); } } public static final class MethodCallTarget extends CodeUnitCallTarget { private final Supplier<Set<JavaMethod>> methods; MethodCallTarget(MethodCallTargetBuilder builder) { super(builder); this.methods = Suppliers.memoize(builder.getMethods()); } /** * Attempts to resolve imported methods that match this target. Note that while usually there is one unique * target (if imported), it is possible that the call is ambiguous. For example consider * <pre><code> * interface A { * void foo(); * } * * interface B { * void foo(); * } * * interface D extends A, B {} * * class X { * D d; * // ... * void bar() { * d.foo(); * } * } * </code></pre> * While, for any concrete implementation, the compiler will naturally resolve one concrete target to link to, * and thus at runtime the called target ist clear, from an analytical point of view the relevant target * can't be uniquely identified here. To sum up, the result can be * <ul> * <li>empty - if no imported method matches the target</li> * <li>a single method - if the method was imported and can uniquely be identified</li> * <li>several methods - in scenarios where there is no unique method that matches the target</li> * </ul> * Note that the target would be uniquely determinable, if D would declare <code>void foo()</code> itself. * * @return Set of matching methods, usually a single target */ @Override public Set<JavaMethod> resolve() { return methods.get(); } } public static final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) public static DescribedPredicate<AccessTarget> declaredIn(Class<?> clazz) { return declaredIn(clazz.getName()); } @PublicAPI(usage = ACCESS) public static DescribedPredicate<AccessTarget> declaredIn(String className) { return declaredIn(GET_NAME.is(equalTo(className)).as(className)); } @PublicAPI(usage = ACCESS) public static DescribedPredicate<AccessTarget> declaredIn(DescribedPredicate<? super JavaClass> predicate) { return Get.<JavaClass>owner().is(predicate) .as("declared in %s", predicate.getDescription()) .forSubType(); } @PublicAPI(usage = ACCESS) public static DescribedPredicate<AccessTarget> constructor() { return new DescribedPredicate<AccessTarget>("constructor") { @Override public boolean apply(AccessTarget input) { return CONSTRUCTOR_NAME.equals(input.getName()); // The constructor name is sufficiently unique } }; } } }