/* * 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.base; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.PublicAPI; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; /** * Matches packages with a syntax similar to AspectJ. In particular '*' stands for any sequence of * characters, '..' stands for any sequence of packages, including zero packages.<br> * For example * <ul> * <li><b>{@code '..pack..'}</b> matches <b>{@code 'a.pack'}</b>, <b>{@code 'a.pack.b'}</b> or <b>{@code 'a.b.pack.c.d'}</b>, * but not <b>{@code 'a.packa.b'}</b></li> * <li><b>{@code '*.pack.*'}</b> matches <b>{@code 'a.pack.b'}</b>, but not <b>{@code 'a.b.pack.c'}</b></li> * <li><b>{@code '..*pack*..'}</b> matches <b>{@code 'a.prepackfix.b'}</b></li> * <li><b>{@code '*.*.pack*..'}</b> matches <b>{@code 'a.b.packfix.c.d'}</b>, * but neither <b>{@code 'a.packfix.b'}</b> nor <b>{@code 'a.b.prepack.d'}</b></li> * </ul> * Furthermore the use of capturing groups is supported. In this case '(*)' matches any sequence of characters, * but not the dot '.', while '(**)' matches any sequence including the dot. <br> * For example * <ul> * <li><b>{@code '..service.(*)..'}</b> matches <b>{@code 'a.service.hello.b'}</b> and group 1 would be <b>{@code 'hello'}</b></li> * <li><b>{@code '..service.(**)'}</b> matches <b>{@code 'a.service.hello.more'}</b> and group 1 would be <b>{@code 'hello.more'}</b></li> * <li><b>{@code 'my.(*)..service.(**)'}</b> matches <b>{@code 'my.company.some.service.hello.more'}</b> * and group 1 would be <b>{@code 'company'}</b>, while group 2 would be <b>{@code 'hello.more'}</b></li> * </ul> * Create via {@link PackageMatcher#of(String) PackageMatcher.of(packageIdentifier)} */ public final class PackageMatcher { private static final String OPT_LETTERS_AT_START = "(?:^\\w*)?"; private static final String OPT_LETTERS_AT_END = "(?:\\w*$)?"; private static final String ARBITRARY_PACKAGES = "\\.(?:\\w+\\.)*"; private static final String TWO_DOTS_REGEX = String.format("(?:%s%s%s)?", OPT_LETTERS_AT_START, ARBITRARY_PACKAGES, OPT_LETTERS_AT_END); private static final String TWO_STAR_CAPTURE_LITERAL = "(**)"; private static final String TWO_STAR_CAPTURE_REGEX = "(\\w+(?:\\.\\w+)*)"; static final String TWO_STAR_REGEX_MARKER = "#%#%#"; private static final Set<Character> PACKAGE_CONTROL_SYMBOLS = ImmutableSet.of('*', '(', ')', '.'); private final String packageIdentifier; private final Pattern packagePattern; private PackageMatcher(String packageIdentifier) { validate(packageIdentifier); this.packageIdentifier = packageIdentifier; this.packagePattern = Pattern.compile(convertToRegex(packageIdentifier)); } private void validate(String packageIdentifier) { if (packageIdentifier.contains("...")) { throw new IllegalArgumentException("Package Identifier may not contain more than two '.' in a row"); } if (packageIdentifier.replace("(**)", "").contains("**")) { throw new IllegalArgumentException("Package Identifier may not contain more than one '*' in a row"); } if (packageIdentifier.contains("(..)")) { throw new IllegalArgumentException("Package Identifier does not support capturing via (..), use (**) instead"); } validateCharacters(packageIdentifier); } private void validateCharacters(String packageIdentifier) { for (int i = 0; i < packageIdentifier.length(); i++) { char c = packageIdentifier.charAt(i); if (!Character.isJavaIdentifierPart(c) && !PACKAGE_CONTROL_SYMBOLS.contains(c)) { throw new IllegalArgumentException("Package Identifier may only consist of valid java identifier parts or the symbols '.)(*'"); } } } private String convertToRegex(String packageIdentifier) { return packageIdentifier. replace(TWO_STAR_CAPTURE_LITERAL, TWO_STAR_REGEX_MARKER). replace("*", "\\w+"). replace(".", "\\."). replace(TWO_STAR_REGEX_MARKER, TWO_STAR_CAPTURE_REGEX). replace("\\.\\.", TWO_DOTS_REGEX); } /** * Creates a new {@link PackageMatcher} * * @param packageIdentifier The package literal to match against (e.g. {@code 'some*..pk*'} --> {@code 'somewhere.in.some.pkg'}) * @return {@link PackageMatcher} to match packages against the supplied literal * supporting AspectJ syntax */ @PublicAPI(usage = ACCESS) public static PackageMatcher of(String packageIdentifier) { return new PackageMatcher(packageIdentifier); } @PublicAPI(usage = ACCESS) public boolean matches(String aPackage) { return packagePattern.matcher(aPackage).matches(); } /** * Returns a matching {@link PackageMatcher.Result Result} * against the provided package name. If the package identifier of this {@link PackageMatcher} does not match the * given package name, then {@link Optional#absent()} is returned. * * @param aPackage The package name to match against * @return A {@link PackageMatcher.Result Result} if the package name matches, * otherwise {@link Optional#absent()} */ @PublicAPI(usage = ACCESS) public Optional<Result> match(String aPackage) { Matcher matcher = packagePattern.matcher(aPackage); return matcher.matches() ? Optional.of(new Result(matcher)) : Optional.<Result>absent(); } @Override public String toString() { return "PackageMatcher{" + packageIdentifier + '}'; } public static final class Result { private final Matcher matcher; private Result(Matcher matcher) { this.matcher = matcher; } @PublicAPI(usage = ACCESS) public int getNumberOfGroups() { return matcher.groupCount(); } @PublicAPI(usage = ACCESS) public String getGroup(int number) { return matcher.group(number); } } @PublicAPI(usage = ACCESS) public static final Function<Result, List<String>> TO_GROUPS = new Function<Result, List<String>>() { @Override public List<String> apply(Result input) { List<String> result = new ArrayList<>(); for (int i = 0; i < input.getNumberOfGroups(); i++) { result.add(input.getGroup(i + 1)); } return result; } }; }