All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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: *
    *
  1. Components must have a name
  2. *
  3. Components must have at least one stereotype. Each stereotype in the diagram must be unique
  4. *
  5. Components may have an optional alias
  6. *
  7. Components must be defined before declaring dependencies
  8. *
  9. Dependencies must use arrows only consisting of dashes, pointing right, e.g. -->
  10. *
*/ 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 ignorePredicate) { return ignoreDependencies(GET_ORIGIN_CLASS.is(ignorePredicate) .as("ignoring dependencies with origin " + ignorePredicate.getDescription())); } @PublicAPI(usage = ACCESS) public PlantUmlArchCondition ignoreDependenciesWithTarget(DescribedPredicate 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 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy