/* * 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.library; import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.EvaluationResult; import com.tngtech.archunit.lang.Priority; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Lists.newArrayList; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage; import static com.tngtech.archunit.lang.ArchRule.Assertions.assertNoViolation; import static com.tngtech.archunit.lang.conditions.ArchConditions.onlyBeAccessedByAnyPackage; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.all; import static com.tngtech.archunit.lang.syntax.ClassesIdentityTransformer.classes; import static java.lang.System.lineSeparator; import static java.util.Arrays.asList; /** * Offers convenience to assert typical architectures, like a {@link #layeredArchitecture()}. */ public final class Architectures { private Architectures() { } /** * Can be used to assert a typical layered architecture, e.g. with an UI layer, a business logic layer and * a persistence layer, where specific access rules should be adhered to, like UI may not access persistence * and each layer may only access lower layers, i.e. UI --> business logic --> persistence. * <br><br> * A layered architecture can for example be defined like this: * <pre><code>layeredArchitecture() * .layer("UI").definedBy("my.application.ui..") * .layer("Business Logic").definedBy("my.application.domain..") * .layer("Persistence").definedBy("my.application.persistence..") * * .whereLayer("UI").mayNotBeAccessedByAnyLayer() * .whereLayer("Business Logic").mayOnlyBeAccessedByLayers("UI") * .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Business Logic") * </code></pre> * NOTE: Principally it would be possible to assert such an architecture in a white list or black list way. * <br>I.e. one can specify layer 'Persistence' MAY ONLY be accessed by layer 'Business Logic' (white list), or * layer 'Persistence' MAY NOT access layer 'Business Logic' AND MAY NOT access layer 'UI' (black list).<br> * {@link LayeredArchitecture LayeredArchitecture} only supports the white list way, because it prevents detours "outside of * the architecture", e.g.<br> * 'Persistence' --> 'my.application.somehelper' --> 'Business Logic'<br> * The white list way enforces, that every class that wants to interact with classes inside of * the layered architecture must be part of the layered architecture itself and thus adhere to the same rules. * * @return An {@link ArchRule} enforcing the specified layered architecture */ @PublicAPI(usage = ACCESS) public static LayeredArchitecture layeredArchitecture() { return new LayeredArchitecture(); } public static final class LayeredArchitecture implements ArchRule { private final Map<String, LayerDefinition> layerDefinitions; private final Set<LayerDependencySpecification> dependencySpecifications; private final Optional<String> overriddenDescription; private LayeredArchitecture() { this(new LinkedHashMap<String, LayerDefinition>(), new LinkedHashSet<LayerDependencySpecification>(), Optional.<String>absent()); } private LayeredArchitecture(Map<String, LayerDefinition> layerDefinitions, Set<LayerDependencySpecification> dependencySpecifications, Optional<String> overriddenDescription) { this.layerDefinitions = layerDefinitions; this.dependencySpecifications = dependencySpecifications; this.overriddenDescription = overriddenDescription; } private LayeredArchitecture addLayerDefinition(LayerDefinition definition) { layerDefinitions.put(definition.name, definition); return this; } private LayeredArchitecture addDependencySpecification(LayerDependencySpecification dependencySpecification) { dependencySpecifications.add(dependencySpecification); return this; } @PublicAPI(usage = ACCESS) public LayerDefinition layer(String name) { return new LayerDefinition(name); } @Override public String getDescription() { if (overriddenDescription.isPresent()) { return overriddenDescription.get(); } List<String> lines = newArrayList("Layered architecture consisting of"); for (LayerDefinition definition : layerDefinitions.values()) { lines.add(definition.toString()); } for (LayerDependencySpecification specification : dependencySpecifications) { lines.add(specification.toString()); } return Joiner.on(lineSeparator()).join(lines); } @Override public EvaluationResult evaluate(JavaClasses classes) { EvaluationResult result = new EvaluationResult(this, Priority.MEDIUM); for (LayerDependencySpecification specification : dependencySpecifications) { SortedSet<String> packagesOfOwnLayer = packagesOf(specification.layerName); SortedSet<String> packagesOfAllowedAccessors = packagesOf(specification.allowedAccessors); packagesOfAllowedAccessors.addAll(packagesOfOwnLayer); EvaluationResult partial = all(classes().that(resideInAnyPackage(toArray(packagesOfOwnLayer)))) .should(onlyBeAccessedByAnyPackage(toArray(packagesOfAllowedAccessors))) .evaluate(classes); result.add(partial); } return result; } @Override public void check(JavaClasses classes) { assertNoViolation(evaluate(classes)); } @Override public ArchRule because(String reason) { return ArchRule.Factory.withBecause(this, reason); } @Override public LayeredArchitecture as(String newDescription) { return new LayeredArchitecture(layerDefinitions, dependencySpecifications, Optional.of(newDescription)); } private String[] toArray(Set<String> strings) { return strings.toArray(new String[strings.size()]); } private SortedSet<String> packagesOf(String layerName) { return packagesOf(Collections.singleton(layerName)); } private SortedSet<String> packagesOf(Set<String> allowedAccessorLayerNames) { SortedSet<String> packageIdentifiers = new TreeSet<>(); for (String accessor : allowedAccessorLayerNames) { packageIdentifiers.addAll(layerDefinitions.get(accessor).packageIdentifiers); } return packageIdentifiers; } @PublicAPI(usage = ACCESS) public LayerDependencySpecification whereLayer(String name) { checkArgument(layerDefinitions.containsKey(name), "There is no layer named '%s'", name); return new LayerDependencySpecification(name); } public final class LayerDefinition { private final String name; private Set<String> packageIdentifiers; private LayerDefinition(String name) { this.name = name; } @PublicAPI(usage = ACCESS) public LayeredArchitecture definedBy(String... packageIdentifiers) { this.packageIdentifiers = ImmutableSet.copyOf(packageIdentifiers); return LayeredArchitecture.this.addLayerDefinition(this); } @Override public String toString() { return String.format("layer '%s' ('%s')", name, Joiner.on("', '").join(packageIdentifiers)); } } public final class LayerDependencySpecification { private final String layerName; private final Set<String> allowedAccessors = new LinkedHashSet<>(); private String descriptionSuffix; private LayerDependencySpecification(String layerName) { this.layerName = layerName; } @PublicAPI(usage = ACCESS) public LayeredArchitecture mayNotBeAccessedByAnyLayer() { descriptionSuffix = "may not be accessed by any layer"; return LayeredArchitecture.this.addDependencySpecification(this); } @PublicAPI(usage = ACCESS) public LayeredArchitecture mayOnlyBeAccessedByLayers(String... layerNames) { allowedAccessors.addAll(asList(layerNames)); descriptionSuffix = String.format("may only be accessed by layers ['%s']", Joiner.on("', '").join(allowedAccessors)); return LayeredArchitecture.this.addDependencySpecification(this); } @Override public String toString() { return String.format("where layer '%s' %s", layerName, descriptionSuffix); } } } }