/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.apache.beam.sdk.util; import static org.hamcrest.Matchers.anyOf; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Predicate; import com.google.common.base.Supplier; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; import com.google.common.reflect.Invokable; import com.google.common.reflect.Parameter; import com.google.common.reflect.TypeToken; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; 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.Collection; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import javax.annotation.Nonnull; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.StringDescription; import org.hamcrest.TypeSafeDiagnosingMatcher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Represents the API surface of a package prefix. Used for accessing public classes, methods, and * the types they reference, to control what dependencies are re-exported. * * <p>For the purposes of calculating the public API surface, exposure includes any public or * protected occurrence of: * * <ul> * <li>superclasses * <li>interfaces implemented * <li>actual type arguments to generic types * <li>array component types * <li>method return types * <li>method parameter types * <li>type variable bounds * <li>wildcard bounds * </ul> * * <p>Exposure is a transitive property. The resulting map excludes primitives and array classes * themselves. * * <p>It is prudent (though not required) to prune prefixes like "java" via the builder method * {@link #pruningPrefix} to halt the traversal so it does not uselessly catalog references that are * not interesting. */ @SuppressWarnings("rawtypes") public class ApiSurface { private static final Logger LOG = LoggerFactory.getLogger(ApiSurface.class); /** A factory method to create a {@link Class} matcher for classes residing in a given package. */ public static Matcher<Class<?>> classesInPackage(final String packageName) { return new Matchers.ClassInPackage(packageName); } /** * A factory method to create an {@link ApiSurface} matcher, producing a positive match if the * queried api surface contains ONLY classes described by the provided matchers. */ public static Matcher<ApiSurface> containsOnlyClassesMatching( final Set<Matcher<Class<?>>> classMatchers) { return new Matchers.ClassesInSurfaceMatcher(classMatchers); } /** See {@link ApiSurface#containsOnlyClassesMatching(Set)}. */ @SafeVarargs public static Matcher<ApiSurface> containsOnlyClassesMatching( final Matcher<Class<?>>... classMatchers) { return new Matchers.ClassesInSurfaceMatcher(Sets.newHashSet(classMatchers)); } /** See {@link ApiSurface#containsOnlyPackages(Set)}. */ public static Matcher<ApiSurface> containsOnlyPackages(final String... packageNames) { return containsOnlyPackages(Sets.newHashSet(packageNames)); } /** * A factory method to create an {@link ApiSurface} matcher, producing a positive match if the * queried api surface contains classes ONLY from specified package names. */ public static Matcher<ApiSurface> containsOnlyPackages(final Set<String> packageNames) { final Function<String, Matcher<Class<?>>> packageNameToClassMatcher = new Function<String, Matcher<Class<?>>>() { @Override public Matcher<Class<?>> apply(@Nonnull final String packageName) { return classesInPackage(packageName); } }; final ImmutableSet<Matcher<Class<?>>> classesInPackages = FluentIterable.from(packageNames).transform(packageNameToClassMatcher).toSet(); return containsOnlyClassesMatching(classesInPackages); } /** * {@link Matcher}s for use in {@link ApiSurface} related tests that aim to keep the public API * conformant to a hard-coded policy by controlling what classes are allowed to be exposed by an * API surface. */ // based on previous code by @kennknowles and others. private static class Matchers { private static class ClassInPackage extends TypeSafeDiagnosingMatcher<Class<?>> { private final String packageName; private ClassInPackage(final String packageName) { this.packageName = packageName; } @Override public void describeTo(final Description description) { description.appendText("Classes in package \""); description.appendText(packageName); description.appendText("\""); } @Override protected boolean matchesSafely(final Class<?> clazz, final Description mismatchDescription) { return clazz.getName().startsWith(packageName + "."); } } private static class ClassesInSurfaceMatcher extends TypeSafeDiagnosingMatcher<ApiSurface> { private final Set<Matcher<Class<?>>> classMatchers; private ClassesInSurfaceMatcher(final Set<Matcher<Class<?>>> classMatchers) { this.classMatchers = classMatchers; } private boolean verifyNoAbandoned( final ApiSurface checkedApiSurface, final Set<Matcher<Class<?>>> allowedClasses, final Description mismatchDescription) { // <helper_lambdas> final Function<Matcher<Class<?>>, String> toMessage = new Function<Matcher<Class<?>>, String>() { @Override public String apply(@Nonnull final Matcher<Class<?>> abandonedClassMacther) { final StringDescription description = new StringDescription(); description.appendText("No "); abandonedClassMacther.describeTo(description); return description.toString(); } }; final Predicate<Matcher<Class<?>>> matchedByExposedClasses = new Predicate<Matcher<Class<?>>>() { @Override public boolean apply(@Nonnull final Matcher<Class<?>> classMatcher) { return FluentIterable.from(checkedApiSurface.getExposedClasses()) .anyMatch( new Predicate<Class<?>>() { @Override public boolean apply(@Nonnull final Class<?> aClass) { return classMatcher.matches(aClass); } }); } }; // </helper_lambdas> final ImmutableSet<Matcher<Class<?>>> matchedClassMatchers = FluentIterable.from(allowedClasses).filter(matchedByExposedClasses).toSet(); final Sets.SetView<Matcher<Class<?>>> abandonedClassMatchers = Sets.difference(allowedClasses, matchedClassMatchers); final ImmutableList<String> messages = FluentIterable.from(abandonedClassMatchers) .transform(toMessage) .toSortedList(Ordering.<String>natural()); if (!messages.isEmpty()) { mismatchDescription.appendText( "The following white-listed scopes did not have matching classes on the API surface:" + "\n\t" + Joiner.on("\n\t").join(messages)); } return messages.isEmpty(); } private boolean verifyNoDisallowed( final ApiSurface checkedApiSurface, final Set<Matcher<Class<?>>> allowedClasses, final Description mismatchDescription) { /* <helper_lambdas> */ final Function<Class<?>, List<Class<?>>> toExposure = new Function<Class<?>, List<Class<?>>>() { @Override public List<Class<?>> apply(@Nonnull final Class<?> aClass) { return checkedApiSurface.getAnyExposurePath(aClass); } }; final Maps.EntryTransformer<Class<?>, List<Class<?>>, String> toMessage = new Maps.EntryTransformer<Class<?>, List<Class<?>>, String>() { @Override public String transformEntry( @Nonnull final Class<?> aClass, @Nonnull final List<Class<?>> exposure) { return aClass + " exposed via:\n\t\t" + Joiner.on("\n\t\t").join(exposure); } }; final Predicate<Class<?>> disallowed = new Predicate<Class<?>>() { @Override public boolean apply(@Nonnull final Class<?> aClass) { return !classIsAllowed(aClass, allowedClasses); } }; /* </helper_lambdas> */ final FluentIterable<Class<?>> disallowedClasses = FluentIterable.from(checkedApiSurface.getExposedClasses()).filter(disallowed); final ImmutableMap<Class<?>, List<Class<?>>> exposures = Maps.toMap(disallowedClasses, toExposure); final ImmutableList<String> messages = FluentIterable.from(Maps.transformEntries(exposures, toMessage).values()) .toSortedList(Ordering.<String>natural()); if (!messages.isEmpty()) { mismatchDescription.appendText( "The following disallowed classes appeared on the API surface:\n\t" + Joiner.on("\n\t").join(messages)); } return messages.isEmpty(); } @SuppressWarnings({"rawtypes", "unchecked"}) private boolean classIsAllowed( final Class<?> clazz, final Set<Matcher<Class<?>>> allowedClasses) { // Safe cast inexpressible in Java without rawtypes return anyOf((Iterable) allowedClasses).matches(clazz); } @Override protected boolean matchesSafely( final ApiSurface apiSurface, final Description mismatchDescription) { final boolean noDisallowed = verifyNoDisallowed(apiSurface, classMatchers, mismatchDescription); final boolean noAbandoned = verifyNoAbandoned(apiSurface, classMatchers, mismatchDescription); return noDisallowed & noAbandoned; } @Override public void describeTo(final Description description) { description.appendText("API surface to include only:" + "\n\t"); for (final Matcher<Class<?>> classMatcher : classMatchers) { classMatcher.describeTo(description); description.appendText("\n\t"); } } } } /////////////// /** Returns an empty {@link ApiSurface}. */ public static ApiSurface empty() { LOG.debug("Returning an empty ApiSurface"); return new ApiSurface(Collections.<Class<?>>emptySet(), Collections.<Pattern>emptySet()); } /** Returns an {@link ApiSurface} object representing the given package and all subpackages. */ public static ApiSurface ofPackage(String packageName, ClassLoader classLoader) throws IOException { return ApiSurface.empty().includingPackage(packageName, classLoader); } /** Returns an {@link ApiSurface} object representing the given package and all subpackages. */ public static ApiSurface ofPackage(Package aPackage, ClassLoader classLoader) throws IOException { return ofPackage(aPackage.getName(), classLoader); } /** Returns an {@link ApiSurface} object representing just the surface of the given class. */ public static ApiSurface ofClass(Class<?> clazz) { return ApiSurface.empty().includingClass(clazz); } /** * Returns an {@link ApiSurface} like this one, but also including the named package and all of * its subpackages. */ public ApiSurface includingPackage(String packageName, ClassLoader classLoader) throws IOException { ClassPath classPath = ClassPath.from(classLoader); Set<Class<?>> newRootClasses = Sets.newHashSet(); for (ClassPath.ClassInfo classInfo : classPath.getTopLevelClassesRecursive(packageName)) { Class clazz = null; try { clazz = classInfo.load(); } catch (NoClassDefFoundError e) { // TODO: Ignore any NoClassDefFoundError errors as a workaround. (BEAM-2231) LOG.warn("Failed to load class: {}", classInfo.toString(), e); continue; } if (exposed(clazz.getModifiers())) { newRootClasses.add(clazz); } } LOG.debug("Including package {} and subpackages: {}", packageName, newRootClasses); newRootClasses.addAll(rootClasses); return new ApiSurface(newRootClasses, patternsToPrune); } /** Returns an {@link ApiSurface} like this one, but also including the given class. */ public ApiSurface includingClass(Class<?> clazz) { Set<Class<?>> newRootClasses = Sets.newHashSet(); LOG.debug("Including class {}", clazz); newRootClasses.add(clazz); newRootClasses.addAll(rootClasses); return new ApiSurface(newRootClasses, patternsToPrune); } /** * Returns an {@link ApiSurface} like this one, but pruning transitive references from classes * whose full name (including package) begins with the provided prefix. */ public ApiSurface pruningPrefix(String prefix) { return pruningPattern(Pattern.compile(Pattern.quote(prefix) + ".*")); } /** Returns an {@link ApiSurface} like this one, but pruning references from the named class. */ public ApiSurface pruningClassName(String className) { return pruningPattern(Pattern.compile(Pattern.quote(className))); } /** * Returns an {@link ApiSurface} like this one, but pruning references from the provided class. */ public ApiSurface pruningClass(Class<?> clazz) { return pruningClassName(clazz.getName()); } /** * Returns an {@link ApiSurface} like this one, but pruning transitive references from classes * whose full name (including package) begins with the provided prefix. */ public ApiSurface pruningPattern(Pattern pattern) { Set<Pattern> newPatterns = Sets.newHashSet(); newPatterns.addAll(patternsToPrune); newPatterns.add(pattern); return new ApiSurface(rootClasses, newPatterns); } /** See {@link #pruningPattern(Pattern)}. */ public ApiSurface pruningPattern(String patternString) { return pruningPattern(Pattern.compile(patternString)); } /** Returns all public classes originally belonging to the package in the {@link ApiSurface}. */ public Set<Class<?>> getRootClasses() { return rootClasses; } /** Returns exposed types in this set, including arrays and primitives as specified. */ public Set<Class<?>> getExposedClasses() { return getExposedToExposers().keySet(); } /** * Returns a path from an exposed class to a root class. There may be many, but this gives only * one. * * <p>If there are only cycles, with no path back to a root class, throws IllegalStateException. */ public List<Class<?>> getAnyExposurePath(Class<?> exposedClass) { Set<Class<?>> excluded = Sets.newHashSet(); excluded.add(exposedClass); List<Class<?>> path = getAnyExposurePath(exposedClass, excluded); if (path == null) { throw new IllegalArgumentException( "Class " + exposedClass + " has no path back to any root class." + " It should never have been considered exposed."); } else { return path; } } /** * Returns a path from an exposed class to a root class. There may be many, but this gives only * one. It will not return a path that crosses the excluded classes. * * <p>If there are only cycles or paths through the excluded classes, returns null. * * <p>If the class is not actually in the exposure map, throws IllegalArgumentException */ private List<Class<?>> getAnyExposurePath(Class<?> exposedClass, Set<Class<?>> excluded) { List<Class<?>> exposurePath = Lists.newArrayList(); exposurePath.add(exposedClass); Collection<Class<?>> exposers = getExposedToExposers().get(exposedClass); if (exposers.isEmpty()) { throw new IllegalArgumentException("Class " + exposedClass + " is not exposed."); } for (Class<?> exposer : exposers) { if (excluded.contains(exposer)) { continue; } // A null exposer means this is already a root class. if (exposer == null) { return exposurePath; } List<Class<?>> restOfPath = getAnyExposurePath(exposer, Sets.union(excluded, Sets.newHashSet(exposer))); if (restOfPath != null) { exposurePath.addAll(restOfPath); return exposurePath; } } return null; } //////////////////////////////////////////////////////////////////// // Fields initialized upon construction private final Set<Class<?>> rootClasses; private final Set<Pattern> patternsToPrune; // Fields computed on-demand private Multimap<Class<?>, Class<?>> exposedToExposers = null; private Pattern prunedPattern = null; private Set<Type> visited = null; private ApiSurface(Set<Class<?>> rootClasses, Set<Pattern> patternsToPrune) { this.rootClasses = rootClasses; this.patternsToPrune = patternsToPrune; } /** * A map from exposed types to place where they are exposed, in the sense of being a part of a * public-facing API surface. * * <p>This map is the adjencency list representation of a directed graph, where an edge from type * {@code T1} to type {@code T2} indicates that {@code T2} directly exposes {@code T1} in its API * surface. * * <p>The traversal methods in this class are designed to avoid repeatedly processing types, since * there will almost always be cyclic references. */ private Multimap<Class<?>, Class<?>> getExposedToExposers() { if (exposedToExposers == null) { constructExposedToExposers(); } return exposedToExposers; } /** See {@link #getExposedToExposers}. */ private void constructExposedToExposers() { visited = Sets.newHashSet(); exposedToExposers = Multimaps.newSetMultimap( Maps.<Class<?>, Collection<Class<?>>>newHashMap(), new Supplier<Set<Class<?>>>() { @Override public Set<Class<?>> get() { return Sets.newHashSet(); } }); for (Class<?> clazz : rootClasses) { addExposedTypes(clazz, null); } } /** A combined {@code Pattern} that implements all the pruning specified. */ private Pattern getPrunedPattern() { if (prunedPattern == null) { constructPrunedPattern(); } return prunedPattern; } /** See {@link #getPrunedPattern}. */ private void constructPrunedPattern() { Set<String> prunedPatternStrings = Sets.newHashSet(); for (Pattern patternToPrune : patternsToPrune) { prunedPatternStrings.add(patternToPrune.pattern()); } prunedPattern = Pattern.compile("(" + Joiner.on(")|(").join(prunedPatternStrings) + ")"); } /** Whether a type and all that it references should be pruned from the graph. */ private boolean pruned(Type type) { return pruned(TypeToken.of(type).getRawType()); } /** Whether a class and all that it references should be pruned from the graph. */ private boolean pruned(Class<?> clazz) { return clazz.isPrimitive() || clazz.isArray() || getPrunedPattern().matcher(clazz.getName()).matches(); } /** Whether a type has already beens sufficiently processed. */ private boolean done(Type type) { return visited.contains(type); } private void recordExposure(Class<?> exposed, Class<?> cause) { exposedToExposers.put(exposed, cause); } private void recordExposure(Type exposed, Class<?> cause) { exposedToExposers.put(TypeToken.of(exposed).getRawType(), cause); } private void visit(Type type) { visited.add(type); } /** See {@link #addExposedTypes(Type, Class)}. */ private void addExposedTypes(TypeToken type, Class<?> cause) { LOG.debug( "Adding exposed types from {}, which is the type in type token {}", type.getType(), type); addExposedTypes(type.getType(), cause); } /** * Adds any references learned by following a link from {@code cause} to {@code type}. This will * dispatch according to the concrete {@code Type} implementation. See the other overloads of * {@code addExposedTypes} for their details. */ private void addExposedTypes(Type type, Class<?> cause) { if (type instanceof TypeVariable) { LOG.debug("Adding exposed types from {}, which is a type variable", type); addExposedTypes((TypeVariable) type, cause); } else if (type instanceof WildcardType) { LOG.debug("Adding exposed types from {}, which is a wildcard type", type); addExposedTypes((WildcardType) type, cause); } else if (type instanceof GenericArrayType) { LOG.debug("Adding exposed types from {}, which is a generic array type", type); addExposedTypes((GenericArrayType) type, cause); } else if (type instanceof ParameterizedType) { LOG.debug("Adding exposed types from {}, which is a parameterized type", type); addExposedTypes((ParameterizedType) type, cause); } else if (type instanceof Class) { LOG.debug("Adding exposed types from {}, which is a class", type); addExposedTypes((Class) type, cause); } else { throw new IllegalArgumentException("Unknown implementation of Type"); } } /** * Adds any types exposed to this set. These will come from the (possibly absent) bounds on the * type variable. */ private void addExposedTypes(TypeVariable type, Class<?> cause) { if (done(type)) { return; } visit(type); for (Type bound : type.getBounds()) { LOG.debug("Adding exposed types from {}, which is a type bound on {}", bound, type); addExposedTypes(bound, cause); } } /** * Adds any types exposed to this set. These will come from the (possibly absent) bounds on the * wildcard. */ private void addExposedTypes(WildcardType type, Class<?> cause) { visit(type); for (Type lowerBound : type.getLowerBounds()) { LOG.debug( "Adding exposed types from {}, which is a type lower bound on wildcard type {}", lowerBound, type); addExposedTypes(lowerBound, cause); } for (Type upperBound : type.getUpperBounds()) { LOG.debug( "Adding exposed types from {}, which is a type upper bound on wildcard type {}", upperBound, type); addExposedTypes(upperBound, cause); } } /** * Adds any types exposed from the given array type. The array type itself is not added. The cause * of the exposure of the underlying type is considered whatever type exposed the array type. */ private void addExposedTypes(GenericArrayType type, Class<?> cause) { if (done(type)) { return; } visit(type); LOG.debug( "Adding exposed types from {}, which is the component type on generic array type {}", type.getGenericComponentType(), type); addExposedTypes(type.getGenericComponentType(), cause); } /** * Adds any types exposed to this set. Even if the root type is to be pruned, the actual type * arguments are processed. */ private void addExposedTypes(ParameterizedType type, Class<?> cause) { // Even if the type is already done, this link to it may be new boolean alreadyDone = done(type); if (!pruned(type)) { visit(type); recordExposure(type, cause); } if (alreadyDone) { return; } // For a parameterized type, pruning does not take place // here, only for the raw class. // The type parameters themselves may not be pruned, // for example with List<MyApiType> probably the // standard List is pruned, but MyApiType is not. LOG.debug( "Adding exposed types from {}, which is the raw type on parameterized type {}", type.getRawType(), type); addExposedTypes(type.getRawType(), cause); for (Type typeArg : type.getActualTypeArguments()) { LOG.debug( "Adding exposed types from {}, which is a type argument on parameterized type {}", typeArg, type); addExposedTypes(typeArg, cause); } } /** * Adds a class and all of the types it exposes. The cause of the class being exposed is given, * and the cause of everything within the class is that class itself. */ private void addExposedTypes(Class<?> clazz, Class<?> cause) { if (pruned(clazz)) { return; } // Even if `clazz` has been visited, the link from `cause` may be new boolean alreadyDone = done(clazz); visit(clazz); recordExposure(clazz, cause); if (alreadyDone || pruned(clazz)) { return; } TypeToken<?> token = TypeToken.of(clazz); for (TypeToken<?> superType : token.getTypes()) { if (!superType.equals(token)) { LOG.debug( "Adding exposed types from {}, which is a super type token on {}", superType, clazz); addExposedTypes(superType, clazz); } } for (Class innerClass : clazz.getDeclaredClasses()) { if (exposed(innerClass.getModifiers())) { LOG.debug( "Adding exposed types from {}, which is an exposed inner class of {}", innerClass, clazz); addExposedTypes(innerClass, clazz); } } for (Field field : clazz.getDeclaredFields()) { if (exposed(field.getModifiers())) { LOG.debug("Adding exposed types from {}, which is an exposed field on {}", field, clazz); addExposedTypes(field, clazz); } } for (Invokable invokable : getExposedInvokables(token)) { LOG.debug( "Adding exposed types from {}, which is an exposed invokable on {}", invokable, clazz); addExposedTypes(invokable, clazz); } } private void addExposedTypes(Invokable<?, ?> invokable, Class<?> cause) { addExposedTypes(invokable.getReturnType(), cause); for (Annotation annotation : invokable.getAnnotations()) { LOG.debug( "Adding exposed types from {}, which is an annotation on invokable {}", annotation, invokable); addExposedTypes(annotation.annotationType(), cause); } for (Parameter parameter : invokable.getParameters()) { LOG.debug( "Adding exposed types from {}, which is a parameter on invokable {}", parameter, invokable); addExposedTypes(parameter, cause); } for (TypeToken<?> exceptionType : invokable.getExceptionTypes()) { LOG.debug( "Adding exposed types from {}, which is an exception type on invokable {}", exceptionType, invokable); addExposedTypes(exceptionType, cause); } } private void addExposedTypes(Parameter parameter, Class<?> cause) { LOG.debug( "Adding exposed types from {}, which is the type of parameter {}", parameter.getType(), parameter); addExposedTypes(parameter.getType(), cause); for (Annotation annotation : parameter.getAnnotations()) { LOG.debug( "Adding exposed types from {}, which is an annotation on parameter {}", annotation, parameter); addExposedTypes(annotation.annotationType(), cause); } } private void addExposedTypes(Field field, Class<?> cause) { addExposedTypes(field.getGenericType(), cause); for (Annotation annotation : field.getDeclaredAnnotations()) { LOG.debug( "Adding exposed types from {}, which is an annotation on field {}", annotation, field); addExposedTypes(annotation.annotationType(), cause); } } /** Returns an {@link Invokable} for each public methods or constructors of a type. */ private Set<Invokable> getExposedInvokables(TypeToken<?> type) { Set<Invokable> invokables = Sets.newHashSet(); for (Constructor constructor : type.getRawType().getConstructors()) { if (0 != (constructor.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED))) { invokables.add(type.constructor(constructor)); } } for (Method method : type.getRawType().getMethods()) { if (0 != (method.getModifiers() & (Modifier.PUBLIC | Modifier.PROTECTED))) { invokables.add(type.method(method)); } } return invokables; } /** Returns true of the given modifier bitmap indicates exposure (public or protected access). */ private boolean exposed(int modifiers) { return 0 != (modifiers & (Modifier.PUBLIC | Modifier.PROTECTED)); } //////////////////////////////////////////////////////////////////////////// /** * All classes transitively reachable via only public method signatures of the SDK. * * <p>Note that our idea of "public" does not include various internal-only APIs. */ public static ApiSurface getSdkApiSurface(final ClassLoader classLoader) throws IOException { return ApiSurface.ofPackage("org.apache.beam", classLoader) .pruningPattern("org[.]apache[.]beam[.].*Test") // Exposes Guava, but not intended for users .pruningClassName("org.apache.beam.sdk.util.common.ReflectHelpers") .pruningPrefix("java"); } }