org.apache.beam.sdk.util.ApiSurface.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.beam.sdk.util.ApiSurface.java

Source

/*
 * 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");
    }
}