From d887a376272655cb371acc4196d8c8aa48516b70 Mon Sep 17 00:00:00 2001 From: Christer Fahlgren Date: Sun, 3 Nov 2013 23:48:03 -0800 Subject: [PATCH] Committing source --- .gitignore | 3 + README.md | 146 +++++++++- pom.xml | 91 ++++++ .../java/com/twilio/wiztowar/DWAdapter.java | 271 ++++++++++++++++++ .../wiztowar/ServletContextCallback.java | 30 ++ .../config/ExtendedEnvironment.java | 161 +++++++++++ 6 files changed, 700 insertions(+), 2 deletions(-) create mode 100644 pom.xml create mode 100644 src/main/java/com/twilio/wiztowar/DWAdapter.java create mode 100644 src/main/java/com/twilio/wiztowar/ServletContextCallback.java create mode 100644 src/main/java/com/yammer/dropwizard/config/ExtendedEnvironment.java diff --git a/.gitignore b/.gitignore index 0f182a0..7374267 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +target +.idea +*.iml *.class # Package Files # diff --git a/README.md b/README.md index 56cb68e..56b98e7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,144 @@ -wiztowar -======== +WizToWar - Have your cake and eat it too +======================================== + +WizToWar is a simple library that enables a Dropwizard service to also be deployable in a WAR container such as Tomcat. + +By following the steps in the usage section below you will be able to create both a Dropwizard jar and a WAR of the same +service. + + +Caveat emptor: +-------------- + +* Only tested on Tomcat 7 +* No support for bundles +* Many features untested +* Goes against the whole philosophy of Dropwizard... + +Usage +------ + +Include the wiztowar jar as a dependency: + + + com.twilio + wiztowar + 1.0 + + +Create a new class for your application like this: + + package com.twilio.mixerstate; + + import com.google.common.io.Resources; + import com.twilio.wiztowar.DWAdapter; + import com.yammer.dropwizard.Service; + + import java.io.File; + import java.net.URISyntaxException; + import java.net.URL; + + + public class MixerStateDWApplication extends DWAdapter { + final static MixerStateService service = new MixerStateService(); + + /** + * Return the Dropwizard service you want to run. + */ + public Service getSingletonService(){ + return service; + } + + /** + * Return the File where the configuration lives. + */ + @Override + public File getConfigurationFile() { + + URL url = Resources.getResource("mixer-state-server.yml"); + try { + return new File(url.toURI()); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + } + } + + +Create a main/webapp/WEB-INF/web.xml file: +------------------------------------------ + + + + + + com.twilio.wiztowar.ServletContextCallback + + + Jersey REST Service + com.sun.jersey.spi.container.servlet.ServletContainer + + + javax.ws.rs.Application + com.twilio.mixerstate.MixerStateDWApplication + + + + com.sun.jersey.config.property.packages + com.twilio.mixerstate.resources + + + com.sun.jersey.api.json.POJOMappingFeature + true + + + 1 + + + + Jersey REST Service + /* + + + +Make sure you also build a WAR artifact +--------------------------------------------- + +There are two alternatives to building a war: + +### Add instructions to also build a WAR + +This goes in `` section: + + + org.apache.maven.plugins + maven-war-plugin + 2.4 + + + default-war + package + + war + + + + + target/webapp + + + +### Change packaging of your Dropwizard service + +If you do not intend to run the Dropwizard service standalone, you can simply change the "packaging" element in pom.xml to be "war" instead of "jar". + + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..b7a41ad --- /dev/null +++ b/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + com.twilio + wiztowar + 1.0-SNAPSHOT + jar + + 0.6.2 + 2.2.0 + + + + scm:git:git://github.com/twilio/wiztowar.git + scm:git:git@github.com:twilio/wiztowar.git + http://github.com/twilio/wiztowar + + + + + sonatype-nexus-snapshots + Sonatype Nexus snapshot repository + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + Sonatype Nexus release repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + + + com.yammer.dropwizard + dropwizard-core + ${dropwizard.version} + + + com.yammer.metrics + metrics-annotation + ${metrics.version} + + + + + + + org.apache.maven.plugins + maven-release-plugin + 2.2.2 + + -Dgpg.passphrase=${gpg.passphrase} + + + + + + + release-sign-artifacts + + + performRelease + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.4 + + ${gpg.passphrase} + + + + sign-artifacts + verify + + sign + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/twilio/wiztowar/DWAdapter.java b/src/main/java/com/twilio/wiztowar/DWAdapter.java new file mode 100644 index 0000000..2dd8fb9 --- /dev/null +++ b/src/main/java/com/twilio/wiztowar/DWAdapter.java @@ -0,0 +1,271 @@ +package com.twilio.wiztowar; + +import com.google.common.collect.ImmutableMap; +import com.yammer.dropwizard.Service; +import com.yammer.dropwizard.config.*; +import com.yammer.dropwizard.jersey.JacksonMessageBodyProvider; +import com.yammer.dropwizard.json.ObjectMapperFactory; +import com.yammer.dropwizard.servlets.ThreadNameFilter; +import com.yammer.dropwizard.tasks.TaskServlet; +import com.yammer.dropwizard.util.Generics; +import com.yammer.dropwizard.validation.Validator; +import com.yammer.metrics.HealthChecks; +import com.yammer.metrics.core.HealthCheck; +import com.yammer.metrics.reporting.AdminServlet; +import com.yammer.metrics.util.DeadlockHealthCheck; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHolder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletContext; +import javax.servlet.ServletRegistration; +import javax.ws.rs.core.Application; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.EventListener; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * The {@link DWAdapter} adapts a Dropwizard {@link Service} to be hooked in to the lifecycle of a WAR. + */ +public abstract class DWAdapter extends Application { + + /** + * The {@link Logger} to use. + */ + final static Logger logger = LoggerFactory.getLogger(DWAdapter.class); + + /** + * The Jersey singletons. + */ + private HashSet singletons; + + /** + * The Jersey classes. + */ + private HashSet> classes; + + /** + * The {@link Service} which we are adapting. + */ + private Service dwService; + + /** + * Implementation of the Jersey Application. Returns the classes. + * + * @return the Jersey configured classes + */ + @Override + public Set> getClasses() { + synchronized (this) { + if (dwService == null) { + initialize(); + } + } + return classes; + } + + /** + * Implementation of the Jersey Application. Returns the singletons. + * + * @return the Jersey configured singletons. + */ + @Override + public Set getSingletons() { + synchronized (this) { + if (dwService == null) { + initialize(); + } + } + return singletons; + } + + /** + * Initialize the {@link Service} and configure the Servlet and Jersey environment. + */ + private void initialize() { + try { + dwService = getSingletonService(); + if (dwService == null) { + throw new IllegalStateException("The singleton service is null"); + } + final Bootstrap bootstrap = new Bootstrap(dwService); + dwService.initialize(bootstrap); + final T configuration = parseConfiguration(getConfigurationFile(), + getConfigurationClass(), + bootstrap.getObjectMapperFactory().copy()); + if (configuration != null) { + logger.info("The WizToWar adapter defers logging configuration to the application server"); + new LoggingFactory(configuration.getLoggingConfiguration(), + bootstrap.getName()).configure(); + } + + final Validator validator = new Validator(); + final ExtendedEnvironment env = new ExtendedEnvironment(bootstrap.getName(), configuration, bootstrap.getObjectMapperFactory(), validator); + try { + + env.start(); + + dwService.run(configuration, env); + addHealthChecks(env); + final ServletContext servletContext = ServletContextCallback.getServletContext(); + if (servletContext == null) { + throw new IllegalStateException("ServletContext is null"); + } + createInternalServlet(env, servletContext); + createExternalServlet(env, configuration.getHttpConfiguration(), servletContext); + env.validateJerseyResources(); + env.logEndpoints(configuration); + + // Now collect the Jersey configuration + singletons = new HashSet(); + singletons.addAll(env.getJerseyResourceConfig().getSingletons()); + classes = new HashSet>(); + classes.addAll(env.getJerseyResourceConfig().getClasses()); + + } catch (Exception e) { + logger.error("Error {} ", e); + throw new IllegalStateException(e); + } + } catch (Exception e) { + logger.error("Error {} ", e); + throw new IllegalStateException(e); + } + } + + /** + * Parse the configuration from the {@link File}. + * + * @param file the {@link File} containing the configuration. + * @param configurationClass the configuration class + * @param objectMapperFactory the {@link ObjectMapperFactory} to use + * @return the configuration instance + * @throws IOException + * @throws ConfigurationException + */ + private T parseConfiguration(final File file, + final Class configurationClass, + final ObjectMapperFactory objectMapperFactory) throws IOException, ConfigurationException { + final ConfigurationFactory configurationFactory = + ConfigurationFactory.forClass(configurationClass, new Validator(), objectMapperFactory); + if (file != null) { + if (!file.exists()) { + throw new FileNotFoundException("File " + file + " not found"); + } + return configurationFactory.build(file); + } + return configurationFactory.build(); + } + + /** + * Retrieve the configuration class. + * + * @return the configuration class. + */ + protected Class getConfigurationClass() { + return Generics.getTypeParameter(getClass(), Configuration.class); + } + + /** + * This method is adapted from ServerFactory.createInternalServlet. + */ + private void createInternalServlet(final ExtendedEnvironment env, final ServletContext context) { + if (context.getMajorVersion() >= 3) { + + // Add the Task servlet + final ServletRegistration.Dynamic taskServlet = context.addServlet("TaskServlet", new TaskServlet(env.getTasks())); + taskServlet.setAsyncSupported(true); + taskServlet.addMapping("/tasks/*"); + + // Add the Admin servlet + final ServletRegistration.Dynamic adminServlet = context.addServlet("AdminServlet", new AdminServlet()); + adminServlet.setAsyncSupported(true); + adminServlet.addMapping("/*"); + } else throw new IllegalStateException("The WizToWar adapter doesn't support servlet versions under 3"); + } + + /** + * This method is adapted from ServerFactory.createExternalServlet. + * + * @param env the {@link ExtendedEnvironment} from which we find the resources to act on. + * @param context the {@link ServletContext} to add to + */ + private void createExternalServlet(ExtendedEnvironment env, HttpConfiguration config, ServletContext context) { + context.addFilter("ThreadNameFilter", ThreadNameFilter.class); + + if (!env.getProtectedTargets().isEmpty()) { + logger.warn("The WizToWar adapter doesn't support protected targets"); + } + + for (ImmutableMap.Entry entry : env.getServlets().entrySet()) { + context.addServlet(entry.getKey(), entry.getValue().getServletInstance()); + } + + env.addProvider(new JacksonMessageBodyProvider(env.getObjectMapperFactory().build(), + env.getValidator())); + + for (ImmutableMap.Entry entry : env.getFilters().entries()) { + context.addFilter(entry.getKey(), entry.getValue().getFilter()); + } + + for (EventListener listener : env.getServletListeners()) { + context.addListener(listener); + } + + for (Map.Entry entry : config.getContextParameters().entrySet()) { + context.setInitParameter(entry.getKey(), entry.getValue()); + } + + if (env.getSessionHandler() != null) { + logger.warn("The WizToWar adapter doesn't support custom session handlers."); + + } + } + + /** + * This method is adapted from ServerFactory.buildServer. + * + * @param env the {@link ExtendedEnvironment} to get {@link HealthCheck}s from. + */ + private void addHealthChecks(ExtendedEnvironment env) { + HealthChecks.defaultRegistry().register(new DeadlockHealthCheck()); + for (HealthCheck healthCheck : env.getHealthChecks()) { + HealthChecks.defaultRegistry().register(healthCheck); + } + + if (env.getHealthChecks().isEmpty()) { + logger.warn('\n' + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "! THIS SERVICE HAS NO HEALTHCHECKS. THIS MEANS YOU WILL NEVER KNOW IF IT !\n" + + "! DIES IN PRODUCTION, WHICH MEANS YOU WILL NEVER KNOW IF YOU'RE LETTING !\n" + + "! YOUR USERS DOWN. YOU SHOULD ADD A HEALTHCHECK FOR EACH DEPENDENCY OF !\n" + + "! YOUR SERVICE WHICH FULLY (BUT LIGHTLY) TESTS YOUR SERVICE'S ABILITY TO !\n" + + "! USE THAT SERVICE. THINK OF IT AS A CONTINUOUS INTEGRATION TEST. !\n" + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + ); + } + + + } + + /** + * Override to provide your particular Dropwizard Service. + * + * @return your {@link Service} + */ + public abstract Service getSingletonService(); + + /** + * Override to provide your configuration {@link File} location. + * + * @return the {@link File} to read the configuration from. + */ + public abstract File getConfigurationFile(); +} + diff --git a/src/main/java/com/twilio/wiztowar/ServletContextCallback.java b/src/main/java/com/twilio/wiztowar/ServletContextCallback.java new file mode 100644 index 0000000..f02a320 --- /dev/null +++ b/src/main/java/com/twilio/wiztowar/ServletContextCallback.java @@ -0,0 +1,30 @@ +package com.twilio.wiztowar; + +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; + +/** + * The {@link ServletContextCallback} captures the {@link ServletContext} for use by the {@link DWAdapter}. + */ + +public class ServletContextCallback implements ServletContextListener { + + /** + * The {@link ServletContext} to capture. + */ + private static ServletContext ctxt; + + public static ServletContext getServletContext() { + return ctxt; + } + + @Override + public void contextInitialized(ServletContextEvent sce) { + ctxt = sce.getServletContext(); + } + + @Override + public void contextDestroyed(ServletContextEvent sce) { + } +} \ No newline at end of file diff --git a/src/main/java/com/yammer/dropwizard/config/ExtendedEnvironment.java b/src/main/java/com/yammer/dropwizard/config/ExtendedEnvironment.java new file mode 100644 index 0000000..d19b631 --- /dev/null +++ b/src/main/java/com/yammer/dropwizard/config/ExtendedEnvironment.java @@ -0,0 +1,161 @@ +package com.yammer.dropwizard.config; + +import com.google.common.collect.*; +import com.sun.jersey.core.reflection.AnnotatedMethod; +import com.sun.jersey.core.reflection.MethodList; +import com.yammer.dropwizard.json.ObjectMapperFactory; +import com.yammer.dropwizard.tasks.Task; +import com.yammer.dropwizard.validation.Validator; +import com.yammer.metrics.core.HealthCheck; +import org.eclipse.jetty.servlet.FilterHolder; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.resource.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.HttpMethod; +import javax.ws.rs.Path; +import java.util.EventListener; + +/** + * This class is here to enable us to pull out various things from the {@link Environment}. + */ +public class ExtendedEnvironment extends Environment { + + final private static Logger logger = LoggerFactory.getLogger(ExtendedEnvironment.class); + /** + * Creates a new environment. + * + * @param name the name of the service + * @param configuration the service's {@link com.yammer.dropwizard.config.Configuration} + * @param objectMapperFactory the {@link com.yammer.dropwizard.json.ObjectMapperFactory} for the service + */ + public ExtendedEnvironment(String name, + Configuration configuration, + ObjectMapperFactory objectMapperFactory, + Validator validator) { + super(name, configuration, objectMapperFactory, validator); + + } + + @Override + public ImmutableSet getTasks() { + return super.getTasks(); + } + + @Override + public ImmutableSet getHealthChecks() { + return super.getHealthChecks(); + } + + @Override + public Resource getBaseResource() { + return super.getBaseResource(); + } + + @Override + public ImmutableSet getProtectedTargets() { + return super.getProtectedTargets(); + } + + @Override + public ImmutableMap getServlets() { + return super.getServlets(); + } + + @Override + public ImmutableMultimap getFilters() { + return super.getFilters(); + } + + @Override + public ImmutableSet getServletListeners() { + return super.getServletListeners(); + } + + /** + * Log all the resources. + */ + private void logResources() { + final ImmutableSet.Builder builder = ImmutableSet.builder(); + + for (Class klass : super.getJerseyResourceConfig().getClasses()) { + if (klass.isAnnotationPresent(Path.class)) { + builder.add(klass.getCanonicalName()); + } + } + + for (Object o : super.getJerseyResourceConfig().getSingletons()) { + if (o.getClass().isAnnotationPresent(Path.class)) { + builder.add(o.getClass().getCanonicalName()); + } + } + + logger.debug("resources = {}", builder.build()); + } + + /** + * Log all endpoints. + */ + public void logEndpoints(Configuration configuration) { + final StringBuilder stringBuilder = new StringBuilder(1024).append("The following paths were found for the configured resources:\n\n"); + + final ImmutableList.Builder> builder = ImmutableList.builder(); + for (Object o : super.getJerseyResourceConfig().getSingletons()) { + if (o.getClass().isAnnotationPresent(Path.class)) { + builder.add(o.getClass()); + } + } + for (Class klass : super.getJerseyResourceConfig().getClasses()) { + if (klass.isAnnotationPresent(Path.class)) { + builder.add(klass); + } + } + + for (Class klass : builder.build()) { + final String path = klass.getAnnotation(Path.class).value(); + String rootPath = configuration.getHttpConfiguration().getRootPath(); + if (rootPath.endsWith("/*")) { + rootPath = rootPath.substring(0, rootPath.length() - (path.startsWith("/") ? 2 : 1)); + } + + final ImmutableList.Builder endpoints = ImmutableList.builder(); + for (AnnotatedMethod method : annotatedMethods(klass)) { + final StringBuilder pathBuilder = new StringBuilder() + .append(rootPath) + .append(path); + if (method.isAnnotationPresent(Path.class)) { + final String methodPath = method.getAnnotation(Path.class).value(); + if (!methodPath.startsWith("/") && !path.endsWith("/")) { + pathBuilder.append('/'); + } + pathBuilder.append(methodPath); + } + for (HttpMethod verb : method.getMetaMethodAnnotations(HttpMethod.class)) { + endpoints.add(String.format(" %-7s %s (%s)", + verb.value(), + pathBuilder.toString(), + klass.getCanonicalName())); + } + } + + for (String line : Ordering.natural().sortedCopy(endpoints.build())) { + stringBuilder.append(line).append('\n'); + } + } + + logger.info(stringBuilder.toString()); + } + + + private MethodList annotatedMethods(Class resource) { + return new MethodList(resource, true).hasMetaAnnotation(HttpMethod.class); + } + + /** + * Validate the Jersey resources before launching. + */ + public void validateJerseyResources() { + logResources(); + } +} \ No newline at end of file