All Downloads are FREE. Search and download functionalities are using the official Maven repository.
Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.tngtech.archunit.library.plantuml.PlantUmlArchCondition Maven / Gradle / Ivy
/*
* Copyright 2019 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.plantuml;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import com.google.common.base.Joiner;
import com.google.common.collect.FluentIterable;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.PackageMatcher;
import com.tngtech.archunit.base.PackageMatchers;
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.base.Guava.toGuava;
import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_ORIGIN_CLASS;
import static com.tngtech.archunit.core.domain.Dependency.Functions.GET_TARGET_CLASS;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.lang.conditions.ArchConditions.onlyHaveDependenciesInAnyPackage;
import static java.util.Collections.singleton;
/**
* Allows to evaluate PlantUML Component Diagrams
* as ArchUnit rules.
*
* The general syntax to use is
*
*
* classes().should(adhereToPlantUmlDiagram(someDiagramUrl, consideringAllDependencies()));
*
* The supported diagram syntax uses component diagram stereotypes to associate package patterns
* (compare {@link PackageMatcher}) with components. An example could look like
*
* [Some Source] <<..some.source..>>
* [Some Target] <<..some.target..>>
*
* [Some Source] --> [Some Target]
*
* Applying such a diagram as an ArchUnit rule would demand dependencies only from ..some.source..
* to ..some.target..
, but forbid them vice versa.
* There are various factory method for different input formats (file, url, ...), compare
*
* {@link #adhereToPlantUmlDiagram(URL, Configuration)}
* {@link #adhereToPlantUmlDiagram(File, Configuration)}
* {@link #adhereToPlantUmlDiagram(Path, Configuration)}
* {@link #adhereToPlantUmlDiagram(String, Configuration)}
*
* Which dependencies should be considered by the rule can be configured via {@link Configuration}.
* Candidates are
*
* {@link Configurations#consideringAllDependencies()}
* {@link Configurations#consideringOnlyDependenciesInDiagram()}
* {@link Configurations#consideringOnlyDependenciesInAnyPackage(String, String...)}
*
*
* A PlantUML diagram used with ArchUnit must abide by a certain set of rules:
*
* Components must have a name
* Components must have at least one stereotype. Each stereotype in the diagram must be unique
* Components may have an optional alias
* Components must be defined before declaring dependencies
* Dependencies must use arrows only consisting of dashes, pointing right, e.g. -->
*
*/
public class PlantUmlArchCondition extends ArchCondition {
private final DescribedPredicate ignorePredicate;
private final JavaClassDiagramAssociation javaClassDiagramAssociation;
private PlantUmlArchCondition(
String description,
DescribedPredicate ignorePredicate,
JavaClassDiagramAssociation javaClassDiagramAssociation) {
super(description);
this.ignorePredicate = ignorePredicate;
this.javaClassDiagramAssociation = javaClassDiagramAssociation;
}
@PublicAPI(usage = ACCESS)
public PlantUmlArchCondition ignoreDependenciesWithOrigin(DescribedPredicate super JavaClass> ignorePredicate) {
return ignoreDependencies(GET_ORIGIN_CLASS.is(ignorePredicate)
.as("ignoring dependencies with origin " + ignorePredicate.getDescription()));
}
@PublicAPI(usage = ACCESS)
public PlantUmlArchCondition ignoreDependenciesWithTarget(DescribedPredicate super JavaClass> ignorePredicate) {
return ignoreDependencies(GET_TARGET_CLASS.is(ignorePredicate)
.as("ignoring dependencies with target " + ignorePredicate.getDescription()));
}
@PublicAPI(usage = ACCESS)
public PlantUmlArchCondition ignoreDependencies(final Class> origin, final Class> target) {
return ignoreDependencies(origin.getName(), target.getName());
}
@PublicAPI(usage = ACCESS)
public PlantUmlArchCondition ignoreDependencies(final String origin, final String target) {
return ignoreDependencies(
GET_ORIGIN_CLASS.is(name(origin)).and(GET_TARGET_CLASS.is(name(target)))
.as("ignoring dependencies from %s to %s", origin, target));
}
@PublicAPI(usage = ACCESS)
public PlantUmlArchCondition ignoreDependencies(DescribedPredicate super Dependency> ignorePredicate) {
String description = getDescription() + ", " + ignorePredicate.getDescription();
return new PlantUmlArchCondition(description,
this.ignorePredicate.or(ignorePredicate),
javaClassDiagramAssociation);
}
@Override
public void check(JavaClass item, ConditionEvents events) {
if (allDependenciesAreIgnored(item)) {
return;
}
String[] allAllowedTargets = FluentIterable
.from(javaClassDiagramAssociation.getPackageIdentifiersFromComponentOf(item))
.append(javaClassDiagramAssociation.getTargetPackageIdentifiers(item))
.toArray(String.class);
ArchCondition delegate = onlyHaveDependenciesInAnyPackage(allAllowedTargets)
.ignoreDependency(ignorePredicate);
delegate.check(item, events);
}
private boolean allDependenciesAreIgnored(JavaClass item) {
return FluentIterable.from(item.getDirectDependenciesFromSelf()).allMatch(toGuava(ignorePredicate));
}
/**
* @see PlantUmlArchCondition
*/
@PublicAPI(usage = ACCESS)
public static PlantUmlArchCondition adhereToPlantUmlDiagram(URL url, Configuration configuration) {
return create(url, configuration);
}
/**
* @see PlantUmlArchCondition
*/
@PublicAPI(usage = ACCESS)
public static PlantUmlArchCondition adhereToPlantUmlDiagram(String fileName, Configuration configuration) {
return create(toUrl(Paths.get(fileName)), configuration);
}
/**
* @see PlantUmlArchCondition
*/
@PublicAPI(usage = ACCESS)
public static PlantUmlArchCondition adhereToPlantUmlDiagram(Path path, Configuration configuration) {
return create(toUrl(path), configuration);
}
/**
* @see PlantUmlArchCondition
*/
@PublicAPI(usage = ACCESS)
public static PlantUmlArchCondition adhereToPlantUmlDiagram(File file, Configuration configuration) {
return create(toUrl(file.toPath()), configuration);
}
private static PlantUmlArchCondition create(URL url, Configuration configuration) {
PlantUmlDiagram diagram = new PlantUmlParser().parse(url);
JavaClassDiagramAssociation javaClassDiagramAssociation = new JavaClassDiagramAssociation(diagram);
DescribedPredicate ignorePredicate = configuration.asIgnorePredicate(javaClassDiagramAssociation);
return new PlantUmlArchCondition(getDescription(url, ignorePredicate.getDescription()), ignorePredicate, javaClassDiagramAssociation);
}
private static String getDescription(URL plantUmlUrl, String ignoreDescription) {
return String.format("adhere to PlantUML diagram <%s>%s", getFileNameOf(plantUmlUrl), ignoreDescription);
}
private static String getFileNameOf(URL url) {
return new File(url.getFile()).getName();
}
private static URL toUrl(Path path) {
try {
return path.toUri().toURL();
} catch (MalformedURLException e) {
throw new PlantUmlParseException(e);
}
}
public static final class Configurations {
private Configurations() {
}
/**
* Considers all dependencies of every imported class, including basic Java classes like {@link Object}
*/
@PublicAPI(usage = ACCESS)
public static Configuration consideringAllDependencies() {
return new Configuration() {
@Override
public DescribedPredicate asIgnorePredicate(JavaClassDiagramAssociation javaClassDiagramAssociation) {
return DescribedPredicate.alwaysFalse().as("");
}
};
}
/**
* Considers only dependencies of the imported classes that are contained within diagram components.
* This makes it easy to ignore dependencies to irrelevant classes like {@link Object}, but bears the
* danger of missing dependencies to components that have simply been forgotten to be added to the diagram.
*/
@PublicAPI(usage = ACCESS)
public static Configuration consideringOnlyDependenciesInDiagram() {
return new Configuration() {
@Override
public DescribedPredicate asIgnorePredicate(final JavaClassDiagramAssociation javaClassDiagramAssociation) {
return new NotContainedInDiagramPredicate(javaClassDiagramAssociation);
}
};
}
/**
* Considers only dependencies of the imported classes that have targets in the package identifiers.
* This can for example be used to limit checked dependencies to those contained in the own project,
* e.g. 'com.myapp..
'.
*/
@PublicAPI(usage = ACCESS)
public static Configuration consideringOnlyDependenciesInAnyPackage(String packageIdentifier, final String... furtherPackageIdentifiers) {
final List packageIdentifiers = FluentIterable.from(singleton(packageIdentifier))
.append(furtherPackageIdentifiers)
.toList();
return new Configuration() {
@Override
public DescribedPredicate asIgnorePredicate(final JavaClassDiagramAssociation javaClassDiagramAssociation) {
return new NotContainedInPackagesPredicate(packageIdentifiers);
}
};
}
private static class NotContainedInDiagramPredicate extends DescribedPredicate {
private final JavaClassDiagramAssociation javaClassDiagramAssociation;
NotContainedInDiagramPredicate(JavaClassDiagramAssociation javaClassDiagramAssociation) {
super(" while ignoring dependencies not contained in the diagram");
this.javaClassDiagramAssociation = javaClassDiagramAssociation;
}
@Override
public boolean apply(Dependency input) {
return !javaClassDiagramAssociation.contains(input.getTargetClass());
}
}
private static class NotContainedInPackagesPredicate extends DescribedPredicate {
private final List packageIdentifiers;
NotContainedInPackagesPredicate(List packageIdentifiers) {
super(" while ignoring dependencies outside of packages ['%s']", Joiner.on("', '").join(packageIdentifiers));
this.packageIdentifiers = packageIdentifiers;
}
@Override
public boolean apply(Dependency input) {
return !PackageMatchers.of(packageIdentifiers).apply(input.getTargetClass().getPackageName());
}
}
}
/**
* Used to specify which dependencies should be checked by the condition. Compare concrete instances:
*
* {@link Configurations#consideringAllDependencies()}
* {@link Configurations#consideringOnlyDependenciesInDiagram()}
* {@link Configurations#consideringOnlyDependenciesInAnyPackage(String, String...)}
*
*/
interface Configuration {
DescribedPredicate asIgnorePredicate(JavaClassDiagramAssociation javaClassDiagramAssociation);
}
}