/* * 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.junit; import java.util.Collection; import java.util.HashSet; import java.util.Objects; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.base.ArchUnitException.ReflectionException; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.core.importer.ImportOptions; import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.core.importer.Locations; class ClassCache { private final ConcurrentHashMap<Class<?>, JavaClasses> cachedByTest = new ConcurrentHashMap<>(); private final ConcurrentHashMap<LocationsKey, LazyJavaClasses> cachedByLocations = new ConcurrentHashMap<>(); private CacheClassFileImporter cacheClassFileImporter = new CacheClassFileImporter(); JavaClasses getClassesToAnalyzeFor(Class<?> testClass) { checkArgument(testClass); if (cachedByTest.containsKey(testClass)) { return cachedByTest.get(testClass); } LocationsKey locations = locationsToImport(testClass); cachedByLocations.putIfAbsent(locations, new LazyJavaClasses(locations)); cachedByTest.put(testClass, cachedByLocations.get(locations).get()); return cachedByLocations.get(locations).get(); } private LocationsKey locationsToImport(Class<?> testClass) { AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class); Set<String> packages = ImmutableSet.<String>builder() .add(analyzeClasses.packages()) .addAll(toPackageStrings(analyzeClasses.packagesOf())) .build(); Set<Location> locations = packages.isEmpty() ? Locations.inClassPath() : locationsOf(packages); return new LocationsKey(analyzeClasses.importOptions(), locations); } private Set<String> toPackageStrings(Class[] classes) { ImmutableSet.Builder<String> result = ImmutableSet.builder(); for (Class clazz : classes) { result.add(clazz.getPackage().getName()); } return result.build(); } private Set<Location> locationsOf(Set<String> packages) { Set<Location> result = new HashSet<>(); for (String pkg : packages) { result.addAll(Locations.ofPackage(pkg)); } return result; } // Would be great, if we could just pass the import option on to the ClassFileImporter, but this would be // problematic with respect to caching classes for certain Location combinations private Set<Location> filter(Set<Location> locations, Class<? extends ImportOption> importOption) { ImportOption option = newInstanceOf(importOption); Set<Location> result = new HashSet<>(); for (Location location : locations) { if (option.includes(location)) { result.add(location); } } return result; } private static <T> T newInstanceOf(Class<T> type) { try { return type.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new ReflectionException(e); } } private void checkArgument(Class<?> testClass) { if (testClass.getAnnotation(AnalyzeClasses.class) == null) { throw new IllegalArgumentException(String.format("Class %s must be annotated with @%s", testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName())); } } private class LazyJavaClasses { private final LocationsKey locationsKey; private volatile JavaClasses javaClasses; private LazyJavaClasses(LocationsKey locationsKey) { this.locationsKey = locationsKey; } public JavaClasses get() { if (javaClasses == null) { initialize(); } return javaClasses; } private synchronized void initialize() { if (javaClasses == null) { ImportOptions importOptions = new ImportOptions(); for (Class<? extends ImportOption> optionClass : locationsKey.importOptionClasses) { importOptions = importOptions.with(newInstanceOf(optionClass)); } javaClasses = cacheClassFileImporter.importClasses(importOptions, locationsKey.locations); } } } // Used for testing -> that's also the reason it's declared top level static class CacheClassFileImporter { JavaClasses importClasses(ImportOptions importOptions, Collection<Location> locations) { return new ClassFileImporter(importOptions).importLocations(locations); } } private static class LocationsKey { private final Set<Class<? extends ImportOption>> importOptionClasses; private final Set<Location> locations; private LocationsKey(Class<? extends ImportOption>[] importOptionClasses, Set<Location> locations) { this.importOptionClasses = ImmutableSet.copyOf(importOptionClasses); this.locations = locations; } @Override public int hashCode() { return Objects.hash(importOptionClasses, locations); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final LocationsKey other = (LocationsKey) obj; return Objects.equals(this.importOptionClasses, other.importOptionClasses) && Objects.equals(this.locations, other.locations); } } }