/* * #%L * Alfresco Records Management Module * %% * Copyright (C) 2005 - 2016 Alfresco Software Limited * %% * This file is part of the Alfresco software. * - * If the software was purchased under a paid Alfresco license, the terms of * the paid license agreement will prevail. Otherwise, the software is * provided under the following open source license terms: * - * Alfresco is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * Alfresco is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * - * You should have received a copy of the GNU Lesser General Public License * along with Alfresco. If not, see <http://www.gnu.org/licenses/>. * #L% */ package org.alfresco.module.org_alfresco_module_rm.api; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import com.google.common.collect.HashMultimap; import com.google.common.collect.SetMultimap; import com.google.common.collect.Sets; import org.alfresco.api.AlfrescoPublicApi; import org.apache.commons.lang3.StringUtils; import org.reflections.Reflections; /** * A utility class to help testing the Alfresco public API. * * @author Tom Page * @since 2.5 */ public class PublicAPITestUtil { private static final String ALFRESCO_PACKAGE = "org.alfresco"; /** * Check the consistency of the public API exposed from the given package. For each class in the package that is * annotated {@link AlfrescoPublicApi}, check that no exposed methods (or fields, constructors, etc.) use * non-public-API classes from Alfresco. * * @param basePackageName The package to check classes within. * @param knownBadReferences Any references that would cause this test to fail, but which we don't want to change. * The keys should be public API classes within our code and the values should be the non-public-API * class that is being referenced. */ public static void testPublicAPIConsistency(String basePackageName, SetMultimap<Class<?>, Class<?>> knownBadReferences) { Reflections reflections = new Reflections(basePackageName); Set<Class<?>> publicAPIClasses = reflections.getTypesAnnotatedWith(AlfrescoPublicApi.class, true); SetMultimap<Class<?>, Class<?>> referencedFrom = HashMultimap.create(); Set<Class<?>> referencedClasses = new HashSet<>(); for (Class<?> publicAPIClass : publicAPIClasses) { Set<Class<?>> referencedClassesFromClass = getReferencedClassesFromClass(publicAPIClass, new HashSet<>()); referencedClassesFromClass.forEach(clazz -> referencedFrom.put(clazz, publicAPIClass)); // Remove any references in knownBadReferences and error if an expected reference wasn't found. if (knownBadReferences.containsKey(publicAPIClass)) { for (Class<?> clazz : knownBadReferences.get(publicAPIClass)) { assertTrue("Supplied knownBadReferences expects " + clazz + " to be referenced by " + publicAPIClass + ", but no such error was found", referencedClassesFromClass.remove(clazz)); } } referencedClasses.addAll(referencedClassesFromClass); } List<String> errorMessages = new ArrayList<>(); for (Class<?> referencedClass : referencedClasses) { if (isInAlfresco(referencedClass) && !isPartOfPublicApi(referencedClass)) { Set<String> referencerNames = referencedFrom.get(referencedClass).stream().map(c -> c.getName()) .collect(Collectors.toSet()); errorMessages.add(referencedClass.getName() + " <- " + StringUtils.join(referencerNames, ", ")); } } if (!errorMessages.isEmpty()) { System.out.println("Errors found:"); System.out.println(StringUtils.join(errorMessages, "\n")); } assertEquals("Found references to non-public API classes from public API classes.", Collections.emptyList(), errorMessages); } /** * Check if the given class is a part of the Alfresco public API. * * @param clazz The class to check. * @return {@code true} if the given class is annotated with {@link AlfrescoPublicApi}. */ private static boolean isPartOfPublicApi(Class<?> clazz) { if (clazz.getAnnotation(AlfrescoPublicApi.class) != null) { return true; } if (clazz.getEnclosingClass() != null) { return isPartOfPublicApi(clazz.getEnclosingClass()); } return false; } /** * Get all the classes referenced by the given class, which might be used by an extension. We consider visible * methods, constructors, fields and inner classes, as well as superclasses and interfaces extended by the class. * * @param initialClass The class to analyse. * @param consideredClasses Classes that have already been considered, and which should not be considered again. If * the given class has already been considered then an empty set will be returned. This set will be * updated with the given class. * @return The set of classes that might be accessible by an extension of this class. */ private static Set<Class<?>> getReferencedClassesFromClass(Class<?> initialClass, Set<Class<?>> consideredClasses) { Set<Class<?>> referencedClasses = new HashSet<>(); if (consideredClasses.add(initialClass)) { for (Method method : initialClass.getDeclaredMethods()) { if (isVisibleToExtender(method.getModifiers())) { referencedClasses.addAll(getClassesFromMethod(method)); } } for (Constructor<?> constructor : initialClass.getDeclaredConstructors()) { if (isVisibleToExtender(constructor.getModifiers())) { referencedClasses.addAll(getClassesFromConstructor(constructor)); } } for (Field field : initialClass.getDeclaredFields()) { if (isVisibleToExtender(field.getModifiers())) { referencedClasses.addAll(getClassesFromField(field)); } } for (Class<?> clazz : initialClass.getDeclaredClasses()) { if (isVisibleToExtender(clazz.getModifiers())) { referencedClasses.addAll(getReferencedClassesFromClass(clazz, consideredClasses)); } } if (initialClass.getSuperclass() != null) { referencedClasses .addAll(getReferencedClassesFromClass(initialClass.getSuperclass(), consideredClasses)); } for (Class<?> clazz : initialClass.getInterfaces()) { referencedClasses.addAll(getReferencedClassesFromClass(clazz, consideredClasses)); } } return referencedClasses; } /** * Check if the supplied {@link Executable#getModifiers() modifiers} indicate that an extension can access the * element. Here we assume that an extension can see public and protected items, but not package protected (or * private). * * @param modifiers The java language modifiers. * @return {@code true} if the item is visible to an extension. */ private static boolean isVisibleToExtender(int modifiers) { return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers); } /** * Get all classes involved in the signature of the given method. * * @param method The method to analyse. * @return The set of classes. */ private static Set<Class<?>> getClassesFromMethod(Method method) { Set<Type> types = getTypesFromMethod(method); return getClassesFromTypes(types); } /** * Get all classes involved in the signature of the given constructor. * * @param constructor The constructor to analyse. * @return The set of classes. */ private static Set<Class<?>> getClassesFromConstructor(Constructor<?> constructor) { Set<Type> types = getTypesFromConstructor(constructor); return getClassesFromTypes(types); } /** * Get all classes involved in the type of the supplied field. For example {@code Pair<Set<String>, Integer> foo} * involves four classes. * * @param field The field to look at. * @return The set of classes. */ private static Set<Class<?>> getClassesFromField(Field field) { Set<Type> types = Sets.newHashSet(field.getGenericType()); return getClassesFromTypes(types); } /** * Get all types references by the supplied method signature (i.e. the parameters, return type and exceptions). * * @param method The method to analyse. * @return The set of types. */ private static Set<Type> getTypesFromMethod(Method method) { Set<Type> methodTypes = new HashSet<>(); methodTypes.addAll(Sets.newHashSet(method.getGenericParameterTypes())); methodTypes.add(method.getGenericReturnType()); methodTypes.addAll(Sets.newHashSet(method.getGenericExceptionTypes())); return methodTypes; } /** * Get all types referenced by the supplied constructor (i.e. the parameters and exceptions). * * @param constructor The constructor to analyse. * @return The set of types. */ private static Set<Type> getTypesFromConstructor(Constructor<?> constructor) { Set<Type> methodTypes = new HashSet<>(); methodTypes.addAll(Sets.newHashSet(constructor.getGenericParameterTypes())); methodTypes.addAll(Sets.newHashSet(constructor.getGenericExceptionTypes())); return methodTypes; } /** * Find all classes that are within the supplied types. For example a {@code Pair<Set<String>, Integer>} contains * references to four classes. * * @param methodTypes The set of types to examine. * @return The set of classes used to form the given types. */ private static Set<Class<?>> getClassesFromTypes(Set<Type> methodTypes) { Set<Class<?>> methodClasses = new HashSet<>(); for (Type type : methodTypes) { methodClasses.addAll(getClassesFromType(type, new HashSet<>())); } return methodClasses; } /** * Find all classes that are within the supplied type. For example a {@code Pair<Set<String>, Integer>} contains * references to four classes. * * @param type The type to examine. * @param processedTypes The set of types which have already been processed. If {@code type} is within this set then * the method returns an empty set, to prevent analysis of the same type multiple times, and to guard * against circular references. The underlying set is updated with the given type. * @return The set of classes used to form the given type. */ private static Set<Class<?>> getClassesFromType(Type type, Set<Type> processedTypes) { Set<Class<?>> returnClasses = new HashSet<>(); if (processedTypes.add(type)) { if (type instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) type; returnClasses.add((Class<?>) parameterizedType.getRawType()); for (Type t : parameterizedType.getActualTypeArguments()) { returnClasses.addAll(getClassesFromType(t, processedTypes)); } } else if (type instanceof Class) { Class<?> clazz = (Class<?>) type; if (clazz.isArray()) { returnClasses.add(clazz.getComponentType()); } returnClasses.add(clazz); } else if (type instanceof WildcardType) { // No-op - Caller can choose what type to use. } else if (type instanceof TypeVariable<?>) { TypeVariable<?> typeVariable = (TypeVariable<?>) type; for (Type bound : typeVariable.getBounds()) { returnClasses.addAll(getClassesFromType(bound, processedTypes)); } } else if (type instanceof GenericArrayType) { GenericArrayType genericArrayType = (GenericArrayType) type; returnClasses.addAll(getClassesFromType(genericArrayType.getGenericComponentType(), processedTypes)); } else { throw new IllegalStateException("This test was not written to work with type " + type); } } return returnClasses; } /** * Check if a class is within org.alfresco, and so whether it could potentially be part of the public API. * * @param type The class to check. * @return {@code true} if this is an Alfresco class. */ private static boolean isInAlfresco(Class<?> type) { if (type.getPackage() == null) { return false; } return type.getPackage().getName().startsWith(ALFRESCO_PACKAGE); } }