/* * 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.dependencies; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.DescribedIterable; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.Guava; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.base.PackageMatcher; import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ClassesTransformer; import com.tngtech.archunit.lang.syntax.PredicateAggregator; import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.PackageMatcher.TO_GROUPS; import static com.tngtech.archunit.core.domain.Dependency.toTargetClasses; /** * Basic collection of {@link Slice} for tests on dependencies of package slices, e.g. to avoid cycles. * Example to specify a {@link ClassesTransformer} to run {@link ArchRule ArchRules} against {@link Slices}: * <pre><code> * Slices.matching("some.pkg.(*)..") * </code></pre> * would group the packages * <ul> * <li><code>some.pkg.one.any</code></li> * <li><code>some.pkg.one.other</code></li> * </ul> * in the same slice, the package * <ul> * <li><code>some.pkg.two.any</code></li> * </ul> * in a different slice.<br> * The resulting {@link ClassesTransformer} can be used to specify an {@link ArchRule} on slices. */ public final class Slices implements DescribedIterable<Slice> { private final Iterable<Slice> slices; private final String description; private Slices(Iterable<Slice> slices) { this(slices, "Slices"); } private Slices(Iterable<Slice> slices, String description) { this.slices = slices; this.description = description; } @Override public Iterator<Slice> iterator() { return slices.iterator(); } public Slices as(String description) { return new Slices(slices, description); } @Override public String getDescription() { return description; } /** * Allows the naming of single slices, where back references to the matching pattern can be denoted by '$' followed * by capturing group number. <br> * E.g. {@code namingSlices("Slice $1")} would name a slice matching {@code '*..service.(*)..*'} * against {@code 'com.some.company.service.hello.something'} as 'Slice hello'. * * @param pattern The naming pattern, e.g. 'Slice $1' * @return The same slices with adjusted naming for each single slice */ public Slices namingSlices(String pattern) { for (Slice slice : slices) { slice.as(pattern); } return this; } /** * @see Creator#matching(String) */ public static Transformer matching(String packageIdentifier) { return new Transformer(packageIdentifier, slicesMatchingDescription(packageIdentifier)); } /** * Specifies how to transform a set of {@link JavaClass} into a set of {@link Slice}, e.g. to test that * no cycles between certain package slices appear. * * @see Slices */ public static class Transformer implements ClassesTransformer<Slice> { private final String packageIdentifier; private final String description; private final Optional<String> namingPattern; private final PredicateAggregator<Slice> predicate; private Transformer(String packageIdentifier, String description) { this(packageIdentifier, description, new PredicateAggregator<Slice>()); } private Transformer(String packageIdentifier, String description, PredicateAggregator<Slice> predicate) { this(packageIdentifier, description, Optional.<String>absent(), predicate); } private Transformer(String packageIdentifier, String description, Optional<String> namingPattern, PredicateAggregator<Slice> predicate) { this.packageIdentifier = checkNotNull(packageIdentifier); this.description = checkNotNull(description); this.namingPattern = checkNotNull(namingPattern); this.predicate = checkNotNull(predicate); } /** * @see Slices#namingSlices(String) */ Transformer namingSlices(String pattern) { return namingSlices(Optional.of(pattern)); } private Transformer namingSlices(Optional<String> pattern) { return new Transformer(packageIdentifier, description, pattern, predicate); } @Override public Transformer as(String description) { return new Transformer(packageIdentifier, description).namingSlices(namingPattern); } public Slices of(JavaClasses classes) { return new Slices(transform(classes)); } public Slices transform(Iterable<Dependency> dependencies) { return new Slices(transform(toTargetClasses(dependencies))); } @Override public Slices transform(JavaClasses classes) { Slices slices = new Creator(classes).matching(packageIdentifier); if (predicate.isPresent()) { slices = new Slices(Guava.Iterables.filter(slices, predicate.get())); } if (namingPattern.isPresent()) { slices.namingSlices(namingPattern.get()); } return slices.as(getDescription()); } @Override public Slices.Transformer that(final DescribedPredicate<? super Slice> predicate) { String newDescription = getDescription() + " that " + predicate.getDescription(); return new Transformer(packageIdentifier, newDescription, namingPattern, this.predicate.add(predicate)); } @Override public String getDescription() { return description; } } public static final class Creator { private final JavaClasses classes; private Creator(JavaClasses classes) { this.classes = classes; } /** * Supports partitioning a set of {@link JavaClasses} into different slices by matching the supplied * package identifier. For identifier syntax, see {@link PackageMatcher}.<br> * The slicing is done according to capturing groups (thus if none are contained in the identifier, no more than * a single slice will be the result). For example * <p> * Suppose there are three classes:<br><br> * {@code com.example.slice.one.SomeClass}<br> * {@code com.example.slice.one.AnotherClass}<br> * {@code com.example.slice.two.YetAnotherClass}<br><br> * If slices are created by specifying<br><br> * {@code Slices.of(classes).byMatching("..slice.(*)..")}<br><br> * then the result will be two slices, the slice where the capturing group is 'one' and the slice where the * capturing group is 'two'. * </p> * * @param packageIdentifier The identifier to match against * @return Slices partitioned according the supplied package identifier */ @PublicAPI(usage = ACCESS) public Slices matching(String packageIdentifier) { SliceBuilders sliceBuilders = new SliceBuilders(); PackageMatcher matcher = PackageMatcher.of(packageIdentifier); for (JavaClass clazz : classes) { Optional<List<String>> groups = matcher.match(clazz.getPackage()).transform(TO_GROUPS); sliceBuilders.add(groups, clazz); } return new Slices(sliceBuilders.build()).as(slicesMatchingDescription(packageIdentifier)); } } private static String slicesMatchingDescription(String packageIdentifier) { return String.format("slices matching '%s'", packageIdentifier); } private static class SliceBuilders { Map<List<String>, Slice.Builder> sliceBuilders = new HashMap<>(); void add(Optional<List<String>> matchingGroups, JavaClass clazz) { if (matchingGroups.isPresent()) { put(matchingGroups.get(), clazz); } } private void put(List<String> matchingGroups, JavaClass clazz) { if (!sliceBuilders.containsKey(matchingGroups)) { sliceBuilders.put(matchingGroups, Slice.Builder.from(matchingGroups)); } sliceBuilders.get(matchingGroups).addClass(clazz); } Set<Slice> build() { Set<Slice> result = new HashSet<>(); for (Slice.Builder builder : sliceBuilders.values()) { result.add(builder.build()); } return result; } } }