package io.dropwizard.jersey;

import com.codahale.metrics.Clock;
import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListener;
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import io.dropwizard.jersey.caching.CacheControlledResponseFeature;
import io.dropwizard.jersey.params.AbstractParamConverterProvider;
import io.dropwizard.jersey.sessions.SessionFactoryProvider;
import io.dropwizard.jersey.validation.FuzzyEnumParamConverterProvider;
import io.dropwizard.util.JavaVersion;
import io.dropwizard.util.Strings;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import org.glassfish.jersey.internal.inject.AbstractBinder;
import org.glassfish.jersey.internal.inject.Providers;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static java.util.Objects.requireNonNull;

public class DropwizardResourceConfig extends ResourceConfig {
    private static final Logger LOGGER = LoggerFactory.getLogger(DropwizardResourceConfig.class);
    private static final String NEWLINE = String.format("%n");
    private static final TypeResolver TYPE_RESOLVER = new TypeResolver();

    private static final Pattern PATH_DIRTY_SLASHES = Pattern.compile("\\s*/\\s*/+\\s*");

    private String urlPattern = "/*";
    private String contextPath = "/";
    private final ComponentLoggingListener loggingListener = new ComponentLoggingListener(this);

    public DropwizardResourceConfig() {
        this(null);
    }

    public DropwizardResourceConfig(@Nullable MetricRegistry metricRegistry) {
        super();

        if (metricRegistry == null) {
            metricRegistry = new MetricRegistry();
        }

        property(ServerProperties.WADL_FEATURE_DISABLE, Boolean.TRUE);
        register(loggingListener);

        register(new MetricRegistryBinder(metricRegistry));
        register(new InstrumentedResourceMethodApplicationListener(metricRegistry, Clock.defaultClock(), true));
        register(CacheControlledResponseFeature.class);
        register(io.dropwizard.jersey.guava.OptionalMessageBodyWriter.class);
        register(new io.dropwizard.jersey.guava.OptionalParamBinder());
        register(io.dropwizard.jersey.optional.OptionalMessageBodyWriter.class);
        register(io.dropwizard.jersey.optional.OptionalDoubleMessageBodyWriter.class);
        register(io.dropwizard.jersey.optional.OptionalIntMessageBodyWriter.class);
        register(io.dropwizard.jersey.optional.OptionalLongMessageBodyWriter.class);
        register(new io.dropwizard.jersey.optional.OptionalParamBinder());
        register(AbstractParamConverterProvider.class);
        register(new FuzzyEnumParamConverterProvider());
        register(new SessionFactoryProvider.Binder());
    }

    
Build a DropwizardResourceConfig which makes Jersey Test run on a random port, also see org.glassfish.jersey.test.TestProperties#CONTAINER_PORT.
Since:2.0
/** * Build a {@link DropwizardResourceConfig} which makes Jersey Test run on a random port, * also see {@code org.glassfish.jersey.test.TestProperties#CONTAINER_PORT}. * * @since 2.0 */
public static DropwizardResourceConfig forTesting() { return forTesting(null); }
Build a DropwizardResourceConfig which makes Jersey Test run on a random port, also see org.glassfish.jersey.test.TestProperties#CONTAINER_PORT.
Since:2.0
/** * Build a {@link DropwizardResourceConfig} which makes Jersey Test run on a random port, * also see {@code org.glassfish.jersey.test.TestProperties#CONTAINER_PORT}. * * @since 2.0 */
public static DropwizardResourceConfig forTesting(@Nullable MetricRegistry metricRegistry) { final DropwizardResourceConfig config = new DropwizardResourceConfig(metricRegistry); // See org.glassfish.jersey.test.TestProperties#CONTAINER_PORT config.property("jersey.config.test.container.port", "0"); return config; } public String getUrlPattern() { return urlPattern; } public void setUrlPattern(String urlPattern) { this.urlPattern = urlPattern; }
Since:2.0
/** * @since 2.0 */
public String getContextPath() { return contextPath; } public void setContextPath(String contextPath) { this.contextPath = contextPath; }
Since:2.0
/** * @since 2.0 */
public String getEndpointsInfo() { return loggingListener.getEndpointsInfo(); }
Combines types of getClasses() and getSingletons in one Set.
Returns:all registered types
/** * Combines types of getClasses() and getSingletons in one Set. * * @return all registered types */
Set<Class<?>> allClasses() { final Set<Class<?>> allClasses = new HashSet<>(getClasses()); for (Object singleton : getSingletons()) { allClasses.add(singleton.getClass()); } return allClasses; } @Override public ResourceConfig register(final Object component) { final Object object = requireNonNull(component); Class<?> clazz = object.getClass(); // If a class gets passed through as an object, cast to Class and register directly if (component instanceof Class<?>) { return super.register((Class<?>) component); } else if (Providers.isProvider(clazz) || org.glassfish.hk2.utilities.Binder.class.isAssignableFrom(clazz)) { // If Jersey supports this component's class (including Binders), register directly return super.register(object); } else { // Else register a binder that binds the instance to its class type try { // Need to create a new subclass dynamically here because Jersey // doesn't add new bindings for the same class final ClassPool pool = ClassPool.getDefault(); pool.insertClassPath(new LoaderClassPath(this.getClass().getClassLoader())); final CtClass cc = pool.makeClass(SpecificBinder.class.getName() + UUID.randomUUID()); cc.setSuperclass(pool.get(SpecificBinder.class.getName())); final Object binderProxy; if (JavaVersion.isJava8()) { binderProxy = cc.toClass().getConstructor(Object.class, Class.class).newInstance(object, clazz); } else { binderProxy = cc.toClass(SpecificBinder.class).getConstructor(Object.class, Class.class).newInstance(object, clazz); } super.register(binderProxy); return super.register(clazz); } catch (Exception e) { throw new RuntimeException(e); } } } static String cleanUpPath(String path) { return PATH_DIRTY_SLASHES.matcher(path).replaceAll("/").trim(); } private static String mergePaths(@NotNull String context, String... pathSegments) { if (pathSegments == null || pathSegments.length == 0) { return cleanUpPath(context); } final StringBuilder path = new StringBuilder(); if (context.endsWith("/")) { path.append(context, 0, context.length() - 1); } else { path.append(context); } for (String segment : pathSegments) { if (Strings.isNullOrEmpty(segment)) { continue; } if ("/".equals(segment)) { path.append('/'); } else { final int startIndex = segment.startsWith("/") ? 1 : 0; final int endIndex = segment.endsWith("/") ? segment.length() - 1 : segment.length(); path.append('/').append(segment, startIndex, endIndex); } } return cleanUpPath(path.toString()); }
Since:2.0
/** * @since 2.0 */
public static class SpecificBinder extends AbstractBinder { private Object object; private Class<?> clazz; public SpecificBinder(Object object, Class<?> clazz) { this.object = object; this.clazz = clazz; } @Override public void configure() { bind(object).to(clazz); } } private static class EndpointLogLine { private final String httpMethod; private final String basePath; private final Class<?> klass; EndpointLogLine(String httpMethod, String basePath, Class<?> klass) { this.basePath = basePath; this.klass = klass; this.httpMethod = httpMethod; } @Override public String toString() { final String method = httpMethod == null ? "UNKNOWN" : httpMethod; return String.format(" %-7s %s (%s)", method, basePath, klass.getCanonicalName()); } } private static class EndpointComparator implements Comparator<EndpointLogLine>, Serializable { private static final long serialVersionUID = 1L; @Override public int compare(EndpointLogLine endpointA, EndpointLogLine endpointB) { return Comparator.<EndpointLogLine, String>comparing(endpoint -> endpoint.basePath) .thenComparing(endpoint -> endpoint.httpMethod, Comparator.nullsLast(Comparator.naturalOrder())) .compare(endpointA, endpointB); } } private static class ComponentLoggingListener implements ApplicationEventListener { private final DropwizardResourceConfig config; private List<Resource> resources = Collections.emptyList(); private Set<Class<?>> providers = Collections.emptySet(); ComponentLoggingListener(DropwizardResourceConfig config) { this.config = config; } @Override @SuppressWarnings("Slf4jFormatShouldBeConst") public void onEvent(ApplicationEvent event) { if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) { resources = event.getResourceModel().getResources(); providers = event.getProviders(); final String resourceClasses = resources.stream() .map(x -> x.getClass().getCanonicalName()) .collect(Collectors.joining(", ")); final String providerClasses = providers.stream() .map(Class::getCanonicalName) .collect(Collectors.joining(", ")); LOGGER.debug("resources = {}", resourceClasses); LOGGER.debug("providers = {}", providerClasses); LOGGER.info(getEndpointsInfo()); } } private List<EndpointLogLine> logMethodLines(Resource resource, String contextPath) { final List<EndpointLogLine> methodLines = new ArrayList<>(); for (ResourceMethod method : resource.getAllMethods()) { if ("OPTIONS".equalsIgnoreCase(method.getHttpMethod())) { continue; } final String path = mergePaths(contextPath, resource.getPath()); final Class<?> handler = method.getInvocable().getHandler().getHandlerClass(); switch (method.getType()) { case RESOURCE_METHOD: methodLines.add(new EndpointLogLine(method.getHttpMethod(), path, handler)); break; case SUB_RESOURCE_LOCATOR: final ResolvedType responseType = TYPE_RESOLVER .resolve(method.getInvocable().getResponseType()); final Class<?> erasedType = !responseType.getTypeBindings().isEmpty() ? responseType.getTypeBindings().getBoundType(0).getErasedType() : responseType.getErasedType(); final Resource res = Resource.from(erasedType); if (res == null) { methodLines.add(new EndpointLogLine(method.getHttpMethod(), path, handler)); } else { methodLines.addAll(logResourceLines(res, path)); } break; default: break; } } return methodLines; } private List<EndpointLogLine> logResourceLines(Resource resource, String contextPath) { final List<EndpointLogLine> resourceLines = new ArrayList<>(); for (Resource child : resource.getChildResources()) { resourceLines.addAll(logResourceLines(child, mergePaths(contextPath, resource.getPath()))); } resourceLines.addAll(logMethodLines(resource, contextPath)); return resourceLines; } String getEndpointsInfo() { final StringBuilder msg = new StringBuilder(1024); final Set<EndpointLogLine> endpointLogLines = new TreeSet<>(new EndpointComparator()); final String contextPath = config.getContextPath(); final String normalizedContextPath = contextPath.isEmpty() || contextPath.equals("/") ? "" : contextPath.startsWith("/") ? contextPath : "/" + contextPath; final String pattern = config.getUrlPattern().endsWith("/*") ? config.getUrlPattern().substring(0, config.getUrlPattern().length() - 1) : config.getUrlPattern(); final String path = mergePaths(normalizedContextPath, pattern); msg.append("The following paths were found for the configured resources:"); msg.append(NEWLINE).append(NEWLINE); for (Resource resource : resources) { endpointLogLines.addAll(logResourceLines(resource, path)); } final List<EndpointLogLine> providerLines = providers.stream() .map(Resource::from) .filter(Objects::nonNull) .flatMap(res -> logResourceLines(res, path).stream()) .collect(Collectors.toList()); endpointLogLines.addAll(providerLines); if (!endpointLogLines.isEmpty()) { for (EndpointLogLine line : endpointLogLines) { msg.append(line).append(NEWLINE); } } else { msg.append(" NONE").append(NEWLINE); } return msg.toString(); } @Override @Nullable public RequestEventListener onRequest(RequestEvent requestEvent) { return null; } } static final class MetricRegistryBinder extends AbstractBinder { private final MetricRegistry metricRegistry; public MetricRegistryBinder(MetricRegistry metricRegistry) { this.metricRegistry = metricRegistry; } @Override protected void configure() { bind(metricRegistry).to(MetricRegistry.class); } } }