[dropwizard] 01/05: Imported Upstream version 0.7.1

Tim Potter tpot-guest at moszumanska.debian.org
Thu Nov 6 00:21:34 UTC 2014


This is an automated email from the git hooks/post-receive script.

tpot-guest pushed a commit to branch master
in repository dropwizard.

commit 7a29dfb527c6221791233ee5d0dfd22fb279d885
Author: Tim Potter <tpot at hp.com>
Date:   Wed Nov 5 14:19:39 2014 +1100

    Imported Upstream version 0.7.1
---
 .gitignore                                         |   12 +
 .travis.yml                                        |   10 +
 CONTRIBUTING.md                                    |   53 +
 LICENSE                                            |  202 +++
 NOTICE                                             |    4 +
 README.md                                          |   38 +
 docs/Guardfile                                     |    6 +
 docs/Makefile                                      |  166 +++
 docs/dropwizard-hat.eps                            |  Bin 0 -> 729078 bytes
 docs/pom.xml                                       |   36 +
 docs/source/_static/dropwizard-hat.png             |  Bin 0 -> 15524 bytes
 docs/source/_themes/dropwizard/genindex.html       |   77 ++
 docs/source/_themes/dropwizard/layout.html         |  131 ++
 docs/source/_themes/dropwizard/less/accordion.less |   28 +
 docs/source/_themes/dropwizard/less/alerts.less    |   70 +
 docs/source/_themes/dropwizard/less/bootstrap.less |   62 +
 .../_themes/dropwizard/less/breadcrumbs.less       |   22 +
 .../_themes/dropwizard/less/button-groups.less     |  147 +++
 docs/source/_themes/dropwizard/less/buttons.less   |  165 +++
 docs/source/_themes/dropwizard/less/carousel.less  |  121 ++
 docs/source/_themes/dropwizard/less/close.less     |   18 +
 docs/source/_themes/dropwizard/less/code.less      |   44 +
 .../dropwizard/less/component-animations.less      |   18 +
 docs/source/_themes/dropwizard/less/dropdowns.less |  131 ++
 .../source/_themes/dropwizard/less/dropwizard.less |  231 ++++
 docs/source/_themes/dropwizard/less/forms.less     |  515 ++++++++
 docs/source/_themes/dropwizard/less/grid.less      |    8 +
 docs/source/_themes/dropwizard/less/hero-unit.less |   20 +
 docs/source/_themes/dropwizard/less/labels.less    |   16 +
 docs/source/_themes/dropwizard/less/layouts.less   |   17 +
 docs/source/_themes/dropwizard/less/mixins.less    |  537 ++++++++
 docs/source/_themes/dropwizard/less/modals.less    |   72 ++
 docs/source/_themes/dropwizard/less/navbar.less    |  292 +++++
 docs/source/_themes/dropwizard/less/navs.less      |  343 +++++
 docs/source/_themes/dropwizard/less/pager.less     |   30 +
 .../source/_themes/dropwizard/less/pagination.less |   55 +
 docs/source/_themes/dropwizard/less/patterns.less  |   13 +
 docs/source/_themes/dropwizard/less/popovers.less  |   49 +
 docs/source/_themes/dropwizard/less/print.less     |   18 +
 .../_themes/dropwizard/less/progress-bars.less     |   95 ++
 docs/source/_themes/dropwizard/less/reset.less     |  126 ++
 .../source/_themes/dropwizard/less/responsive.less |  323 +++++
 .../_themes/dropwizard/less/scaffolding.less       |   29 +
 docs/source/_themes/dropwizard/less/sprites.less   |  156 +++
 docs/source/_themes/dropwizard/less/tables.less    |  139 ++
 .../source/_themes/dropwizard/less/thumbnails.less |   35 +
 docs/source/_themes/dropwizard/less/tooltip.less   |   35 +
 docs/source/_themes/dropwizard/less/type.less      |  217 ++++
 docs/source/_themes/dropwizard/less/utilities.less |   23 +
 docs/source/_themes/dropwizard/less/variables.less |   99 ++
 docs/source/_themes/dropwizard/less/wells.less     |   17 +
 docs/source/_themes/dropwizard/page.html           |   13 +
 docs/source/_themes/dropwizard/search.html         |   56 +
 .../_themes/dropwizard/static/dropwizard.css       |  305 +++++
 docs/source/_themes/dropwizard/theme.conf          |   16 +
 docs/source/about/contributors.rst                 |   57 +
 docs/source/about/faq.rst                          |   26 +
 docs/source/about/index.rst                        |   14 +
 docs/source/about/release-notes.rst                |  513 ++++++++
 docs/source/about/todos.rst                        |    7 +
 docs/source/conf.py                                |  300 +++++
 docs/source/dropwizard-logo.png                    |  Bin 0 -> 961 bytes
 docs/source/getting-started.rst                    |  791 ++++++++++++
 docs/source/index.rst                              |   27 +
 docs/source/manual/auth.rst                        |  131 ++
 docs/source/manual/client.rst                      |  152 +++
 docs/source/manual/configuration.rst               |  808 ++++++++++++
 docs/source/manual/core.rst                        | 1342 ++++++++++++++++++++
 docs/source/manual/example.rst                     |   30 +
 docs/source/manual/hibernate.rst                   |  168 +++
 docs/source/manual/index.rst                       |   25 +
 docs/source/manual/jdbi.rst                        |  152 +++
 docs/source/manual/migrations.rst                  |  220 ++++
 docs/source/manual/scala.rst                       |    9 +
 docs/source/manual/testing.rst                     |  239 ++++
 docs/source/manual/views.rst                       |   98 ++
 dropwizard-assets/pom.xml                          |   26 +
 .../java/io/dropwizard/assets/AssetsBundle.java    |  115 ++
 .../io/dropwizard/assets/AssetsBundleTest.java     |  151 +++
 .../src/test/resources/assets/git-turd.txt         |    0
 .../src/test/resources/json/git-turd.txt           |    0
 dropwizard-auth/pom.xml                            |   45 +
 .../src/main/java/io/dropwizard/auth/Auth.java     |   20 +
 .../dropwizard/auth/AuthenticationException.java   |   23 +
 .../java/io/dropwizard/auth/Authenticator.java     |   26 +
 .../io/dropwizard/auth/CachingAuthenticator.java   |  126 ++
 .../dropwizard/auth/basic/BasicAuthProvider.java   |  117 ++
 .../io/dropwizard/auth/basic/BasicCredentials.java |   70 +
 .../io/dropwizard/auth/oauth/OAuthProvider.java    |  104 ++
 .../dropwizard/auth/CachingAuthenticatorTest.java  |  112 ++
 .../auth/basic/BasicAuthProviderTest.java          |  111 ++
 .../auth/basic/BasicCredentialsTest.java           |   61 +
 .../dropwizard/auth/oauth/OAuthProviderTest.java   |  109 ++
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-client/pom.xml                          |   63 +
 .../io/dropwizard/client/HttpClientBuilder.java    |  243 ++++
 .../dropwizard/client/HttpClientConfiguration.java |  137 ++
 .../io/dropwizard/client/JerseyClientBuilder.java  |  247 ++++
 .../client/JerseyClientConfiguration.java          |   83 ++
 .../dropwizard/client/HttpClientBuilderTest.java   |  269 ++++
 .../dropwizard/client/JerseyClientBuilderTest.java |  200 +++
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-configuration/pom.xml                   |   36 +
 .../configuration/ConfigurationException.java      |   54 +
 .../configuration/ConfigurationFactory.java        |  242 ++++
 .../configuration/ConfigurationFactoryFactory.java |   12 +
 .../ConfigurationParsingException.java             |  377 ++++++
 .../configuration/ConfigurationSourceProvider.java |   21 +
 .../ConfigurationValidationException.java          |   36 +
 .../DefaultConfigurationFactoryFactory.java        |   16 +
 .../FileConfigurationSourceProvider.java           |   19 +
 .../UrlConfigurationSourceProvider.java            |   16 +
 .../ConfigurationFactoryFactoryTest.java           |   36 +
 .../configuration/ConfigurationFactoryTest.java    |  272 ++++
 .../ConfigurationValidationExceptionTest.java      |   48 +
 .../FileConfigurationSourceProviderTest.java       |   22 +
 .../UrlConfigurationSourceProviderTest.java        |   22 +
 .../src/test/resources/example.txt                 |    1 +
 .../src/test/resources/factory-test-invalid.yml    |    1 +
 .../src/test/resources/factory-test-malformed.yml  |    1 +
 .../src/test/resources/factory-test-valid.yml      |   11 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-core/pom.xml                            |  163 +++
 .../src/main/java/io/dropwizard/Application.java   |   77 ++
 .../src/main/java/io/dropwizard/Bundle.java        |   23 +
 .../src/main/java/io/dropwizard/Configuration.java |  127 ++
 .../main/java/io/dropwizard/ConfiguredBundle.java  |   28 +
 .../main/java/io/dropwizard/cli/CheckCommand.java  |   40 +
 .../src/main/java/io/dropwizard/cli/Cli.java       |  150 +++
 .../src/main/java/io/dropwizard/cli/Command.java   |   58 +
 .../java/io/dropwizard/cli/ConfiguredCommand.java  |  118 ++
 .../java/io/dropwizard/cli/EnvironmentCommand.java |   55 +
 .../main/java/io/dropwizard/cli/ServerCommand.java |   58 +
 .../dropwizard/errors/EarlyEofExceptionMapper.java |   29 +
 .../dropwizard/server/AbstractServerFactory.java   |  542 ++++++++
 .../io/dropwizard/server/DefaultServerFactory.java |  196 +++
 .../java/io/dropwizard/server/ServerFactory.java   |   22 +
 .../io/dropwizard/server/SimpleServerFactory.java  |  133 ++
 .../java/io/dropwizard/setup/AdminEnvironment.java |   88 ++
 .../main/java/io/dropwizard/setup/Bootstrap.java   |  208 +++
 .../main/java/io/dropwizard/setup/Environment.java |  165 +++
 .../services/io.dropwizard.jackson.Discoverable    |    1 +
 .../services/io.dropwizard.server.ServerFactory    |    2 +
 .../test/java/io/dropwizard/ApplicationTest.java   |   58 +
 .../test/java/io/dropwizard/ConfigurationTest.java |   45 +
 .../java/io/dropwizard/cli/CheckCommandTest.java   |   51 +
 .../src/test/java/io/dropwizard/cli/CliTest.java   |  280 ++++
 .../io/dropwizard/cli/ConfiguredCommandTest.java   |   67 +
 .../java/io/dropwizard/cli/ServerCommandTest.java  |   99 ++
 .../errors/EarlyEofExceptionMapperTest.java        |   18 +
 .../server/DefaultServerFactoryTest.java           |  182 +++
 .../dropwizard/server/SimpleServerFactoryTest.java |   14 +
 .../io/dropwizard/setup/AdminEnvironmentTest.java  |   38 +
 .../java/io/dropwizard/setup/BootstrapTest.java    |   74 ++
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-core/src/test/resources/yaml/server.yml |   22 +
 dropwizard-db/pom.xml                              |   32 +
 .../java/io/dropwizard/db/DataSourceFactory.java   |  723 +++++++++++
 .../io/dropwizard/db/DatabaseConfiguration.java    |    7 +
 .../java/io/dropwizard/db/ManagedDataSource.java   |    9 +
 .../io/dropwizard/db/ManagedPooledDataSource.java  |   61 +
 .../io/dropwizard/db/DataSourceFactoryTest.java    |   49 +
 .../dropwizard/db/ManagedPooledDataSourceTest.java |   26 +
 dropwizard-db/src/test/resources/logback-test.xml  |   11 +
 dropwizard-example/README.md                       |   51 +
 dropwizard-example/example.keystore                |  Bin 0 -> 1338 bytes
 dropwizard-example/example.yml                     |   68 +
 dropwizard-example/pom.xml                         |  192 +++
 .../example/helloworld/HelloWorldApplication.java  |   68 +
 .../helloworld/HelloWorldConfiguration.java        |   56 +
 .../helloworld/auth/ExampleAuthenticator.java      |   17 +
 .../com/example/helloworld/cli/RenderCommand.java  |   48 +
 .../java/com/example/helloworld/core/Person.java   |   51 +
 .../java/com/example/helloworld/core/Saying.java   |   30 +
 .../java/com/example/helloworld/core/Template.java |   19 +
 .../java/com/example/helloworld/core/User.java     |   13 +
 .../java/com/example/helloworld/db/PersonDAO.java  |   26 +
 .../helloworld/health/TemplateHealthCheck.java     |   20 +
 .../helloworld/resources/HelloWorldResource.java   |   41 +
 .../helloworld/resources/PeopleResource.java       |   36 +
 .../helloworld/resources/PersonResource.java       |   56 +
 .../helloworld/resources/ProtectedResource.java    |   18 +
 .../example/helloworld/resources/ViewResource.java |   43 +
 .../com/example/helloworld/views/PersonView.java   |   31 +
 .../src/main/resources/assets/example.txt          |    1 +
 dropwizard-example/src/main/resources/banner.txt   |    8 +
 .../example/helloworld/views/freemarker/person.ftl |    7 +
 .../helloworld/views/mustache/person.mustache      |    5 +
 .../src/main/resources/migrations.xml              |   20 +
 .../src/main/resources/views/ftl/iso88591.ftl      |   10 +
 .../src/main/resources/views/ftl/utf8.ftl          |    9 +
 .../resources/views/mustache/iso88591.mustache     |   10 +
 .../main/resources/views/mustache/utf8.mustache    |    9 +
 dropwizard-forms/pom.xml                           |   22 +
 dropwizard-hibernate/pom.xml                       |   87 ++
 .../java/io/dropwizard/hibernate/AbstractDAO.java  |  168 +++
 .../io/dropwizard/hibernate/HibernateBundle.java   |   51 +
 .../hibernate/ScanningHibernateBundle.java         |   52 +
 .../hibernate/SessionFactoryFactory.java           |   97 ++
 .../hibernate/SessionFactoryHealthCheck.java       |   45 +
 .../hibernate/SessionFactoryManager.java           |   25 +
 .../java/io/dropwizard/hibernate/UnitOfWork.java   |   50 +
 .../hibernate/UnitOfWorkRequestDispatcher.java     |   89 ++
 .../UnitOfWorkResourceMethodDispatchAdapter.java   |   25 +
 .../UnitOfWorkResourceMethodDispatchProvider.java  |   35 +
 .../io/dropwizard/hibernate/AbstractDAOTest.java   |  182 +++
 .../dropwizard/hibernate/HibernateBundleTest.java  |  103 ++
 .../hibernate/JerseyIntegrationTest.java           |  194 +++
 .../test/java/io/dropwizard/hibernate/Person.java  |   52 +
 .../hibernate/ScanningHibernateBundleTest.java     |   23 +
 .../hibernate/SessionFactoryFactoryTest.java       |   99 ++
 .../hibernate/SessionFactoryHealthCheckTest.java   |   79 ++
 .../hibernate/SessionFactoryManagerTest.java       |   28 +
 .../hibernate/UnitOfWorkRequestDispatcherTest.java |  209 +++
 ...nitOfWorkResourceMethodDispatchAdapterTest.java |   34 +
 ...itOfWorkResourceMethodDispatchProviderTest.java |   65 +
 .../io/dropwizard/hibernate/UnitOfWorkTest.java    |   49 +
 .../hibernate/fake/entities/FakeEntity1.java       |    8 +
 .../hibernate/fake/entities/pckg/FakeEntity2.java  |    8 +
 .../fake/entities/pckg/deep/FakeEntity1.java       |    8 +
 .../entities/pckg/deep/deeper/FakeEntity1.java     |    9 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-jackson/pom.xml                         |   94 ++
 .../AnnotationSensitivePropertyNamingStrategy.java |   65 +
 .../java/io/dropwizard/jackson/Discoverable.java   |    8 +
 .../jackson/DiscoverableSubtypeResolver.java       |   77 ++
 .../io/dropwizard/jackson/FuzzyEnumModule.java     |   80 ++
 .../io/dropwizard/jackson/GuavaExtrasModule.java   |   64 +
 .../main/java/io/dropwizard/jackson/Jackson.java   |   30 +
 .../java/io/dropwizard/jackson/JsonSnakeCase.java  |   20 +
 .../java/io/dropwizard/jackson/LogbackModule.java  |   79 ++
 ...otationSensitivePropertyNamingStrategyTest.java |   68 +
 .../jackson/DiscoverableSubtypeResolverTest.java   |   26 +
 .../java/io/dropwizard/jackson/ExampleSPI.java     |    7 +
 .../java/io/dropwizard/jackson/ExampleTag.java     |    4 +
 .../io/dropwizard/jackson/FuzzyEnumModuleTest.java |   77 ++
 .../dropwizard/jackson/GuavaExtrasModuleTest.java  |   46 +
 .../src/test/java/io/dropwizard/jackson/ImplA.java |    7 +
 .../src/test/java/io/dropwizard/jackson/ImplB.java |    7 +
 .../io/dropwizard/jackson/LogbackModuleTest.java   |   35 +
 .../services/io.dropwizard.jackson.ExampleSPI      |    2 +
 .../services/io.dropwizard.jackson.ExampleTag      |    1 +
 dropwizard-jdbi/pom.xml                            |   47 +
 .../main/java/io/dropwizard/jdbi/DBIFactory.java   |   72 ++
 .../java/io/dropwizard/jdbi/DBIHealthCheck.java    |   23 +
 .../jdbi/ImmutableListContainerFactory.java        |   32 +
 .../jdbi/ImmutableSetContainerFactory.java         |   33 +
 .../jdbi/NamePrependingStatementRewriter.java      |   31 +
 .../dropwizard/jdbi/OptionalContainerFactory.java  |   34 +
 .../dropwizard/jdbi/args/JodaDateTimeArgument.java |   33 +
 .../jdbi/args/JodaDateTimeArgumentFactory.java     |   26 +
 .../dropwizard/jdbi/args/JodaDateTimeMapper.java   |   32 +
 .../jdbi/args/OptionalArgumentFactory.java         |   65 +
 .../jdbi/bundles/DBIExceptionsBundle.java          |   23 +
 .../jdbi/jersey/LoggingDBIExceptionMapper.java     |   29 +
 .../jdbi/jersey/LoggingSQLExceptionMapper.java     |   24 +
 .../io/dropwizard/jdbi/logging/LogbackLog.java     |   55 +
 .../src/test/java/io/dropwizard/jdbi/JDBITest.java |  181 +++
 .../test/java/io/dropwizard/jdbi/PersonDAO.java    |   34 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-jersey/pom.xml                          |   93 ++
 .../jersey/DropwizardResourceConfig.java           |  205 +++
 .../src/main/java/io/dropwizard/jersey/PATCH.java  |   12 +
 .../io/dropwizard/jersey/caching/CacheControl.java |  192 +++
 ...cheControlledResourceMethodDispatchAdapter.java |   70 +
 .../io/dropwizard/jersey/errors/ErrorMessage.java  |   13 +
 .../jersey/errors/LoggingExceptionMapper.java      |   40 +
 .../jersey/filter/AllowedMethodsFilter.java        |   56 +
 .../OptionalQueryParamInjectableProvider.java      |  123 ++
 .../OptionalResourceMethodDispatchAdapter.java     |   54 +
 .../jersey/jackson/JacksonMessageBodyProvider.java |  139 ++
 .../jackson/JsonProcessingExceptionMapper.java     |   46 +
 .../io/dropwizard/jersey/params/AbstractParam.java |  115 ++
 .../io/dropwizard/jersey/params/BooleanParam.java  |   29 +
 .../io/dropwizard/jersey/params/DateTimeParam.java |   19 +
 .../java/io/dropwizard/jersey/params/IntParam.java |   21 +
 .../io/dropwizard/jersey/params/LongParam.java     |   21 +
 .../io/dropwizard/jersey/params/UUIDParam.java     |   25 +
 .../java/io/dropwizard/jersey/sessions/Flash.java  |   28 +
 .../dropwizard/jersey/sessions/FlashProvider.java  |   47 +
 .../jersey/sessions/HttpSessionProvider.java       |   43 +
 .../io/dropwizard/jersey/sessions/Session.java     |   10 +
 .../jersey/setup/JerseyContainerHolder.java        |   19 +
 .../dropwizard/jersey/setup/JerseyEnvironment.java |  113 ++
 .../ConstraintViolationExceptionMapper.java        |   20 +
 .../jersey/validation/ValidationErrorMessage.java  |   21 +
 .../jersey/DropwizardResourceConfigTest.java       |   46 +
 ...ontrolledResourceMethodDispatchAdapterTest.java |   95 ++
 .../dropwizard/jersey/caching/CachingResource.java |   74 ++
 .../io/dropwizard/jersey/dummy/DummyResource.java  |   12 +
 .../errors/DefaultJacksonMessageBodyProvider.java  |   14 +
 .../errors/DefaultLoggingExceptionMapper.java      |    7 +
 .../jersey/errors/ExceptionResource.java           |   16 +
 .../jersey/errors/LoggingExceptionMapperTest.java  |   38 +
 .../jersey/filter/AllowedMethodsFilterTest.java    |  106 ++
 .../io/dropwizard/jersey/filter/DummyResource.java |   34 +
 .../jersey/guava/OptionalParamResource.java        |   18 +
 .../OptionalQueryParamInjectableProviderTest.java  |   35 +
 .../OptionalResourceMethodDispatchAdapterTest.java |   41 +
 .../jersey/guava/OptionalReturnResource.java       |   18 +
 .../jersey/jackson/BrokenRepresentation.java       |   23 +
 .../jackson/DefaultJacksonMessageBodyProvider.java |   13 +
 .../jackson/JacksonMessageBodyProviderTest.java    |  576 +++++++++
 .../jackson/JsonProcessingExceptionMapperTest.java |   51 +
 .../io/dropwizard/jersey/jackson/JsonResource.java |   28 +
 .../jersey/jackson/OkRepresentation.java           |   17 +
 .../dropwizard/jersey/params/BooleanParamTest.java |   77 ++
 .../jersey/params/DateTimeParamTest.java           |   17 +
 .../io/dropwizard/jersey/params/IntParamTest.java  |   36 +
 .../io/dropwizard/jersey/params/LongParamTest.java |   36 +
 .../io/dropwizard/jersey/params/UUIDParamTest.java |   40 +
 .../jersey/sessions/FlashProviderTest.java         |   59 +
 .../dropwizard/jersey/sessions/FlashResource.java  |   25 +
 .../jersey/sessions/HttpSessionProviderTest.java   |   46 +
 .../jersey/sessions/SessionResource.java           |   22 +
 .../ConstraintViolationExceptionMapperTest.java    |   43 +
 .../DefaultJacksonMessageBodyProvider.java         |   15 +
 .../jersey/validation/ValidRepresentation.java     |   19 +
 .../jersey/validation/ValidatingResource.java      |   20 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-jetty/pom.xml                           |   63 +
 .../java/io/dropwizard/jetty/BiDiGzipFilter.java   |  238 ++++
 .../java/io/dropwizard/jetty/ConnectorFactory.java |   28 +
 .../io/dropwizard/jetty/ContextRoutingHandler.java |   41 +
 .../io/dropwizard/jetty/GzipFilterFactory.java     |  168 +++
 .../io/dropwizard/jetty/HttpConnectorFactory.java  |  500 ++++++++
 .../io/dropwizard/jetty/HttpsConnectorFactory.java |  675 ++++++++++
 .../jetty/MutableServletContextHandler.java        |   29 +
 .../dropwizard/jetty/NonblockingServletHolder.java |   54 +
 .../io/dropwizard/jetty/RequestLogFactory.java     |  103 ++
 .../java/io/dropwizard/jetty/RoutingHandler.java   |   57 +
 .../java/io/dropwizard/jetty/Slf4jRequestLog.java  |   66 +
 .../dropwizard/jetty/setup/ServletEnvironment.java |  151 +++
 .../services/io.dropwizard.jackson.Discoverable    |    1 +
 .../services/io.dropwizard.jetty.ConnectorFactory  |    2 +
 .../jetty/ContextRoutingHandlerTest.java           |   72 ++
 .../io/dropwizard/jetty/GzipFilterFactoryTest.java |   63 +
 .../dropwizard/jetty/HttpConnectorFactoryTest.java |   14 +
 .../jetty/HttpsConnectorFactoryTest.java           |   87 ++
 .../jetty/MutableServletContextHandlerTest.java    |   67 +
 .../jetty/NonblockingServletHolderTest.java        |   51 +
 .../io/dropwizard/jetty/RequestLogFactoryTest.java |   40 +
 .../io/dropwizard/jetty/RoutingHandlerTest.java    |   62 +
 .../io/dropwizard/jetty/Slf4jRequestLogTest.java   |   78 ++
 .../jetty/setup/ServletEnvironmentTest.java        |  159 +++
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-jetty/src/test/resources/yaml/gzip.yml  |    5 +
 .../src/test/resources/yaml/requestLog.yml         |    5 +
 dropwizard-lifecycle/pom.xml                       |   42 +
 .../lifecycle/ExecutorServiceManager.java          |   34 +
 .../java/io/dropwizard/lifecycle/JettyManaged.java |   39 +
 .../main/java/io/dropwizard/lifecycle/Managed.java |   21 +
 .../lifecycle/ServerLifecycleListener.java         |    9 +
 .../lifecycle/setup/ExecutorServiceBuilder.java    |   78 ++
 .../lifecycle/setup/LifecycleEnvironment.java      |   94 ++
 .../setup/ScheduledExecutorServiceBuilder.java     |   51 +
 .../io/dropwizard/lifecycle/JettyManagedTest.java  |   22 +
 .../lifecycle/setup/LifecycleEnvironmentTest.java  |   45 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-logging/pom.xml                         |   82 ++
 .../logging/AbstractAppenderFactory.java           |  148 +++
 .../io/dropwizard/logging/AppenderFactory.java     |   38 +
 .../java/io/dropwizard/logging/AsyncAppender.java  |  117 ++
 .../dropwizard/logging/ConsoleAppenderFactory.java |  116 ++
 .../io/dropwizard/logging/DropwizardLayout.java    |   26 +
 .../io/dropwizard/logging/FileAppenderFactory.java |  197 +++
 .../java/io/dropwizard/logging/LoggingFactory.java |  163 +++
 .../PrefixedExtendedThrowableProxyConverter.java   |   17 +
 ...fixedRootCauseFirstThrowableProxyConverter.java |   24 +
 .../logging/PrefixedThrowableProxyConverter.java   |   20 +
 .../dropwizard/logging/SyslogAppenderFactory.java  |  217 ++++
 .../services/io.dropwizard.jackson.Discoverable    |    1 +
 .../services/io.dropwizard.logging.AppenderFactory |    3 +
 .../io/dropwizard/logging/AsyncAppenderTest.java   |   34 +
 .../logging/ConsoleAppenderFactoryTest.java        |   14 +
 .../dropwizard/logging/DropwizardLayoutTest.java   |   39 +
 .../logging/FileAppenderFactoryTest.java           |   34 +
 .../io/dropwizard/logging/LoggingFactoryTest.java  |   45 +
 ...refixedExtendedThrowableProxyConverterTest.java |   28 +
 ...dRootCauseFirstThrowableProxyConverterTest.java |   86 ++
 .../PrefixedThrowableProxyConverterTest.java       |   28 +
 .../logging/SyslogAppenderFactoryTest.java         |   60 +
 .../src/test/resources/logback-test.xml            |   11 +
 .../src/test/resources/yaml/logging.yml            |   14 +
 dropwizard-metrics-ganglia/pom.xml                 |   32 +
 .../metrics/ganglia/GangliaReporterFactory.java    |  211 +++
 .../services/io.dropwizard.metrics.ReporterFactory |    1 +
 .../ganglia/GangliaReporterFactoryTest.java        |   13 +
 dropwizard-metrics-graphite/pom.xml                |   32 +
 .../metrics/graphite/GraphiteReporterFactory.java  |   94 ++
 .../services/io.dropwizard.metrics.ReporterFactory |    1 +
 .../graphite/GraphiteReporterFactoryTest.java      |   14 +
 dropwizard-metrics/pom.xml                         |   59 +
 .../metrics/BaseFormattedReporterFactory.java      |   43 +
 .../io/dropwizard/metrics/BaseReporterFactory.java |  150 +++
 .../dropwizard/metrics/ConsoleReporterFactory.java |   95 ++
 .../io/dropwizard/metrics/CsvReporterFactory.java  |   70 +
 .../java/io/dropwizard/metrics/MetricsFactory.java |   94 ++
 .../io/dropwizard/metrics/ReporterFactory.java     |   42 +
 .../metrics/ScheduledReporterManager.java          |   44 +
 .../dropwizard/metrics/Slf4jReporterFactory.java   |   81 ++
 .../services/io.dropwizard.jackson.Discoverable    |    1 +
 .../services/io.dropwizard.metrics.ReporterFactory |    3 +
 .../metrics/ConsoleReporterFactoryTest.java        |   14 +
 .../dropwizard/metrics/CsvReporterFactoryTest.java |   47 +
 .../io/dropwizard/metrics/MetricsFactoryTest.java  |   50 +
 .../metrics/Slf4jReporterFactoryTest.java          |   14 +
 .../src/test/resources/yaml/metrics.yml            |   11 +
 dropwizard-migrations/pom.xml                      |   41 +
 .../migrations/AbstractLiquibaseCommand.java       |   99 ++
 .../dropwizard/migrations/CloseableLiquibase.java  |   34 +
 .../migrations/DbCalculateChecksumCommand.java     |   35 +
 .../migrations/DbClearChecksumsCommand.java        |   21 +
 .../java/io/dropwizard/migrations/DbCommand.java   |   54 +
 .../io/dropwizard/migrations/DbDropAllCommand.java |   28 +
 .../io/dropwizard/migrations/DbDumpCommand.java    |  215 ++++
 .../migrations/DbFastForwardCommand.java           |   72 ++
 .../migrations/DbGenerateDocsCommand.java          |   25 +
 .../io/dropwizard/migrations/DbLocksCommand.java   |   46 +
 .../io/dropwizard/migrations/DbMigrateCommand.java |   69 +
 .../migrations/DbPrepareRollbackCommand.java       |   54 +
 .../dropwizard/migrations/DbRollbackCommand.java   |   92 ++
 .../io/dropwizard/migrations/DbStatusCommand.java  |   49 +
 .../io/dropwizard/migrations/DbTagCommand.java     |   25 +
 .../io/dropwizard/migrations/DbTestCommand.java    |   40 +
 .../io/dropwizard/migrations/MigrationsBundle.java |   21 +
 dropwizard-servlets/pom.xml                        |   76 ++
 .../io/dropwizard/servlets/CacheBustingFilter.java |   31 +
 .../main/java/io/dropwizard/servlets/Servlets.java |   24 +
 .../io/dropwizard/servlets/SlowRequestFilter.java  |   68 +
 .../io/dropwizard/servlets/ThreadNameFilter.java   |   38 +
 .../dropwizard/servlets/assets/AssetServlet.java   |  175 +++
 .../servlets/assets/ResourceNotFoundException.java |    9 +
 .../io/dropwizard/servlets/assets/ResourceURL.java |  118 ++
 .../servlets/tasks/GarbageCollectionTask.java      |   58 +
 .../java/io/dropwizard/servlets/tasks/Task.java    |   42 +
 .../io/dropwizard/servlets/tasks/TaskServlet.java  |  224 ++++
 .../servlets/CacheBustingFilterTest.java           |   39 +
 .../java/io/dropwizard/servlets/ServletsTest.java  |   34 +
 .../servlets/assets/AssetServletTest.java          |  306 +++++
 .../servlets/assets/ResourceURLTest.java           |  147 +++
 .../servlets/tasks/GarbageCollectionTaskTest.java  |   36 +
 .../dropwizard/servlets/tasks/TaskServletTest.java |  106 ++
 .../io/dropwizard/servlets/tasks/TaskTest.java     |   24 +
 .../src/test/more-resources/assets/example2.txt    |    1 +
 .../src/test/resources/assets/encoded example.txt  |    1 +
 .../src/test/resources/assets/example.txt          |    1 +
 .../src/test/resources/assets/foo.bar              |    1 +
 .../src/test/resources/assets/index.htm            |    5 +
 .../resources/assets/some_directory/example.txt    |    1 +
 .../test/resources/assets/some_directory/index.htm |    5 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-spdy/pom.xml                            |   34 +
 .../dropwizard/spdy/NonePushStrategyFactory.java   |   17 +
 .../io/dropwizard/spdy/PushStrategyFactory.java    |   13 +
 .../spdy/ReferrerPushStrategyFactory.java          |  176 +++
 .../io/dropwizard/spdy/Spdy3ConnectorFactory.java  |  102 ++
 .../services/io.dropwizard.jackson.Discoverable    |    1 +
 .../services/io.dropwizard.jetty.ConnectorFactory  |    1 +
 .../io.dropwizard.spdy.PushStrategyFactory         |    2 +
 .../spdy/NonePushStrategyFactoryTest.java          |   23 +
 .../spdy/ReferrerPushStrategyFactoryTest.java      |   14 +
 .../dropwizard/spdy/Spdy3ConnectorFactoryTest.java |   14 +
 dropwizard-testing/pom.xml                         |   73 ++
 .../java/io/dropwizard/testing/FixtureHelpers.java |   39 +
 .../dropwizard/testing/junit/ConfigOverride.java   |   20 +
 .../testing/junit/DropwizardAppRule.java           |  141 ++
 .../dropwizard/testing/junit/ResourceTestRule.java |  159 +++
 .../io/dropwizard/testing/FixtureHelpersTest.java  |   14 +
 .../test/java/io/dropwizard/testing/Person.java    |   53 +
 .../testing/app/GzipDefaultVaryBehaviourTest.java  |   35 +
 .../io/dropwizard/testing/app/PeopleStore.java     |    7 +
 .../io/dropwizard/testing/app/PersonResource.java  |   26 +
 .../dropwizard/testing/app/PersonResourceTest.java |   44 +
 .../testing/junit/DropwizardAppRuleTest.java       |  127 ++
 .../junit/DropwizardAppRuleWithoutConfigTest.java  |   52 +
 .../DropwizardServiceRuleConfigOverrideTest.java   |   27 +
 .../dropwizard/testing/junit/TestApplication.java  |   17 +
 .../testing/junit/TestConfiguration.java           |   16 +
 .../io/dropwizard/testing/junit/TestResource.java  |   20 +
 .../src/test/resources/fixtures/fixture.txt        |    1 +
 .../src/test/resources/fixtures/person.json        |    4 +
 .../src/test/resources/logback-test.xml            |   11 +
 .../src/test/resources/test-config.yaml            |   12 +
 dropwizard-util/pom.xml                            |   36 +
 .../src/main/java/io/dropwizard/util/Duration.java |  152 +++
 .../src/main/java/io/dropwizard/util/Generics.java |   76 ++
 .../main/java/io/dropwizard/util/JarLocation.java  |   40 +
 .../src/main/java/io/dropwizard/util/Size.java     |  132 ++
 .../src/main/java/io/dropwizard/util/SizeUnit.java |   98 ++
 .../test/java/io/dropwizard/util/DurationTest.java |  179 +++
 .../java/io/dropwizard/util/JarLocationTest.java   |   20 +
 .../src/test/java/io/dropwizard/util/SizeTest.java |  151 +++
 .../test/java/io/dropwizard/util/SizeUnitTest.java |  243 ++++
 dropwizard-validation/pom.xml                      |   31 +
 .../validation/ConstraintViolations.java           |   55 +
 .../io/dropwizard/validation/DurationRange.java    |   54 +
 .../java/io/dropwizard/validation/MaxDuration.java |   39 +
 .../validation/MaxDurationValidator.java           |   28 +
 .../java/io/dropwizard/validation/MaxSize.java     |   40 +
 .../io/dropwizard/validation/MaxSizeValidator.java |   28 +
 .../io/dropwizard/validation/MethodValidator.java  |   19 +
 .../java/io/dropwizard/validation/MinDuration.java |   39 +
 .../validation/MinDurationValidator.java           |   28 +
 .../java/io/dropwizard/validation/MinSize.java     |   40 +
 .../io/dropwizard/validation/MinSizeValidator.java |   28 +
 .../main/java/io/dropwizard/validation/OneOf.java  |   40 +
 .../io/dropwizard/validation/OneOfValidator.java   |   39 +
 .../java/io/dropwizard/validation/PortRange.java   |   31 +
 .../dropwizard/validation/PortRangeValidator.java  |   24 +
 .../java/io/dropwizard/validation/SizeRange.java   |   55 +
 .../java/io/dropwizard/validation/Validated.java   |   22 +
 .../io/dropwizard/validation/ValidationMethod.java |   26 +
 .../OptionalValidatedValueUnwrapper.java           |   29 +
 .../validation/DurationValidatorTest.java          |   63 +
 .../dropwizard/validation/MethodValidatorTest.java |   47 +
 .../dropwizard/validation/OneOfValidatorTest.java  |   62 +
 .../validation/PortRangeValidatorTest.java         |   72 ++
 .../dropwizard/validation/SizeValidatorTest.java   |   58 +
 .../OptionalValidatedValueUnwrapperTest.java       |   84 ++
 dropwizard-views-freemarker/pom.xml                |   50 +
 .../views/freemarker/FreemarkerViewRenderer.java   |   65 +
 .../services/io.dropwizard.views.ViewRenderer      |    1 +
 .../dropwizard/views/freemarker/AbsoluteView.java  |   16 +
 .../io/dropwizard/views/freemarker/BadView.java    |    9 +
 .../freemarker/FreemarkerViewRendererTest.java     |   84 ++
 .../dropwizard/views/freemarker/RelativeView.java  |   10 +
 .../src/test/resources/example.ftl                 |    2 +
 .../io/dropwizard/views/freemarker/relative.ftl    |    2 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-views-mustache/pom.xml                  |   56 +
 .../views/mustache/MustacheViewRenderer.java       |   57 +
 .../views/mustache/PerClassMustacheFactory.java    |   31 +
 .../services/io.dropwizard.views.ViewRenderer      |    1 +
 .../io/dropwizard/views/mustache/AbsoluteView.java |   16 +
 .../java/io/dropwizard/views/mustache/BadView.java |    9 +
 .../views/mustache/MustacheViewRendererTest.java   |   84 ++
 .../io/dropwizard/views/mustache/RelativeView.java |    9 +
 .../src/test/resources/example.mustache            |    1 +
 .../io/dropwizard/views/mustache/relative.mustache |    1 +
 .../src/test/resources/logback-test.xml            |   11 +
 dropwizard-views/pom.xml                           |   21 +
 .../src/main/java/io/dropwizard/views/View.java    |   62 +
 .../main/java/io/dropwizard/views/ViewBundle.java  |  103 ++
 .../io/dropwizard/views/ViewMessageBodyWriter.java |  101 ++
 .../io/dropwizard/views/ViewRenderException.java   |   20 +
 .../java/io/dropwizard/views/ViewRenderer.java     |   33 +
 .../java/io/dropwizard/views/ViewBundleTest.java   |   66 +
 .../test/java/io/dropwizard/views/ViewTest.java    |   15 +
 .../src/test/resources/logback-test.xml            |   11 +
 findbugs-exclude.xml                               |   32 +
 pom.xml                                            |  480 +++++++
 552 files changed, 39906 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7849106
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.idea
+target
+atlassian-ide-plugin.xml
+dependency-reduced-pom.xml
+logs
+*.iml
+*.ipr
+#Eclipse-specific
+.settings/
+.classpath
+.project
+nb-configuration.xml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..d4ed563
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,10 @@
+language: java
+
+install: echo "I trust Maven."
+
+# don't just run the tests, also run Findbugs and friends
+script: mvn verify
+
+jdk:
+  - oraclejdk7
+  - oraclejdk8
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..fb2406f
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,53 @@
+Contributing/Development
+===
+Dropwizard is always looking for people to contribute to the project. We welcome your
+feedback and want to listen and discuss your ideas and issues.
+
+There are many different ways to help contribute to the Dropwizard project.
+
+* Helping others by participating in the [Dropwizard User Google Group](https://groups.google.com/forum/#!forum/dropwizard-user)
+* Improving or enhancing our [documentation](http://dropwizard.github.io/dropwizard/)
+* Fixing open issues listed in the [issue tracker](https://github.com/dropwizard/dropwizard/issues?state=open)
+* Adding new features to the Dropwizard codebase
+
+Guidelines
+===
+When submitting a pull request, please make sure to fork the repository and create a
+separate branch for your feature or fix for an issue.
+
+All contributions are welcome to be submitted for review for inclusion, but before
+they will be accepted, we ask that you follow these simple guidelines:
+
+Code style
+---
+When submitting code, please make every effort to follow existing conventions and
+style in order to keep the code as readable as possible. We realize that the style
+used in Dropwizard might be different that what is used in your projects, but in the end
+ it makes it easier to merge changes and maintain in the future.
+
+Testing
+---
+We kindly ask that all new features and fixes for an issue should include any unit tests.
+Even if it is small improvement, adding a unit test will help to ensure no regressions or the
+issue is not re-introduced. If you need help with writing a test for your feature, please
+don't be shy and ask!
+
+Documentation
+---
+Up-to-date documentation makes all our lives easier. If you are adding a new feature,
+enhancing an existing feature, or fixing an issue, please add or modify the documentation
+as needed and include it with your pull request.
+
+New Features
+===
+If you would like to implement a new feature, please raise an issue before sending a
+pull request so the feature can be discussed. **We appreciate the effort and want
+to avoid a situation where a contribution requires extensive rework on either side,
+it sits in the queue for a long time, or cannot be accepted at all.**
+
+Committers
+===
+The list of people with committer access is kept in the developer section of the pom.xml located in the parent directory.
+
+* Committers aren't allowed to merge their own changes, the exception being bug fixes
+* A commit may be reverted, but it requires 2+ committer's approval. The goal is to keep it democratic
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..84a9df3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2010-2013 Coda Hale and Yammer, Inc.
+
+   Licensed 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..59caf94
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,4 @@
+Dropwizard
+Copyright 2010-2013 Coda Hale and Yammer, Inc.
+
+This product includes software developed by Coda Hale and Yammer, Inc.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..be1b2b3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+Dropwizard [![Build Status](https://travis-ci.org/dropwizard/dropwizard.png?branch=master)](https://travis-ci.org/dropwizard/dropwizard)
+==========
+
+*Dropwizard is a sneaky way of making fast Java web applications.*
+
+It's a little bit of opinionated glue code which bangs together a set of libraries which have
+historically not sucked:
+
+* [Jetty](http://www.eclipse.org/jetty/) for HTTP servin'.
+* [Jersey](http://jersey.java.net/) for REST modelin'.
+* [Jackson](http://jackson.codehaus.org) for JSON parsin' and generatin'.
+* [Logback](http://logback.qos.ch/) for loggin'.
+* [Hibernate Validator](http://www.hibernate.org/subprojects/validator.html) for validatin'.
+* [Metrics](http://metrics.codahale.com) for figurin' out what your application is doin' in production.
+* [JDBI](http://www.jdbi.org) and [Hibernate](http://www.hibernate.org/) for databasin'.
+* [Liquibase](http://www.liquibase.org/) for migratin'.
+
+Read more at [dropwizard.io](http://www.dropwizard.io).
+
+Want to contribute to Dropwizard?
+---
+Before working on the code, if you plan to contribute changes, please read the following.
+the [CONTRIBUTING](CONTRIBUTING.md) document.
+
+Need help or found an issue?
+---
+When reporting an issue through the [issue tracker](https://github.com/dropwizard/dropwizard/issues?state=open)
+on GitHub or sending an email to the
+[Dropwizard User Google Group](https://groups.google.com/forum/#!forum/dropwizard-user)
+mailing list, please use the following guidelines:
+
+* Check existing issues to see if it has been addressed already
+* The version of Dropwizard you are using
+* A short description of the issue you are experiencing and the expected outcome
+* Description of how someone else can reproduce the problem
+* Paste error output or logs in your issue or in a Gist. If pasting them in the GitHub
+issue, wrap it in three backticks: ```  so that it renders nicely
+* Write a unit test to show the issue!
\ No newline at end of file
diff --git a/docs/Guardfile b/docs/Guardfile
new file mode 100644
index 0000000..c561391
--- /dev/null
+++ b/docs/Guardfile
@@ -0,0 +1,6 @@
+#!/usr/bin/env python
+from livereload.task import Task
+from livereload.compiler import shell
+
+# You may have a different path, e.g. _source/
+Task.add('source/', shell('make html'))
\ No newline at end of file
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..25f30d3
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,166 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+PAPER         =
+BUILDDIR      = target
+
+# Internal variables.
+PAPEROPT_a4     = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS  = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext livehtml
+
+help:
+	@echo "Please use \`make <target>' where <target> is one of"
+	@echo "  html       to make standalone HTML files"
+	@echo "  dirhtml    to make HTML files named index.html in directories"
+	@echo "  singlehtml to make a single large HTML file"
+	@echo "  pickle     to make pickle files"
+	@echo "  json       to make JSON files"
+	@echo "  htmlhelp   to make HTML files and a HTML help project"
+	@echo "  qthelp     to make HTML files and a qthelp project"
+	@echo "  devhelp    to make HTML files and a Devhelp project"
+	@echo "  epub       to make an epub"
+	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
+	@echo "  text       to make text files"
+	@echo "  man        to make manual pages"
+	@echo "  texinfo    to make Texinfo files"
+	@echo "  info       to make Texinfo files and run them through makeinfo"
+	@echo "  gettext    to make PO message catalogs"
+	@echo "  changes    to make an overview of all changed/added/deprecated items"
+	@echo "  linkcheck  to check all external links for integrity"
+	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"
+	@echo "  livehtml   build html and point livereload server at them"
+
+clean:
+	-rm -rf $(BUILDDIR)/*
+
+html: less
+	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml: less
+	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+	@echo
+	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+	@echo
+	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+	@echo
+	@echo "Build finished; now you can process the pickle files."
+
+json:
+	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+	@echo
+	@echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+	@echo
+	@echo "Build finished; now you can run HTML Help Workshop with the" \
+	      ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+	@echo
+	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
+	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Dropwizard.qhcp"
+	@echo "To view the help file:"
+	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Dropwizard.qhc"
+
+devhelp:
+	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+	@echo
+	@echo "Build finished."
+	@echo "To view the help file:"
+	@echo "# mkdir -p $$HOME/.local/share/devhelp/Dropwizard"
+	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Dropwizard"
+	@echo "# devhelp"
+
+epub:
+	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+	@echo
+	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo
+	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+	@echo "Run \`make' in that directory to run these through (pdf)latex" \
+	      "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+	@echo "Running LaTeX files through pdflatex..."
+	$(MAKE) -C $(BUILDDIR)/latex all-pdf
+	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+	@echo
+	@echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+	@echo
+	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo
+	@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+	@echo "Run \`make' in that directory to run these through makeinfo" \
+	      "(use \`make info' here to do that automatically)."
+
+info:
+	$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+	@echo "Running Texinfo files through makeinfo..."
+	make -C $(BUILDDIR)/texinfo info
+	@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+	$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+	@echo
+	@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+	@echo
+	@echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+	@echo
+	@echo "Link check complete; look for any errors in the above output " \
+	      "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+	@echo "Testing of doctests in the sources finished, look at the " \
+	      "results in $(BUILDDIR)/doctest/output.txt."
+
+# And now the target; depends on 'make html', since livereload will only build if things change.
+# We need to make sure the docs are current with any existing changes
+# See http://serialized.net/2013/01/live-sphinx-documentation-preview/ for more details
+livehtml: html
+	livereload -b $(BUILDDIR)/html	      	   
+
+less:
+	lessc --compress source/_themes/dropwizard/less/dropwizard.less > source/_themes/dropwizard/static/dropwizard.css
+
+upload: clean dirhtml
+	rsync -avz --delete --exclude=maven $(BUILDDIR)/dirhtml/ codahale.com:/home/codahale/dropwizard.io/
diff --git a/docs/dropwizard-hat.eps b/docs/dropwizard-hat.eps
new file mode 100644
index 0000000..cd6feb5
Binary files /dev/null and b/docs/dropwizard-hat.eps differ
diff --git a/docs/pom.xml b/docs/pom.xml
new file mode 100644
index 0000000..0d061e0
--- /dev/null
+++ b/docs/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>docs</artifactId>
+    <name>Dropwizard Documentation</name>
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <version>2.7</version>
+                <reportSets>
+                    <reportSet>
+                        <reports />
+                    </reportSet>
+                </reportSets>
+            </plugin>
+            <plugin>
+                <groupId>org.tomdz.maven</groupId>
+                <artifactId>sphinx-maven-plugin</artifactId>
+                <version>1.0.3</version>
+                <configuration>
+                    <sourceDirectory>${basedir}/source</sourceDirectory>
+                </configuration>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>
diff --git a/docs/source/_static/dropwizard-hat.png b/docs/source/_static/dropwizard-hat.png
new file mode 100644
index 0000000..8081ccb
Binary files /dev/null and b/docs/source/_static/dropwizard-hat.png differ
diff --git a/docs/source/_themes/dropwizard/genindex.html b/docs/source/_themes/dropwizard/genindex.html
new file mode 100644
index 0000000..7bc002b
--- /dev/null
+++ b/docs/source/_themes/dropwizard/genindex.html
@@ -0,0 +1,77 @@
+{#
+    basic/genindex.html
+    ~~~~~~~~~~~~~~~~~~~
+
+    Template for an "all-in-one" index.
+
+    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+#}
+{% macro indexentries(firstname, links) %}
+  <dt>
+  {%- if links -%}
+    <a href="{{ links[0][1] }}">
+    {%- if links[0][0] %}<strong>{% endif -%}
+    {{ firstname|e }}
+    {%- if links[0][0] %}</strong>{% endif -%}
+    </a>
+
+    {%- for ismain, link in links[1:] -%}
+      , <a href="{{ link }}">{% if ismain %}<strong>{% endif -%}
+      [{{ loop.index }}]
+      {%- if ismain %}</strong>{% endif -%}
+      </a>
+    {%- endfor %}
+  {%- else %}
+    {{ firstname|e }}
+  {%- endif %}
+  </dt>
+{% endmacro %}
+
+{% extends "layout.html" %}
+{% set title = _('Index') %}
+{% block body %}
+
+<h1 id="index">{{ _('Index') }}</h1>
+
+<div class="genindex-jumpbox">
+ {% for key, dummy in genindexentries -%}
+ <a href="#{{ key }}"><strong>{{ key }}</strong></a>
+ {% if not loop.last %}| {% endif %}
+ {%- endfor %}
+</div>
+
+{%- for key, entries in genindexentries %}
+<h2 id="{{ key }}">{{ key }}</h2>
+<table style="width: 100%" class="indextable genindextable"><tr>
+  {%- for column in entries|slice(2) if column %}
+  <td style="width: 33%" valign="top"><dl>
+    {%- for entryname, (links, subitems) in column %}
+      {{ indexentries(entryname, links) }}
+      {%- if subitems %}
+      <dd><dl>
+      {%- for subentryname, subentrylinks in subitems %}
+        {{ indexentries(subentryname, subentrylinks) }}
+      {%- endfor %}
+      </dl></dd>
+      {%- endif -%}
+    {%- endfor %}
+  </dl></td>
+  {%- endfor %}
+</tr></table>
+{% endfor %}
+
+{% endblock %}
+
+{% block sidebarrel %}
+{% if split_index %}
+   <h4>{{ _('Index') }}</h4>
+   <p>{% for key, dummy in genindexentries -%}
+   <a href="{{ pathto('genindex-' + key) }}"><strong>{{ key }}</strong></a>
+     {% if not loop.last %}| {% endif %}
+   {%- endfor %}</p>
+
+   <p><a href="{{ pathto('genindex-all') }}"><strong>{{ _('Full index on one page') }}</strong></a></p>
+{% endif %}
+   {{ super() }}
+{% endblock %}
diff --git a/docs/source/_themes/dropwizard/layout.html b/docs/source/_themes/dropwizard/layout.html
new file mode 100644
index 0000000..892c45f
--- /dev/null
+++ b/docs/source/_themes/dropwizard/layout.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<html lang="en">
+{%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %}
+{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
+{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
+(sidebars != []) %}
+{%- set url_root = pathto('', 1) %}
+{# XXX necessary? #}
+{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
+{%- if not embedded and docstitle %}
+{%- set titlesuffix = " | "|safe + docstitle|e %}
+{%- else %}
+{%- set titlesuffix = "" %}
+{%- endif %}
+<head>
+    <meta charset="{{ encoding }}">
+    <title>{{ title|striptags|e }}{{ titlesuffix }}</title>
+    <link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css"/>
+    {%- for cssfile in css_files %}
+    <link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css"/>
+    {%- endfor %}
+    <link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css"/>
+    {%- if favicon %}
+    <link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
+    {%- endif %}
+    {%- block linktags %}
+    <link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}"/>
+    {%- if parents %}
+    <link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}"/>
+    {%- endif %}
+    {%- if next %}
+    <link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}"/>
+    {%- endif %}
+    {%- if prev %}
+    <link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}"/>
+    {%- endif %}
+    {%- endblock %}
+    <style lang="text/css">
+        #top-bar, #top-bar small, #top-bar a {
+            text-shadow: 0px -1px 0px {{theme_gradient_end}};
+            color: {{theme_gradient_text}};
+        }
+        
+        #top-bar {
+            background-color: {{theme_gradient_bg}};
+            background-image: -moz-linear-gradient(top, {{theme_gradient_start}}, {{theme_gradient_end}});
+            background-image: -ms-linear-gradient(top, {{theme_gradient_start}}, {{theme_gradient_end}});
+            background-image: -webkit-gradient(linear, 0 0, 0 100%, from({{theme_gradient_start}}), to({{theme_gradient_end}}));
+            background-image: -webkit-linear-gradient(top, {{theme_gradient_start}}, {{theme_gradient_end}});
+            background-image: -o-linear-gradient(top, {{theme_gradient_start}}, {{theme_gradient_end}});
+            background-image: linear-gradient(top, {{theme_gradient_start}}, {{theme_gradient_end}});
+            filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '{{theme_gradient_start}}', endColorstr = '{{theme_gradient_end}}', GradientType = 0);
+            -webkit-border-radius: 0;
+            -moz-border-radius: 0;
+            border-radius: 0;
+        }
+
+
+        .hero-unit {
+            background-image: url("{{ pathto('_static/' + theme_landing_logo, 1) }}") !important;
+            background-repeat: no-repeat !important;
+            background-position: 30px 50px;
+        }
+
+        .hero-unit div.section {
+            padding-left: {{theme_landing_logo_width}} !important;
+        }
+    </style>
+</head>
+<body>
+    <a href="{{theme_github_page}}">
+        <img style="position: absolute; top: 0; right: 0; border: 0;"
+             src="https://s3.amazonaws.com/github/ribbons/forkme_right_orange_ff7600.png"
+             alt="Fork me on GitHub"></a>
+    <div class="navbar">
+        <div class="navbar-inner container-fluid" id="top-bar">
+            <header class="row-fluid">
+                <h1 class="span12" id="title">
+                    {%- block sidebarlogo %}
+                    {%- if logo %}<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>{%- endif %}
+                    {%- endblock %}
+                    <a href="{{ pathto(master_doc) }}">{{ docstitle|striptags|e }}</a>
+                    <small>{{theme_tagline|striptags|e}}</small>
+                </h1>
+            </header>
+        </div>
+    </div>
+    <div class="container-fluid">
+        <div class="row-fluid">
+            {%- block sidebar %}
+            {%- if title != "Home" %}
+            <div class="span3" id="sidebar">
+                {{ toctree(maxdepth=-1) }}
+                <hr/>
+                <ul>
+                    <li><a href="{{theme_mailing_list}}">Mailing List</a></li>
+                </ul>
+            </div>
+            {%- endif %}
+            {%- endblock %}
+            <div class="{%- if title == "Home" %}span12{%- else %}span9{%- endif %}" id="body">
+                {% block body %} {% endblock %}
+            </div>
+        </div>
+        <hr/>
+        <footer>
+            <p style="float: left">
+            {%- if show_copyright %}
+            {%- if hasdoc('copyright') %}
+            {% trans path=pathto('copyright'), copyright=copyright|e %}© <a href="{{ path }}">Copyright</a>
+            {{ copyright }}.{% endtrans %}
+            {%- else %}
+            {% trans copyright=copyright|e %}© Copyright {{ copyright }}.{% endtrans %}
+            {%- endif %}
+            {%- endif %}
+            {%- if last_updated %}
+            {% trans last_updated=last_updated|e %}Last updated on {{ last_updated }}.{% endtrans %}
+            {%- endif %}
+            {%- if show_sphinx %}
+            {% trans sphinx_version=sphinx_version|e %}Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>
+            {{ sphinx_version }}.{% endtrans %}
+            {%- endif %}
+            </p>
+            <p style="float: right">Dropwizard v{{ release }}</p>
+        </footer>
+    </div>
+</body>
+</html>
+
+
+
diff --git a/docs/source/_themes/dropwizard/less/accordion.less b/docs/source/_themes/dropwizard/less/accordion.less
new file mode 100644
index 0000000..11a36b5
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/accordion.less
@@ -0,0 +1,28 @@
+// ACCORDION
+// ---------
+
+
+// Parent container
+.accordion {
+  margin-bottom: @baseLineHeight;
+}
+
+// Group == heading + body
+.accordion-group {
+  margin-bottom: 2px;
+  border: 1px solid #e5e5e5;
+  .border-radius(4px);
+}
+.accordion-heading {
+  border-bottom: 0;
+}
+.accordion-heading .accordion-toggle {
+  display: block;
+  padding: 8px 15px;
+}
+
+// Inner needs the styles because you can't animate properly with any styles on the element
+.accordion-inner {
+  padding: 9px 15px;
+  border-top: 1px solid #e5e5e5;
+}
diff --git a/docs/source/_themes/dropwizard/less/alerts.less b/docs/source/_themes/dropwizard/less/alerts.less
new file mode 100644
index 0000000..562826f
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/alerts.less
@@ -0,0 +1,70 @@
+// ALERT STYLES
+// ------------
+
+// Base alert styles
+.alert {
+  padding: 8px 35px 8px 14px;
+  margin-bottom: @baseLineHeight;
+  text-shadow: 0 1px 0 rgba(255,255,255,.5);
+  background-color: @warningBackground;
+  border: 1px solid @warningBorder;
+  .border-radius(4px);
+}
+.alert,
+.alert-heading {
+  color: @warningText;
+}
+
+// Adjust close link position
+.alert .close {
+  position: relative;
+  top: -2px;
+  right: -21px;
+  line-height: 18px;
+}
+
+// Alternate styles
+// ----------------
+
+.alert-success {
+  background-color: @successBackground;
+  border-color: @successBorder;  
+}
+.alert-success,
+.alert-success .alert-heading {
+  color: @successText;
+}
+.alert-danger,
+.alert-error {
+  background-color: @errorBackground;
+  border-color: @errorBorder;
+}
+.alert-danger,
+.alert-error,
+.alert-danger .alert-heading,
+.alert-error .alert-heading {
+  color: @errorText;
+}
+.alert-info {
+  background-color: @infoBackground;
+  border-color: @infoBorder;
+}
+.alert-info,
+.alert-info .alert-heading {
+  color: @infoText;
+}
+
+
+// Block alerts
+// ------------------------
+.alert-block {
+  padding-top: 14px;
+  padding-bottom: 14px;
+}
+.alert-block > p,
+.alert-block > ul {
+  margin-bottom: 0;
+}
+.alert-block p + p {
+  margin-top: 5px;
+}
diff --git a/docs/source/_themes/dropwizard/less/bootstrap.less b/docs/source/_themes/dropwizard/less/bootstrap.less
new file mode 100644
index 0000000..ea84f48
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/bootstrap.less
@@ -0,0 +1,62 @@
+/*!
+ * Bootstrap v2.0.0
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+
+// CSS Reset
+ at import "reset.less";
+
+// Core variables and mixins
+ at import "variables.less"; // Modify this for custom colors, font-sizes, etc
+ at import "mixins.less";
+
+// Grid system and page structure
+ at import "scaffolding.less";
+ at import "grid.less";
+ at import "layouts.less";
+
+// Base CSS
+ at import "type.less";
+ at import "code.less";
+ at import "forms.less";
+ at import "tables.less";
+
+// Components: common
+ at import "sprites.less";
+ at import "dropdowns.less";
+ at import "wells.less";
+ at import "component-animations.less";
+ at import "close.less";
+
+// Components: Buttons & Alerts
+ at import "buttons.less";
+ at import "button-groups.less";
+ at import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less
+
+// Components: Nav
+ at import "navs.less";
+ at import "navbar.less";
+ at import "breadcrumbs.less";
+ at import "pagination.less";
+ at import "pager.less";
+
+// Components: Popovers
+ at import "modals.less";
+ at import "tooltip.less";
+ at import "popovers.less";
+
+// Components: Misc
+ at import "thumbnails.less";
+ at import "labels.less";
+ at import "progress-bars.less";
+ at import "accordion.less";
+ at import "carousel.less";
+ at import "hero-unit.less";
+
+// Utility classes
+ at import "utilities.less"; // Has to be last to override when necessary
diff --git a/docs/source/_themes/dropwizard/less/breadcrumbs.less b/docs/source/_themes/dropwizard/less/breadcrumbs.less
new file mode 100644
index 0000000..19b8081
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/breadcrumbs.less
@@ -0,0 +1,22 @@
+// BREADCRUMBS
+// -----------
+
+.breadcrumb {
+  padding: 7px 14px;
+  margin: 0 0 @baseLineHeight;
+  #gradient > .vertical(@white, #f5f5f5);
+  border: 1px solid #ddd;
+  .border-radius(3px);
+  .box-shadow(inset 0 1px 0 @white);
+  li {
+    display: inline;
+    text-shadow: 0 1px 0 @white;
+  }
+  .divider {
+    padding: 0 5px;
+    color: @grayLight;
+  }
+  .active a {
+    color: @grayDark;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/button-groups.less b/docs/source/_themes/dropwizard/less/button-groups.less
new file mode 100644
index 0000000..4b0523d
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/button-groups.less
@@ -0,0 +1,147 @@
+// BUTTON GROUPS
+// -------------
+
+
+// Make the div behave like a button
+.btn-group {
+  position: relative;
+  .clearfix(); // clears the floated buttons
+  .ie7-restore-left-whitespace();
+}
+
+// Space out series of button groups
+.btn-group + .btn-group {
+  margin-left: 5px;
+}
+
+// Optional: Group multiple button groups together for a toolbar
+.btn-toolbar {
+  margin-top: @baseLineHeight / 2;
+  margin-bottom: @baseLineHeight / 2;
+  .btn-group {
+    display: inline-block;
+    .ie7-inline-block();
+  }
+}
+
+// Float them, remove border radius, then re-add to first and last elements
+.btn-group .btn {
+  position: relative;
+  float: left;
+  margin-left: -1px;
+  .border-radius(0);
+}
+// Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match
+.btn-group .btn:first-child {
+  margin-left: 0;
+     -webkit-border-top-left-radius: 4px;
+         -moz-border-radius-topleft: 4px;
+             border-top-left-radius: 4px;
+  -webkit-border-bottom-left-radius: 4px;
+      -moz-border-radius-bottomleft: 4px;
+          border-bottom-left-radius: 4px;
+}
+.btn-group .btn:last-child,
+.btn-group .dropdown-toggle {
+     -webkit-border-top-right-radius: 4px;
+         -moz-border-radius-topright: 4px;
+             border-top-right-radius: 4px;
+  -webkit-border-bottom-right-radius: 4px;
+      -moz-border-radius-bottomright: 4px;
+          border-bottom-right-radius: 4px;
+}
+// Reset corners for large buttons
+.btn-group .btn.large:first-child {
+  margin-left: 0;
+     -webkit-border-top-left-radius: 6px;
+         -moz-border-radius-topleft: 6px;
+             border-top-left-radius: 6px;
+  -webkit-border-bottom-left-radius: 6px;
+      -moz-border-radius-bottomleft: 6px;
+          border-bottom-left-radius: 6px;
+}
+.btn-group .btn.large:last-child,
+.btn-group .large.dropdown-toggle {
+     -webkit-border-top-right-radius: 6px;
+         -moz-border-radius-topright: 6px;
+             border-top-right-radius: 6px;
+  -webkit-border-bottom-right-radius: 6px;
+      -moz-border-radius-bottomright: 6px;
+          border-bottom-right-radius: 6px;
+}
+
+// On hover/focus/active, bring the proper btn to front
+.btn-group .btn:hover,
+.btn-group .btn:focus,
+.btn-group .btn:active,
+.btn-group .btn.active {
+  z-index: 2;
+}
+
+// On active and open, don't show outline
+.btn-group .dropdown-toggle:active,
+.btn-group.open .dropdown-toggle {
+  outline: 0;
+}
+
+
+
+// Split button dropdowns
+// ----------------------
+
+// Give the line between buttons some depth
+.btn-group .dropdown-toggle {
+  padding-left: 8px;
+  padding-right: 8px;
+  @shadow: inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  .box-shadow(@shadow);
+  *padding-top: 5px;
+  *padding-bottom: 5px;
+}
+
+.btn-group.open {
+  // IE7's z-index only goes to the nearest positioned ancestor, which would
+  // make the menu appear below buttons that appeared later on the page
+  *z-index: @zindexDropdown;
+
+  // Reposition menu on open and round all corners
+  .dropdown-menu {
+    display: block;
+    margin-top: 1px;
+    .border-radius(5px);
+  }
+
+  .dropdown-toggle {
+    background-image: none;
+    @shadow: inset 0 1px 6px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);
+    .box-shadow(@shadow);
+  }
+}
+
+// Reposition the caret
+.btn .caret {
+  margin-top: 7px;
+  margin-left: 0;
+}
+.btn:hover .caret,
+.open.btn-group .caret {
+  .opacity(100);
+}
+
+
+// Account for other colors
+.btn-primary,
+.btn-danger,
+.btn-info,
+.btn-success {
+  .caret {
+    border-top-color: @white;
+    .opacity(75);
+  }
+}
+
+// Small button dropdowns
+.btn-small .caret {
+  margin-top: 4px;
+}
+
diff --git a/docs/source/_themes/dropwizard/less/buttons.less b/docs/source/_themes/dropwizard/less/buttons.less
new file mode 100644
index 0000000..07a2b58
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/buttons.less
@@ -0,0 +1,165 @@
+// BUTTON STYLES
+// -------------
+
+
+// Base styles
+// --------------------------------------------------
+
+// Core
+.btn {
+  display: inline-block;
+  padding: 4px 10px 4px;
+  font-size: @baseFontSize;
+  line-height: @baseLineHeight;
+  color: @grayDark;
+  text-align: center;
+  text-shadow: 0 1px 1px rgba(255,255,255,.75);
+  #gradient > .vertical-three-colors(@white, @white, 25%, darken(@white, 10%)); // Don't use .gradientbar() here since it does a three-color gradient
+  border: 1px solid #ccc;
+  border-bottom-color: #bbb;
+  .border-radius(4px);
+  @shadow: inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05);
+  .box-shadow(@shadow);
+  cursor: pointer;
+
+  // Give IE7 some love
+  .ie7-restore-left-whitespace();
+}
+
+// Hover state
+.btn:hover {
+  color: @grayDark;
+  text-decoration: none;
+  background-color: darken(@white, 10%);
+  background-position: 0 -15px;
+
+  // transition is only when going to hover, otherwise the background
+  // behind the gradient (there for IE<=9 fallback) gets mismatched
+  .transition(background-position .1s linear);
+}
+
+// Focus state for keyboard and accessibility
+.btn:focus {
+  .tab-focus();
+}
+
+// Active state
+.btn.active,
+.btn:active {
+  background-image: none;
+  @shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);
+  .box-shadow(@shadow);
+  background-color: darken(@white, 10%);
+  background-color: darken(@white, 15%) e("\9");
+  color: rgba(0,0,0,.5);
+  outline: 0;
+}
+
+// Disabled state
+.btn.disabled,
+.btn[disabled] {
+  cursor: default;
+  background-image: none;
+  background-color: darken(@white, 10%);
+  .opacity(65);
+  .box-shadow(none);
+}
+
+
+// Button Sizes
+// --------------------------------------------------
+
+// Large
+.btn-large {
+  padding: 9px 14px;
+  font-size: @baseFontSize + 2px;
+  line-height: normal;
+  .border-radius(5px);
+}
+.btn-large .icon {
+  margin-top: 1px;
+}
+
+// Small
+.btn-small {
+  padding: 5px 9px;
+  font-size: @baseFontSize - 2px;
+  line-height: @baseLineHeight - 2px;
+}
+.btn-small .icon {
+  margin-top: -1px;
+}
+
+
+// Alternate buttons
+// --------------------------------------------------
+
+// Set text color
+// -------------------------
+.btn-primary,
+.btn-primary:hover,
+.btn-warning,
+.btn-warning:hover,
+.btn-danger,
+.btn-danger:hover,
+.btn-success,
+.btn-success:hover,
+.btn-info,
+.btn-info:hover {
+  text-shadow: 0 -1px 0 rgba(0,0,0,.25);
+  color: @white
+}
+// Provide *some* extra contrast for those who can get it
+.btn-primary.active,
+.btn-warning.active,
+.btn-danger.active,
+.btn-success.active,
+.btn-info.active {
+  color: rgba(255,255,255,.75);
+}
+
+// Set the backgrounds
+// -------------------------
+.btn-primary {
+  .buttonBackground(@primaryButtonBackground, spin(@primaryButtonBackground, 20));
+}
+// Warning appears are orange
+.btn-warning {
+  .buttonBackground(lighten(@orange, 15%), @orange);
+}
+// Danger and error appear as red
+.btn-danger {
+  .buttonBackground(#ee5f5b, #bd362f);
+}
+// Success appears as green
+.btn-success {
+  .buttonBackground(#62c462, #51a351);
+}
+// Info appears as a neutral blue
+.btn-info {
+  .buttonBackground(#5bc0de, #2f96b4);
+}
+
+
+// Cross-browser Jank
+// --------------------------------------------------
+
+button.btn,
+input[type="submit"].btn {
+  &::-moz-focus-inner {
+  padding: 0;
+    border: 0;
+  }
+
+  // IE7 has some default padding on button controls
+  *padding-top: 2px;
+  *padding-bottom: 2px;
+  &.large {
+    *padding-top: 7px;
+    *padding-bottom: 7px;
+  }
+  &.small {
+    *padding-top: 3px;
+    *padding-bottom: 3px;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/carousel.less b/docs/source/_themes/dropwizard/less/carousel.less
new file mode 100644
index 0000000..8fbd303
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/carousel.less
@@ -0,0 +1,121 @@
+// CAROUSEL
+// --------
+
+.carousel {
+  position: relative;
+  margin-bottom: @baseLineHeight;
+  line-height: 1;
+}
+
+.carousel-inner {
+  overflow: hidden;
+  width: 100%;
+  position: relative;
+}
+
+.carousel {
+
+  .item {
+    display: none;
+    position: relative;
+    .transition(.6s ease-in-out left);
+  }
+
+  // Account for jankitude on images
+  .item > img {
+    display: block;
+    line-height: 1;
+  }
+
+  .active,
+  .next,
+  .prev { display: block; }
+
+  .active {
+    left: 0;
+  }
+
+  .next,
+  .prev {
+    position: absolute;
+    top: 0;
+    width: 100%;
+  }
+
+  .next {
+    left: 100%;
+  }
+  .prev {
+    left: -100%;
+  }
+  .next.left,
+  .prev.right {
+    left: 0;
+  }
+
+  .active.left {
+    left: -100%;
+  }
+  .active.right {
+    left: 100%;
+  }
+
+}
+
+// Left/right controls for nav
+// ---------------------------
+
+.carousel-control {
+  position: absolute;
+  top: 40%;
+  left: 15px;
+  width: 40px;
+  height: 40px;
+  margin-top: -20px;
+  font-size: 60px;
+  font-weight: 100;
+  line-height: 30px;
+  color: @white;
+  text-align: center;
+  background: @grayDarker;
+  border: 3px solid @white;
+  .border-radius(23px);
+  .opacity(50);
+
+  // we can't have this transition here
+  // because webkit cancels the carousel
+  // animation if you trip this while
+  // in the middle of another animation
+  // ;_;
+  // .transition(opacity .2s linear);
+
+  // Reposition the right one
+  &.right {
+    left: auto;
+    right: 15px;
+  }
+
+  // Hover state
+  &:hover {
+    color: @white;
+    text-decoration: none;
+    .opacity(90);
+  }
+}
+
+// Caption for text below images
+// -----------------------------
+
+.carousel-caption {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  padding: 10px 15px 5px;
+  background: @grayDark;
+  background: rgba(0,0,0,.75);
+}
+.carousel-caption h4,
+.carousel-caption p {
+  color: @white;
+}
diff --git a/docs/source/_themes/dropwizard/less/close.less b/docs/source/_themes/dropwizard/less/close.less
new file mode 100644
index 0000000..a0e5edb
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/close.less
@@ -0,0 +1,18 @@
+// CLOSE ICONS
+// -----------
+
+.close {
+  float: right;
+  font-size: 20px;
+  font-weight: bold;
+  line-height: @baseLineHeight;
+  color: @black;
+  text-shadow: 0 1px 0 rgba(255,255,255,1);
+  .opacity(20);
+  &:hover {
+    color: @black;
+    text-decoration: none;
+    .opacity(40);
+    cursor: pointer;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/code.less b/docs/source/_themes/dropwizard/less/code.less
new file mode 100644
index 0000000..c640537
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/code.less
@@ -0,0 +1,44 @@
+// Code.less
+// Code typography styles for the <code> and <pre> elements
+// --------------------------------------------------------
+
+// Inline and block code styles
+.code-and-pre,
+pre {
+  padding: 0 3px 2px;
+  #font > #family > .monospace;
+  font-size: @baseFontSize - 1;
+  color: @grayDark;
+  .border-radius(3px);
+}
+.code, code {
+  .code-and-pre();
+  color: #d14;
+  background-color: #f7f7f9;
+  border: 1px solid #e1e1e8;
+}
+pre {
+  display: block;
+  padding: (@baseLineHeight - 1) / 2;
+  margin: 0 0 @baseLineHeight / 2;
+  font-size: 12px;
+  line-height: @baseLineHeight;
+  background-color: #f5f5f5;
+  border: 1px solid #ccc; // fallback for IE7-8
+  border: 1px solid rgba(0,0,0,.15);
+  .border-radius(4px);
+  white-space: pre;
+  white-space: pre-wrap;
+  word-break: break-all;
+
+  // Make prettyprint styles more spaced out for readability
+  &.prettyprint {
+    margin-bottom: @baseLineHeight;
+  }
+
+  // Account for some code outputs that place code tags in pre tags
+  code {
+    padding: 0;
+    background-color: transparent;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/component-animations.less b/docs/source/_themes/dropwizard/less/component-animations.less
new file mode 100644
index 0000000..4f2a4fd
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/component-animations.less
@@ -0,0 +1,18 @@
+// COMPONENT ANIMATIONS
+// --------------------
+
+.fade {
+  .transition(opacity .15s linear);
+  opacity: 0;
+  &.in {
+    opacity: 1;
+  }
+}
+
+.collapse {
+  .transition(height .35s ease);
+  position:relative;
+  overflow:hidden;
+  height: 0;
+  &.in { height: auto; }
+}
diff --git a/docs/source/_themes/dropwizard/less/dropdowns.less b/docs/source/_themes/dropwizard/less/dropdowns.less
new file mode 100644
index 0000000..83f535a
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/dropdowns.less
@@ -0,0 +1,131 @@
+// DROPDOWN MENUS
+// --------------
+
+// Use the .menu class on any <li> element within the topbar or ul.tabs and you'll get some superfancy dropdowns
+.dropdown {
+  position: relative;
+}
+.dropdown-toggle {
+  // The caret makes the toggle a bit too tall in IE7
+  *margin-bottom: -3px;
+}
+.dropdown-toggle:active,
+.open .dropdown-toggle {
+  outline: 0;
+}
+// Dropdown arrow/caret
+.caret {
+  display: inline-block;
+  width: 0;
+  height: 0;
+  text-indent: -99999px;
+  // IE7 won't do the border trick if there's a text indent, but it doesn't
+  // do the content that text-indent is hiding, either, so we're ok.
+  *text-indent: 0;
+  vertical-align: top;
+  border-left:  4px solid transparent;
+  border-right: 4px solid transparent;
+  border-top:   4px solid @black;
+  .opacity(30);
+  content: "\2193";
+}
+.dropdown .caret {
+  margin-top: 8px;
+  margin-left: 2px;
+}
+.dropdown:hover .caret,
+.open.dropdown .caret {
+  .opacity(100);
+}
+// The dropdown menu (ul)
+.dropdown-menu {
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: @zindexDropdown;
+  float: left;
+  display: none; // none by default, but block on "open" of the menu
+  min-width: 160px;
+  max-width: 220px;
+  _width: 160px;
+  padding: 4px 0;
+  margin: 0; // override default ul
+  list-style: none;
+  background-color: @white;
+  border-color: #ccc;
+  border-color: rgba(0,0,0,.2);
+  border-style: solid;
+  border-width: 1px;
+  .border-radius(0 0 5px 5px);
+  .box-shadow(0 5px 10px rgba(0,0,0,.2));
+  -webkit-background-clip: padding-box;
+     -moz-background-clip: padding;
+          background-clip: padding-box;
+  *border-right-width: 2px;
+  *border-bottom-width: 2px;
+
+  // Allow for dropdowns to go bottom up (aka, dropup-menu)
+  &.bottom-up {
+    top: auto;
+    bottom: 100%;
+    margin-bottom: 2px;
+  }
+
+  // Dividers (basically an hr) within the dropdown
+  .divider {
+    height: 1px;
+    margin: 5px 1px;
+    overflow: hidden;
+    background-color: #e5e5e5;
+    border-bottom: 1px solid @white;
+
+    // IE7 needs a set width since we gave a height. Restricting just
+    // to IE7 to keep the 1px left/right space in other browsers.
+    // It is unclear where IE is getting the extra space that we need
+    // to negative-margin away, but so it goes.
+    *width: 100%;
+    *margin: -5px 0 5px;
+  }
+
+  // Links within the dropdown menu
+  a {
+    display: block;
+    padding: 3px 15px;
+    clear: both;
+    font-weight: normal;
+    line-height: 18px;
+    color: @gray;
+    white-space: nowrap;
+  }
+}
+
+// Hover state
+.dropdown-menu li > a:hover,
+.dropdown-menu .active > a,
+.dropdown-menu .active > a:hover {
+  color: @white;
+  text-decoration: none;
+  background-color: @linkColor;
+}
+
+// Open state for the dropdown
+.dropdown.open {
+  // IE7's z-index only goes to the nearest positioned ancestor, which would
+  // make the menu appear below buttons that appeared later on the page
+  *z-index: @zindexDropdown;
+
+  .dropdown-toggle {
+    color: @white;
+    background: #ccc;
+    background: rgba(0,0,0,.3);
+  }
+  .dropdown-menu {
+    display: block;
+  }
+}
+
+// Typeahead
+.typeahead {
+  margin-top: 2px; // give it some space to breathe
+  .border-radius(4px);
+}
diff --git a/docs/source/_themes/dropwizard/less/dropwizard.less b/docs/source/_themes/dropwizard/less/dropwizard.less
new file mode 100644
index 0000000..a1dd9e4
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/dropwizard.less
@@ -0,0 +1,231 @@
+ at import "reset.less";
+ at import "variables.less"; // Modify this for custom colors, font-sizes, etc
+ at import "mixins.less";
+ at import "scaffolding.less";
+ at import "grid.less";
+ at import "layouts.less";
+ at import "type.less";
+ at import "code.less";
+ at import "tables.less";
+ at import "buttons.less";
+ at import "navs.less";
+ at import "navbar.less";
+ at import "hero-unit.less";
+ at import "utilities.less"; // Has to be last to override when necessary
+
+#call-to-action {
+    text-align: right;
+}
+
+a.headerlink {
+    display: none;
+}
+
+#title {
+    color: #ffffff;
+}
+
+.hero-unit h1 {
+    padding-bottom: 20px ! important;
+}
+
+#top-bar small {
+    color: #f8f8ff;
+    text-shadow: 0px -1px 0px #5f0c17;
+}
+
+.admonition {
+    padding: 14px 35px 14px 14px;
+    margin-bottom: 18px;
+    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
+    background-color: #fcf8e3;
+    border: 1px solid #fbeed5;
+    -webkit-border-radius: 4px;
+    -moz-border-radius: 4px;
+    border-radius: 4px;
+}
+
+.admonition .admonition-title {
+    font-size: 14pt;
+    font-weight: bold;
+}
+
+.admonition.note .admonition-title,
+.admonition-todo .admonition-title {
+    color: #c09853;
+}
+
+.admonition.tip,
+.admonition.hint {
+    background-color: #dff0d8;
+    border-color: #d6e9c6;
+}
+
+.admonition.tip .admonition-title,
+.admonition.hint .admonition-title {
+    color: #468847;
+}
+
+.admonition.error,
+.admonition.warning,
+.admonition.caution,
+.admonition.danger,
+.admonition.attention {
+    background-color: #f2dede;
+    border-color: #eed3d7;
+}
+
+.admonition.error .admonition-title,
+.admonition.warning .admonition-title,
+.admonition.caution .admonition-title,
+.admonition.danger .admonition-title,
+.admonition.attention .admonition-title {
+    color: #b94a48;
+}
+
+.admonition.important {
+    background-color: #d9edf7;
+    border-color: #bce8f1;
+}
+
+.admonition.important .admonition-title {
+    color: #3a87ad;
+}
+
+.admonition > p, .admonition > ul {
+    margin-bottom: 0;
+}
+
+.admonition p + p {
+    margin-top: 5px;
+}
+
+a.internal.reference > em {
+    font-style: normal ! important;
+    text-decoration: none ! important;
+}
+
+tt {
+    .code();
+}
+
+.section > p, .section ul li, .admonition p, .section dt, .section dl {
+    font-size: 13pt;
+    line-height: 18pt;
+}
+
+.section tt {
+    font-size: 11pt;
+    line-height: 11pt;
+}
+
+.section > * {
+    margin-bottom: 20px;
+}
+
+pre {
+    font-family: 'Panic Sans', Menlo, Monaco, Consolas, Andale Mono, Courier New, monospace !important;
+    font-size: 12pt !important;
+    line-height: 22px !important;
+    display: block !important;
+    width: auto !important;
+    height: auto !important;
+    overflow: auto !important;
+    white-space: pre !important;
+    word-wrap: normal !important;
+}
+
+#body h1, h1 tt {
+    font-size: 28pt;
+}
+
+h1 tt {
+    background-color: transparent;
+    font-size: 26pt !important;
+}
+
+#body h2 {
+    font-size: 24pt;
+}
+
+h2 tt {
+    background-color: transparent;
+    font-size: 22pt !important;
+}
+
+#body h3 {
+    font-size: 20pt;
+}
+
+h3 tt {
+    background-color: transparent;
+    font-size: 18pt !important;
+}
+
+#body h4 {
+    font-size: 16pt;
+}
+
+h4 tt {
+    background-color: transparent;
+    font-size: 14pt !important;
+}
+
+#sidebar tt {
+    color: #08c;
+    background-color: transparent;
+}
+
+.hero-unit .toctree-wrapper {
+    text-align: center;
+}
+
+.hero-unit li {
+    display: inline;
+    list-style-type: none;
+    padding-right: 20px;
+}
+
+.hero-unit li a {
+    .btn();
+    .btn-success();
+    padding:10px 10px 10px;
+    font-size:16pt;
+    &:hover {
+        color: @grayDark;
+        text-decoration: none;
+        background-color: darken(@white, 10%);
+        background-position: 0 -15px;
+
+        // transition is only when going to hover, otherwise the background
+        // behind the gradient (there for IE<=9 fallback) gets mismatched
+        .transition(background-position .1s linear);
+        .btn-success();
+    }
+
+    &:focus {
+        .tab-focus();
+        .btn-success();
+    }
+
+    &:active {
+        background-image: none;
+        @shadow: inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05);
+        .box-shadow(@shadow);
+        background-color: darken(@white, 10%);
+        background-color: darken(@white, 15%) e("\9");
+        color: rgba(0,0,0,.5);
+        outline: 0;
+        .btn-success();
+    }
+}
+
+.hero-unit li a:after {
+    content: " »";
+}
+
+table.docutils {
+    border: 1px solid #DDD;
+    .table();
+    .table-striped();
+}
diff --git a/docs/source/_themes/dropwizard/less/forms.less b/docs/source/_themes/dropwizard/less/forms.less
new file mode 100644
index 0000000..d70d532
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/forms.less
@@ -0,0 +1,515 @@
+// Forms.less
+// Base styles for various input types, form layouts, and states
+// -------------------------------------------------------------
+
+
+// GENERAL STYLES
+// --------------
+
+// Make all forms have space below them
+form {
+  margin: 0 0 @baseLineHeight;
+}
+
+fieldset {
+  padding: 0;
+  margin: 0;
+  border: 0;
+}
+
+// Groups of fields with labels on top (legends)
+legend {
+  display: block;
+  width: 100%;
+  padding: 0;
+  margin-bottom: @baseLineHeight * 1.5;
+  font-size: @baseFontSize * 1.5;
+  line-height: @baseLineHeight * 2;
+  color: @grayDark;
+  border: 0;
+  border-bottom: 1px solid #eee;
+}
+
+// Set font for forms
+label,
+input,
+button,
+select,
+textarea {
+  #font > .sans-serif(@baseFontSize,normal, at baseLineHeight);
+}
+
+// Identify controls by their labels
+label {
+  display: block;
+  margin-bottom: 5px;
+  color: @grayDark;
+}
+
+// Inputs, Textareas, Selects
+input,
+textarea,
+select,
+.uneditable-input {
+  display: inline-block;
+  width: 210px;
+  height: @baseLineHeight;
+  padding: 4px;
+  margin-bottom: 9px;
+  font-size: @baseFontSize;
+  line-height: @baseLineHeight;
+  color: @gray;
+  border: 1px solid #ccc;
+  .border-radius(3px);
+}
+.uneditable-textarea {
+  width: auto;
+  height: auto;
+}
+
+// Inputs within a label
+label input,
+label textarea,
+label select {
+  display: block;
+}
+
+// Mini reset for unique input types
+input[type="image"],
+input[type="checkbox"],
+input[type="radio"] {
+  width: auto;
+  height: auto;
+  padding: 0;
+  margin: 3px 0;
+  *margin-top: 0; /* IE7 */
+  line-height: normal;
+  border: 0;
+  cursor: pointer;
+  border-radius: 0 e("\0/"); // Nuke border-radius for IE9 only
+}
+
+// Reset the file input to browser defaults
+input[type="file"] {
+  padding: initial;
+  line-height: initial;
+  border: initial;
+  background-color: @white;
+  background-color: initial;
+  .box-shadow(none);
+}
+
+// Help out input buttons
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  width: auto;
+  height: auto;
+}
+
+// Set the height of select and file controls to match text inputs
+select,
+input[type="file"] {
+  height: 28px; /* In IE7, the height of the select element cannot be changed by height, only font-size */
+  *margin-top: 4px; /* For IE7, add top margin to align select with labels */
+  line-height: 28px;
+}
+
+// Chrome on Linux and Mobile Safari need background-color
+select {
+  width: 220px; // default input width + 10px of padding that doesn't get applied
+  background-color: @white;
+}
+
+// Make multiple select elements height not fixed
+select[multiple],
+select[size] {
+  height: auto;
+}
+
+// Remove shadow from image inputs
+input[type="image"] {
+  .box-shadow(none);
+}
+
+// Make textarea height behave
+textarea {
+  height: auto;
+}
+
+// Hidden inputs
+input[type="hidden"] {
+  display: none;
+}
+
+
+
+// CHECKBOXES & RADIOS
+// -------------------
+
+// Indent the labels to position radios/checkboxes as hanging
+.radio,
+.checkbox {
+  padding-left: 18px;
+}
+.radio input[type="radio"],
+.checkbox input[type="checkbox"] {
+  float: left;
+  margin-left: -18px;
+}
+
+// Move the options list down to align with labels
+.controls > .radio:first-child,
+.controls > .checkbox:first-child {
+  padding-top: 5px; // has to be padding because margin collaspes
+}
+
+// Radios and checkboxes on same line
+.radio.inline,
+.checkbox.inline {
+  display: inline-block;
+  margin-bottom: 0;
+  vertical-align: middle;
+}
+.radio.inline + .radio.inline,
+.checkbox.inline + .checkbox.inline {
+  margin-left: 10px; // space out consecutive inline controls
+}
+// But don't forget to remove their padding on first-child
+.controls > .radio.inline:first-child,
+.controls > .checkbox.inline:first-child {
+  padding-top: 0;
+}
+
+
+
+// FOCUS STATE
+// -----------
+
+input,
+textarea {
+  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
+  @transition: border linear .2s, box-shadow linear .2s;
+  .transition(@transition);
+}
+input:focus,
+textarea:focus {
+  border-color: rgba(82,168,236,.8);
+  @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
+  .box-shadow(@shadow);
+  outline: 0;
+  outline: thin dotted \9; /* IE6-8 */
+}
+input[type="file"]:focus,
+input[type="checkbox"]:focus,
+select:focus {
+  .box-shadow(none); // override for file inputs
+  .tab-focus();
+}
+
+
+
+// INPUT SIZES
+// -----------
+
+// General classes for quick sizes
+.input-mini       { width: 60px; }
+.input-small      { width: 90px; }
+.input-medium     { width: 150px; }
+.input-large      { width: 210px; }
+.input-xlarge     { width: 270px; }
+.input-xxlarge    { width: 530px; }
+
+// Grid style input sizes
+input[class*="span"],
+select[class*="span"],
+textarea[class*="span"],
+.uneditable-input {
+  float: none;
+  margin-left: 0;
+}
+
+
+
+// GRID SIZING FOR INPUTS
+// ----------------------
+
+#inputGridSystem > .generate(@gridColumns, @gridColumnWidth, @gridGutterWidth);
+
+
+
+
+// DISABLED STATE
+// --------------
+
+// Disabled and read-only inputs
+input[disabled],
+select[disabled],
+textarea[disabled],
+input[readonly],
+select[readonly],
+textarea[readonly] {
+  background-color: #f5f5f5;
+  border-color: #ddd;
+  cursor: not-allowed;
+}
+
+
+
+
+// FORM FIELD FEEDBACK STATES
+// --------------------------
+
+// Mixin for form field states
+.formFieldState(@textColor: #555, @borderColor: #ccc, @backgroundColor: #f5f5f5) {
+  // Set the text color
+  > label,
+  .help-block,
+  .help-inline {
+    color: @textColor;
+  }
+  // Style inputs accordingly
+  input,
+  select,
+  textarea {
+    color: @textColor;
+    border-color: @borderColor;
+    &:focus {
+      border-color: darken(@borderColor, 10%);
+      .box-shadow(0 0 6px lighten(@borderColor, 20%));
+    }
+  }
+  // Give a small background color for input-prepend/-append
+  .input-prepend .add-on,
+  .input-append .add-on {
+    color: @textColor;
+    background-color: @backgroundColor;
+    border-color: @textColor;
+  }
+}
+// Warning
+.control-group.warning {
+  .formFieldState(@warningText, @warningText, @warningBackground);
+}
+// Error
+.control-group.error {
+  .formFieldState(@errorText, @errorText, @errorBackground);
+}
+// Success
+.control-group.success {
+  .formFieldState(@successText, @successText, @successBackground);
+}
+
+// HTML5 invalid states
+// Shares styles with the .control-group.error above
+input:focus:required:invalid,
+textarea:focus:required:invalid,
+select:focus:required:invalid {
+  color: #b94a48;
+  border-color: #ee5f5b;
+  &:focus {
+    border-color: darken(#ee5f5b, 10%);
+    .box-shadow(0 0 6px lighten(#ee5f5b, 20%));    
+  }
+}
+
+
+
+// FORM ACTIONS
+// ------------
+
+.form-actions {
+  padding: (@baseLineHeight - 1) 20px @baseLineHeight;
+  margin-top: @baseLineHeight;
+  margin-bottom: @baseLineHeight;
+  background-color: #f5f5f5;
+  border-top: 1px solid #ddd;
+}
+
+// For text that needs to appear as an input but should not be an input
+.uneditable-input {
+  display: block;
+  background-color: @white;
+  border-color: #eee;
+  .box-shadow(inset 0 1px 2px rgba(0,0,0,.025));
+  cursor: not-allowed;
+}
+
+// Placeholder text gets special styles; can't be bundled together though for some reason
+.placeholder(@grayLight);
+
+
+
+// HELP TEXT
+// ---------
+
+.help-block {
+  margin-top: 5px;
+  margin-bottom: 0;
+  color: @grayLight;
+}
+
+.help-inline {
+  display: inline-block;
+  .ie7-inline-block();
+  margin-bottom: 9px;
+  vertical-align: middle;
+  padding-left: 5px;
+}
+
+
+
+// INPUT GROUPS
+// ------------
+
+// Allow us to put symbols and text within the input field for a cleaner look
+.input-prepend,
+.input-append {
+  margin-bottom: 5px;
+  .clearfix(); // Clear the float to prevent wrapping
+  input,
+  .uneditable-input {
+    .border-radius(0 3px 3px 0);
+    &:focus {
+      position: relative;
+      z-index: 2;
+    }
+  }
+  .uneditable-input {
+    border-left-color: #ccc;
+  }
+  .add-on {
+    float: left;
+    display: block;
+    width: auto;
+    min-width: 16px;
+    height: @baseLineHeight;
+    margin-right: -1px;
+    padding: 4px 5px;
+    font-weight: normal;
+    line-height: @baseLineHeight;
+    color: @grayLight;
+    text-align: center;
+    text-shadow: 0 1px 0 @white;
+    background-color: #f5f5f5;
+    border: 1px solid #ccc;
+    .border-radius(3px 0 0 3px);
+  }
+  .active {
+    background-color: lighten(@green, 30);
+    border-color: @green;
+  }
+}
+.input-prepend {
+  .add-on {
+    *margin-top: 1px; /* IE6-7 */
+  }
+}
+.input-append {
+  input,
+  .uneditable-input {
+    float: left;
+    .border-radius(3px 0 0 3px);
+  }
+  .uneditable-input {
+    border-right-color: #ccc;    
+  }
+  .add-on {
+    margin-right: 0;
+    margin-left: -1px;
+    .border-radius(0 3px 3px 0);
+  }
+  input:first-child {
+    // In IE7, having a hasLayout container (from clearfix's zoom:1) can make the first input
+    // inherit the sum of its ancestors' margins.
+    *margin-left: -160px;
+
+    &+.add-on {
+      *margin-left: -21px;
+    }
+  }
+}
+
+
+
+// SEARCH FORM
+// -----------
+
+.search-query {
+  padding-left: 14px;
+  padding-right: 14px;
+  margin-bottom: 0; // remove the default margin on all inputs
+  .border-radius(14px);
+}
+
+
+
+// HORIZONTAL & VERTICAL FORMS
+// ---------------------------
+
+// Common properties
+// -----------------
+
+.form-search,
+.form-inline,
+.form-horizontal {
+  input,
+  textarea,
+  select,
+  .help-inline,
+  .uneditable-input {
+    display: inline-block;
+    margin-bottom: 0;
+  }
+}
+.form-search label,
+.form-inline label,
+.form-search .input-append,
+.form-inline .input-append,
+.form-search .input-prepend,
+.form-inline .input-prepend {
+  display: inline-block;
+}
+// Make the prepend and append add-on vertical-align: middle;
+.form-search .input-append .add-on,
+.form-inline .input-prepend .add-on,
+.form-search .input-append .add-on,
+.form-inline .input-prepend .add-on {
+  vertical-align: middle;
+}
+
+// Margin to space out fieldsets
+.control-group {
+  margin-bottom: @baseLineHeight / 2;
+}
+
+// Horizontal-specific styles
+// --------------------------
+
+.form-horizontal {
+  // Legend collapses margin, so we're relegated to padding
+  legend + .control-group {
+    margin-top: @baseLineHeight;
+    -webkit-margin-top-collapse: separate;
+  }
+  // Increase spacing between groups
+  .control-group {
+    margin-bottom: @baseLineHeight;
+    .clearfix();
+  }
+  // Float the labels left
+  .control-group > label {
+    float: left;
+    width: 140px;
+    padding-top: 5px;
+    text-align: right;
+  }
+  // Move over all input controls and content
+  .controls {
+    margin-left: 160px;
+  }
+  // Move over buttons in .form-actions to align with .controls
+  .form-actions {
+    padding-left: 160px;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/grid.less b/docs/source/_themes/dropwizard/less/grid.less
new file mode 100644
index 0000000..4acb0a4
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/grid.less
@@ -0,0 +1,8 @@
+// GRID SYSTEM
+// -----------
+
+// Fixed (940px)
+#gridSystem > .generate(@gridColumns, @gridColumnWidth, @gridGutterWidth);
+
+// Fluid (940px)
+#fluidGridSystem > .generate(@gridColumns, @fluidGridColumnWidth, @fluidGridGutterWidth);
diff --git a/docs/source/_themes/dropwizard/less/hero-unit.less b/docs/source/_themes/dropwizard/less/hero-unit.less
new file mode 100644
index 0000000..cba1cc4
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/hero-unit.less
@@ -0,0 +1,20 @@
+// HERO UNIT
+// ---------
+
+.hero-unit {
+  padding: 60px;
+  margin-bottom: 30px;
+  background-color: #f5f5f5;
+  .border-radius(6px);
+  h1 {
+    margin-bottom: 0;
+    font-size: 60px;
+    line-height: 1;
+    letter-spacing: -1px;
+  }
+  p {
+    font-size: 18px;
+    font-weight: 200;
+    line-height: @baseLineHeight * 1.5;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/labels.less b/docs/source/_themes/dropwizard/less/labels.less
new file mode 100644
index 0000000..c0f4277
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/labels.less
@@ -0,0 +1,16 @@
+// LABELS
+// ------
+
+.label {
+  padding: 1px 3px 2px;
+  font-size: @baseFontSize * .75;
+  font-weight: bold;
+  color: @white;
+  text-transform: uppercase;
+  background-color: @grayLight;
+  .border-radius(3px);
+}
+.label-important { background-color: @errorText; }
+.label-warning   { background-color: @orange; }
+.label-success   { background-color: @successText; }
+.label-info      { background-color: @infoText; }
diff --git a/docs/source/_themes/dropwizard/less/layouts.less b/docs/source/_themes/dropwizard/less/layouts.less
new file mode 100644
index 0000000..c8d358b
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/layouts.less
@@ -0,0 +1,17 @@
+//
+// Layouts
+// Fixed-width and fluid (with sidebar) layouts
+// --------------------------------------------
+
+
+// Container (centered, fixed-width layouts)
+.container {
+  .container-fixed();
+}
+
+// Fluid layouts (left aligned, with sidebar, min- & max-width content)
+.container-fluid {
+  padding-left: @gridGutterWidth;
+  padding-right: @gridGutterWidth;
+  .clearfix();
+}
\ No newline at end of file
diff --git a/docs/source/_themes/dropwizard/less/mixins.less b/docs/source/_themes/dropwizard/less/mixins.less
new file mode 100644
index 0000000..30e868e
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/mixins.less
@@ -0,0 +1,537 @@
+// Mixins.less
+// Snippets of reusable CSS to develop faster and keep code readable
+// -----------------------------------------------------------------
+
+
+// UTILITY MIXINS
+// --------------------------------------------------
+
+// Clearfix
+// --------
+// For clearing floats like a boss h5bp.com/q
+.clearfix() {
+  *zoom: 1;
+  &:before,
+  &:after {
+    display: table;
+    content: "";
+  }
+  &:after {
+    clear: both;
+  }
+}
+
+// Webkit-style focus
+// ------------------
+.tab-focus() {
+  // Default
+  outline: thin dotted;
+  // Webkit
+  outline: 5px auto -webkit-focus-ring-color;
+  outline-offset: -2px;
+}
+
+// Center-align a block level element
+// ----------------------------------
+.center-block() {
+  display: block;
+  margin-left: auto;
+  margin-right: auto;
+}
+
+// IE7 inline-block
+// ----------------
+.ie7-inline-block() {
+  *display: inline; /* IE7 inline-block hack */
+  *zoom: 1;
+}
+
+// IE7 likes to collapse whitespace on either side of the inline-block elements.
+// Ems because we're attempting to match the width of a space character. Left
+// version is for form buttons, which typically come after other elements, and
+// right version is for icons, which come before. Applying both is ok, but it will
+// mean that space between those elements will be .6em (~2 space characters) in IE7,
+// instead of the 1 space in other browsers.
+.ie7-restore-left-whitespace() {
+  *margin-left: .3em;
+
+  &:first-child {
+    *margin-left: 0;
+  }
+}
+
+.ie7-restore-right-whitespace() {
+  *margin-right: .3em;
+
+  &:last-child {
+    *margin-left: 0;
+  }
+}
+
+// Sizing shortcuts
+// -------------------------
+.size(@height: 5px, @width: 5px) {
+  width: @width;
+  height: @height;
+}
+.square(@size: 5px) {
+  .size(@size, @size);
+}
+
+// Placeholder text
+// -------------------------
+.placeholder(@color: @placeholderText) {
+  :-moz-placeholder {
+    color: @color;
+  }
+  ::-webkit-input-placeholder {
+    color: @color;
+  }
+}
+
+
+
+// FONTS
+// --------------------------------------------------
+
+#font {
+  #family {
+    .serif() {
+      font-family: Georgia, "Times New Roman", Times, serif;
+    }
+    .sans-serif() {
+      font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+    }
+    .monospace() {
+      font-family: "Panic Sans", Menlo, Monaco, Consolas, "Courier New", monospace;
+    }
+  }
+  .shorthand(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) {
+    font-size: @size;
+    font-weight: @weight;
+    line-height: @lineHeight;
+  }
+  .serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) {
+    #font > #family > .serif;
+    #font > .shorthand(@size, @weight, @lineHeight);
+  }
+  .sans-serif(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) {
+    #font > #family > .sans-serif;
+    #font > .shorthand(@size, @weight, @lineHeight);
+  }
+  .monospace(@size: @baseFontSize, @weight: normal, @lineHeight: @baseLineHeight) {
+    #font > #family > .monospace;
+    #font > .shorthand(@size, @weight, @lineHeight);
+  }
+}
+
+
+
+// GRID SYSTEM
+// --------------------------------------------------
+
+// Site container
+// -------------------------
+.container-fixed() {
+  width: @gridRowWidth;
+  margin-left: auto;
+  margin-right: auto;
+  .clearfix();
+}
+
+// Le grid system
+// -------------------------
+#gridSystem {
+  // Setup the mixins to be used
+  .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, @columns) {
+    width: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1));
+  } 
+  .offset(@gridColumnWidth, @gridGutterWidth, @columns) {
+    margin-left: (@gridColumnWidth * @columns) + (@gridGutterWidth * (@columns - 1)) + (@gridGutterWidth * 2);
+  }
+  .gridColumn(@gridGutterWidth) {
+    float: left;
+    margin-left: @gridGutterWidth;
+  }
+  // Take these values and mixins, and make 'em do their thang
+  .generate(@gridColumns, @gridColumnWidth, @gridGutterWidth) {
+    // Row surrounds the columns
+    .row {
+      margin-left: @gridGutterWidth * -1;
+      .clearfix();
+    }
+    // Find all .span# classes within .row and give them the necessary properties for grid columns (supported by all browsers back to IE7, thanks @dhg)
+    [class*="span"] {
+      #gridSystem > .gridColumn(@gridGutterWidth);
+    }
+    // Default columns
+    .span1     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 1); }
+    .span2     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 2); }
+    .span3     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 3); }
+    .span4     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 4); }
+    .span5     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 5); }
+    .span6     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 6); }
+    .span7     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 7); }
+    .span8     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 8); }
+    .span9     { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 9); }
+    .span10    { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 10); }
+    .span11    { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 11); }
+    .span12,
+    .container { #gridSystem > .columns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 12); }
+    // Offset column options
+    .offset1   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 1); }
+    .offset2   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 2); }
+    .offset3   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 3); }
+    .offset4   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 4); }
+    .offset5   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 5); }
+    .offset6   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 6); }
+    .offset7   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 7); }
+    .offset8   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 8); }
+    .offset9   { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 9); }
+    .offset10  { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 10); }
+    .offset11  { #gridSystem > .offset(@gridColumnWidth, @gridGutterWidth, 11); }
+  }
+}
+
+// Fluid grid system
+// -------------------------
+#fluidGridSystem {
+  // Setup the mixins to be used
+  .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, @columns) {
+    width: 1% * (@fluidGridColumnWidth * @columns) + (@fluidGridGutterWidth * (@columns - 1));
+  } 
+  .gridColumn(@fluidGridGutterWidth) {
+    float: left;
+    margin-left: @fluidGridGutterWidth;
+  }
+  // Take these values and mixins, and make 'em do their thang
+  .generate(@gridColumns, @fluidGridColumnWidth, @fluidGridGutterWidth) {      
+    // Row surrounds the columns
+    .row-fluid {
+      width: 100%;
+      .clearfix();
+
+      // Find all .span# classes within .row and give them the necessary properties for grid columns (supported by all browsers back to IE7, thanks @dhg)
+      > [class*="span"] {
+        #fluidGridSystem > .gridColumn(@fluidGridGutterWidth);
+      }
+      > [class*="span"]:first-child {
+        margin-left: 0;
+      }
+      // Default columns
+      .span1     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 1); }
+      .span2     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 2); }
+      .span3     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 3); }
+      .span4     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 4); }
+      .span5     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 5); }
+      .span6     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 6); }
+      .span7     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 7); }
+      .span8     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 8); }
+      .span9     { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 9); }
+      .span10    { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 10); }
+      .span11    { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 11); }
+      .span12    { #fluidGridSystem > .columns(@fluidGridGutterWidth, @fluidGridColumnWidth, 12); }
+    }
+  }
+}
+
+
+
+// Input grid system
+// -------------------------
+#inputGridSystem {
+  .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, @columns) {
+    width: ((@gridColumnWidth) * @columns) + (@gridGutterWidth * (@columns - 1)) - 10;
+  }
+  .generate(@gridColumns, @gridColumnWidth, @gridGutterWidth) {
+    input,
+    textarea,
+    .uneditable-input {
+      &.span1     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 1); }
+      &.span2     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 2); }
+      &.span3     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 3); }
+      &.span4     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 4); }
+      &.span5     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 5); }
+      &.span6     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 6); }
+      &.span7     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 7); }
+      &.span8     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 8); }
+      &.span9     { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 9); }
+      &.span10    { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 10); }
+      &.span11    { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 11); }
+      &.span12    { #inputGridSystem > .inputColumns(@gridGutterWidth, @gridColumnWidth, @gridRowWidth, 12); }
+    }
+  }
+}
+
+
+
+// CSS3 PROPERTIES
+// --------------------------------------------------
+
+// Border Radius
+.border-radius(@radius: 5px) {
+  -webkit-border-radius: @radius;
+     -moz-border-radius: @radius;
+          border-radius: @radius;
+}
+
+// Drop shadows
+.box-shadow(@shadow: 0 1px 3px rgba(0,0,0,.25)) {
+  -webkit-box-shadow: @shadow;
+     -moz-box-shadow: @shadow;
+          box-shadow: @shadow;
+}
+
+// Transitions
+.transition(@transition) {
+  -webkit-transition: @transition;
+     -moz-transition: @transition;
+      -ms-transition: @transition;
+       -o-transition: @transition;
+          transition: @transition;
+}
+
+// Transformations
+.rotate(@degrees) {
+  -webkit-transform: rotate(@degrees);
+     -moz-transform: rotate(@degrees);
+      -ms-transform: rotate(@degrees);
+       -o-transform: rotate(@degrees);
+          transform: rotate(@degrees);
+}
+.scale(@ratio) {
+  -webkit-transform: scale(@ratio);
+     -moz-transform: scale(@ratio);
+      -ms-transform: scale(@ratio);
+       -o-transform: scale(@ratio);
+          transform: scale(@ratio);
+}
+.translate(@x: 0, @y: 0) {
+  -webkit-transform: translate(@x, @y);
+     -moz-transform: translate(@x, @y);
+      -ms-transform: translate(@x, @y);
+       -o-transform: translate(@x, @y);
+          transform: translate(@x, @y);
+}
+.skew(@x: 0, @y: 0) {
+  -webkit-transform: translate(@x, @y);
+     -moz-transform: translate(@x, @y);
+      -ms-transform: translate(@x, @y);
+       -o-transform: translate(@x, @y);
+          transform: translate(@x, @y);
+}
+.skew(@x: 0, @y: 0) {
+  -webkit-transform: skew(@x, @y);
+     -moz-transform: skew(@x, @y);
+      -ms-transform: skew(@x, @y);
+       -o-transform: skew(@x, @y);
+          transform: skew(@x, @y);
+}
+
+// Background clipping
+// Heads up: FF 3.6 and under need "padding" instead of "padding-box"
+.background-clip(@clip) {
+  -webkit-background-clip: @clip;
+     -moz-background-clip: @clip;
+          background-clip: @clip;
+}
+
+// Background sizing
+.background-size(@size){
+  -webkit-background-size: @size;
+     -moz-background-size: @size;
+       -o-background-size: @size;
+          background-size: @size;
+}
+
+
+// Box sizing
+.box-sizing(@boxmodel) {
+  -webkit-box-sizing: @boxmodel;
+     -moz-box-sizing: @boxmodel;
+          box-sizing: @boxmodel;
+}
+
+// User select
+// For selecting text on the page
+.user-select(@select) {
+  -webkit-user-select: @select;
+     -moz-user-select: @select;
+       -o-user-select: @select;
+          user-select: @select;
+}
+
+// Resize anything
+.resizable(@direction: both) {
+  resize: @direction; // Options: horizontal, vertical, both
+  overflow: auto; // Safari fix
+}
+
+// CSS3 Content Columns
+.content-columns(@columnCount, @columnGap: @gridColumnGutter) {
+  -webkit-column-count: @columnCount;
+     -moz-column-count: @columnCount;
+          column-count: @columnCount;
+  -webkit-column-gap: @columnGap;
+     -moz-column-gap: @columnGap;
+          column-gap: @columnGap;
+}
+
+// Opacity
+.opacity(@opacity: 100) {
+  opacity: @opacity / 100;
+   filter: e(%("alpha(opacity=%d)", @opacity));
+}
+
+
+
+// BACKGROUNDS
+// --------------------------------------------------
+
+// Add an alphatransparency value to any background or border color (via Elyse Holladay)
+#translucent {
+  .background(@color: @white, @alpha: 1) {
+    background-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha);
+  }
+  .border(@color: @white, @alpha: 1) {
+    border-color: hsla(hue(@color), saturation(@color), lightness(@color), @alpha);
+    .background-clip(padding-box);
+  }
+}
+
+// Gradient Bar Colors for buttons and alerts
+.gradientBar(@primaryColor, @secondaryColor) {
+  #gradient > .vertical(@primaryColor, @secondaryColor);
+  border-color: @secondaryColor @secondaryColor darken(@secondaryColor, 15%);
+  border-color: rgba(0,0,0,.1) rgba(0,0,0,.1) fadein(rgba(0,0,0,.1), 15%);
+}
+
+// Gradients
+#gradient {
+  .horizontal(@startColor: #555, @endColor: #333) {
+    background-color: @endColor;
+    background-image: -moz-linear-gradient(left, @startColor, @endColor); // FF 3.6+
+    background-image: -ms-linear-gradient(left, @startColor, @endColor); // IE10
+    background-image: -webkit-gradient(linear, 0 0, 100% 0, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+
+    background-image: -webkit-linear-gradient(left, @startColor, @endColor); // Safari 5.1+, Chrome 10+
+    background-image: -o-linear-gradient(left, @startColor, @endColor); // Opera 11.10
+    background-image: linear-gradient(left, @startColor, @endColor); // Le standard
+    background-repeat: repeat-x;
+    filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)", at startColor, at endColor)); // IE9 and down
+  }
+  .vertical(@startColor: #555, @endColor: #333) {
+    background-color: mix(@startColor, @endColor, 60%);
+    background-image: -moz-linear-gradient(top, @startColor, @endColor); // FF 3.6+
+    background-image: -ms-linear-gradient(top, @startColor, @endColor); // IE10
+    background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), to(@endColor)); // Safari 4+, Chrome 2+
+    background-image: -webkit-linear-gradient(top, @startColor, @endColor); // Safari 5.1+, Chrome 10+
+    background-image: -o-linear-gradient(top, @startColor, @endColor); // Opera 11.10
+    background-image: linear-gradient(top, @startColor, @endColor); // The standard
+    background-repeat: repeat-x;
+    filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)", at startColor, at endColor)); // IE9 and down
+  }
+  .directional(@startColor: #555, @endColor: #333, @deg: 45deg) {
+    background-color: @endColor;
+    background-repeat: repeat-x;
+    background-image: -moz-linear-gradient(@deg, @startColor, @endColor); // FF 3.6+
+    background-image: -ms-linear-gradient(@deg, @startColor, @endColor); // IE10
+    background-image: -webkit-linear-gradient(@deg, @startColor, @endColor); // Safari 5.1+, Chrome 10+
+    background-image: -o-linear-gradient(@deg, @startColor, @endColor); // Opera 11.10
+    background-image: linear-gradient(@deg, @startColor, @endColor); // The standard
+  }
+  .vertical-three-colors(@startColor: #00b3ee, @midColor: #7a43b6, @colorStop: 50%, @endColor: #c3325f) {
+    background-color: mix(@midColor, @endColor, 80%);
+    background-image: -webkit-gradient(linear, 0 0, 0 100%, from(@startColor), color-stop(@colorStop, @midColor), to(@endColor));
+    background-image: -webkit-linear-gradient(@startColor, @midColor @colorStop, @endColor);
+    background-image: -moz-linear-gradient(top, @startColor, @midColor @colorStop, @endColor);
+    background-image: -ms-linear-gradient(@startColor, @midColor @colorStop, @endColor);
+    background-image: -o-linear-gradient(@startColor, @midColor @colorStop, @endColor);
+    background-image: linear-gradient(@startColor, @midColor @colorStop, @endColor);
+    background-repeat: no-repeat;
+    filter: e(%("progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)", at startColor, at endColor)); // IE9 and down, gets no color-stop at all for proper fallback
+  }
+  .radial(@innerColor: #555, @outerColor: #333)  {
+    background-color: @outerColor;
+    background-image: -webkit-gradient(radial, center center, 0, center center, 460, from(@innerColor), to(@outerColor));
+    background-image: -webkit-radial-gradient(circle, @innerColor, @outerColor);
+    background-image: -moz-radial-gradient(circle, @innerColor, @outerColor);
+    background-image: -ms-radial-gradient(circle, @innerColor, @outerColor);
+    background-repeat: no-repeat;
+    // Opera cannot do radial gradients yet
+  }
+  .striped(@color, @angle: -45deg) {
+    background-color: @color;
+    background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(.25, rgba(255,255,255,.15)), color-stop(.25, transparent), color-stop(.5, transparent), color-stop(.5, rgba(255,255,255,.15)), color-stop(.75, rgba(255,255,255,.15)), color-stop(.75, transparent), to(transparent));
+    background-image: -webkit-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
+    background-image: -moz-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
+    background-image: -ms-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
+    background-image: -o-linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
+    background-image: linear-gradient(@angle, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
+  }
+}
+// Reset filters for IE
+.reset-filter() {
+  filter: e(%("progid:DXImageTransform.Microsoft.gradient(enabled = false)"));
+}
+
+
+// Mixin for generating button backgrounds
+// ---------------------------------------
+.buttonBackground(@startColor, @endColor) {
+  // gradientBar will set the background to a pleasing blend of these, to support IE<=9
+  .gradientBar(@startColor, @endColor);
+  .reset-filter();
+
+  // in these cases the gradient won't cover the background, so we override
+  &:hover, &:active, &.active, &.disabled, &[disabled] {
+    background-color: @endColor;
+  }
+
+  // IE 7 + 8 can't handle box-shadow to show active, so we darken a bit ourselves
+  &:active,
+  &.active {
+    background-color: darken(@endColor, 10%) e("\9");
+  }
+}
+
+
+// COMPONENT MIXINS
+// --------------------------------------------------
+
+// POPOVER ARROWS
+// -------------------------
+// For tipsies and popovers
+#popoverArrow {
+  .top(@arrowWidth: 5px) {
+    bottom: 0;
+    left: 50%;
+    margin-left: - at arrowWidth;
+    border-left: @arrowWidth solid transparent;
+    border-right: @arrowWidth solid transparent;
+    border-top: @arrowWidth solid @black;
+  }
+  .left(@arrowWidth: 5px) {
+    top: 50%;
+    right: 0;
+    margin-top: - at arrowWidth;
+    border-top: @arrowWidth solid transparent;
+    border-bottom: @arrowWidth solid transparent;
+    border-left: @arrowWidth solid @black;
+  }
+  .bottom(@arrowWidth: 5px) {
+    top: 0;
+    left: 50%;
+    margin-left: - at arrowWidth;
+    border-left: @arrowWidth solid transparent;
+    border-right: @arrowWidth solid transparent;
+    border-bottom: @arrowWidth solid @black;
+  }
+  .right(@arrowWidth: 5px) {
+    top: 50%;
+    left: 0;
+    margin-top: - at arrowWidth;
+    border-top: @arrowWidth solid transparent;
+    border-bottom: @arrowWidth solid transparent;
+    border-right: @arrowWidth solid @black;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/modals.less b/docs/source/_themes/dropwizard/less/modals.less
new file mode 100644
index 0000000..aa14675
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/modals.less
@@ -0,0 +1,72 @@
+// MODALS
+// ------
+
+.modal-open {
+  .dropdown-menu {  z-index: @zindexDropdown + @zindexModal; }
+  .dropdown.open { *z-index: @zindexDropdown + @zindexModal; }
+  .popover       {  z-index: @zindexPopover  + @zindexModal; }
+  .tooltip       {  z-index: @zindexTooltip  + @zindexModal; }
+}
+
+.modal-backdrop {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 0;
+  z-index: @zindexModalBackdrop;
+  background-color: @black;
+  // Fade for backdrop
+  &.fade { opacity: 0; }
+}
+
+.modal-backdrop,
+.modal-backdrop.fade.in {
+  .opacity(80);
+}
+
+.modal {
+  position: fixed;
+  top: 50%;
+  left: 50%;
+  z-index: @zindexModal;
+  max-height: 500px;
+  overflow: auto;
+  width: 560px;
+  margin: -250px 0 0 -280px;
+  background-color: @white;
+  border: 1px solid #999;
+  border: 1px solid rgba(0,0,0,.3);
+  *border: 1px solid #999; /* IE6-7 */
+  .border-radius(6px);
+  .box-shadow(0 3px 7px rgba(0,0,0,0.3));
+  .background-clip(padding-box);
+  &.fade {
+    .transition(e('opacity .3s linear, top .3s ease-out'));
+    top: -25%;
+  }
+  &.fade.in { top: 50%; }
+}
+.modal-header {
+  padding: 9px 15px;
+  border-bottom: 1px solid #eee;
+  // Close icon
+  .close { margin-top: 2px; }
+}
+.modal-body {
+  padding: 15px;
+}
+.modal-footer {
+  padding: 14px 15px 15px;
+  margin-bottom: 0;
+  background-color: #f5f5f5;
+  border-top: 1px solid #ddd;
+  .border-radius(0 0 6px 6px);
+  .box-shadow(inset 0 1px 0 @white);
+  .clearfix();
+  .btn {
+    float: right;
+    margin-left: 5px;
+    margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/navbar.less b/docs/source/_themes/dropwizard/less/navbar.less
new file mode 100644
index 0000000..93c0400
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/navbar.less
@@ -0,0 +1,292 @@
+// NAVBAR (FIXED AND STATIC)
+// -------------------------
+
+
+// COMMON STYLES
+// -------------
+
+.navbar {
+  overflow: visible;
+  margin-bottom: @baseLineHeight;
+}
+
+// Gradient is applied to it's own element because overflow visible is not honored by IE when filter is present
+.navbar-inner {
+  padding-left:  20px;
+  padding-right: 20px;
+  #gradient > .vertical(@navbarBackgroundHighlight, @navbarBackground);
+  .border-radius(4px);
+  @shadow: 0 1px 3px rgba(0,0,0,.25), inset 0 -1px 0 rgba(0,0,0,.1);
+  .box-shadow(@shadow);
+}
+
+// Navbar button for toggling navbar items in responsive layouts
+.btn-navbar {
+  display: none;
+  float: right;
+  padding: 7px 10px;
+  margin-left: 5px;
+  margin-right: 5px;
+  .buttonBackground(@navbarBackgroundHighlight, @navbarBackground);
+  @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.075);
+  .box-shadow(@shadow);
+}
+.btn-navbar .icon-bar {
+  display: block;
+  width: 18px;
+  height: 2px;
+  background-color: #f5f5f5;
+  .border-radius(1px);
+  .box-shadow(0 1px 0 rgba(0,0,0,.25));
+}
+.btn-navbar .icon-bar + .icon-bar {
+  margin-top: 3px;
+}
+// Override the default collapsed state
+.nav-collapse.collapse {
+  height: auto;
+}
+
+
+// Brand, links, text, and buttons
+.navbar {
+  // Hover and active states
+  .brand:hover {
+    text-decoration: none;
+  }
+  // Website or project name
+  .brand {
+    float: left;
+    display: block;
+    padding: 8px 20px 12px;
+    margin-left: -20px; // negative indent to left-align the text down the page
+    font-size: 20px;
+    font-weight: 200;
+    line-height: 1;
+    color: @white;
+  }
+  // Plain text in topbar
+  .navbar-text {
+    margin-bottom: 0;
+    line-height: 40px;
+    color: @navbarText;
+    a:hover {
+      color: @white;
+      background-color: transparent;
+    }
+  }
+  // Buttons in navbar
+  .btn,
+  .btn-group {
+    margin-top: 5px; // make buttons vertically centered in navbar
+  }
+  .btn-group .btn {
+    margin-top: 0;
+  }
+}
+
+// Navbar forms
+.navbar-form {
+  margin-bottom: 0; // remove default bottom margin
+  .clearfix();
+  input,
+  select {
+    display: inline-block;
+    margin-top: 5px;
+    margin-bottom: 0;
+  }
+  .radio,
+  .checkbox {
+    margin-top: 5px;
+  }
+  input[type="image"],
+  input[type="checkbox"],
+  input[type="radio"] {
+    margin-top: 3px;
+  }
+}
+
+// Navbar search
+.navbar-search {
+  position: relative;
+  float: left;
+  margin-top: 6px;
+  margin-bottom: 0;
+  .search-query {
+    padding: 4px 9px;
+    #font > .sans-serif(13px, normal, 1);
+    color: @white;
+    color: rgba(255,255,255,.75);
+    background: #666;
+    background: rgba(255,255,255,.3);
+    border: 1px solid #111;
+    @shadow: inset 0 1px 2px rgba(0,0,0,.1), 0 1px 0px rgba(255,255,255,.15);
+    .box-shadow(@shadow);
+    .transition(none);
+
+    // Placeholder text gets special styles; can't be bundled together though for some reason
+    .placeholder(@grayLighter);
+
+    // Hover states
+    &:hover {
+      color: @white;
+      background-color: @grayLight;
+      background-color: rgba(255,255,255,.5);
+    }
+    // Focus states (we use .focused since IE8 and down doesn't support :focus)
+    &:focus,
+    &.focused {
+      padding: 5px 10px;
+      color: @grayDark;
+      text-shadow: 0 1px 0 @white;
+      background-color: @white;
+      border: 0;
+      .box-shadow(0 0 3px rgba(0,0,0,.15));
+      outline: 0;
+    }
+  }
+}
+
+
+// FIXED NAVBAR
+// ------------
+
+.navbar-fixed-top {
+  position: fixed;
+  top: 0;
+  right: 0;
+  left: 0;
+  z-index: @zindexFixedNavbar;
+}
+.navbar-fixed-top .navbar-inner {
+  padding-left:  0;
+  padding-right: 0;
+  .border-radius(0);
+}
+
+
+// NAVIGATION
+// ----------
+
+.navbar .nav {
+  position: relative;
+  left: 0;
+  display: block;
+  float: left;
+  margin: 0 10px 0 0;
+}
+.navbar .nav.pull-right {
+  float: right; // redeclare due to specificity
+}
+.navbar .nav > li {
+  display: block;
+  float: left;
+}
+
+// Links
+.navbar .nav > li > a {
+  float: none;
+  padding: 10px 10px 11px;
+  line-height: 19px;
+  color: @navbarLinkColor;
+  text-decoration: none;
+  text-shadow: 0 -1px 0 rgba(0,0,0,.25);
+}
+// Hover
+.navbar .nav > li > a:hover {
+  background-color: transparent;
+  color: @navbarLinkColorHover;
+  text-decoration: none;
+}
+
+// Active nav items
+.navbar .nav .active > a,
+.navbar .nav .active > a:hover {
+  color: @navbarLinkColorHover;
+  text-decoration: none;
+  background-color: @navbarBackground;
+  background-color: rgba(0,0,0,.5);
+}
+
+// Dividers (basically a vertical hr)
+.navbar .divider-vertical {
+  height: @navbarHeight;
+  width: 1px;
+  margin: 0 9px;
+  overflow: hidden;
+  background-color: @navbarBackground;
+  border-right: 1px solid @navbarBackgroundHighlight;
+}
+
+// Secondary (floated right) nav in topbar
+.navbar .nav.pull-right {
+  margin-left: 10px;
+  margin-right: 0;
+}
+
+
+
+// Dropdown menus
+// --------------
+
+// Menu position and menu carets
+.navbar .dropdown-menu {
+  margin-top: 1px;
+  .border-radius(4px);
+  &:before {
+    content: '';
+    display: inline-block;
+    border-left:   7px solid transparent;
+    border-right:  7px solid transparent;
+    border-bottom: 7px solid #ccc;
+    border-bottom-color: rgba(0,0,0,.2);
+    position: absolute;
+    top: -7px;
+    left: 9px;
+  }
+  &:after {
+    content: '';
+    display: inline-block;
+    border-left:   6px solid transparent;
+    border-right:  6px solid transparent;
+    border-bottom: 6px solid @white;
+    position: absolute;
+    top: -6px;
+    left: 10px;
+  }
+}
+
+// Dropdown toggle caret
+.navbar .nav .dropdown-toggle .caret,
+.navbar .nav .open.dropdown .caret {
+  border-top-color: @white;
+}
+.navbar .nav .active .caret {
+  .opacity(100);
+}
+
+// Remove background color from open dropdown
+.navbar .nav .open > .dropdown-toggle,
+.navbar .nav .active > .dropdown-toggle,
+.navbar .nav .open.active > .dropdown-toggle {
+  background-color: transparent;
+}
+
+// Dropdown link on hover
+.navbar .nav .active > .dropdown-toggle:hover {
+  color: @white;
+}
+
+// Right aligned menus need alt position
+.navbar .nav.pull-right .dropdown-menu {
+  left: auto;
+  right: 0;
+  &:before {
+    left: auto;
+    right: 12px;
+  }
+  &:after {
+    left: auto;
+    right: 13px;
+  }
+}
\ No newline at end of file
diff --git a/docs/source/_themes/dropwizard/less/navs.less b/docs/source/_themes/dropwizard/less/navs.less
new file mode 100644
index 0000000..dfb2996
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/navs.less
@@ -0,0 +1,343 @@
+// NAVIGATIONS
+// -----------
+
+
+
+// BASE CLASS
+// ----------
+
+.nav {
+  margin-left: 0;
+  margin-bottom: @baseLineHeight;
+  list-style: none;
+}
+
+// Make links block level
+.nav > li > a {
+  display: block;
+}
+.nav > li > a:hover {
+  text-decoration: none;
+  background-color: @grayLighter;
+}
+
+
+
+// NAV LIST
+// --------
+
+.nav-list {
+  padding-left: 14px;
+  padding-right: 14px;
+  margin-bottom: 0;
+}
+.nav-list > li > a,
+.nav-list .nav-header {
+  display: block;
+  padding: 3px 15px;
+  margin-left:  -15px;
+  margin-right: -15px;
+  text-shadow: 0 1px 0 rgba(255,255,255,.5);
+}
+.nav-list .nav-header {
+  font-size: 11px;
+  font-weight: bold;
+  line-height: @baseLineHeight;
+  color: @grayLight;
+  text-transform: uppercase;
+}
+.nav-list > li + .nav-header {
+  margin-top: 9px;
+}
+.nav-list .active > a {
+  color: @white;
+  text-shadow: 0 -1px 0 rgba(0,0,0,.2);
+  background-color: @linkColor;
+}
+.nav-list .icon {
+  margin-right: 2px;
+}
+
+
+
+// TABS AND PILLS
+// -------------
+
+// Common styles
+.nav-tabs,
+.nav-pills {
+  .clearfix();
+}
+.nav-tabs > li,
+.nav-pills > li {
+  float: left;
+}
+.nav-tabs > li > a,
+.nav-pills > li > a {
+  padding-right: 12px;
+  padding-left: 12px;
+  margin-right: 2px;
+  line-height: 14px; // keeps the overall height an even number
+}
+
+// TABS
+// ----
+
+// Give the tabs something to sit on
+.nav-tabs {
+  border-bottom: 1px solid #ddd;
+}
+
+// Make the list-items overlay the bottom border
+.nav-tabs > li {
+  margin-bottom: -1px;
+}
+
+// Actual tabs (as links)
+.nav-tabs > li > a {
+  padding-top: 9px;
+  padding-bottom: 9px;
+  border: 1px solid transparent;
+  .border-radius(4px 4px 0 0);
+  &:hover {
+    border-color: @grayLighter @grayLighter #ddd;
+  }
+}
+// Active state, and it's :hover to override normal :hover
+.nav-tabs > .active > a,
+.nav-tabs > .active > a:hover {
+  color: @gray;
+  background-color: @white;
+  border: 1px solid #ddd;
+  border-bottom-color: transparent;
+  cursor: default;
+}
+
+// PILLS
+// -----
+
+// Links rendered as pills
+.nav-pills > li > a {
+  padding-top: 8px;
+  padding-bottom: 8px;
+  margin-top: 2px;
+  margin-bottom: 2px;
+  .border-radius(5px);
+}
+
+// Active state
+.nav-pills .active > a,
+.nav-pills .active > a:hover {
+  color: @white;
+  background-color: @linkColor;
+}
+
+
+
+// STACKED NAV
+// -----------
+
+// Stacked tabs and pills
+.nav-stacked > li {
+  float: none;
+}
+.nav-stacked > li > a {
+  margin-right: 0; // no need for the gap between nav items
+}
+
+// Tabs
+.nav-tabs.nav-stacked {
+  border-bottom: 0;
+}
+.nav-tabs.nav-stacked > li > a {
+  border: 1px solid #ddd;
+  .border-radius(0);
+}
+.nav-tabs.nav-stacked > li:first-child > a {
+  .border-radius(4px 4px 0 0);
+}
+.nav-tabs.nav-stacked > li:last-child > a {
+  .border-radius(0 0 4px 4px);
+}
+.nav-tabs.nav-stacked > li > a:hover {
+  border-color: #ddd;
+  z-index: 2;
+}
+
+// Pills
+.nav-pills.nav-stacked > li > a {
+  margin-bottom: 3px;
+}
+.nav-pills.nav-stacked > li:last-child > a {
+  margin-bottom: 1px; // decrease margin to match sizing of stacked tabs
+}
+
+
+
+// DROPDOWNS
+// ---------
+
+// Position the menu
+.nav-tabs .dropdown-menu,
+.nav-pills .dropdown-menu {
+  margin-top: 1px;
+  border-width: 1px;
+}
+.nav-pills .dropdown-menu {
+  .border-radius(4px);
+}
+
+// Default dropdown links
+// -------------------------
+// Make carets use linkColor to start
+.nav-tabs .dropdown-toggle .caret,
+.nav-pills .dropdown-toggle .caret {
+  border-top-color: @linkColor;
+  margin-top: 6px;
+}
+.nav-tabs .dropdown-toggle:hover .caret,
+.nav-pills .dropdown-toggle:hover .caret {
+  border-top-color: @linkColorHover;
+}
+
+// Active dropdown links
+// -------------------------
+.nav-tabs .active .dropdown-toggle .caret,
+.nav-pills .active .dropdown-toggle .caret {
+  border-top-color: @grayDark;
+}
+
+// Active:hover dropdown links
+// -------------------------
+.nav > .dropdown.active > a:hover {
+  color: @black;
+  cursor: pointer;
+}
+
+// Open dropdowns
+// -------------------------
+.nav-tabs .open .dropdown-toggle,
+.nav-pills .open .dropdown-toggle,
+.nav > .open.active > a:hover {
+  color: @white;
+  background-color: @grayLight;
+  border-color: @grayLight;
+}
+.nav .open .caret,
+.nav .open.active .caret,
+.nav .open a:hover .caret {
+  border-top-color: @white;
+  .opacity(100);
+}
+
+// Dropdowns in stacked tabs
+.tabs-stacked .open > a:hover {
+  border-color: @grayLight;
+}
+
+
+
+// TABBABLE
+// --------
+
+
+// COMMON STYLES
+// -------------
+
+// Clear any floats
+.tabbable {
+  .clearfix();
+}
+
+// Remove border on bottom, left, right
+.tabs-below .nav-tabs,
+.tabs-right .nav-tabs,
+.tabs-left .nav-tabs {
+  border-bottom: 0;
+}
+
+// Show/hide tabbable areas
+.tab-content > .tab-pane,
+.pill-content > .pill-pane {
+  display: none;
+}
+.tab-content > .active,
+.pill-content > .active {
+  display: block;
+}
+
+
+// BOTTOM
+// ------
+
+.tabs-below .nav-tabs {
+  border-top: 1px solid #ddd;
+}
+.tabs-below .nav-tabs > li {
+  margin-top: -1px;
+  margin-bottom: 0;
+}
+.tabs-below .nav-tabs > li > a {
+  .border-radius(0 0 4px 4px);
+  &:hover {
+    border-bottom-color: transparent;
+    border-top-color: #ddd;
+  }
+}
+.tabs-below .nav-tabs .active > a,
+.tabs-below .nav-tabs .active > a:hover {
+  border-color: transparent #ddd #ddd #ddd;
+}
+
+// LEFT & RIGHT
+// ------------
+
+// Common styles
+.tabs-left .nav-tabs > li,
+.tabs-right .nav-tabs > li {
+  float: none;
+}
+.tabs-left .nav-tabs > li > a,
+.tabs-right .nav-tabs > li > a {
+  min-width: 74px;
+  margin-right: 0;
+  margin-bottom: 3px;
+}
+
+// Tabs on the left
+.tabs-left .nav-tabs {
+  float: left;
+  margin-right: 19px;
+  border-right: 1px solid #ddd;
+}
+.tabs-left .nav-tabs > li > a {
+  margin-right: -1px;
+  .border-radius(4px 0 0 4px);
+}
+.tabs-left .nav-tabs > li > a:hover {
+  border-color: @grayLighter #ddd @grayLighter @grayLighter;
+}
+.tabs-left .nav-tabs .active > a,
+.tabs-left .nav-tabs .active > a:hover {
+  border-color: #ddd transparent #ddd #ddd;
+  *border-right-color: @white;
+}
+
+// Tabs on the right
+.tabs-right .nav-tabs {
+  float: right;
+  margin-left: 19px;
+  border-left: 1px solid #ddd;
+}
+.tabs-right .nav-tabs > li > a {
+  margin-left: -1px;
+  .border-radius(0 4px 4px 0);
+}
+.tabs-right .nav-tabs > li > a:hover {
+  border-color: @grayLighter @grayLighter @grayLighter #ddd;
+}
+.tabs-right .nav-tabs .active > a,
+.tabs-right .nav-tabs .active > a:hover {
+  border-color: #ddd #ddd #ddd transparent;
+  *border-left-color: @white;
+}
diff --git a/docs/source/_themes/dropwizard/less/pager.less b/docs/source/_themes/dropwizard/less/pager.less
new file mode 100644
index 0000000..104e41c
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/pager.less
@@ -0,0 +1,30 @@
+// PAGER
+// -----
+
+.pager {
+  margin-left: 0;
+  margin-bottom: @baseLineHeight;
+  list-style: none;
+  text-align: center;
+  .clearfix();
+}
+.pager li {
+  display: inline;
+}
+.pager a {
+  display: inline-block;
+  padding: 5px 14px;
+  background-color: #fff;
+  border: 1px solid #ddd;
+  .border-radius(15px);
+}
+.pager a:hover {
+  text-decoration: none;
+  background-color: #f5f5f5;
+}
+.pager .next a {
+  float: right;
+}
+.pager .previous a {
+  float: left;
+}
diff --git a/docs/source/_themes/dropwizard/less/pagination.less b/docs/source/_themes/dropwizard/less/pagination.less
new file mode 100644
index 0000000..de57807
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/pagination.less
@@ -0,0 +1,55 @@
+// PAGINATION
+// ----------
+
+.pagination {
+  height: @baseLineHeight * 2;
+  margin: @baseLineHeight 0;
+ }
+.pagination ul {
+  display: inline-block;
+  .ie7-inline-block();
+  margin-left: 0;
+  margin-bottom: 0;
+  .border-radius(3px);
+  .box-shadow(0 1px 2px rgba(0,0,0,.05));
+}
+.pagination li {
+    display: inline;
+  }
+.pagination a {
+  float: left;
+  padding: 0 14px;
+  line-height: (@baseLineHeight * 2) - 2;
+  text-decoration: none;
+  border: 1px solid #ddd;
+  border-left-width: 0;
+}
+.pagination a:hover,
+.pagination .active a {
+  background-color: #f5f5f5;
+}
+.pagination .active a {
+  color: @grayLight;
+  cursor: default;
+}
+.pagination .disabled a,
+.pagination .disabled a:hover {
+  color: @grayLight;
+  background-color: transparent;
+  cursor: default;
+}
+.pagination li:first-child a {
+  border-left-width: 1px;
+  .border-radius(3px 0 0 3px);
+}
+.pagination li:last-child a {
+  .border-radius(0 3px 3px 0);
+}
+
+// Centered
+.pagination-centered {
+  text-align: center;
+}
+.pagination-right {
+  text-align: right;
+}
diff --git a/docs/source/_themes/dropwizard/less/patterns.less b/docs/source/_themes/dropwizard/less/patterns.less
new file mode 100644
index 0000000..d94b921
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/patterns.less
@@ -0,0 +1,13 @@
+// Patterns.less
+// Repeatable UI elements outside the base styles provided from the scaffolding
+// ----------------------------------------------------------------------------
+
+
+// PAGE HEADERS
+// ------------
+
+footer {
+  padding-top: @baseLineHeight - 1;
+  margin-top: @baseLineHeight - 1;
+  border-top: 1px solid #eee;
+}
diff --git a/docs/source/_themes/dropwizard/less/popovers.less b/docs/source/_themes/dropwizard/less/popovers.less
new file mode 100644
index 0000000..558d99e
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/popovers.less
@@ -0,0 +1,49 @@
+// POPOVERS
+// --------
+
+.popover {
+  position: absolute;
+  top: 0;
+  left: 0;
+  z-index: @zindexPopover;
+  display: none;
+  padding: 5px;
+  &.top    { margin-top:  -5px; }
+  &.right  { margin-left:  5px; }
+  &.bottom { margin-top:   5px; }
+  &.left   { margin-left: -5px; }
+  &.top .arrow    { #popoverArrow > .top(); }
+  &.right .arrow  { #popoverArrow > .right(); }
+  &.bottom .arrow { #popoverArrow > .bottom(); }
+  &.left .arrow   { #popoverArrow > .left();  }
+  .arrow {
+    position: absolute;
+    width: 0;
+    height: 0;
+  }
+}
+.popover-inner {
+  padding: 3px;
+  width: 280px;
+  overflow: hidden;
+  background: @black; // has to be full background declaration for IE fallback
+  background: rgba(0,0,0,.8);
+  .border-radius(6px);
+  .box-shadow(0 3px 7px rgba(0,0,0,0.3));
+}
+.popover-title {
+  padding: 9px 15px;
+  line-height: 1;
+  background-color: #f5f5f5;
+  border-bottom:1px solid #eee;
+  .border-radius(3px 3px 0 0);
+}
+.popover-content {
+  padding: 14px;
+  background-color: @white;
+  .border-radius(0 0 3px 3px);
+  .background-clip(padding-box);
+  p, ul, ol {
+    margin-bottom: 0;
+  }
+}
diff --git a/docs/source/_themes/dropwizard/less/print.less b/docs/source/_themes/dropwizard/less/print.less
new file mode 100644
index 0000000..4fd45e2
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/print.less
@@ -0,0 +1,18 @@
+/*!
+ * Bootstrap @VERSION for Print
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ * Date: @DATE
+ */
+
+
+// HIDE UNECESSARY COMPONENTS
+// --------------------------
+
+.navbar-fixed {
+  display: none;
+}
\ No newline at end of file
diff --git a/docs/source/_themes/dropwizard/less/progress-bars.less b/docs/source/_themes/dropwizard/less/progress-bars.less
new file mode 100644
index 0000000..c3144e1
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/progress-bars.less
@@ -0,0 +1,95 @@
+// PROGRESS BARS
+// -------------
+
+
+// ANIMATIONS
+// ----------
+
+// Webkit
+ at -webkit-keyframes progress-bar-stripes {
+  from  { background-position: 0 0; }
+  to    { background-position: 40px 0; }
+}
+
+// Firefox
+ at -moz-keyframes progress-bar-stripes {
+  from  { background-position: 0 0; }
+  to    { background-position: 40px 0; }
+}
+
+// Spec
+ at keyframes progress-bar-stripes {
+  from  { background-position: 0 0; }
+  to    { background-position: 40px 0; }
+}
+
+
+
+// THE BARS
+// --------
+
+// Outer container
+.progress {
+  overflow: hidden;
+  height: 18px;
+  margin-bottom: 18px;
+  #gradient > .vertical(#f5f5f5, #f9f9f9);
+  .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));
+  .border-radius(4px);
+}
+
+// Bar of progress
+.progress .bar {
+  width: 0%;
+  height: 18px;
+  color: @white;
+  font-size: 12px;
+  text-align: center;
+  text-shadow: 0 -1px 0 rgba(0,0,0,.25);
+  #gradient > .vertical(#149bdf, #0480be);
+  .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
+  .box-sizing(border-box);
+  .transition(width .6s ease);
+}
+
+// Striped bars
+.progress-striped .bar {
+  #gradient > .striped(#62c462);
+  .background-size(40px 40px);
+}
+
+// Call animation for the active one
+.progress.active .bar {
+  -webkit-animation: progress-bar-stripes 2s linear infinite;
+     -moz-animation: progress-bar-stripes 2s linear infinite;
+          animation: progress-bar-stripes 2s linear infinite;
+}
+
+
+
+// COLORS
+// ------
+
+// Danger (red)
+.progress-danger .bar {
+  #gradient > .vertical(#ee5f5b, #c43c35);
+}
+.progress-danger.progress-striped .bar {
+  #gradient > .striped(#ee5f5b);
+}
+
+// Success (green)
+.progress-success .bar {
+  #gradient > .vertical(#62c462, #57a957);
+}
+.progress-success.progress-striped .bar {
+  #gradient > .striped(#62c462);
+}
+
+// Info (teal)
+.progress-info .bar {
+  #gradient > .vertical(#5bc0de, #339bb9);
+}
+.progress-info.progress-striped .bar {
+  #gradient > .striped(#5bc0de);
+}
diff --git a/docs/source/_themes/dropwizard/less/reset.less b/docs/source/_themes/dropwizard/less/reset.less
new file mode 100644
index 0000000..28d8eb6
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/reset.less
@@ -0,0 +1,126 @@
+// Reset.less
+// Adapted from Normalize.css http://github.com/necolas/normalize.css
+// ------------------------------------------------------------------------
+
+// Display in IE6-9 and FF3
+// -------------------------
+
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+  display: block;
+}
+
+// Display block in IE6-9 and FF3
+// -------------------------
+
+audio,
+canvas,
+video {
+  display: inline-block;
+  *display: inline;
+  *zoom: 1;
+}
+
+// Prevents modern browsers from displaying 'audio' without controls
+// -------------------------
+
+audio:not([controls]) {
+    display: none;
+}
+
+// Base settings
+// -------------------------
+
+html {
+  font-size: 100%;
+  -webkit-text-size-adjust: 100%;
+      -ms-text-size-adjust: 100%;
+}
+// Focus states
+a:focus {
+  .tab-focus();
+}
+// Hover & Active
+a:hover,
+a:active {
+  outline: 0;
+}
+
+// Prevents sub and sup affecting line-height in all browsers
+// -------------------------
+
+sub,
+sup {
+  position: relative;
+  font-size: 75%;
+  line-height: 0;
+  vertical-align: baseline;
+}
+sup {
+  top: -0.5em;
+}
+sub {
+  bottom: -0.25em;
+}
+
+// Img border in a's and image quality
+// -------------------------
+
+img {
+  max-width: 100%;
+  height: auto;
+  border: 0;
+  -ms-interpolation-mode: bicubic;
+}
+
+// Forms
+// -------------------------
+
+// Font size in all browsers, margin changes, misc consistency
+button,
+input,
+select,
+textarea {
+  margin: 0;
+  font-size: 100%;
+  vertical-align: middle;
+}
+button,
+input {
+  *overflow: visible; // Inner spacing ie IE6/7
+  line-height: normal; // FF3/4 have !important on line-height in UA stylesheet
+}
+button::-moz-focus-inner,
+input::-moz-focus-inner { // Inner padding and border oddities in FF3/4
+  padding: 0;
+  border: 0;
+}
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+  cursor: pointer; // Cursors on all buttons applied consistently
+  -webkit-appearance: button; // Style clicable inputs in iOS
+}
+input[type="search"] { // Appearance in Safari/Chrome
+  -webkit-appearance: textfield;
+  -webkit-box-sizing: content-box;
+     -moz-box-sizing: content-box;
+          box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-decoration,
+input[type="search"]::-webkit-search-cancel-button {
+  -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5
+}
+textarea {
+  overflow: auto; // Remove vertical scrollbar in IE6-9
+  vertical-align: top; // Readability and alignment cross-browser
+}
diff --git a/docs/source/_themes/dropwizard/less/responsive.less b/docs/source/_themes/dropwizard/less/responsive.less
new file mode 100644
index 0000000..7d494a3
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/responsive.less
@@ -0,0 +1,323 @@
+/*!
+ * Bootstrap Responsive v2.0.0
+ *
+ * Copyright 2012 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ */
+
+// Responsive.less
+// For phone and tablet devices
+// -------------------------------------------------------------
+
+
+// REPEAT VARIABLES & MIXINS
+// -------------------------
+// Required since we compile the responsive stuff separately
+
+ at import "variables.less"; // Modify this for custom colors, font-sizes, etc
+ at import "mixins.less";
+
+
+// RESPONSIVE CLASSES
+// ------------------
+
+// Hide from screenreaders and browsers
+// Credit: HTML5 Boilerplate
+.hidden {
+  display: none;
+  visibility: hidden;
+}
+
+
+
+// UP TO LANDSCAPE PHONE
+// ---------------------
+
+ at media (max-width: 480px) {
+
+  // Smooth out the collapsing/expanding nav
+  .nav-collapse {
+    -webkit-transform: translate3d(0, 0, 0); // activate the GPU
+  }
+
+  // Block level the page header small tag for readability
+  .page-header h1 small {
+    display: block;
+    line-height: @baseLineHeight;
+  }
+
+  // Make span* classes full width
+  input[class*="span"],
+  select[class*="span"],
+  textarea[class*="span"],
+  .uneditable-input {
+    display: block;
+    width: 100%;
+    height: 28px; /* Make inputs at least the height of their button counterpart */
+    /* Makes inputs behave like true block-level elements */
+    -webkit-box-sizing: border-box; /* Older Webkit */
+       -moz-box-sizing: border-box; /* Older FF */
+        -ms-box-sizing: border-box; /* IE8 */
+            box-sizing: border-box; /* CSS3 spec*/
+  }
+  // But don't let it screw up prepend/append inputs
+  .input-prepend input[class*="span"],
+  .input-append input[class*="span"] {
+    width: auto;
+  }
+
+  // Update checkboxes for iOS
+  input[type="checkbox"],
+  input[type="radio"] {
+    border: 1px solid #ccc;
+  }
+
+  // Remove the horizontal form styles
+  .form-horizontal .control-group > label {
+    float: none;
+    width: auto;
+    padding-top: 0;
+    text-align: left;
+  }
+  // Move over all input controls and content
+  .form-horizontal .controls {
+    margin-left: 0;
+  }
+  // Move the options list down to align with labels
+  .form-horizontal .control-list {
+    padding-top: 0; // has to be padding because margin collaspes
+  }
+  // Move over buttons in .form-actions to align with .controls
+  .form-horizontal .form-actions {
+    padding-left: 10px;
+    padding-right: 10px;
+  }
+
+  // Modals
+  .modal {
+    position: absolute;
+    top:   10px;
+    left:  10px;
+    right: 10px;
+    width: auto;
+    margin: 0;
+    &.fade.in { top: auto; }
+  }
+  .modal-header .close {
+    padding: 10px;
+    margin: -10px;
+  }
+
+  // Carousel
+  .carousel-caption {
+    position: static;
+  }
+
+}
+
+
+
+// LANDSCAPE PHONE TO SMALL DESKTOP & PORTRAIT TABLET
+// --------------------------------------------------
+
+ at media (max-width: 768px) {
+  // GRID & CONTAINERS
+  // -----------------
+  // Remove width from containers
+  .container {
+    width: auto;
+    padding: 0 20px;
+  }
+  // Fluid rows
+  .row-fluid {
+    width: 100%;
+  }
+  // Undo negative margin on rows
+  .row {
+    margin-left: 0;
+  }
+  // Make all columns even
+  .row > [class*="span"],
+  .row-fluid > [class*="span"] {
+    float: none;
+    display: block;
+    width: auto;
+    margin: 0;
+  }
+}
+
+
+
+// PORTRAIT TABLET TO DEFAULT DESKTOP
+// ----------------------------------
+
+ at media (min-width: 768px) and (max-width: 980px) {
+
+  // Fixed grid
+  #gridSystem > .generate(12, 42px, 20px);
+
+  // Fluid grid
+  #fluidGridSystem > .generate(12, 5.801104972%, 2.762430939%);
+
+  // Input grid
+  #inputGridSystem > .generate(12, 42px, 20px);
+
+}
+
+
+
+// TABLETS AND BELOW
+// -----------------
+ at media (max-width: 980px) {
+
+  // UNFIX THE TOPBAR
+  // ----------------
+  // Remove any padding from the body
+  body {
+    padding-top: 0;
+  }
+  // Unfix the navbar
+  .navbar-fixed-top {
+    position: static;
+    margin-bottom: @baseLineHeight;
+  }
+  .navbar-fixed-top .navbar-inner {
+    padding: 5px;
+  }
+  .navbar .container {
+    width: auto;
+    padding: 0;
+  }
+  // Account for brand name
+  .navbar .brand {
+    padding-left: 10px;
+    padding-right: 10px;
+    margin: 0 0 0 -5px;
+  }
+  // Nav collapse clears brand
+  .navbar .nav-collapse {
+    clear: left;
+  }
+  // Block-level the nav
+  .navbar .nav {
+    float: none;
+    margin: 0 0 (@baseLineHeight / 2);
+  }
+  .navbar .nav > li {
+    float: none;
+  }
+  .navbar .nav > li > a {
+    margin-bottom: 2px;
+  }
+  .navbar .nav > .divider-vertical {
+    display: none;
+  }
+  // Nav and dropdown links in navbar
+  .navbar .nav > li > a,
+  .navbar .dropdown-menu a {
+    padding: 6px 15px;
+    font-weight: bold;
+    color: @navbarLinkColor;
+    .border-radius(3px);
+  }
+  .navbar .dropdown-menu li + li a {
+    margin-bottom: 2px;
+  }
+  .navbar .nav > li > a:hover,
+  .navbar .dropdown-menu a:hover {
+    background-color: @navbarBackground;
+  }
+  // Dropdowns in the navbar
+  .navbar .dropdown-menu {
+    position: static;
+    top: auto;
+    left: auto;
+    float: none;
+    display: block;
+    max-width: none;
+    margin: 0 15px;
+    padding: 0;
+    background-color: transparent;
+    border: none;
+    .border-radius(0);
+    .box-shadow(none);
+  }
+  .navbar .dropdown-menu:before,
+  .navbar .dropdown-menu:after {
+    display: none;
+  }
+  .navbar .dropdown-menu .divider {
+    display: none;
+  }
+  // Forms in navbar
+  .navbar-form,
+  .navbar-search {
+    float: none;
+    padding: (@baseLineHeight / 2) 15px;
+    margin: (@baseLineHeight / 2) 0;
+    border-top: 1px solid @navbarBackground;
+    border-bottom: 1px solid @navbarBackground;
+    @shadow: inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1);
+    .box-shadow(@shadow);
+  }
+  // Pull right (secondary) nav content
+  .navbar .nav.pull-right {
+    float: none;
+    margin-left: 0;
+  }
+  // Static navbar
+  .navbar-static .navbar-inner {
+    padding-left:  10px;
+    padding-right: 10px;
+  }
+  // Navbar button
+  .btn-navbar {
+    display: block;
+  }
+
+  // Hide everything in the navbar save .brand and toggle button */
+  .nav-collapse {
+    overflow: hidden;
+    height: 0;
+  }
+}
+
+
+
+// DEFAULT DESKTOP
+// ---------------
+
+ at media (min-width: 980px) {
+  .nav-collapse.collapse {
+    height: auto !important;
+  }
+}
+
+
+
+// LARGE DESKTOP & UP
+// ------------------
+
+ at media (min-width: 1200px) {
+
+  // Fixed grid
+  #gridSystem > .generate(12, 70px, 30px);
+
+  // Fluid grid
+  #fluidGridSystem > .generate(12, 5.982905983%, 2.564102564%);
+
+  // Input grid
+  #inputGridSystem > .generate(12, 70px, 30px);
+
+  // Thumbnails
+  .thumbnails {
+    margin-left: -30px;
+  }
+  .thumbnails > li {
+    margin-left: 30px;
+  }
+
+}
diff --git a/docs/source/_themes/dropwizard/less/scaffolding.less b/docs/source/_themes/dropwizard/less/scaffolding.less
new file mode 100644
index 0000000..47ce538
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/scaffolding.less
@@ -0,0 +1,29 @@
+// Scaffolding
+// Basic and global styles for generating a grid system, structural layout, and page templates
+// -------------------------------------------------------------------------------------------
+
+
+// STRUCTURAL LAYOUT
+// -----------------
+
+body {
+  margin: 0;
+  font-family: @baseFontFamily;
+  font-size: @baseFontSize;
+  line-height: @baseLineHeight;
+  color: @textColor;
+  background-color: @white;
+}
+
+
+// LINKS
+// -----
+
+a {
+  color: @linkColor;
+  text-decoration: none;
+}
+a:hover {
+  color: @linkColorHover;
+  text-decoration: underline;
+}
diff --git a/docs/source/_themes/dropwizard/less/sprites.less b/docs/source/_themes/dropwizard/less/sprites.less
new file mode 100644
index 0000000..a56216c
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/sprites.less
@@ -0,0 +1,156 @@
+// SPRITES
+// Glyphs and icons for buttons, nav, and more
+// -------------------------------------------
+
+
+// ICONS
+// -----
+
+// All icons receive the styles of the <i> tag with a base class 
+// of .i and are then given a unique class to add width, height, 
+// and background-position. Your resulting HTML will look like
+// <i class="i icon-inbox"></i>.
+
+// For the white version of the  icons, just add the .icon-white class:
+// <i class="i icon-inbox icon-white"></i>
+
+[class^="icon-"] {
+  display: inline-block;
+  width: 14px;
+  height: 14px;
+  vertical-align: text-top;
+  background-image: url(../img/glyphicons-halflings.png);
+  background-position: 14px 14px;
+  background-repeat: no-repeat;
+
+  .ie7-restore-right-whitespace();
+}
+.icon-white {
+  background-image: url(../img/glyphicons-halflings-white.png);
+}
+
+.icon-glass              { background-position: 0      0; }
+.icon-music              { background-position: -24px  0; }
+.icon-search             { background-position: -48px  0; }
+.icon-envelope           { background-position: -72px  0; }
+.icon-heart              { background-position: -96px  0; }
+.icon-star               { background-position: -120px 0; }
+.icon-star-empty         { background-position: -144px 0; }
+.icon-user               { background-position: -168px 0; }
+.icon-film               { background-position: -192px 0; }
+.icon-th-large           { background-position: -216px 0; }
+.icon-th                 { background-position: -240px 0; }
+.icon-th-list            { background-position: -264px 0; }
+.icon-ok                 { background-position: -288px 0; }
+.icon-remove             { background-position: -312px 0; }
+.icon-zoom-in            { background-position: -336px 0; }
+.icon-zoom-out           { background-position: -360px 0; }
+.icon-off                { background-position: -384px 0; }
+.icon-signal             { background-position: -408px 0; }
+.icon-cog                { background-position: -432px 0; }
+.icon-trash              { background-position: -456px 0; }
+
+.icon-home               { background-position: 0      -24px; }
+.icon-file               { background-position: -24px  -24px; }
+.icon-time               { background-position: -48px  -24px; }
+.icon-road               { background-position: -72px  -24px; }
+.icon-download-alt       { background-position: -96px  -24px; }
+.icon-download           { background-position: -120px -24px; }
+.icon-upload             { background-position: -144px -24px; }
+.icon-inbox              { background-position: -168px -24px; }
+.icon-play-circle        { background-position: -192px -24px; }
+.icon-repeat             { background-position: -216px -24px; }
+.icon-refresh            { background-position: -240px -24px; }
+.icon-list-alt           { background-position: -264px -24px; }
+.icon-lock               { background-position: -287px -24px; } // 1px off
+.icon-flag               { background-position: -312px -24px; }
+.icon-headphones         { background-position: -336px -24px; }
+.icon-volume-off         { background-position: -360px -24px; }
+.icon-volume-down        { background-position: -384px -24px; }
+.icon-volume-up          { background-position: -408px -24px; }
+.icon-qrcode             { background-position: -432px -24px; }
+.icon-barcode            { background-position: -456px -24px; }
+
+.icon-tag                { background-position: 0      -48px; }
+.icon-tags               { background-position: -25px  -48px; } // 1px off
+.icon-book               { background-position: -48px  -48px; }
+.icon-bookmark           { background-position: -72px  -48px; }
+.icon-print              { background-position: -96px  -48px; }
+.icon-camera             { background-position: -120px -48px; }
+.icon-font               { background-position: -144px -48px; }
+.icon-bold               { background-position: -167px -48px; } // 1px off
+.icon-italic             { background-position: -192px -48px; }
+.icon-text-height        { background-position: -216px -48px; }
+.icon-text-width         { background-position: -240px -48px; }
+.icon-align-left         { background-position: -264px -48px; }
+.icon-align-center       { background-position: -288px -48px; }
+.icon-align-right        { background-position: -312px -48px; }
+.icon-align-justify      { background-position: -336px -48px; }
+.icon-list               { background-position: -360px -48px; }
+.icon-indent-left        { background-position: -384px -48px; }
+.icon-indent-right       { background-position: -408px -48px; }
+.icon-facetime-video     { background-position: -432px -48px; }
+.icon-picture            { background-position: -456px -48px; }
+
+.icon-pencil             { background-position: 0      -72px; }
+.icon-map-marker         { background-position: -24px  -72px; }
+.icon-adjust             { background-position: -48px  -72px; }
+.icon-tint               { background-position: -72px  -72px; }
+.icon-edit               { background-position: -96px  -72px; }
+.icon-share              { background-position: -120px -72px; }
+.icon-check              { background-position: -144px -72px; }
+.icon-move               { background-position: -168px -72px; }
+.icon-step-backward      { background-position: -192px -72px; }
+.icon-fast-backward      { background-position: -216px -72px; }
+.icon-backward           { background-position: -240px -72px; }
+.icon-play               { background-position: -264px -72px; }
+.icon-pause              { background-position: -288px -72px; }
+.icon-stop               { background-position: -312px -72px; }
+.icon-forward            { background-position: -336px -72px; }
+.icon-fast-forward       { background-position: -360px -72px; }
+.icon-step-forward       { background-position: -384px -72px; }
+.icon-eject              { background-position: -408px -72px; }
+.icon-chevron-left       { background-position: -432px -72px; }
+.icon-chevron-right      { background-position: -456px -72px; }
+
+.icon-plus-sign          { background-position: 0      -96px; }
+.icon-minus-sign         { background-position: -24px  -96px; }
+.icon-remove-sign        { background-position: -48px  -96px; }
+.icon-ok-sign            { background-position: -72px  -96px; }
+.icon-question-sign      { background-position: -96px  -96px; }
+.icon-info-sign          { background-position: -120px -96px; }
+.icon-screenshot         { background-position: -144px -96px; }
+.icon-remove-circle      { background-position: -168px -96px; }
+.icon-ok-circle          { background-position: -192px -96px; }
+.icon-ban-circle         { background-position: -216px -96px; }
+.icon-arrow-left         { background-position: -240px -96px; }
+.icon-arrow-right        { background-position: -264px -96px; }
+.icon-arrow-up           { background-position: -289px -96px; } // 1px off
+.icon-arrow-down         { background-position: -312px -96px; }
+.icon-share-alt          { background-position: -336px -96px; }
+.icon-resize-full        { background-position: -360px -96px; }
+.icon-resize-small       { background-position: -384px -96px; }
+.icon-plus               { background-position: -408px -96px; }
+.icon-minus              { background-position: -433px -96px; }
+.icon-asterisk           { background-position: -456px -96px; }
+
+.icon-exclamation-sign   { background-position: 0      -120px; }
+.icon-gift               { background-position: -24px  -120px; }
+.icon-leaf               { background-position: -48px  -120px; }
+.icon-fire               { background-position: -72px  -120px; }
+.icon-eye-open           { background-position: -96px  -120px; }
+.icon-eye-close          { background-position: -120px -120px; }
+.icon-warning-sign       { background-position: -144px -120px; }
+.icon-plane              { background-position: -168px -120px; }
+.icon-calendar           { background-position: -192px -120px; }
+.icon-random             { background-position: -216px -120px; }
+.icon-comment            { background-position: -240px -120px; }
+.icon-magnet             { background-position: -264px -120px; }
+.icon-chevron-up         { background-position: -288px -120px; }
+.icon-chevron-down       { background-position: -313px -119px; } // 1px off
+.icon-retweet            { background-position: -336px -120px; }
+.icon-shopping-cart      { background-position: -360px -120px; }
+.icon-folder-close       { background-position: -384px -120px; }
+.icon-folder-open        { background-position: -408px -120px; }
+.icon-resize-vertical    { background-position: -432px -119px; }
+.icon-resize-horizontal  { background-position: -456px -118px; }
diff --git a/docs/source/_themes/dropwizard/less/tables.less b/docs/source/_themes/dropwizard/less/tables.less
new file mode 100644
index 0000000..f98564d
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/tables.less
@@ -0,0 +1,139 @@
+//
+// Tables.less
+// Tables for, you guessed it, tabular data
+// ----------------------------------------
+
+
+// BASE TABLES
+// -----------------
+
+table {
+  max-width: 100%;
+  border-collapse: collapse;
+  border-spacing: 0;
+}
+
+// BASELINE STYLES
+// ---------------
+
+.table {
+  width: 100%;
+  margin-bottom: @baseLineHeight;
+  // Cells
+  th,
+  td {
+    padding: 16px;
+    line-height: @baseLineHeight;
+    text-align: left;
+    border-top: 1px solid #ddd;
+  }
+  th {
+    font-weight: bold;
+    vertical-align: bottom;
+  }
+  td {
+    vertical-align: top;
+  }
+  // Remove top border from thead by default
+  thead:first-child tr th,
+  thead:first-child tr td {
+    border-top: 0;
+  }
+  // Account for multiple tbody instances
+  tbody + tbody {
+    border-top: 2px solid #ddd;
+  }
+}
+
+
+
+// CONDENSED TABLE W/ HALF PADDING
+// -------------------------------
+
+.table-condensed {
+  th,
+  td {
+    padding: 4px 5px;
+  }
+}
+
+
+// BORDERED VERSION
+// ----------------
+
+.table-bordered {
+  border: 1px solid #ddd;
+  border-collapse: separate; // Done so we can round those corners!
+  *border-collapse: collapsed; // IE7 can't round corners anyway
+  .border-radius(4px);
+  th + th,
+  td + td,
+  th + td,
+  td + th {
+    border-left: 1px solid #ddd;
+  }
+  // Prevent a double border
+  thead:first-child tr:first-child th,
+  tbody:first-child tr:first-child th,
+  tbody:first-child tr:first-child td {
+    border-top: 0;
+  }
+  // For first th or td in the first row in the first thead or tbody
+  thead:first-child tr:first-child th:first-child,
+  tbody:first-child tr:first-child td:first-child {
+    .border-radius(4px 0 0 0);
+  }
+  thead:first-child tr:first-child th:last-child,
+  tbody:first-child tr:first-child td:last-child {
+    .border-radius(0 4px 0 0);
+  }
+  // For first th or td in the first row in the first thead or tbody
+  thead:last-child tr:last-child th:first-child,
+  tbody:last-child tr:last-child td:first-child {
+    .border-radius(0 0 0 4px);
+  }
+  thead:last-child tr:last-child th:last-child,
+  tbody:last-child tr:last-child td:last-child {
+    .border-radius(0 0 4px 0);
+  }
+}
+
+
+// ZEBRA-STRIPING
+// --------------
+
+// Default zebra-stripe styles (alternating gray and transparent backgrounds)
+.table-striped {
+  tbody {
+    tr:nth-child(odd) td,
+    tr:nth-child(odd) th {
+      background-color: #f9f9f9;
+    }
+  }
+}
+
+
+
+// TABLE CELL SIZING
+// -----------------
+
+// Change the columns
+.tableColumns(@columnSpan: 1) {
+  float: none;
+  width: ((@gridColumnWidth) * @columnSpan) + (@gridGutterWidth * (@columnSpan - 1)) - 16;
+  margin-left: 0;
+}
+table {
+  .span1     { .tableColumns(1); }
+  .span2     { .tableColumns(2); }
+  .span3     { .tableColumns(3); }
+  .span4     { .tableColumns(4); }
+  .span5     { .tableColumns(5); }
+  .span6     { .tableColumns(6); }
+  .span7     { .tableColumns(7); }
+  .span8     { .tableColumns(8); }
+  .span9     { .tableColumns(9); }
+  .span10    { .tableColumns(10); }
+  .span11    { .tableColumns(11); }
+  .span12    { .tableColumns(12); }
+}
diff --git a/docs/source/_themes/dropwizard/less/thumbnails.less b/docs/source/_themes/dropwizard/less/thumbnails.less
new file mode 100644
index 0000000..541fbd6
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/thumbnails.less
@@ -0,0 +1,35 @@
+// THUMBNAILS
+// ----------
+
+.thumbnails {
+  margin-left: -20px;
+  list-style: none;
+  .clearfix();
+}
+.thumbnails > li {
+  float: left;
+  margin: 0 0 @baseLineHeight 20px;
+}
+.thumbnail {
+  display: block;
+  padding: 4px;
+  line-height: 1;
+  border: 1px solid #ddd;
+  .border-radius(4px);
+  .box-shadow(0 1px 1px rgba(0,0,0,.075));
+}
+// Add a hover state for linked versions only
+a.thumbnail:hover {
+  border-color: @linkColor;
+  .box-shadow(0 1px 4px rgba(0,105,214,.25));
+}
+// Images and captions
+.thumbnail > img {
+  display: block;
+  max-width: 100%;
+  margin-left: auto;
+  margin-right: auto;
+}
+.thumbnail .caption {
+  padding: 9px;
+}
diff --git a/docs/source/_themes/dropwizard/less/tooltip.less b/docs/source/_themes/dropwizard/less/tooltip.less
new file mode 100644
index 0000000..5111a19
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/tooltip.less
@@ -0,0 +1,35 @@
+// TOOLTIP
+// ------=
+
+.tooltip {
+  position: absolute;
+  z-index: @zindexTooltip;
+  display: block;
+  visibility: visible;
+  padding: 5px;
+  font-size: 11px;
+  .opacity(0);
+  &.in     { .opacity(80); }
+  &.top    { margin-top:  -2px; }
+  &.right  { margin-left:  2px; }
+  &.bottom { margin-top:   2px; }
+  &.left   { margin-left: -2px; }
+  &.top .tooltip-arrow    { #popoverArrow > .top(); }
+  &.left .tooltip-arrow   { #popoverArrow > .left(); }
+  &.bottom .tooltip-arrow { #popoverArrow > .bottom(); }
+  &.right .tooltip-arrow  { #popoverArrow > .right(); }
+}
+.tooltip-inner {
+  max-width: 200px;
+  padding: 3px 8px;
+  color: @white;
+  text-align: center;
+  text-decoration: none;
+  background-color: @black;
+  .border-radius(4px);
+}
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+}
diff --git a/docs/source/_themes/dropwizard/less/type.less b/docs/source/_themes/dropwizard/less/type.less
new file mode 100644
index 0000000..7841bb0
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/type.less
@@ -0,0 +1,217 @@
+// Typography.less
+// Headings, body text, lists, code, and more for a versatile and durable typography system
+// ----------------------------------------------------------------------------------------
+
+
+// BODY TEXT
+// ---------
+
+p {
+  margin: 0 0 @baseLineHeight / 2;
+  font-family: @baseFontFamily;
+  font-size: @baseFontSize;
+  line-height: @baseLineHeight;
+  small {
+    font-size: @baseFontSize - 2;
+    color: @grayLight;
+  }
+}
+.lead {
+  margin-bottom: @baseLineHeight;
+  font-size: 20px;
+  font-weight: 200;
+  line-height: @baseLineHeight * 1.5;
+}
+
+// HEADINGS
+// --------
+
+h1, h2, h3, h4, h5, h6 {
+  margin: 0;
+  font-weight: bold;
+  color: @grayDark;
+  text-rendering: optimizelegibility; // Fix the character spacing for headings
+  small {
+    font-weight: normal;
+    color: @grayLight;
+  }
+}
+h1 {
+  font-size: 30px;
+  line-height: @baseLineHeight * 2;
+  small {
+    font-size: 18px;
+  }
+}
+h2 {
+  font-size: 24px;
+  line-height: @baseLineHeight * 2;
+  small {
+    font-size: 18px;
+  }
+}
+h3 {
+  line-height: @baseLineHeight * 1.5;
+  font-size: 18px;
+  small {
+    font-size: 14px;
+  }
+}
+h4, h5, h6 {
+  line-height: @baseLineHeight;
+}
+h4 {
+  font-size: 14px;
+  small {
+    font-size: 12px;
+  }
+}
+h5 {
+  font-size: 12px;
+}
+h6 {
+  font-size: 11px;
+  color: @grayLight;
+  text-transform: uppercase;
+}
+
+// Page header
+.page-header {
+  padding-bottom: @baseLineHeight - 1;
+  margin: @baseLineHeight 0;
+  border-bottom: 1px solid @grayLighter;
+}
+.page-header h1 {
+  line-height: 1;
+}
+
+
+
+// LISTS
+// -----
+
+// Unordered and Ordered lists
+ul, ol {
+  padding: 0;
+  margin: 0 0 @baseLineHeight / 2 25px;
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+  margin-bottom: 0;
+}
+ul {
+  list-style: disc;
+}
+ol {
+  list-style: decimal;
+}
+li {
+  line-height: @baseLineHeight;
+}
+ul.unstyled {
+  margin-left: 0;
+  list-style: none;
+}
+
+// Description Lists
+dl {
+  margin-bottom: @baseLineHeight;
+}
+dt,
+dd {
+  line-height: @baseLineHeight;
+}
+dt {
+  font-weight: bold;
+}
+dd {
+  margin-left: @baseLineHeight / 2;
+}
+
+// MISC
+// ----
+
+// Horizontal rules
+hr {
+  margin: @baseLineHeight 0;
+  border: 0;
+  border-top: 1px solid #e5e5e5;
+  border-bottom: 1px solid @white;
+}
+
+// Emphasis
+strong {
+  font-weight: bold;
+}
+em {
+  font-style: italic;
+}
+.muted {
+  color: @grayLight;
+}
+
+// Abbreviations and acronyms
+abbr {
+  font-size: 90%;
+  text-transform: uppercase;
+  border-bottom: 1px dotted #ddd;
+  cursor: help;
+}
+
+// Blockquotes
+blockquote {
+  padding: 0 0 0 15px;
+  margin: 0 0 @baseLineHeight;
+  border-left: 5px solid @grayLighter;
+  p {
+    margin-bottom: 0;
+    #font > .shorthand(16px,300, at baseLineHeight * 1.25);
+  }
+  small {
+    display: block;
+    line-height: @baseLineHeight;
+    color: @grayLight;
+    &:before {
+      content: '\2014 \00A0';
+    }
+  }
+
+  // Float right with text-align: right
+  &.pull-right {
+    float: right;
+    padding-left: 0;
+    padding-right: 15px;
+    border-left: 0;
+    border-right: 5px solid @grayLighter;
+    p,
+    small {
+      text-align: right;
+    }
+  }
+}
+
+// Quotes
+q:before,
+q:after,
+blockquote:before, 
+blockquote:after {
+  content: "";
+}
+
+// Addresses
+address {
+  display: block;
+  margin-bottom: @baseLineHeight;
+  line-height: @baseLineHeight;
+  font-style: normal;
+}
+
+// Misc
+small {
+  font-size: 100%;
+}
+cite {
+  font-style: normal;
+}
diff --git a/docs/source/_themes/dropwizard/less/utilities.less b/docs/source/_themes/dropwizard/less/utilities.less
new file mode 100644
index 0000000..d60d220
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/utilities.less
@@ -0,0 +1,23 @@
+// UTILITY CLASSES
+// ---------------
+
+// Quick floats
+.pull-right {
+  float: right;
+}
+.pull-left {
+  float: left;
+}
+
+// Toggling content
+.hide {
+  display: none;
+}
+.show {
+  display: block;
+}
+
+// Visibility
+.invisible {
+  visibility: hidden;
+}
diff --git a/docs/source/_themes/dropwizard/less/variables.less b/docs/source/_themes/dropwizard/less/variables.less
new file mode 100644
index 0000000..f01c232
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/variables.less
@@ -0,0 +1,99 @@
+// Variables.less
+// Variables to customize the look and feel of Bootstrap
+// -----------------------------------------------------
+
+
+
+// GLOBAL VALUES
+// --------------------------------------------------
+
+// Links
+ at linkColor:             #08c;
+ at linkColorHover:        darken(@linkColor, 15%);
+
+// Grays
+ at black:                 #000;
+ at grayDarker:            #222;
+ at grayDark:              #333;
+ at gray:                  #555;
+ at grayLight:             #999;
+ at grayLighter:           #eee;
+ at white:                 #fff;
+
+// Accent colors
+ at blue:                  #049cdb;
+ at blueDark:              #0064cd;
+ at green:                 #46a546;
+ at red:                   #9d261d;
+ at yellow:                #ffc40d;
+ at orange:                #f89406;
+ at pink:                  #c3325f;
+ at purple:                #7a43b6;
+
+// Typography
+ at baseFontSize:          13px;
+ at baseFontFamily:        "Helvetica Neue", Helvetica, Arial, sans-serif;
+ at baseLineHeight:        18px;
+ at textColor:             @grayDark;
+
+// Buttons
+ at primaryButtonBackground:    @linkColor;
+
+
+
+// COMPONENT VARIABLES
+// --------------------------------------------------
+
+// Z-index master list
+// Used for a bird's eye view of components dependent on the z-axis
+// Try to avoid customizing these :)
+ at zindexDropdown:        1000;
+ at zindexPopover:         1010;
+ at zindexTooltip:         1020;
+ at zindexFixedNavbar:     1030;
+ at zindexModalBackdrop:   1040;
+ at zindexModal:           1050;
+
+// Input placeholder text color
+ at placeholderText:       @grayLight;
+
+// Navbar
+ at navbarHeight:                    40px;
+ at navbarBackground:                @grayDarker;
+ at navbarBackgroundHighlight:       @grayDark;
+
+ at navbarText:                      @grayLight;
+ at navbarLinkColor:                 @grayLight;
+ at navbarLinkColorHover:            @white;
+
+// Form states and alerts
+ at warningText:             #c09853;
+ at warningBackground:       #fcf8e3;
+ at warningBorder:           darken(spin(@warningBackground, -10), 3%);
+
+ at errorText:               #b94a48;
+ at errorBackground:         #f2dede;
+ at errorBorder:             darken(spin(@errorBackground, -10), 3%);
+
+ at successText:             #468847;
+ at successBackground:       #dff0d8;
+ at successBorder:           darken(spin(@successBackground, -10), 5%);
+
+ at infoText:                #3a87ad;
+ at infoBackground:          #d9edf7;
+ at infoBorder:              darken(spin(@infoBackground, -10), 7%);
+
+
+
+// GRID
+// --------------------------------------------------
+
+// Default 940px grid
+ at gridColumns:             12;
+ at gridColumnWidth:         60px;
+ at gridGutterWidth:         20px;
+ at gridRowWidth:            (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1));
+
+// Fluid grid
+ at fluidGridColumnWidth:    6.382978723%;
+ at fluidGridGutterWidth:    2.127659574%;
diff --git a/docs/source/_themes/dropwizard/less/wells.less b/docs/source/_themes/dropwizard/less/wells.less
new file mode 100644
index 0000000..244b8ca
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/wells.less
@@ -0,0 +1,17 @@
+// WELLS
+// -----
+
+.well {
+  min-height: 20px;
+  padding: 19px;
+  margin-bottom: 20px;
+  background-color: #f5f5f5;
+  border: 1px solid #eee;
+  border: 1px solid rgba(0,0,0,.05);
+  .border-radius(4px);
+  .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
+  blockquote {
+    border-color: #ddd;
+    border-color: rgba(0,0,0,.15);
+  }
+}
diff --git a/docs/source/_themes/dropwizard/page.html b/docs/source/_themes/dropwizard/page.html
new file mode 100644
index 0000000..f6e7a68
--- /dev/null
+++ b/docs/source/_themes/dropwizard/page.html
@@ -0,0 +1,13 @@
+{#
+    basic/page.html
+    ~~~~~~~~~~~~~~~
+
+    Master template for simple pages.
+
+    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+#}
+{% extends "layout.html" %}
+{% block body %}
+  {{ body }}
+{% endblock %}
diff --git a/docs/source/_themes/dropwizard/search.html b/docs/source/_themes/dropwizard/search.html
new file mode 100644
index 0000000..4cdc693
--- /dev/null
+++ b/docs/source/_themes/dropwizard/search.html
@@ -0,0 +1,56 @@
+{#
+    basic/search.html
+    ~~~~~~~~~~~~~~~~~
+
+    Template for the search page.
+
+    :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
+    :license: BSD, see LICENSE for details.
+#}
+{% extends "layout.html" %}
+{% set title = _('Search') %}
+{% set script_files = script_files + ['_static/searchtools.js'] %}
+{% block extrahead %}
+  <script type="text/javascript">
+    jQuery(function() { Search.loadIndex("{{ pathto('searchindex.js', 1) }}"); });
+  </script>
+  {{ super() }}
+{% endblock %}
+{% block body %}
+  <h1 id="search-documentation">{{ _('Search') }}</h1>
+  <div id="fallback" class="admonition warning">
+  <script type="text/javascript">$('#fallback').hide();</script>
+  <p>
+    {% trans %}Please activate JavaScript to enable the search
+    functionality.{% endtrans %}
+  </p>
+  </div>
+  <p>
+    {% trans %}From here you can search these documents. Enter your search
+    words into the box below and click "search". Note that the search
+    function will automatically search for all of the words. Pages
+    containing fewer words won't appear in the result list.{% endtrans %}
+  </p>
+  <form action="" method="get">
+    <input type="text" name="q" value="" />
+    <input type="submit" value="{{ _('search') }}" />
+    <span id="search-progress" style="padding-left: 10px"></span>
+  </form>
+  {% if search_performed %}
+    <h2>{{ _('Search Results') }}</h2>
+    {% if not search_results %}
+      <p>{{ _('Your search did not match any results.') }}</p>
+    {% endif %}
+  {% endif %}
+  <div id="search-results">
+  {% if search_results %}
+    <ul>
+    {% for href, caption, context in search_results %}
+      <li><a href="{{ pathto(item.href) }}">{{ caption }}</a>
+        <div class="context">{{ context|e }}</div>
+      </li>
+    {% endfor %}
+    </ul>
+  {% endif %}
+  </div>
+{% endblock %}
diff --git a/docs/source/_themes/dropwizard/static/dropwizard.css b/docs/source/_themes/dropwizard/static/dropwizard.css
new file mode 100644
index 0000000..0b3d58e
--- /dev/null
+++ b/docs/source/_themes/dropwizard/static/dropwizard.css
@@ -0,0 +1,305 @@
+article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
+audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
+audio:not([controls]){display:none;}
+html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
+a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+a:hover,a:active{outline:0;}
+sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline;}
+sup{top:-0.5em;}
+sub{bottom:-0.25em;}
+img{max-width:100%;height:auto;border:0;-ms-interpolation-mode:bicubic;}
+button,input,select,textarea{margin:0;font-size:100%;vertical-align:middle;}
+button,input{*overflow:visible;line-height:normal;}
+button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0;}
+button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
+input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
+input[type="search"]::-webkit-search-decoration,input[type="search"]::-webkit-search-cancel-button{-webkit-appearance:none;}
+textarea{overflow:auto;vertical-align:top;}
+body{margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;color:#333333;background-color:#ffffff;}
+a{color:#0088cc;text-decoration:none;}
+a:hover{color:#005580;text-decoration:underline;}
+.row{margin-left:-20px;*zoom:1;}.row:before,.row:after{display:table;content:"";}
+.row:after{clear:both;}
+[class*="span"]{float:left;margin-left:20px;}
+.span1{width:60px;}
+.span2{width:140px;}
+.span3{width:220px;}
+.span4{width:300px;}
+.span5{width:380px;}
+.span6{width:460px;}
+.span7{width:540px;}
+.span8{width:620px;}
+.span9{width:700px;}
+.span10{width:780px;}
+.span11{width:860px;}
+.span12,.container{width:940px;}
+.offset1{margin-left:100px;}
+.offset2{margin-left:180px;}
+.offset3{margin-left:260px;}
+.offset4{margin-left:340px;}
+.offset5{margin-left:420px;}
+.offset6{margin-left:500px;}
+.offset7{margin-left:580px;}
+.offset8{margin-left:660px;}
+.offset9{margin-left:740px;}
+.offset10{margin-left:820px;}
+.offset11{margin-left:900px;}
+.row-fluid{width:100%;*zoom:1;}.row-fluid:before,.row-fluid:after{display:table;content:"";}
+.row-fluid:after{clear:both;}
+.row-fluid>[class*="span"]{float:left;margin-left:2.127659574%;}
+.row-fluid>[class*="span"]:first-child{margin-left:0;}
+.row-fluid .span1{width:6.382978723%;}
+.row-fluid .span2{width:14.89361702%;}
+.row-fluid .span3{width:23.404255317%;}
+.row-fluid .span4{width:31.914893614%;}
+.row-fluid .span5{width:40.425531911%;}
+.row-fluid .span6{width:48.93617020799999%;}
+.row-fluid .span7{width:57.446808505%;}
+.row-fluid .span8{width:65.95744680199999%;}
+.row-fluid .span9{width:74.468085099%;}
+.row-fluid .span10{width:82.97872339599999%;}
+.row-fluid .span11{width:91.489361693%;}
+.row-fluid .span12{width:99.99999998999999%;}
+.container{width:940px;margin-left:auto;margin-right:auto;*zoom:1;}.container:before,.container:after{display:table;content:"";}
+.container:after{clear:both;}
+.container-fluid{padding-left:20px;padding-right:20px;*zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";}
+.container-fluid:after{clear:both;}
+p{margin:0 0 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;line-height:18px;}p small{font-size:11px;color:#999999;}
+.lead{margin-bottom:18px;font-size:20px;font-weight:200;line-height:27px;}
+h1,h2,h3,h4,h5,h6{margin:0;font-weight:bold;color:#333333;text-rendering:optimizelegibility;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-weight:normal;color:#999999;}
+h1{font-size:30px;line-height:36px;}h1 small{font-size:18px;}
+h2{font-size:24px;line-height:36px;}h2 small{font-size:18px;}
+h3{line-height:27px;font-size:18px;}h3 small{font-size:14px;}
+h4,h5,h6{line-height:18px;}
+h4{font-size:14px;}h4 small{font-size:12px;}
+h5{font-size:12px;}
+h6{font-size:11px;color:#999999;text-transform:uppercase;}
+.page-header{padding-bottom:17px;margin:18px 0;border-bottom:1px solid #eeeeee;}
+.page-header h1{line-height:1;}
+ul,ol{padding:0;margin:0 0 9px 25px;}
+ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
+ul{list-style:disc;}
+ol{list-style:decimal;}
+li{line-height:18px;}
+ul.unstyled{margin-left:0;list-style:none;}
+dl{margin-bottom:18px;}
+dt,dd{line-height:18px;}
+dt{font-weight:bold;}
+dd{margin-left:9px;}
+hr{margin:18px 0;border:0;border-top:1px solid #e5e5e5;border-bottom:1px solid #ffffff;}
+strong{font-weight:bold;}
+em{font-style:italic;}
+.muted{color:#999999;}
+abbr{font-size:90%;text-transform:uppercase;border-bottom:1px dotted #ddd;cursor:help;}
+blockquote{padding:0 0 0 15px;margin:0 0 18px;border-left:5px solid #eeeeee;}blockquote p{margin-bottom:0;font-size:16px;font-weight:300;line-height:22.5px;}
+blockquote small{display:block;line-height:18px;color:#999999;}blockquote small:before{content:'\2014 \00A0';}
+blockquote.pull-right{float:right;padding-left:0;padding-right:15px;border-left:0;border-right:5px solid #eeeeee;}blockquote.pull-right p,blockquote.pull-right small{text-align:right;}
+q:before,q:after,blockquote:before,blockquote:after{content:"";}
+address{display:block;margin-bottom:18px;line-height:18px;font-style:normal;}
+small{font-size:100%;}
+cite{font-style:normal;}
+.code-and-pre,pre{padding:0 3px 2px;font-family:"Panic Sans",Menlo,Monaco,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
+.code,code{padding:0 3px 2px;font-family:"Panic Sans",Menlo,Monaco,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
+pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12px;line-height:18px;background-color:#f5f5f5;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;white-space:pre;white-space:pre-wrap;word-break:break-all;}pre.prettyprint{margin-bottom:18px;}
+pre code{padding:0;background-color:transparent;}
+table{max-width:100%;border-collapse:collapse;border-spacing:0;}
+.table{width:100%;margin-bottom:18px;}.table th,.table td{padding:16px;line-height:18px;text-align:left;border-top:1px solid #ddd;}
+.table th{font-weight:bold;vertical-align:bottom;}
+.table td{vertical-align:top;}
+.table thead:first-child tr th,.table thead:first-child tr td{border-top:0;}
+.table tbody+tbody{border-top:2px solid #ddd;}
+.table-condensed th,.table-condensed td{padding:4px 5px;}
+.table-bordered{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapsed;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.table-bordered th+th,.table-bordered td+td,.table-bordered th+td,.table-bordered td+th{border-left:1px solid #ddd;}
+.table-bordered thead:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child th,.table-bordered tbody:first-child tr:first-child td{border-top:0;}
+.table-bordered thead:first-child tr:first-child th:first-child,.table-bordered tbody:first-child tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
+.table-bordered thead:first-child tr:first-child th:last-child,.table-bordered tbody:first-child tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
+.table-bordered thead:last-child tr:last-child th:first-child,.table-bordered tbody:last-child tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
+.table-bordered thead:last-child tr:last-child th:last-child,.table-bordered tbody:last-child tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
+.table-striped tbody tr:nth-child(odd) td,.table-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
+table .span1{float:none;width:44px;margin-left:0;}
+table .span2{float:none;width:124px;margin-left:0;}
+table .span3{float:none;width:204px;margin-left:0;}
+table .span4{float:none;width:284px;margin-left:0;}
+table .span5{float:none;width:364px;margin-left:0;}
+table .span6{float:none;width:444px;margin-left:0;}
+table .span7{float:none;width:524px;margin-left:0;}
+table .span8{float:none;width:604px;margin-left:0;}
+table .span9{float:none;width:684px;margin-left:0;}
+table .span10{float:none;width:764px;margin-left:0;}
+table .span11{float:none;width:844px;margin-left:0;}
+table .span12{float:none;width:924px;margin-left:0;}
+.btn{display:inline-block;padding:4px 10px 4px;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);background-color:#fafafa;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff [...]
+.btn:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;}
+.btn:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;}
+.btn.active,.btn:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;color:rgba(0, 0, 0, 0.5);outline:0;}
+.btn.disabled,.btn[disabled]{cursor:default;background-image:none;background-color:#e6e6e6;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
+.btn-large{padding:9px 14px;font-size:15px;line-height:normal;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.btn-large .icon{margin-top:1px;}
+.btn-small{padding:5px 9px;font-size:11px;line-height:16px;}
+.btn-small .icon{margin-top:-1px;}
+.btn-primary,.btn-primary:hover,.btn-warning,.btn-warning:hover,.btn-danger,.btn-danger:hover,.btn-success,.btn-success:hover,.btn-info,.btn-info:hover{text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;}
+.btn-primary.active,.btn-warning.active,.btn-danger.active,.btn-success.active,.btn-info.active{color:rgba(255, 255, 255, 0.75);}
+.btn-primary{background-color:#006dcc;background-image:-moz-linear-gradient(top, #0088cc, #0044cc);background-image:-ms-linear-gradient(top, #0088cc, #0044cc);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));background-image:-webkit-linear-gradient(top, #0088cc, #0044cc);background-image:-o-linear-gradient(top, #0088cc, #0044cc);background-image:linear-gradient(top, #0088cc, #0044cc);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gra [...]
+.btn-primary:active,.btn-primary.active{background-color:#003399 \9;}
+.btn-warning{background-color:#faa732;background-image:-moz-linear-gradient(top, #fbb450, #f89406);background-image:-ms-linear-gradient(top, #fbb450, #f89406);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));background-image:-webkit-linear-gradient(top, #fbb450, #f89406);background-image:-o-linear-gradient(top, #fbb450, #f89406);background-image:linear-gradient(top, #fbb450, #f89406);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gra [...]
+.btn-warning:active,.btn-warning.active{background-color:#c67605 \9;}
+.btn-danger{background-color:#da4f49;background-image:-moz-linear-gradient(top, #ee5f5b, #bd362f);background-image:-ms-linear-gradient(top, #ee5f5b, #bd362f);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ee5f5b), to(#bd362f));background-image:-webkit-linear-gradient(top, #ee5f5b, #bd362f);background-image:-o-linear-gradient(top, #ee5f5b, #bd362f);background-image:linear-gradient(top, #ee5f5b, #bd362f);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.grad [...]
+.btn-danger:active,.btn-danger.active{background-color:#942a25 \9;}
+.btn-success{background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gra [...]
+.btn-success:active,.btn-success.active{background-color:#408140 \9;}
+.btn-info{background-color:#49afcd;background-image:-moz-linear-gradient(top, #5bc0de, #2f96b4);background-image:-ms-linear-gradient(top, #5bc0de, #2f96b4);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#5bc0de), to(#2f96b4));background-image:-webkit-linear-gradient(top, #5bc0de, #2f96b4);background-image:-o-linear-gradient(top, #5bc0de, #2f96b4);background-image:linear-gradient(top, #5bc0de, #2f96b4);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradie [...]
+.btn-info:active,.btn-info.active{background-color:#24748c \9;}
+button.btn,input[type="submit"].btn{*padding-top:2px;*padding-bottom:2px;}button.btn::-moz-focus-inner,input[type="submit"].btn::-moz-focus-inner{padding:0;border:0;}
+button.btn.large,input[type="submit"].btn.large{*padding-top:7px;*padding-bottom:7px;}
+button.btn.small,input[type="submit"].btn.small{*padding-top:3px;*padding-bottom:3px;}
+.nav{margin-left:0;margin-bottom:18px;list-style:none;}
+.nav>li>a{display:block;}
+.nav>li>a:hover{text-decoration:none;background-color:#eeeeee;}
+.nav-list{padding-left:14px;padding-right:14px;margin-bottom:0;}
+.nav-list>li>a,.nav-list .nav-header{display:block;padding:3px 15px;margin-left:-15px;margin-right:-15px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
+.nav-list .nav-header{font-size:11px;font-weight:bold;line-height:18px;color:#999999;text-transform:uppercase;}
+.nav-list>li+.nav-header{margin-top:9px;}
+.nav-list .active>a{color:#ffffff;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.2);background-color:#0088cc;}
+.nav-list .icon{margin-right:2px;}
+.nav-tabs,.nav-pills{*zoom:1;}.nav-tabs:before,.nav-pills:before,.nav-tabs:after,.nav-pills:after{display:table;content:"";}
+.nav-tabs:after,.nav-pills:after{clear:both;}
+.nav-tabs>li,.nav-pills>li{float:left;}
+.nav-tabs>li>a,.nav-pills>li>a{padding-right:12px;padding-left:12px;margin-right:2px;line-height:14px;}
+.nav-tabs{border-bottom:1px solid #ddd;}
+.nav-tabs>li{margin-bottom:-1px;}
+.nav-tabs>li>a{padding-top:9px;padding-bottom:9px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #dddddd;}
+.nav-tabs>.active>a,.nav-tabs>.active>a:hover{color:#555555;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
+.nav-pills>li>a{padding-top:8px;padding-bottom:8px;margin-top:2px;margin-bottom:2px;-webkit-border-radius:5px;-moz-border-radius:5px;border-radius:5px;}
+.nav-pills .active>a,.nav-pills .active>a:hover{color:#ffffff;background-color:#0088cc;}
+.nav-stacked>li{float:none;}
+.nav-stacked>li>a{margin-right:0;}
+.nav-tabs.nav-stacked{border-bottom:0;}
+.nav-tabs.nav-stacked>li>a{border:1px solid #ddd;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.nav-tabs.nav-stacked>li:first-child>a{-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}
+.nav-tabs.nav-stacked>li:last-child>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}
+.nav-tabs.nav-stacked>li>a:hover{border-color:#ddd;z-index:2;}
+.nav-pills.nav-stacked>li>a{margin-bottom:3px;}
+.nav-pills.nav-stacked>li:last-child>a{margin-bottom:1px;}
+.nav-tabs .dropdown-menu,.nav-pills .dropdown-menu{margin-top:1px;border-width:1px;}
+.nav-pills .dropdown-menu{-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.nav-tabs .dropdown-toggle .caret,.nav-pills .dropdown-toggle .caret{border-top-color:#0088cc;margin-top:6px;}
+.nav-tabs .dropdown-toggle:hover .caret,.nav-pills .dropdown-toggle:hover .caret{border-top-color:#005580;}
+.nav-tabs .active .dropdown-toggle .caret,.nav-pills .active .dropdown-toggle .caret{border-top-color:#333333;}
+.nav>.dropdown.active>a:hover{color:#000000;cursor:pointer;}
+.nav-tabs .open .dropdown-toggle,.nav-pills .open .dropdown-toggle,.nav>.open.active>a:hover{color:#ffffff;background-color:#999999;border-color:#999999;}
+.nav .open .caret,.nav .open.active .caret,.nav .open a:hover .caret{border-top-color:#ffffff;opacity:1;filter:alpha(opacity=100);}
+.tabs-stacked .open>a:hover{border-color:#999999;}
+.tabbable{*zoom:1;}.tabbable:before,.tabbable:after{display:table;content:"";}
+.tabbable:after{clear:both;}
+.tabs-below .nav-tabs,.tabs-right .nav-tabs,.tabs-left .nav-tabs{border-bottom:0;}
+.tab-content>.tab-pane,.pill-content>.pill-pane{display:none;}
+.tab-content>.active,.pill-content>.active{display:block;}
+.tabs-below .nav-tabs{border-top:1px solid #ddd;}
+.tabs-below .nav-tabs>li{margin-top:-1px;margin-bottom:0;}
+.tabs-below .nav-tabs>li>a{-webkit-border-radius:0 0 4px 4px;-moz-border-radius:0 0 4px 4px;border-radius:0 0 4px 4px;}.tabs-below .nav-tabs>li>a:hover{border-bottom-color:transparent;border-top-color:#ddd;}
+.tabs-below .nav-tabs .active>a,.tabs-below .nav-tabs .active>a:hover{border-color:transparent #ddd #ddd #ddd;}
+.tabs-left .nav-tabs>li,.tabs-right .nav-tabs>li{float:none;}
+.tabs-left .nav-tabs>li>a,.tabs-right .nav-tabs>li>a{min-width:74px;margin-right:0;margin-bottom:3px;}
+.tabs-left .nav-tabs{float:left;margin-right:19px;border-right:1px solid #ddd;}
+.tabs-left .nav-tabs>li>a{margin-right:-1px;-webkit-border-radius:4px 0 0 4px;-moz-border-radius:4px 0 0 4px;border-radius:4px 0 0 4px;}
+.tabs-left .nav-tabs>li>a:hover{border-color:#eeeeee #dddddd #eeeeee #eeeeee;}
+.tabs-left .nav-tabs .active>a,.tabs-left .nav-tabs .active>a:hover{border-color:#ddd transparent #ddd #ddd;*border-right-color:#ffffff;}
+.tabs-right .nav-tabs{float:right;margin-left:19px;border-left:1px solid #ddd;}
+.tabs-right .nav-tabs>li>a{margin-left:-1px;-webkit-border-radius:0 4px 4px 0;-moz-border-radius:0 4px 4px 0;border-radius:0 4px 4px 0;}
+.tabs-right .nav-tabs>li>a:hover{border-color:#eeeeee #eeeeee #eeeeee #dddddd;}
+.tabs-right .nav-tabs .active>a,.tabs-right .nav-tabs .active>a:hover{border-color:#ddd #ddd #ddd transparent;*border-left-color:#ffffff;}
+.navbar{overflow:visible;margin-bottom:18px;}
+.navbar-inner{padding-left:20px;padding-right:20px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);background-repeat:repeat-x;filter [...]
+.btn-navbar{display:none;float:right;padding:7px 10px;margin-left:5px;margin-right:5px;background-color:#2c2c2c;background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#333333), to(#222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #22222 [...]
+.btn-navbar:active,.btn-navbar.active{background-color:#080808 \9;}
+.btn-navbar .icon-bar{display:block;width:18px;height:2px;background-color:#f5f5f5;-webkit-border-radius:1px;-moz-border-radius:1px;border-radius:1px;-webkit-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);-moz-box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);box-shadow:0 1px 0 rgba(0, 0, 0, 0.25);}
+.btn-navbar .icon-bar+.icon-bar{margin-top:3px;}
+.nav-collapse.collapse{height:auto;}
+.navbar .brand:hover{text-decoration:none;}
+.navbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;font-size:20px;font-weight:200;line-height:1;color:#ffffff;}
+.navbar .navbar-text{margin-bottom:0;line-height:40px;color:#999999;}.navbar .navbar-text a:hover{color:#ffffff;background-color:transparent;}
+.navbar .btn,.navbar .btn-group{margin-top:5px;}
+.navbar .btn-group .btn{margin-top:0;}
+.navbar-form{margin-bottom:0;*zoom:1;}.navbar-form:before,.navbar-form:after{display:table;content:"";}
+.navbar-form:after{clear:both;}
+.navbar-form input,.navbar-form select{display:inline-block;margin-top:5px;margin-bottom:0;}
+.navbar-form .radio,.navbar-form .checkbox{margin-top:5px;}
+.navbar-form input[type="image"],.navbar-form input[type="checkbox"],.navbar-form input[type="radio"]{margin-top:3px;}
+.navbar-search{position:relative;float:left;margin-top:6px;margin-bottom:0;}.navbar-search .search-query{padding:4px 9px;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:1;color:#ffffff;color:rgba(255, 255, 255, 0.75);background:#666;background:rgba(255, 255, 255, 0.3);border:1px solid #111;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px  [...]
+.navbar-search .search-query ::-webkit-input-placeholder{color:#eeeeee;}
+.navbar-search .search-query:hover{color:#ffffff;background-color:#999999;background-color:rgba(255, 255, 255, 0.5);}
+.navbar-search .search-query:focus,.navbar-search .search-query.focused{padding:5px 10px;color:#333333;text-shadow:0 1px 0 #ffffff;background-color:#ffffff;border:0;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);outline:0;}
+.navbar-fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030;}
+.navbar-fixed-top .navbar-inner{padding-left:0;padding-right:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;}
+.navbar .nav{position:relative;left:0;display:block;float:left;margin:0 10px 0 0;}
+.navbar .nav.pull-right{float:right;}
+.navbar .nav>li{display:block;float:left;}
+.navbar .nav>li>a{float:none;padding:10px 10px 11px;line-height:19px;color:#999999;text-decoration:none;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
+.navbar .nav>li>a:hover{background-color:transparent;color:#ffffff;text-decoration:none;}
+.navbar .nav .active>a,.navbar .nav .active>a:hover{color:#ffffff;text-decoration:none;background-color:#222222;background-color:rgba(0, 0, 0, 0.5);}
+.navbar .divider-vertical{height:40px;width:1px;margin:0 9px;overflow:hidden;background-color:#222222;border-right:1px solid #333333;}
+.navbar .nav.pull-right{margin-left:10px;margin-right:0;}
+.navbar .dropdown-menu{margin-top:1px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.navbar .dropdown-menu:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0, 0, 0, 0.2);position:absolute;top:-7px;left:9px;}
+.navbar .dropdown-menu:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #ffffff;position:absolute;top:-6px;left:10px;}
+.navbar .nav .dropdown-toggle .caret,.navbar .nav .open.dropdown .caret{border-top-color:#ffffff;}
+.navbar .nav .active .caret{opacity:1;filter:alpha(opacity=100);}
+.navbar .nav .open>.dropdown-toggle,.navbar .nav .active>.dropdown-toggle,.navbar .nav .open.active>.dropdown-toggle{background-color:transparent;}
+.navbar .nav .active>.dropdown-toggle:hover{color:#ffffff;}
+.navbar .nav.pull-right .dropdown-menu{left:auto;right:0;}.navbar .nav.pull-right .dropdown-menu:before{left:auto;right:12px;}
+.navbar .nav.pull-right .dropdown-menu:after{left:auto;right:13px;}
+.hero-unit{padding:60px;margin-bottom:30px;background-color:#f5f5f5;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;}
+.hero-unit p{font-size:18px;font-weight:200;line-height:27px;}
+.pull-right{float:right;}
+.pull-left{float:left;}
+.hide{display:none;}
+.show{display:block;}
+.invisible{visibility:hidden;}
+#call-to-action{text-align:right;}
+a.headerlink{display:none;}
+#title{color:#ffffff;}
+.hero-unit h1{padding-bottom:20px ! important;}
+#top-bar small{color:#f8f8ff;text-shadow:0px -1px 0px #5f0c17;}
+.admonition{padding:14px 35px 14px 14px;margin-bottom:18px;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);background-color:#fcf8e3;border:1px solid #fbeed5;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
+.admonition .admonition-title{font-size:14pt;font-weight:bold;}
+.admonition.note .admonition-title,.admonition-todo .admonition-title{color:#c09853;}
+.admonition.tip,.admonition.hint{background-color:#dff0d8;border-color:#d6e9c6;}
+.admonition.tip .admonition-title,.admonition.hint .admonition-title{color:#468847;}
+.admonition.error,.admonition.warning,.admonition.caution,.admonition.danger,.admonition.attention{background-color:#f2dede;border-color:#eed3d7;}
+.admonition.error .admonition-title,.admonition.warning .admonition-title,.admonition.caution .admonition-title,.admonition.danger .admonition-title,.admonition.attention .admonition-title{color:#b94a48;}
+.admonition.important{background-color:#d9edf7;border-color:#bce8f1;}
+.admonition.important .admonition-title{color:#3a87ad;}
+.admonition>p,.admonition>ul{margin-bottom:0;}
+.admonition p+p{margin-top:5px;}
+a.internal.reference>em{font-style:normal ! important;text-decoration:none ! important;}
+tt{padding:0 3px 2px;font-family:"Panic Sans",Menlo,Monaco,Consolas,"Courier New",monospace;font-size:12px;color:#333333;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;color:#d14;background-color:#f7f7f9;border:1px solid #e1e1e8;}
+.section>p,.section ul li,.admonition p,.section dt,.section dl{font-size:13pt;line-height:18pt;}
+.section tt{font-size:11pt;line-height:11pt;}
+.section>*{margin-bottom:20px;}
+pre{font-family:'Panic Sans',Menlo,Monaco,Consolas,Andale Mono,Courier New,monospace !important;font-size:12pt !important;line-height:22px !important;display:block !important;width:auto !important;height:auto !important;overflow:auto !important;white-space:pre !important;word-wrap:normal !important;}
+#body h1,h1 tt{font-size:28pt;}
+h1 tt{background-color:transparent;font-size:26pt !important;}
+#body h2{font-size:24pt;}
+h2 tt{background-color:transparent;font-size:22pt !important;}
+#body h3{font-size:20pt;}
+h3 tt{background-color:transparent;font-size:18pt !important;}
+#body h4{font-size:16pt;}
+h4 tt{background-color:transparent;font-size:14pt !important;}
+#sidebar tt{color:#08c;background-color:transparent;}
+.hero-unit .toctree-wrapper{text-align:center;}
+.hero-unit li{display:inline;list-style-type:none;padding-right:20px;}
+.hero-unit li a{display:inline-block;padding:4px 10px 4px;font-size:13px;line-height:18px;color:#333333;text-align:center;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);background-color:#fafafa;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffff [...]
+.hero-unit li a:hover,.hero-unit li a:active,.hero-unit li a.active,.hero-unit li a.disabled,.hero-unit li a[disabled]{background-color:#51a351;}
+.hero-unit li a:active,.hero-unit li a.active{background-color:#408140 \9;}
+.hero-unit li a:hover{color:#333333;text-decoration:none;background-color:#e6e6e6;background-position:0 -15px;-webkit-transition:background-position 0.1s linear;-moz-transition:background-position 0.1s linear;-ms-transition:background-position 0.1s linear;-o-transition:background-position 0.1s linear;transition:background-position 0.1s linear;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);backgr [...]
+.hero-unit li a:hover:active,.hero-unit li a:hover.active{background-color:#408140 \9;}
+.hero-unit li a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;background-color:#5bb75b;background-image:-moz-linear-gradient(top, #62c462, #51a351);background-image:-ms-linear-gradient(top, #62c462, #51a351);background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#62c462), to(#51a351));background-image:-webkit-linear-gradient(top, #62c462, #51a351);background-image:-o-linear-gradient(top, [...]
+.hero-unit li a:focus:active,.hero-unit li a:focus.active{background-color:#408140 \9;}
+.hero-unit li a:active{background-image:none;-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.15),0 1px 2px rgba(0, 0, 0, 0.05);background-color:#e6e6e6;background-color:#d9d9d9 \9;color:rgba(0, 0, 0, 0.5);outline:0;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);color:#ffffff;background-color:#5bb75b;background-image:-moz-linear-gradie [...]
+.hero-unit li a:active:active,.hero-unit li a:active.active{background-color:#408140 \9;}
+.hero-unit li a:after{content:" »";}
+table.docutils{border:1px solid #DDD;width:100%;margin-bottom:18px;}table.docutils th,table.docutils td{padding:16px;line-height:18px;text-align:left;border-top:1px solid #ddd;}
+table.docutils th{font-weight:bold;vertical-align:bottom;}
+table.docutils td{vertical-align:top;}
+table.docutils thead:first-child tr th,table.docutils thead:first-child tr td{border-top:0;}
+table.docutils tbody+tbody{border-top:2px solid #ddd;}
+table.docutils tbody tr:nth-child(odd) td,table.docutils tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
diff --git a/docs/source/_themes/dropwizard/theme.conf b/docs/source/_themes/dropwizard/theme.conf
new file mode 100644
index 0000000..58d0399
--- /dev/null
+++ b/docs/source/_themes/dropwizard/theme.conf
@@ -0,0 +1,16 @@
+[theme]
+inherit = none
+stylesheet = dropwizard.css
+pygments_style = trac
+
+[options]
+tagline = Your tagline here.
+gradient_start = #9b4853
+gradient_end = #5f0c17
+gradient_text = #ffffff
+gradient_bg = #7D2A35
+gradient_shadow = #fff
+landing_logo = logo.png
+landing_logo_width = 150px
+github_page = https://github.com/yay
+mailing_list = http://groups.google.com/yay
diff --git a/docs/source/about/contributors.rst b/docs/source/about/contributors.rst
new file mode 100644
index 0000000..d9d6c23
--- /dev/null
+++ b/docs/source/about/contributors.rst
@@ -0,0 +1,57 @@
+.. _about-contributors:
+
+############
+Contributors
+############
+
+Many, many thanks to:
+
+* `Adam Jordens <https://github.com/ajordens>`_
+* `Adam Marcus <https://github.com/marcua>`_
+* `Alex Heneveld <https://github.com/ahgittin>`_
+* `Anders Hedström <https://github.com/andershedstrom>`_
+* `Andrei Savu <https://github.com/andreisavu>`_
+* `Andrew Clay Shafer <https://github.com/littleidea>`_.
+* `Armando Singer <https://github.com/asinger>`_
+* `Arun Horne <https://github.com/arunh>`_
+* `Brandon Beck <https://github.com/bbeck>`_
+* `Brian McCallister <https://github.com/brianm>`_
+* `Brian O'Neill <https://github.com/boneill42>`_
+* `Bruce Ritchie <https://github.com/Omega359>`_
+* `Cagatay Kavukcuoglu <https://github.com/tinkerware>`_
+* `Cameron Fieber <https://github.com/cfieber>`_
+* `Cemalettin Koc <https://github.com/Cemo>`_
+* `Chris Gray <https://github.com/chrisgray>`_
+* `Chris Tierney <https://github.com/christierney>`_
+* `Christopher Currie <https://github.com/christophercurrie>`_
+* `Christopher Elkins <https://github.com/celkins>`_
+* `Collin VanDyck <https://github.com/collinvandyck>`_
+* `Dale Wijnand <https://github.com/dwijnand>`_
+* `Dan Everton <https://github.com/deverton>`_
+* `David Morgantini <https://github.com/dmorgantini>`_
+* `David Stendardi <https://github.com/dstendardi>`_
+* `Derek Stainer <https://github.com/dstainer>`_
+* `Eric Tschetter <https://github.com/metamx>`_
+* `Fredrik Sundberg <https://github.com/KingBuzzer>`_
+* `Hal Hildebrand <https://github.com/Hellblazer>`_
+* `Ian Eure <https://github.com/ieure>`_
+* `James Ward <https://github.com/jamesward>`_
+* `Jared Stehler <https://github.com/cengageng>`_
+* `Jochen Schalanda <https://github.com/joschi>`_
+* `Joshua Spiewak <https://github.com/jspiewak>`_
+* `Justin Rudd <https://github.com/seagecko>`_
+* `Mark Wolfe <https://github.com/wolfeidau>`_
+* `Michael Fairley <https://github.com/michaelfairley>`_
+* `Mårten Gustafson <https://github.com/chids>`_
+* `Nick Telford <https://github.com/nicktelford>`_
+* `Ori Schwartz <https://github.com/fleaflicker>`_
+* `Sam Perman <https://github.com/samperman>`_
+* `Sam Quigley <https://github.com/emerose>`_
+* `Scott Askew <https://github.com/scottfromsf>`_
+* `Sebastian Hartte <https://github.com/shartte>`_
+* `Tatu Saloranta <https://github.com/cowtowncoder>`_
+* `Ted Nyman <https://github.com/tnm>`_
+* `Tom Crayford <https://github.com/tcrayford>`_
+* `Tom Morris <https://github.com/tommorris>`_
+* `Vidit Drolia <https://github.com/vdrolia>`_
+* `Xavier Shay <https://github.com/xaviershay>`_
diff --git a/docs/source/about/faq.rst b/docs/source/about/faq.rst
new file mode 100644
index 0000000..f6823cf
--- /dev/null
+++ b/docs/source/about/faq.rst
@@ -0,0 +1,26 @@
+.. title:: FAQ
+
+.. _faq:
+
+##########################
+Frequently Asked Questions
+##########################
+
+What's a Dropwizard?
+  A character in a `K.C. Green web comic`__.
+
+.. __: http://gunshowcomic.com/316
+
+How is Dropwizard licensed?
+  It's licensed under the `Apache License v2`__.
+
+.. __: http://www.apache.org/licenses/LICENSE-2.0.html
+
+How can I commit to Dropwizard?
+  Go to the `GitHub project`__, fork it, and submit a pull request. We prefer small, single-purpose
+  pull requests over large, multi-purpose ones. We reserve the right to turn down any proposed
+  changes, but in general we're delighted when people want to make our projects better!
+
+.. __: https://github.com/dropwizard/dropwizard
+
+
diff --git a/docs/source/about/index.rst b/docs/source/about/index.rst
new file mode 100644
index 0000000..851ed2f
--- /dev/null
+++ b/docs/source/about/index.rst
@@ -0,0 +1,14 @@
+.. title:: About
+
+.. _about:
+
+################
+About Dropwizard
+################
+
+.. toctree::
+
+    contributors
+    faq
+    release-notes
+    todos
diff --git a/docs/source/about/release-notes.rst b/docs/source/about/release-notes.rst
new file mode 100644
index 0000000..3f82b23
--- /dev/null
+++ b/docs/source/about/release-notes.rst
@@ -0,0 +1,513 @@
+.. _release-notes:
+
+#############
+Release Notes
+#############
+
+.. _rel-0.7.1:
+
+v0.7.1
+==============
+
+* Added instrumentation to ``Task``, using metrics annotations.
+* Added ability to blacklist SSL cipher suites.
+* Added ``@PATCH`` annotation for Jersey resource methods to indicate use of the HTTP ``PATCH`` method.
+* Added support for configurble request retry behavior for ``HttpClientBuilder`` and ``JerseyClientBuilder``.
+* Added facility to get the admin HTTP port in ``DropwizardAppTestRule``.
+* Added ``ScanningHibernateBundle``, which scans packages for entities, instead of requiring you to add them individually.
+* Added facility to invalidate credentials from the ``CachingAuthenticator`` that match a specified ``Predicate``.
+* Added a CI build profile for JDK 8 to ensure that Dropwizard builds against the latest version of the JDK.
+* Added ``--catalog`` and ``--schema`` options to Liquibase.
+* Added ``stackTracePrefix`` configuration option to ``SyslogAppenderFactory`` to configure the pattern prepended to each line in the stack-trace sent to syslog. Defaults to the TAB character, "\t". Note: this is different from the bang prepended to text logs (such as "console", and "file"), as syslog has different conventions for multi-line messages.
+* Added ability to validate ``Optional`` values using validation annotations. Such values require the ``@UnwrapValidatedValue`` annotation, in addition to the validations you wish to use.
+* Added facility to configure the ``User-Agent`` for ``HttpClient``. Configurable via the ``userAgent`` configuration option.
+* Added configurable ``AllowedMethodsFilter``. Configure allowed HTTP methods for both the application and admin connnectors with ``allowedMethods``.
+* Added support for specifying a ``CredentialProvider`` for HTTP clients.
+* Fixed silently overriding Servlets or ServletFilters; registering a duplicate will now emit a warning.
+* Fixed ``SyslogAppenderFactory`` failing when the application name contains a PCRE reserved character (e.g. ``/`` or ``$``).
+* Fixed regression causing JMX reporting of metrics to not be enabled by default.
+* Fixed transitive dependencies on log4j and extraneous sl4j backends bleeding in to projects. Dropwizard will now enforce that only Logback and slf4j-logback are used everywhere.
+* Fixed clients disconnecting before the request has been fully received causing a "500 Internal Server Error" to be generated for the request log. Such situations will now correctly generate a "400 Bad Request", as the request is malformed. Clients will never see these responses, but they matter for logging and metrics that were previously considering this situation as a server error.
+* Fixed ``DiscoverableSubtypeResolver`` using the system ``ClassLoader``, instead of the local one.
+* Fixed regression causing Liquibase ``--dump`` to fail to dump the database.
+* Fixed the CSV metrics reporter failing when the output directory doesn't exist. It will now attempt to create the directory on startup.
+* Fixed global frequency for metrics reporters being permenantly overridden by the default frequency for individual reporters.
+* Fixed tests failing on Windows due to platform-specific line separators.
+* Changed ``DropwizardAppTestRule`` so that it no longer requires a configuration path to operate. When no path is specified, it will now use the applications' default configuration.
+* Changed ``Bootstrap`` so that ``getMetricsFactory()`` may now be overridden to provide a custom instance to the framework to use. 
+* Upgraded to Guava 17.0
+  Note: this addresses a bug with BloomFilters that is incompatible with pre-17.0 BloomFilters.
+* Upgraded to Jackson 2.3.3
+* Upgraded to Apache HttpClient 4.3.4
+* Upgraded to Metrics 3.0.2
+* Upgraded to Logback 1.1.2
+* Upgraded to h2 1.4.178
+* Upgraded to jDBI 2.55
+* Upgraded to Hibernate 5.3.5 Final
+* Upgraded to Hibernate Validator 5.1.1 Final
+* Upgraded to Mustache 0.8.15
+
+.. _rel-0.7.0:
+
+v0.7.0: Apr 04 2014
+===============
+
+* Upgraded to Java 7.
+* Moved to the ``io.dropwizard`` group ID and namespace.
+* Extracted out a number of reusable libraries: ``dropwizard-configuration``,
+  ``dropwizard-jackson``, ``dropwizard-jersey``, ``dropwizard-jetty``, ``dropwizard-lifecycle``,
+  ``dropwizard-logging``, ``dropwizard-servlets``, ``dropwizard-util``, ``dropwizard-validation``.
+* Extracted out various elements of ``Environment`` to separate classes: ``JerseyEnvironment``,
+  ``LifecycleEnvironment``, etc.
+* Extracted out ``dropwizard-views-freemarker`` and ``dropwizard-views-mustache``.
+  ``dropwizard-views`` just provides infrastructure now.
+* Renamed ``Service`` to ``Application``.
+* Added ``dropwizard-forms``, which provides support for multipart MIME entities.
+* Added ``dropwizard-spdy``.
+* Added ``AppenderFactory``, allowing for arbitrary logging appenders for application and request
+  logs.
+* Added ``ConnectorFactory``, allowing for arbitrary Jetty connectors.
+* Added ``ServerFactory``, with multi- and single-connector implementations.
+* Added ``ReporterFactory``, for metrics reporters, with Graphite and Ganglia implementations.
+* Added ``ConfigurationSourceProvider`` to allow loading configuration files from sources other than
+  the filesystem.
+* Added setuid support. Configure the user/group to run as and soft/hard open file limits in the
+  ``ServerFactory``. To bind to privileged ports (e.g. 80), enable ``startAsRoot`` and set ``user``
+  and ``group``, then start your application as the root user.
+* Added builders for managed executors.
+* Added a default ``check`` command, which loads and validates the service configuration.
+* Added support for the Jersey HTTP client to ``dropwizard-client``.
+* Added Jackson Afterburner support.
+* Added support for ``deflate``-encoded requests and responses.
+* Added support for HTTP Sessions. Add the annotated parameter to your resource method:
+  ``@Session HttpSession session`` to have the session context injected.
+* Added support for a "flash" message to be propagated across requests. Add the annotated parameter
+  to your resource method: ``@Session Flash message`` to have any existing flash message injected.
+* Added support for deserializing Java ``enums`` with fuzzy matching rules (i.e., whitespace
+  stripping, ``-``/``_`` equivalence, case insensitivity, etc.).
+* Added ``HibernateBundle#configure(Configuration)`` for customization of Hibernate configuration.
+* Added support for Joda Time ``DateTime`` arguments and results when using JDBI.
+* Added configuration option to include Exception stack-traces when logging to syslog. Stack traces
+  are now excluded by default.
+* Added the application name and PID (if detectable) to the beginning of syslog messages, as is the
+  convention.
+* Added ``--migrations-file`` command-line option to ``migrate`` command to supply the migrations
+  file explicitly.
+* Validation errors are now returned as ``application/json`` responses.
+* Simplified ``AsyncRequestLog``; now standardized on Jetty 9 NCSA format.
+* Renamed ``DatabaseConfiguration`` to ``DataSourceFactory``, and ``ConfigurationStrategy`` to
+  ``DatabaseConfiguration``.
+* Changed logging to be asynchronous. Messages are now buffered and batched in-memory before being
+  delivered to the configured appender(s).
+* Changed handling of runtime configuration errors. Will no longer display an Exception stack-trace
+  and will present a more useful description of the problem, including suggestions when appropriate.
+* Changed error handling to depend more heavily on Jersey exception mapping.
+* Changed ``dropwizard-db`` to use ``tomcat-jdbc`` instead of ``tomcat-dbcp``.
+* Changed default formatting when logging nested Exceptions to display the root-cause first.
+* Replaced ``ResourceTest`` with ``ResourceTestRule``, a JUnit ``TestRule``.
+* Dropped Scala support.
+* Dropped ``ManagedSessionFactory``.
+* Dropped ``ObjectMapperFactory``; use ``ObjectMapper`` instead.
+* Dropped ``Validator``; use ``javax.validation.Validator`` instead.
+* Fixed a shutdown bug in ``dropwizard-migrations``.
+* Fixed formatting of "Caused by" lines not being prefixed when logging nested Exceptions.
+* Fixed not all available Jersey endpoints were being logged at startup.
+* Upgraded to argparse4j 0.4.3.
+* Upgraded to Guava 16.0.1.
+* Upgraded to Hibernate Validator 5.0.2.
+* Upgraded to Jackson 2.3.1.
+* Upgraded to JDBI 2.53.
+* Upgraded to Jetty 9.0.7.
+* Upgraded to Liquibase 3.1.1.
+* Upgraded to Logback 1.1.1.
+* Upgraded to Metrics 3.0.1.
+* Upgraded to Mustache 0.8.14.
+* Upgraded to SLF4J 1.7.6.
+* Upgraded to Jersey 1.18.
+* Upgraded to Apache HttpClient 4.3.2.
+* Upgraded to tomcat-jdbc 7.0.50.
+* Upgraded to Hibernate 4.3.1.Final.
+
+.. _rel-0.6.2:
+
+v0.6.2: Mar 18 2013
+===================
+
+* Added support for non-UTF8 views.
+* Fixed an NPE for services in the root package.
+* Fixed exception handling in ``TaskServlet``.
+* Upgraded to Slf4j 1.7.4.
+* Upgraded to Jetty 8.1.10.
+* Upgraded to Jersey 1.17.1.
+* Upgraded to Jackson 2.1.4.
+* Upgraded to Logback 1.0.10.
+* Upgraded to Hibernate 4.1.9.
+* Upgraded to Hibernate Validator 4.3.1.
+* Upgraded to tomcat-dbcp 7.0.37.
+* Upgraded to Mustache.java 0.8.10.
+* Upgraded to Apache HttpClient 4.2.3.
+* Upgraded to Jackson 2.1.3.
+* Upgraded to argparse4j 0.4.0.
+* Upgraded to Guava 14.0.1.
+* Upgraded to Joda Time 2.2.
+* Added ``retries`` to ``HttpClientConfiguration``.
+* Fixed log formatting for extended stack traces, also now using extended stack traces as the
+  default.
+* Upgraded to FEST Assert 2.0M10.
+
+.. _rel-0.6.1:
+
+v0.6.1: Nov 28 2012
+===================
+
+* Fixed incorrect latencies in request logs on Linux.
+* Added ability to register multiple ``ServerLifecycleListener`` instances.
+
+.. _rel-0.6.0:
+
+v0.6.0: Nov 26 2012
+===================
+
+* Added Hibernate support in ``dropwizard-hibernate``.
+* Added Liquibase migrations in ``dropwizard-migrations``.
+* Renamed ``http.acceptorThreadCount`` to ``http.acceptorThreads``.
+* Renamed ``ssl.keyStorePath`` to ``ssl.keyStore``.
+* Dropped ``JerseyClient``. Use Jersey's ``Client`` class instead.
+* Moved JDBI support to ``dropwizard-jdbi``.
+* Dropped ``Database``. Use JDBI's ``DBI`` class instead.
+* Dropped the ``Json`` class. Use ``ObjectMapperFactory`` and ``ObjectMapper`` instead.
+* Decoupled JDBI support from tomcat-dbcp.
+* Added group support to ``Validator``.
+* Moved CLI support to argparse4j.
+* Fixed testing support for ``Optional`` resource method parameters.
+* Fixed Freemarker support to use its internal encoding map.
+* Added property support to ``ResourceTest``.
+* Fixed JDBI metrics support for raw SQL queries.
+* Dropped Hamcrest matchers in favor of FEST assertions in ``dropwizard-testing``.
+* Split ``Environment`` into ``Bootstrap`` and ``Environment``, and broke configuration of each into
+  ``Service``'s ``#initialize(Bootstrap)`` and ``#run(Configuration, Environment)``.
+* Combined ``AbstractService`` and ``Service``.
+* Trimmed down ``ScalaService``, so be sure to add ``ScalaBundle``.
+* Added support for using ``JerseyClientFactory`` without an ``Environment``.
+* Dropped Jerkson in favor of Jackson's Scala module.
+* Added ``Optional`` support for JDBI.
+* Fixed bug in stopping ``AsyncRequestLog``.
+* Added ``UUIDParam``.
+* Upgraded to Metrics 2.2.0.
+* Upgraded to Jetty 8.1.8.
+* Upgraded to Mockito 1.9.5.
+* Upgraded to tomcat-dbcp 7.0.33.
+* Upgraded to Mustache 0.8.8.
+* Upgraded to Jersey 1.15.
+* Upgraded to Apache HttpClient 4.2.2.
+* Upgraded to JDBI 2.41.
+* Upgraded to Logback 1.0.7 and SLF4J 1.7.2.
+* Upgraded to Guava 13.0.1.
+* Upgraded to Jackson 2.1.1.
+* Added support for Joda Time.
+
+.. note:: Upgrading to 0.6.0 will require changing your code. First, your ``Service`` subclass will
+          need to implement both ``#initialize(Bootstrap<T>)`` **and**
+          ``#run(T, Environment)``. What used to be in ``initialize`` should be moved to ``run``.
+          Second, your representation classes need to be migrated to Jackson 2. For the most part,
+          this is just changing imports to ``com.fasterxml.jackson.annotation.*``, but there are
+          `some subtler changes in functionality <http://wiki.fasterxml.com/JacksonUpgradeFrom19To20>`_.
+          Finally, references to 0.5.x's ``Json``, ``JerseyClient``, or ``JDBI`` classes should be
+          changed to Jackon's ``ObjectMapper``, Jersey's ``Client``, and JDBI's ``DBI``
+          respectively.
+
+.. _rel-0.5.1:
+
+v0.5.1: Aug 06 2012
+===================
+
+* Fixed logging of managed objects.
+* Fixed default file logging configuration.
+* Added FEST-Assert as a ``dropwizard-testing`` dependency.
+* Added support for Mustache templates (``*.mustache``) to ``dropwizard-views``.
+* Added support for arbitrary view renderers.
+* Fixed command-line overrides when no configuration file is present.
+* Added support for arbitrary ``DnsResolver`` implementations in ``HttpClientFactory``.
+* Upgraded to Guava 13.0 final.
+* Fixed task path bugs.
+* Upgraded to Metrics 2.1.3.
+* Added ``JerseyClientConfiguration#compressRequestEntity`` for disabling the compression of request
+  entities.
+* Added ``Environment#scanPackagesForResourcesAndProviders`` for automatically detecting Jersey
+  providers and resources.
+* Added ``Environment#setSessionHandler``.
+
+.. _rel-0.5.0:
+
+v0.5.0: Jul 30 2012
+===================
+
+* Upgraded to JDBI 2.38.1.
+* Upgraded to Jackson 1.9.9.
+* Upgraded to Jersey 1.13.
+* Upgraded to Guava 13.0-rc2.
+* Upgraded to HttpClient 4.2.1.
+* Upgraded to tomcat-dbcp 7.0.29.
+* Upgraded to Jetty 8.1.5.
+* Improved ``AssetServlet``:
+
+  * More accurate ``Last-Modified-At`` timestamps.
+  * More general asset specification.
+  * Default filename is now configurable.
+
+* Improved ``JacksonMessageBodyProvider``:
+
+  * Now based on Jackson's JAX-RS support.
+  * Doesn't read or write types annotated with ``@JsonIgnoreType``.
+
+* Added ``@MinSize``, ``@MaxSize``, and ``@SizeRange`` validations.
+* Added ``@MinDuration``, ``@MaxDuration``, and ``@DurationRange`` validations.
+* Fixed race conditions in Logback initialization routines.
+* Fixed ``TaskServlet`` problems with custom context paths.
+* Added ``jersey-text-framework-core`` as an explicit dependency of ``dropwizard-testing``. This
+  helps out some non-Maven build frameworks with bugs in dependency processing.
+* Added ``addProvider`` to ``JerseyClientFactory``.
+* Fixed ``NullPointerException`` problems with anonymous health check classes.
+* Added support for serializing/deserializing ``ByteBuffer`` instances as JSON.
+* Added ``supportedProtocols`` to SSL configuration, and disabled SSLv2 by default.
+* Added support for ``Optional<Integer>`` query parameters and others.
+* Removed ``jersey-freemarker`` dependency from ``dropwizard-views``.
+* Fixed missing thread contexts in logging statements.
+* Made the configuration file argument for the ``server`` command optional.
+* Added support for disabling log rotation.
+* Added support for arbitrary KeyStore types.
+* Added ``Log.forThisClass()``.
+* Made explicit service names optional.
+
+.. _rel-0.4.4:
+
+v0.4.4: Jul 24 2012
+===================
+
+* Added support for ``@JsonIgnoreType`` to ``JacksonMessageBodyProvider``.
+
+.. _rel-0.4.3:
+
+v0.4.3: Jun 22 2012
+===================
+
+* Re-enable immediate flushing for file and console logging appenders.
+
+.. _rel-0.4.2:
+
+v0.4.2: Jun 20 2012
+===================
+
+* Fixed ``JsonProcessingExceptionMapper``. Now returns human-readable error messages for malformed
+  or invalid JSON as a ``400 Bad Request``. Also handles problems with JSON generation and object
+  mapping in a developer-friendly way.
+
+.. _rel-0.4.1:
+
+v0.4.1: Jun 19 2012
+===================
+
+* Fixed type parameter resolution in for subclasses of subclasses of ``ConfiguredCommand``.
+* Upgraded to Jackson 1.9.7.
+* Upgraded to Logback 1.0.6, with asynchronous logging.
+* Upgraded to Hibernate Validator 4.3.0.
+* Upgraded to JDBI 2.34.
+* Upgraded to Jetty 8.1.4.
+* Added ``logging.console.format``, ``logging.file.format``, and ``logging.syslog.format``
+  parameters for custom log formats.
+* Extended ``ResourceTest`` to allow for enabling/disabling specific Jersey features.
+* Made ``Configuration`` serializable as JSON.
+* Stopped lumping command-line options in a group in ``Command``.
+* Fixed ``java.util.logging`` level changes.
+* Upgraded to Apache HttpClient 4.2.
+* Improved performance of ``AssetServlet``.
+* Added ``withBundle`` to ``ScalaService`` to enable bundle mix-ins.
+* Upgraded to SLF4J 1.6.6.
+* Enabled configuration-parameterized Jersey containers.
+* Upgraded to Jackson Guava 1.9.1, with support for ``Optional``.
+* Fixed error message in ``AssetBundle``.
+* Fixed ``WebApplicationException``s being thrown by ``JerseyClient``.
+
+.. _rel-0.4.0:
+
+v0.4.0: May 1 2012
+==================
+
+* Switched logging from Log4j__ to Logback__.
+
+  * Deprecated ``Log#fatal`` methods.
+  * Deprecated Log4j usage.
+  * Removed Log4j JSON support.
+  * Switched file logging to a time-based rotation system with optional GZIP and ZIP compression.
+  * Replaced ``logging.file.filenamePattern`` with ``logging.file.currentLogFilename`` and
+    ``logging.file.archivedLogFilenamePattern``.
+  * Replaced ``logging.file.retainedFileCount`` with ``logging.file.archivedFileCount``.
+  * Moved request logging to use a Logback-backed, time-based rotation system with optional GZIP
+    and ZIP compression. ``http.requestLog`` now has ``console``, ``file``, and ``syslog``
+    sections.
+
+* Fixed validation errors for logging configuration.
+* Added ``ResourceTest#addProvider(Class<?>)``.
+* Added ``ETag`` and ``Last-Modified`` support to ``AssetServlet``.
+* Fixed ``off`` logging levels conflicting with YAML's helpfulness.
+* Improved ``Optional`` support for some JDBC drivers.
+* Added ``ResourceTest#getJson()``.
+* Upgraded to Jackson 1.9.6.
+* Improved syslog logging.
+* Fixed template paths for views.
+* Upgraded to Guava 12.0.
+* Added support for deserializing ``CacheBuilderSpec`` instances from JSON/YAML.
+* Switched ``AssetsBundle`` and servlet to using cache builder specs.
+* Switched ``CachingAuthenticator`` to using cache builder specs.
+* Malformed JSON request entities now produce a ``400 Bad Request`` instead of a
+  ``500 Server Error`` response.
+* Added ``connectionTimeout``, ``maxConnectionsPerRoute``, and ``keepAlive`` to
+  ``HttpClientConfiguration``.
+* Added support for using Guava's ``HostAndPort`` in configuration properties.
+* Upgraded to tomcat-dbcp 7.0.27.
+* Upgraded to JDBI 2.33.2.
+* Upgraded to HttpClient 4.1.3.
+* Upgraded to Metrics 2.1.2.
+* Upgraded to Jetty 8.1.3.
+* Added SSL support.
+
+.. __: http://logging.apache.org/log4j/1.2/
+.. __: http://logback.qos.ch/
+
+
+.. _rel-0.3.1:
+
+v0.3.1: Mar 15 2012
+===================
+
+* Fixed debug logging levels for ``Log``.
+
+.. _rel-0.3.0:
+
+v0.3.0: Mar 13 2012
+===================
+
+* Upgraded to JDBI 2.31.3.
+* Upgraded to Jackson 1.9.5.
+* Upgraded to Jetty 8.1.2. (Jetty 9 is now the experimental branch. Jetty 8 is just Jetty 7 with
+  Servlet 3.0 support.)
+* Dropped ``dropwizard-templates`` and added ``dropwizard-views`` instead.
+* Added ``AbstractParam#getMediaType()``.
+* Fixed potential encoding bug in parsing YAML files.
+* Fixed a ``NullPointerException`` when getting logging levels via JMX.
+* Dropped support for ``@BearerToken`` and added ``dropwizard-auth`` instead.
+* Added ``@CacheControl`` for resource methods.
+* Added ``AbstractService#getJson()`` for full Jackson customization.
+* Fixed formatting of configuration file parsing errors.
+* ``ThreadNameFilter`` is now added by default. The thread names Jetty worker threads are set to the
+  method and URI of the HTTP request they are currently processing.
+* Added command-line overriding of configuration parameters via system properties. For example,
+  ``-Ddw.http.port=8090`` will override the configuration file to set ``http.port`` to ``8090``.
+* Removed ``ManagedCommand``. It was rarely used and confusing.
+* If ``http.adminPort`` is the same as ``http.port``, the admin servlet will be hosted under
+  ``/admin``. This allows Dropwizard applications to be deployed to environments like Heroku, which
+  require applications to open a single port.
+* Added ``http.adminUsername`` and ``http.adminPassword`` to allow for Basic HTTP Authentication
+  for the admin servlet.
+* Upgraded to `Metrics 2.1.1 <http://metrics.codahale.com/about/release-notes/#v2-1-1-mar-13-2012>`_.
+
+.. _rel-0.2.1:
+
+v0.2.1: Feb 24 2012
+===================
+
+* Added ``logging.console.timeZone`` and ``logging.file.timeZone`` to control the time zone of
+  the timestamps in the logs. Defaults to UTC.
+* Upgraded to Jetty 7.6.1.
+* Upgraded to Jersey 1.12.
+* Upgraded to Guava 11.0.2.
+* Upgraded to SnakeYAML 1.10.
+* Upgraded to tomcat-dbcp 7.0.26.
+* Upgraded to Metrics 2.0.3.
+
+.. _rel-0.2.0:
+
+v0.2.0: Feb 15 2012
+===================
+
+* Switched to using ``jackson-datatype-guava`` for JSON serialization/deserialization of Guava
+  types.
+* Use ``InstrumentedQueuedThreadPool`` from ``metrics-jetty``.
+* Upgraded to Jackson 1.9.4.
+* Upgraded to Jetty 7.6.0 final.
+* Upgraded to tomcat-dbcp 7.0.25.
+* Improved fool-proofing for ``Service`` vs. ``ScalaService``.
+* Switched to using Jackson for configuration file parsing. SnakeYAML is used to parse YAML
+  configuration files to a JSON intermediary form, then Jackson is used to map that to your
+  ``Configuration`` subclass and its fields. Configuration files which don't end in ``.yaml`` or
+  ``.yml`` are treated as JSON.
+* Rewrote ``Json`` to no longer be a singleton.
+* Converted ``JsonHelpers`` in ``dropwizard-testing`` to use normalized JSON strings to compare
+  JSON.
+* Collapsed ``DatabaseConfiguration``. It's no longer a map of connection names to configuration
+  objects.
+* Changed ``Database`` to use the validation query in ``DatabaseConfiguration`` for its ``#ping()``
+  method.
+* Changed many ``HttpConfiguration`` defaults to match Jetty's defaults.
+* Upgraded to JDBI 2.31.2.
+* Fixed JAR locations in the CLI usage screens.
+* Upgraded to Metrics 2.0.2.
+* Added support for all servlet listener types.
+* Added ``Log#setLevel(Level)``.
+* Added ``Service#getJerseyContainer``, which allows services to fully customize the Jersey
+  container instance.
+* Added the ``http.contextParameters`` configuration parameter.
+
+.. _rel-0.1.3:
+
+v0.1.3: Jan 19 2012
+===================
+
+* Upgraded to Guava 11.0.1.
+* Fixed logging in ``ServerCommand``. For the last time.
+* Switched to using the instrumented connectors from ``metrics-jetty``. This allows for much
+  lower-level metrics about your service, including whether or not your thread pools are overloaded.
+* Added FindBugs to the build process.
+* Added ``ResourceTest`` to ``dropwizard-testing``, which uses the Jersey Test Framework to provide
+  full testing of resources.
+* Upgraded to Jetty 7.6.0.RC4.
+* Decoupled URIs and resource paths in ``AssetServlet`` and ``AssetsBundle``.
+* Added ``rootPath`` to ``Configuration``. It allows you to serve Jersey assets off a specific path
+  (e.g., ``/resources/*`` vs ``/*``).
+* ``AssetServlet`` now looks for ``index.htm`` when handling requests for the root URI.
+* Upgraded to Metrics 2.0.0-RC0.
+
+.. _rel-0.1.2:
+
+v0.1.2: Jan 07 2012
+===================
+
+* All Jersey resource methods annotated with ``@Timed``, ``@Metered``, or ``@ExceptionMetered`` are
+  now instrumented via ``metrics-jersey``.
+* Now licensed under Apache License 2.0.
+* Upgraded to Jetty 7.6.0.RC3.
+* Upgraded to Metrics 2.0.0-BETA19.
+* Fixed logging in ``ServerCommand``.
+* Made ``ServerCommand#run()`` non-``final``.
+
+
+.. _rel-0.1.1:
+
+v0.1.1: Dec 28 2011
+===================
+
+* Fixed ``ManagedCommand`` to provide access to the ``Environment``, among other things.
+* Made ``JerseyClient``'s thread pool managed.
+* Improved ease of use for ``Duration`` and ``Size`` configuration parameters.
+* Upgraded to Mockito 1.9.0.
+* Upgraded to Jetty 7.6.0.RC2.
+* Removed single-arg constructors for ``ConfiguredCommand``.
+* Added ``Log``, a simple front-end for logging.
+
+.. _rel-0.1.0:
+
+
+v0.1.0: Dec 21 2011
+===================
+
+* Initial release
diff --git a/docs/source/about/todos.rst b/docs/source/about/todos.rst
new file mode 100644
index 0000000..f38ac42
--- /dev/null
+++ b/docs/source/about/todos.rst
@@ -0,0 +1,7 @@
+.. _about-todos:
+
+###################
+Documentation TODOs
+###################
+
+.. todolist::
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..8745ce1
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,300 @@
+# -*- coding: utf-8 -*-
+#
+# Dropwizard documentation build configuration file, created by
+# sphinx-quickstart on Mon Feb 13 11:29:49 2012.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys
+import os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#sys.path.insert(0, os.path.abspath('.'))
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.todo']
+
+# Add any paths that contain templates here, relative to this directory.
+#templates_path = ['ytemplates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'Dropwizard'
+copyright = u'2010-2013, Coda Hale, Yammer Inc.'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.7'
+# The full version, including alpha/beta/rc tags.
+release = '0.7.0'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+#default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+#add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+#add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+#show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+#pygments_style = 'trac'
+
+# A list of ignored prefixes for module index sorting.
+#modindex_common_prefix = []
+
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = 'dropwizard'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {
+    'tagline': u'Production-ready, out of the box.',
+    'gradient_start': u'#545d63',
+    'gradient_end': u'#182127',
+    'gradient_text': u'#ffffff',
+    'gradient_bg': u'#363F45',
+    'landing_logo': u'dropwizard-hat.png',
+    'landing_logo_width': u'150px',
+    'github_page': u'https://github.com/dropwizard/dropwizard',
+    'mailing_list': u'https://groups.google.com/forum/#!forum/dropwizard-user'
+}
+
+# Add any paths that contain custom themes here, relative to this directory.
+html_theme_path = ["./_themes"]
+
+# The name for this set of Sphinx documents.  If None, it defaults to
+# "<project> v<release> documentation".
+html_title = u'Dropwizard'
+
+# A shorter title for the navigation bar.  Default is the same as html_title.
+#html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+html_logo = u'dropwizard-logo.png'
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+#html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+#html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+html_use_smartypants = True
+
+html_add_permalinks = None
+
+# Custom sidebar templates, maps document names to template names.
+#html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+#html_additional_pages = {}
+
+# If false, no module index is generated.
+#html_domain_indices = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+#html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it.  The value of this option must be the
+# base URL from which the finished HTML is served.
+#html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Dropwizarddoc'
+
+todo_include_todos = True
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+# The paper size ('letterpaper' or 'a4paper').
+#'papersize': 'letterpaper',
+
+# The font size ('10pt', '11pt' or '12pt').
+#'pointsize': '10pt',
+
+# Additional stuff for the LaTeX preamble.
+#'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+  ('index', 'Dropwizard.tex', u'Dropwizard Documentation',
+   u'Coda Hale', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    ('index', 'dropwizard', u'Dropwizard Documentation',
+     [u'Coda Hale'], 1)
+]
+
+# If true, show URL addresses after external links.
+#man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+  ('index', 'Dropwizard', u'Dropwizard Documentation',
+   u'Coda Hale', 'Dropwizard', 'One line description of project.',
+   'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+#texinfo_appendices = []
+
+# If false, no module index is generated.
+#texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+#texinfo_show_urls = 'footnote'
+
+
+# -- Options for Epub output ---------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = u'Dropwizard'
+epub_author = u'Coda Hale'
+epub_publisher = u'Coda Hale'
+epub_copyright = u'2013, Coda Hale'
+
+# The language of the text. It defaults to the language option
+# or en if the language is not set.
+#epub_language = ''
+
+# The scheme of the identifier. Typical schemes are ISBN or URL.
+#epub_scheme = ''
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#epub_identifier = ''
+
+# A unique identification for the text.
+#epub_uid = ''
+
+# A tuple containing the cover image and cover page html template filenames.
+#epub_cover = ()
+
+# HTML files that should be inserted before the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_pre_files = []
+
+# HTML files shat should be inserted after the pages created by sphinx.
+# The format is a list of tuples containing the path and title.
+#epub_post_files = []
+
+# A list of files that should not be packed into the epub file.
+#epub_exclude_files = []
+
+# The depth of the table of contents in toc.ncx.
+#epub_tocdepth = 3
+
+# Allow duplicate toc entries.
+#epub_tocdup = True
diff --git a/docs/source/dropwizard-logo.png b/docs/source/dropwizard-logo.png
new file mode 100644
index 0000000..157f688
Binary files /dev/null and b/docs/source/dropwizard-logo.png differ
diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst
new file mode 100644
index 0000000..a69bdeb
--- /dev/null
+++ b/docs/source/getting-started.rst
@@ -0,0 +1,791 @@
+.. _getting-started:
+
+###############
+Getting Started
+###############
+
+.. highlight:: text
+
+.. rubric:: *Getting Started* will guide you through the process of creating a simple Dropwizard
+            project: Hello World. Along the way, we'll explain the various underlying libraries and
+            their roles, important concepts in Dropwizard, and suggest some organizational
+            techniques to help you as your project grows. (Or you can just skip to the
+            :ref:`fun part <gs-maven-setup>`.)
+
+.. _gs-overview:
+
+Overview
+========
+
+Dropwizard straddles the line between being a library and a framework. Its goal is to provide
+performant, reliable implementations of everything a production-ready web application needs. Because
+this functionality is extracted into a reusable library, your application remains lean and focused,
+reducing both time-to-market and maintenance burdens.
+
+.. _gs-jetty:
+
+Jetty for HTTP
+--------------
+
+Because you can't be a web application without HTTP, Dropwizard uses the Jetty_ HTTP library to
+embed an incredibly tuned HTTP server directly into your project. Instead of handing your
+application off to a complicated application server, Dropwizard projects have a ``main`` method
+which spins up an HTTP server. Running your application as a simple process eliminates a number of
+unsavory aspects of Java in production (no PermGen issues, no application server configuration and
+maintenance, no arcane deployment tools, no class loader troubles, no hidden application logs, no
+trying to tune a single garbage collector to work with multiple application workloads) and allows
+you to use all of the existing Unix process management tools instead.
+
+.. _Jetty: http://www.eclipse.org/jetty/
+
+.. _gs-jersey:
+
+Jersey for REST
+---------------
+
+For building RESTful web applications, we've found nothing beats Jersey_ (the `JAX-RS`_ reference
+implementation) in terms of features or performance. It allows you to write clean, testable classes
+which gracefully map HTTP requests to simple Java objects. It supports streaming output, matrix URI
+parameters, conditional ``GET`` requests, and much, much more.
+
+.. _Jersey: http://jersey.java.net
+.. _JAX-RS: http://jcp.org/en/jsr/detail?id=311
+
+.. _gs-jackson:
+
+Jackson for JSON
+----------------
+
+In terms of data formats, JSON has become the web's *lingua franca*, and Jackson_ is the king of
+JSON on the JVM. In addition to being lightning fast, it has a sophisticated object mapper, allowing
+you to export your domain models directly.
+
+.. _Jackson: http://wiki.fasterxml.com/JacksonHome
+
+.. _gs-metrics:
+
+Metrics for metrics
+-------------------
+
+The Metrics_ library rounds things out, providing you with unparalleled insight into your code's
+behavior in your production environment.
+
+.. _Metrics: http://metrics.codahale.com
+
+.. _gs-and-friends:
+
+And Friends
+-----------
+
+In addition to Jetty_, Jersey_, and Jackson_, Dropwizard also includes a number of libraries to help
+you ship more quickly and with less regrets.
+
+* Guava_, which, in addition to highly optimized immutable data structures, provides a growing
+  number of classes to speed up development in Java.
+* Logback_ and slf4j_ for performant and flexible logging.
+* `Hibernate Validator`_, the `JSR-303`_ reference implementation, provides an easy, declarative
+  framework for validating user input and generating helpful, i18n-friendly error messages.
+* The `Apache HttpClient`_ and Jersey_ client libraries allow for both low- and high-level
+  interaction with other web services.
+* JDBI_ is the most straight-forward way to use a relational database with Java.
+* Liquibase_ is a great way to keep your database schema in check throughout your development and
+  release cycles, applying high-level database refactorings instead of one-off DDL scripts.
+* Freemarker_ and Mustache_ are simple templating systems for more user-facing applications.
+* `Joda Time`_ is a very complete, sane library for handling dates and times.
+
+.. _Guava: http://code.google.com/p/guava-libraries/
+.. _Logback: http://logback.qos.ch/
+.. _slf4j: http://www.slf4j.org/
+.. _Hibernate Validator: http://www.hibernate.org/subprojects/validator.html
+.. _JSR-303: http://jcp.org/en/jsr/detail?id=303
+.. _Apache HttpClient: http://hc.apache.org/httpcomponents-client-ga/index.html
+.. _JDBI: http://www.jdbi.org
+.. _Liquibase: http://www.liquibase.org
+.. _Freemarker: http://freemarker.sourceforge.net/
+.. _Mustache: http://mustache.github.io/
+.. _Joda Time: http://joda-time.sourceforge.net/
+
+Now that you've gotten the lay of the land, let's dig in!
+
+.. _gs-maven-setup:
+
+Setting Up Maven
+================
+
+We recommend you use Maven_ for new Dropwizard applications. If you're a big Ant_ / Ivy_, Buildr_,
+Gradle_, SBT_, Leiningen_, or Gant_ fan, that's cool, but we use Maven and we'll be using Maven as
+we go through this example application. If you have any questions about how Maven works,
+`Maven: The Complete Reference`__ should have what you're looking for. (We're assuming you know how
+to create a new Maven project. If not, you can use `this <https://gist.github.com/2019732>`_ to get
+started.)
+
+.. _Maven: http://maven.apache.org
+.. _Ant: http://ant.apache.org/
+.. _Ivy: http://ant.apache.org/ivy/
+.. _Buildr: http://buildr.apache.org/
+.. _Gradle: http://www.gradle.org/
+.. _SBT: https://github.com/harrah/xsbt/wiki
+.. _Gant: http://gant.codehaus.org/
+.. _Leiningen: https://github.com/technomancy/leiningen
+.. __: http://www.sonatype.com/books/mvnref-book/reference/
+
+First, add a ``dropwizard.version`` property to your POM with the current version of Dropwizard
+(which is |release|):
+
+.. code-block:: xml
+
+    <properties>
+        <dropwizard.version>INSERT VERSION HERE</dropwizard.version>
+    </properties>
+
+Add the ``dropwizard-core`` library as a dependency:
+
+.. _gs-pom-dependencies:
+
+.. code-block:: xml
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${dropwizard.version}</version>
+        </dependency>
+    </dependencies>
+
+Alright, that's enough XML. We've got a Maven project set up now, and it's time to start writing
+real code.
+
+.. _gs-configuration:
+
+Creating A Configuration Class
+==============================
+
+Each Dropwizard application has its own subclass of the ``Configuration`` class which specifies
+environment-specific parameters. These parameters are specified in a YAML_ configuration file which
+is deserialized to an instance of your application's configuration class and validated.
+
+.. _YAML: http://www.yaml.org/
+
+The application we'll be building is a high-performance Hello World service, and one of our
+requirements is that we need to be able to vary how it says hello from environment to environment.
+We'll need to specify at least two things to begin with: a template for saying hello and a default
+name to use in case the user doesn't specify their name.
+
+.. _example conf here: https://github.com/dropwizard/dropwizard/blob/master/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java
+
+Here's what our configuration class will looks like, full `example conf here`_ :
+
+.. _gs-configuration-class:
+
+.. code-block:: java
+
+    package com.example.helloworld;
+
+    import io.dropwizard.Configuration;
+    import com.fasterxml.jackson.annotation.JsonProperty;
+    import org.hibernate.validator.constraints.NotEmpty;
+
+    public class HelloWorldConfiguration extends Configuration {
+        @NotEmpty
+        private String template;
+
+        @NotEmpty
+        private String defaultName = "Stranger";
+
+        @JsonProperty
+        public String getTemplate() {
+            return template;
+        }
+
+        @JsonProperty
+        public void setTemplate(String template) {
+            this.template = template;
+        }
+
+        @JsonProperty
+        public String getDefaultName() {
+            return defaultName;
+        }
+
+        @JsonProperty
+        public void setDefaultName(String name) {
+            this.defaultName = name;
+        }
+    }
+
+There's a lot going on here, so let's unpack a bit of it.
+
+When this class is deserialized from the YAML file, it will pull two root-level fields from the YAML
+object: ``template``, the template for our Hello World saying, and ``defaultName``, the default name
+to use. Both ``template`` and ``defaultName`` are annotated with ``@NotEmpty``, so if the YAML
+configuration file has blank values for either or is missing ``template`` entirely an informative
+exception will be thrown and your application won't start.
+
+Both the getters and setters for ``template`` and ``defaultName`` are annotated with
+``@JsonProperty``, which allows Jackson to both deserialize the properties from a YAML file but also
+to serialize it.
+
+.. note::
+
+    The mapping from YAML to your application's ``Configuration`` instance is done
+    by Jackson_. This means your ``Configuration`` class can use all of
+    Jackson's `object-mapping annotations`__. The validation of ``@NotEmpty`` is
+    handled by Hibernate Validator, which has a
+    `wide range of built-in constraints`__ for you to use.
+
+.. __: http://wiki.fasterxml.com/JacksonAnnotations
+.. __: http://docs.jboss.org/hibernate/validator/4.2/reference/en-US/html_single/#validator-defineconstraints-builtin
+
+.. _example yml here: https://github.com/dropwizard/dropwizard/blob/master/dropwizard-example/example.yml
+
+Our YAML file, will then look like the below, full `example yml here`_ :
+
+.. _gs-yaml-file:
+
+.. code-block:: yaml
+
+    template: Hello, %s!
+    defaultName: Stranger
+
+Dropwizard has *many* more configuration parameters than that, but they all have sane defaults so
+you can keep your configuration files small and focused.
+
+So save that YAML file as ``hello-world.yml``, because we'll be getting up and running pretty soon
+and we'll need it. Next up, we're creating our application class!
+
+.. _gs-application:
+
+Creating An Application Class
+=============================
+
+Combined with your project's ``Configuration`` subclass, its ``Application`` subclass forms the core
+of your Dropwizard application. The ``Application`` class pulls together the various bundles and
+commands which provide basic functionality. (More on that later.) For now, though, our
+``HelloWorldApplication`` looks like this:
+
+.. code-block:: java
+
+    package com.example.helloworld;
+
+    import io.dropwizard.Application;
+    import io.dropwizard.setup.Bootstrap;
+    import io.dropwizard.setup.Environment;
+    import com.example.helloworld.resources.HelloWorldResource;
+    import com.example.helloworld.health.TemplateHealthCheck;
+
+    public class HelloWorldApplication extends Application<HelloWorldConfiguration> {
+        public static void main(String[] args) throws Exception {
+            new HelloWorldApplication().run(args);
+        }
+
+        @Override
+        public String getName() {
+            return "hello-world";
+        }
+
+        @Override
+        public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
+            // nothing to do yet
+        }
+
+        @Override
+        public void run(HelloWorldConfiguration configuration,
+                        Environment environment) {
+            // nothing to do yet
+        }
+
+    }
+
+As you can see, ``HelloWorldApplication`` is parameterized with the application's configuration
+type, ``HelloWorldConfiguration``. An ``initialize`` method is used to configure aspects of the
+application required before the application is run, like bundles, configuration source providers,
+etc. Also, we've added a ``static`` ``main`` method, which will be our application's entry point.
+Right now, we don't have any functionality implemented, so our ``run`` method is a little boring.
+Let's fix that!
+
+.. _gs-representation:
+
+Creating A Representation Class
+===============================
+
+Before we can get into the nuts-and-bolts of our Hello World application, we need to stop and think
+about our API. Luckily, our application needs to conform to an industry standard, `RFC 1149`__,
+which specifies the following JSON representation of a Hello World saying:
+
+.. __: http://www.ietf.org/rfc/rfc1149.txt
+
+.. code-block:: javascript
+
+    {
+      "id": 1,
+      "content": "Hi!"
+    }
+
+
+The ``id`` field is a unique identifier for the saying, and ``content`` is the textual
+representation of the saying. (Thankfully, this is a fairly straight-forward industry standard.)
+
+To model this representation, we'll create a representation class:
+
+.. code-block:: java
+
+    package com.example.helloworld.core;
+
+    import com.fasterxml.jackson.annotation.JsonProperty;
+    import org.hibernate.validator.constraints.Length;
+
+    public class Saying {
+        private long id;
+
+        @Length(max = 3)
+        private String content;
+
+        public Saying() {
+            // Jackson deserialization
+        }
+
+        public Saying(long id, String content) {
+            this.id = id;
+            this.content = content;
+        }
+
+        @JsonProperty
+        public long getId() {
+            return id;
+        }
+
+        @JsonProperty
+        public String getContent() {
+            return content;
+        }
+    }
+
+This is a pretty simple POJO, but there are a few things worth noting here.
+
+First, it's immutable. This makes ``Saying`` instances *very* easy to reason about in multi-threaded
+environments as well as single-threaded environments. Second, it uses the Java Bean standard for the
+``id`` and ``content`` properties. This allows Jackson_ to serialize it to the JSON we need. The
+Jackson object mapping code will populate the ``id`` field of the JSON object with the return value
+of ``#getId()``, likewise with ``content`` and ``#getContent()``. Lastly, the bean leverages validation to ensure the content size is no greater than 3.
+
+.. note::
+
+    The JSON serialization here is done by Jackson, which supports far more than simple JavaBean
+    objects like this one. In addition to the sophisticated set of `annotations`__, you can even
+    write your own custom serializers and deserializers.
+
+.. __: http://wiki.fasterxml.com/JacksonAnnotations
+
+Now that we've got our representation class, it makes sense to start in on the resource it
+represents.
+
+.. _gs-resource:
+
+Creating A Resource Class
+=========================
+
+Jersey resources are the meat-and-potatoes of a Dropwizard application. Each resource class is
+associated with a URI template. For our application, we need a resource which returns new ``Saying``
+instances from the URI ``/hello-world``, so our resource class will look like this:
+
+.. code-block:: java
+
+    package com.example.helloworld.resources;
+
+    import com.example.helloworld.core.Saying;
+    import com.google.common.base.Optional;
+    import com.codahale.metrics.annotation.Timed;
+
+    import javax.ws.rs.GET;
+    import javax.ws.rs.Path;
+    import javax.ws.rs.Produces;
+    import javax.ws.rs.QueryParam;
+    import javax.ws.rs.core.MediaType;
+    import java.util.concurrent.atomic.AtomicLong;
+
+    @Path("/hello-world")
+    @Produces(MediaType.APPLICATION_JSON)
+    public class HelloWorldResource {
+        private final String template;
+        private final String defaultName;
+        private final AtomicLong counter;
+
+        public HelloWorldResource(String template, String defaultName) {
+            this.template = template;
+            this.defaultName = defaultName;
+            this.counter = new AtomicLong();
+        }
+
+        @GET
+        @Timed
+        public Saying sayHello(@QueryParam("name") Optional<String> name) {
+            final String value = String.format(template, name.or(defaultName));
+            return new Saying(counter.incrementAndGet(), value);
+        }
+    }
+
+Finally, we're in the thick of it! Let's start from the top and work our way down.
+
+``HelloWorldResource`` has two annotations: ``@Path`` and ``@Produces``. ``@Path("/hello-world")``
+tells Jersey that this resource is accessible at the URI ``/hello-world``, and
+``@Produces(MediaType.APPLICATION_JSON)`` lets Jersey's content negotiation code know that this
+resource produces representations which are ``application/json``.
+
+``HelloWorldResource`` takes two parameters for construction: the ``template`` it uses to produce
+the saying and the ``defaultName`` used when the user declines to tell us their name. An
+``AtomicLong`` provides us with a cheap, thread-safe way of generating unique(ish) IDs.
+
+.. warning::
+
+    Resource classes are used by multiple threads concurrently. In general, we recommend that
+    resources be stateless/immutable, but it's important to keep the context in mind.
+
+``#sayHello(Optional<String>)`` is the meat of this class, and it's a fairly simple method. The
+``@QueryParam("name")`` annotation tells Jersey to map the ``name`` parameter from the query string
+to the ``name`` parameter in the method. If the client sends a request to
+``/hello-world?name=Dougie``, ``sayHello`` will be called with ``Optional.of("Dougie")``; if there
+is no ``name`` parameter in the query string, ``sayHello`` will be called with
+``Optional.absent()``. (Support for Guava's ``Optional`` is a little extra sauce that Dropwizard
+adds to Jersey's existing functionality.)
+
+Inside the ``sayHello`` method, we increment the counter, format the template using
+``String.format(String, Object...)``, and return a new ``Saying`` instance.
+
+Because ``sayHello`` is annotated with ``@Timed``, Dropwizard automatically records the duration and
+rate of its invocations as a Metrics Timer.
+
+Once ``sayHello`` has returned, Jersey takes the ``Saying`` instance and looks for a provider class
+which can write ``Saying`` instances as ``application/json``. Dropwizard has one such provider built
+in which allows for producing and consuming Java objects as JSON objects. The provider writes out
+the JSON and the client receives a ``200 OK`` response with a content type of ``application/json``.
+
+.. _gs-resource-register:
+
+Registering A Resource
+----------------------
+
+Before that will actually work, though, we need to go back to ``HelloWorldApplication`` and add this
+new resource class. In its ``run`` method we can read the template and default name from the
+``HelloWorldConfiguration`` instance, create a new ``HelloWorldResource`` instance, and then add
+it to the application's Jersey environment:
+
+.. code-block:: java
+
+    @Override
+    public void run(HelloWorldConfiguration configuration,
+                    Environment environment) {
+        final HelloWorldResource resource = new HelloWorldResource(
+            configuration.getTemplate(),
+            configuration.getDefaultName()
+        );
+        environment.jersey().register(resource);
+    }
+
+When our application starts, we create a new instance of our resource class with the parameters from
+the configuration file and hand it off to the ``Environment``, which acts like a registry of all the
+things your application can do.
+
+.. note::
+
+    A Dropwizard application can contain *many* resource classes, each corresponding to its own URI
+    pattern. Just add another ``@Path``-annotated resource class and call ``register`` with an
+    instance of the new class.
+
+Before we go too far, we should add a health check for our application.
+
+.. _gs-healthcheck:
+
+Creating A Health Check
+=======================
+
+Health checks give you a way of adding small tests to your application to allow you to verify that
+your application is functioning correctly in production. We **strongly** recommend that all of your
+applications have at least a minimal set of health checks.
+
+.. note::
+
+    We recommend this so strongly, in fact, that Dropwizard will nag you should you neglect to add a
+    health check to your project.
+
+Since formatting strings is not likely to fail while an application is running (unlike, say, a
+database connection pool), we'll have to get a little creative here. We'll add a health check to
+make sure we can actually format the provided template:
+
+.. code-block:: java
+
+    package com.example.helloworld.health;
+
+    import com.codahale.metrics.health.HealthCheck;
+
+    public class TemplateHealthCheck extends HealthCheck {
+        private final String template;
+
+        public TemplateHealthCheck(String template) {
+            this.template = template;
+        }
+
+        @Override
+        protected Result check() throws Exception {
+            final String saying = String.format(template, "TEST");
+            if (!saying.contains("TEST")) {
+                return Result.unhealthy("template doesn't include a name");
+            }
+            return Result.healthy();
+        }
+    }
+
+
+``TemplateHealthCheck`` checks for two things: that the provided template is actually a well-formed
+format string, and that the template actually produces output with the given name.
+
+If the string is not a well-formed format string (for example, someone accidentally put
+``Hello, %s%`` in the configuration file), then ``String.format(String, Object...)`` will throw an
+``IllegalFormatException`` and the health check will implicitly fail. If the rendered saying doesn't
+include the test string, the health check will explicitly fail by returning an unhealthy ``Result``.
+
+.. _gs-healthcheck-add:
+
+Adding A Health Check
+---------------------
+
+As with most things in Dropwizard, we create a new instance with the appropriate parameters and add
+it to the ``Environment``:
+
+.. code-block:: java
+
+    @Override
+    public void run(HelloWorldConfiguration configuration,
+                    Environment environment) {
+        final HelloWorldResource resource = new HelloWorldResource(
+            configuration.getTemplate(),
+            configuration.getDefaultName()
+        );
+        final TemplateHealthCheck healthCheck =
+            new TemplateHealthCheck(configuration.getTemplate());
+        environment.healthChecks().register("template", healthCheck);
+        environment.jersey().register(resource);
+    }
+
+
+Now we're almost ready to go!
+
+.. _gs-building:
+
+Building Fat JARs
+=================
+
+We recommend that you build your Dropwizard applications as "fat" JAR files — single ``.jar`` files
+which contain *all* of the ``.class`` files required to run your application. This allows you to
+build a single deployable artifact which you can promote from your staging environment to your QA
+environment to your production environment without worrying about differences in installed
+libraries. To start building our Hello World application as a fat JAR, we need to configure a Maven
+plugin called ``maven-shade``. In the ``<build><plugins>`` section of your ``pom.xml`` file, add
+this:
+
+.. code-block:: xml
+    :emphasize-lines: 6,8,9,10,11,12,13,14,15,26,27,28,29
+
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <version>1.6</version>
+        <configuration>
+            <createDependencyReducedPom>true</createDependencyReducedPom>
+            <filters>
+                <filter>
+                    <artifact>*:*</artifact>
+                    <excludes>
+                        <exclude>META-INF/*.SF</exclude>
+                        <exclude>META-INF/*.DSA</exclude>
+                        <exclude>META-INF/*.RSA</exclude>
+                    </excludes>
+                </filter>
+            </filters>
+        </configuration>
+        <executions>
+            <execution>
+                <phase>package</phase>
+                <goals>
+                    <goal>shade</goal>
+                </goals>
+                <configuration>
+                    <transformers>
+                        <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                            <mainClass>com.example.helloworld.HelloWorldApplication</mainClass>
+                        </transformer>
+                    </transformers>
+                </configuration>
+            </execution>
+        </executions>
+    </plugin>
+
+This configures Maven to do a couple of things during its ``package`` phase:
+
+* Produce a ``pom.xml`` file which doesn't include dependencies for the libraries whose contents are
+  included in the fat JAR.
+* Exclude all digital signatures from signed JARs. If you don't, then Java considers the signature
+  invalid and won't load or run your JAR file.
+* Collate the various ``META-INF/services`` entries in the JARs instead of overwriting them.
+  (Neither Dropwizard nor Jersey works without those.)
+* Set ``com.example.helloworld.HelloWorldApplication`` as the JAR's ``MainClass``. This will allow
+  you to run the JAR using ``java -jar``.
+
+.. warning::
+
+    If your application has a dependency which *must* be signed (e.g., a `JCA/JCE`__ provider or
+    other trusted library), you have to add an exclusion__ to the ``maven-shade-plugin``
+    configuration for that library and include that JAR in the classpath.
+
+.. __: http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html
+.. __: http://maven.apache.org/plugins/maven-shade-plugin/examples/includes-excludes.html
+
+.. _gs-versions:
+
+Versioning Your JARs
+--------------------
+
+Dropwizard can also use the project version if it's embedded in the JAR's manifest as the
+``Implementation-Version``. To embed this information using Maven, add the following to the
+``<build><plugins>`` section of your ``pom.xml`` file:
+
+.. code-block:: xml
+
+    <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+            <archive>
+                <manifest>
+                    <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                </manifest>
+            </archive>
+        </configuration>
+    </plugin>
+
+This can be handy when trying to figure out what version of your application you have deployed on a
+machine.
+
+Once you've got that configured, go into your project directory and run ``mvn package`` (or run the
+``package`` goal from your IDE). You should see something like this:
+
+.. code-block:: text
+
+    [INFO] Including org.eclipse.jetty:jetty-util:jar:7.6.0.RC0 in the shaded jar.
+    [INFO] Including com.google.guava:guava:jar:10.0.1 in the shaded jar.
+    [INFO] Including com.google.code.findbugs:jsr305:jar:1.3.9 in the shaded jar.
+    [INFO] Including org.hibernate:hibernate-validator:jar:4.2.0.Final in the shaded jar.
+    [INFO] Including javax.validation:validation-api:jar:1.0.0.GA in the shaded jar.
+    [INFO] Including org.yaml:snakeyaml:jar:1.9 in the shaded jar.
+    [INFO] Replacing original artifact with shaded artifact.
+    [INFO] Replacing /Users/yourname/Projects/hello-world/target/hello-world-0.0.1-SNAPSHOT.jar with /Users/yourname/Projects/hello-world/target/hello-world-0.0.1-SNAPSHOT-shaded.jar
+    [INFO] ------------------------------------------------------------------------
+    [INFO] BUILD SUCCESS
+    [INFO] ------------------------------------------------------------------------
+    [INFO] Total time: 8.415s
+    [INFO] Finished at: Fri Dec 02 16:26:42 PST 2011
+    [INFO] Final Memory: 11M/81M
+    [INFO] ------------------------------------------------------------------------
+
+**Congratulations!** You've built your first Dropwizard project! Now it's time to run it!
+
+.. _gs-running:
+
+Running Your Application
+========================
+
+Now that you've built a JAR file, it's time to run it.
+
+In your project directory, run this:
+
+.. code-block:: text
+
+    java -jar target/hello-world-0.0.1-SNAPSHOT.jar
+
+You should see something like the following:
+
+.. code-block:: text
+
+    usage: java -jar hello-world-0.0.1-SNAPSHOT.jar
+           [-h] [-v] {server} ...
+
+    positional arguments:
+      {server}               available commands
+
+    optional arguments:
+      -h, --help             show this help message and exit
+      -v, --version          show the service version and exit
+
+Dropwizard takes the first command line argument and dispatches it to a matching command. In this
+case, the only command available is ``server``, which runs your application as an HTTP server. The
+``server`` command requires a configuration file, so let's go ahead and give it
+:ref:`the YAML file we previously saved <gs-yaml-file>`::
+
+    java -jar target/hello-world-0.0.1-SNAPSHOT.jar server hello-world.yml
+
+You should see something like the following:
+
+.. code-block:: text
+
+    INFO  [2011-12-03 00:38:32,927] io.dropwizard.cli.ServerCommand: Starting hello-world
+    INFO  [2011-12-03 00:38:32,931] org.eclipse.jetty.server.Server: jetty-7.x.y-SNAPSHOT
+    INFO  [2011-12-03 00:38:32,936] org.eclipse.jetty.server.handler.ContextHandler: started o.e.j.s.ServletContextHandler{/,null}
+    INFO  [2011-12-03 00:38:32,999] com.sun.jersey.server.impl.application.WebApplicationImpl: Initiating Jersey application, version 'Jersey: 1.10 11/02/2011 03:53 PM'
+    INFO  [2011-12-03 00:38:33,041] io.dropwizard.setup.Environment:
+
+        GET     /hello-world (com.example.helloworld.resources.HelloWorldResource)
+
+    INFO  [2011-12-03 00:38:33,215] org.eclipse.jetty.server.handler.ContextHandler: started o.e.j.s.ServletContextHandler{/,null}
+    INFO  [2011-12-03 00:38:33,235] org.eclipse.jetty.server.AbstractConnector: Started BlockingChannelConnector at 0.0.0.0:8080 STARTING
+    INFO  [2011-12-03 00:38:33,238] org.eclipse.jetty.server.AbstractConnector: Started SocketConnector at 0.0.0.0:8081 STARTING
+
+Your Dropwizard application is now listening on ports ``8080`` for application requests and ``8081``
+for administration requests. If you press ``^C``, the application will shut down gracefully, first
+closing the server socket, then waiting for in-flight requests to be processed, then shutting down
+the process itself.
+
+But while it's up, let's give it a whirl!
+`Click here to say hello! <http://localhost:8080/hello-world>`_
+`Click here to get even friendlier! <http://localhost:8080/hello-world?name=Successful+Dropwizard+User>`_
+
+So, we're generating sayings. Awesome. But that's not all your application can do. One of the main
+reasons for using Dropwizard is the out-of-the-box operational tools it provides, all of which can
+be found `on the admin port <http://localhost:8081/>`_.
+
+If you click through to the `metrics resource <http://localhost:8081/metrics>`_, you can see all of
+your application's metrics represented as a JSON object.
+
+The `threads resource <http://localhost:8081/threads>`_ allows you to quickly get a thread dump of
+all the threads running in that process.
+
+.. hint:: When a Jetty worker thread is handling an incoming HTTP request, the thread name is set to
+          the method and URI of the request. This can be *very* helpful when debugging a
+          poorly-behaving request.
+
+The `healthcheck resource <http://localhost:8081/healthcheck>`_ runs the
+:ref:`health check class we wrote <gs-healthcheck>`. You should see something like this:
+
+.. code-block:: text
+
+    * deadlocks: OK
+    * template: OK
+
+
+``template`` here is the result of your ``TemplateHealthCheck``, which unsurprisingly passed.
+``deadlocks`` is a built-in health check which looks for deadlocked JVM threads and prints out a
+listing if any are found.
+
+.. _gs-next:
+
+Next Steps
+==========
+
+Well, congratulations. You've got a Hello World application ready for production (except for the
+lack of tests) that's capable of doing 30,000-50,000 requests per second. Hopefully you've gotten a
+feel for how Dropwizard combines Jetty, Jersey, Jackson, and other stable, mature libraries to
+provide a phenomenal platform for developing RESTful web applications.
+
+There's a lot more to Dropwizard than is covered here (commands, bundles, servlets, advanced
+configuration, validation, HTTP clients, database clients, views, etc.), all of which is covered by
+the :ref:`User Manual <manual-index>`.
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..68c429c
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,27 @@
+.. title:: Home
+
+.. raw:: html
+
+    <div class="hero-unit">
+
+###################################################################################################
+Dropwizard is a Java framework for developing ops-friendly, high-performance, RESTful web services.
+###################################################################################################
+
+Dropwizard pulls together **stable**, **mature** libraries from the Java ecosystem into a
+**simple**, **light-weight** package that lets you focus on *getting things done*.
+
+Dropwizard has *out-of-the-box* support for sophisticated **configuration**,
+**application metrics**, **logging**, **operational tools**, and much more, allowing you and your
+team to ship a *production-quality* web service in the shortest time possible.
+
+.. toctree::
+   :maxdepth: 1
+
+   getting-started
+   manual/index
+   about/index
+
+.. raw:: html
+
+    </div>
diff --git a/docs/source/manual/auth.rst b/docs/source/manual/auth.rst
new file mode 100644
index 0000000..3b2f40c
--- /dev/null
+++ b/docs/source/manual/auth.rst
@@ -0,0 +1,131 @@
+.. _man-auth:
+
+#########################
+Dropwizard Authentication
+#########################
+
+.. rubric:: The ``dropwizard-auth`` client provides authentication using either HTTP Basic
+            Authentication or OAuth2 bearer tokens.
+
+.. _man-auth-authenticators:
+
+Authenticators
+==============
+
+An authenticator is a strategy class which, given a set of client-provided credentials, possibly
+returns a principal (i.e., the person or entity on behalf of whom your service will do something).
+
+Authenticators implement the ``Authenticator<C, P>`` interface, which has a single method:
+
+.. code-block:: java
+
+    public class SimpleAuthenticator implements Authenticator<BasicCredentials, User> {
+        @Override
+        public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
+            if ("secret".equals(credentials.getPassword())) {
+                return Optional.of(new User(credentials.getUsername()));
+            }
+            return Optional.absent();
+        }
+    }
+
+This authenticator takes :ref:`basic auth credentials <man-auth-basic>` and if the client-provided
+password is ``secret``, authenticates the client as a ``User`` with the client-provided username.
+
+If the password doesn't match, an absent ``Optional`` is returned instead, indicating that the
+credentials are invalid.
+
+.. warning:: It's important for authentication services to not provide too much information in their
+             errors. The fact that a username or email has an account may be meaningful to an
+             attacker, so the ``Authenticator`` interface doesn't allow you to distinguish between
+             a bad username and a bad password. You should only throw an ``AuthenticationException``
+             if the authenticator is **unable** to check the credentials (e.g., your database is
+             down).
+
+.. _man-auth-authenticators-caching:
+
+Caching
+-------
+
+Because the backing data stores for authenticators may not handle high throughput (an RDBMS or LDAP
+server, for example), Dropwizard provides a decorator class which provides caching:
+
+.. code-block:: java
+
+    CachingAuthenticator.wrap(ldapAuthenticator,
+                              config.getAuthenticationCachePolicy());
+
+Dropwizard can parse Guava's ``CacheBuilderSpec`` from the configuration policy, allowing your
+configuration file to look like this:
+
+.. code-block:: yaml
+
+    authenticationCachePolicy: maximumSize=10000, expireAfterAccess=10m
+
+This caches up to 10,000 principals with an LRU policy, evicting stale entries after 10 minutes.
+
+.. _man-auth-basic:
+
+Basic Authentication
+====================
+
+The ``BasicAuthProvider`` enables HTTP Basic authentication, and requires an authenticator which
+takes instances of ``BasicCredentials``:
+
+.. code-block:: java
+
+    @Override
+    public void run(ExampleConfiguration configuration,
+                    Environment environment) {
+        environment.jersey().register(new BasicAuthProvider<User>(new ExampleAuthenticator(),
+                                                            "SUPER SECRET STUFF"));
+    }
+
+.. _man-auth-oauth2:
+
+OAuth2
+======
+
+The ``OAuthProvider`` enables OAuth2 bearer-token authentication, and requires an authenticator
+which takes an instance of ``String``.
+
+.. code-block:: java
+
+    @Override
+    public void run(ExampleConfiguration configuration,
+                    Environment environment) {
+        environment.jersey().register(new OAuthProvider<User>(new ExampleAuthenticator(),
+                                                        "SUPER SECRET STUFF"));
+    }
+
+.. _man-auth-resources:
+
+Protecting Resources
+====================
+
+To protect a resource, simply include an ``@Auth``-annotated principal as one of your resource
+method parameters:
+
+.. code-block:: java
+
+    @GET
+    public SecretPlan getSecretPlan(@Auth User user) {
+        return dao.findPlanForUser(user);
+    }
+
+If there are no provided credentials for the request, or if the credentials are invalid, the
+provider will return a scheme-appropriate ``401 Unauthorized`` response without calling your
+resource method.
+
+If you have a resource which is optionally protected (e.g., you want to display a logged-in user's
+name but not require login), set the ``required`` attribute of the annotation to ``false``:
+
+.. code-block:: java
+
+    @GET
+    public HomepageView getHomepage(@Auth(required = false) User user) {
+        return new HomepageView(Optional.fromNullable(user));
+    }
+
+If there is no authenticated principal, ``null`` is used instead, and your resource method is still
+called.
diff --git a/docs/source/manual/client.rst b/docs/source/manual/client.rst
new file mode 100644
index 0000000..fb158b3
--- /dev/null
+++ b/docs/source/manual/client.rst
@@ -0,0 +1,152 @@
+.. _man-client:
+
+#################
+Dropwizard Client
+#################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-client`` module provides you with two different performant,
+            instrumented HTTP clients so you can integrate your service with other web
+            services: :ref:`man-client-apache` and :ref:`man-client-jersey`.
+
+.. _man-client-apache:
+
+Apache HttpClient, version 4.3
+===============================
+
+The underlying library for ``dropwizard-client`` is  Apache's HttpClient_, a full-featured,
+well-tested HTTP client library.
+
+.. _HttpClient: http://hc.apache.org/httpcomponents-core-4.3.x/index.html
+
+To create a :ref:`managed <man-core-managed>`, instrumented ``HttpClient`` instance, your
+:ref:`configuration class <man-core-configuration>` needs an :ref:`http client configuration <man-configuration-clients-http>` instance:
+
+.. code-block:: java
+
+    public class ExampleConfiguration extends Configuration {
+        @Valid
+        @NotNull
+        @JsonProperty
+        private HttpClientConfiguration httpClient = new HttpClientConfiguration();
+
+        public HttpClientConfiguration getHttpClientConfiguration() {
+            return httpClient;
+        }
+    }
+
+Then, in your application's ``run`` method, create a new ``HttpClientBuilder``:
+
+.. code-block:: java
+
+    @Override
+    public void run(ExampleConfiguration config,
+                    Environment environment) {
+        final HttpClient httpClient = new HttpClientBuilder(environment).using(config.getHttpClientConfiguration())
+                                                                        .build();
+        environment.addResource(new ExternalServiceResource(httpClient));
+    }
+
+.. _man-client-apache-metrics:
+
+Metrics
+-------
+
+Dropwizard's ``HttpClientBuilder`` actually gives you an instrumented subclass which tracks the
+following pieces of data:
+
+``org.apache.http.conn.ClientConnectionManager.available-connections``
+    The number the number idle connections ready to be used to execute requests.
+
+``org.apache.http.conn.ClientConnectionManager.leased-connections``
+    The number of persistent connections currently being used to execut requests.
+
+``org.apache.http.conn.ClientConnectionManager.max-connections``
+    The maximum number of allowed connections.
+
+``org.apache.http.conn.ClientConnectionManager.pending-connections``
+    The number of connection requests being blocked awaiting a free connection
+
+``org.apache.http.client.HttpClient.get-requests``
+    The rate at which ``GET`` requests are being sent.
+
+``org.apache.http.client.HttpClient.post-requests``
+    The rate at which ``POST`` requests are being sent.
+
+``org.apache.http.client.HttpClient.head-requests``
+    The rate at which ``HEAD`` requests are being sent.
+
+``org.apache.http.client.HttpClient.put-requests``
+    The rate at which ``PUT`` requests are being sent.
+
+``org.apache.http.client.HttpClient.delete-requests``
+    The rate at which ``DELETE`` requests are being sent.
+
+``org.apache.http.client.HttpClient.options-requests``
+    The rate at which ``OPTIONS`` requests are being sent.
+
+``org.apache.http.client.HttpClient.trace-requests``
+    The rate at which ``TRACE`` requests are being sent.
+
+``org.apache.http.client.HttpClient.connect-requests``
+    The rate at which ``CONNECT`` requests are being sent.
+
+``org.apache.http.client.HttpClient.move-requests``
+    The rate at which ``MOVE`` requests are being sent.
+
+``org.apache.http.client.HttpClient.patch-requests``
+    The rate at which ``PATCH`` requests are being sent.
+
+``org.apache.http.client.HttpClient.other-requests``
+    The rate at which requests with none of the above methods are being sent.
+
+.. note::
+
+    The naming strategy for the metrics associated requests is configurable.
+    Specifically, the last part e.g. get-requests.
+    What is displayed is ``HttpClientMetricNameStrategies.METHOD_ONLY``, you can
+    also include the host via ``HttpClientMetricNameStrategies.HOST_AND_METHOD``
+    or a url without query string via ``HttpClientMetricNameStrategies.QUERYLESS_URL_AND_METHOD``
+
+
+.. _man-client-jersey:
+
+Jersey Client, version 1.18
+===========================
+
+If HttpClient_ is too low-level for you, Dropwizard also supports Jersey's `Client API`_.
+Jersey's ``Client`` allows you to use all of the server-side media type support that your service
+uses to, for example, deserialize ``application/json`` request entities as POJOs.
+
+.. _Client API: https://jersey.java.net/documentation/1.18/client-api.html
+
+To create a :ref:`managed <man-core-managed>`, instrumented ``JerseyClient`` instance, your
+:ref:`configuration class <man-core-configuration>` needs an :ref:`jersey client configuration <man-configuration-clients-jersey>` instance:
+
+.. code-block:: java
+
+    public class ExampleConfiguration extends Configuration {
+        @Valid
+        @NotNull
+        @JsonProperty
+        private JerseyClientConfiguration httpClient = new JerseyClientConfiguration();
+
+        public JerseyClientConfiguration getJerseyClientConfiguration() {
+            return httpClient;
+        }
+    }
+
+Then, in your service's ``run`` method, create a new ``JerseyClientBuilder``:
+
+.. code-block:: java
+
+    @Override
+    public void run(ExampleConfiguration config,
+                    Environment environment) {
+
+        final Client client = new JerseyClientBuilder(environment).using(config.getJerseyClientConfiguration())
+                                                                  .build(getName());                                                       
+        environment.addResource(new ExternalServiceResource(client));
+    }
+
diff --git a/docs/source/manual/configuration.rst b/docs/source/manual/configuration.rst
new file mode 100644
index 0000000..fdb456e
--- /dev/null
+++ b/docs/source/manual/configuration.rst
@@ -0,0 +1,808 @@
+.. _man-configuration:
+
+###################################
+Dropwizard Configuration Reference
+###################################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-configuration`` module provides you with a polymorphic configuration
+            mechanism.
+
+
+.. _man-configuration-servers:
+
+Servers
+========
+
+.. code-block:: yaml
+
+    server:
+      type: default
+      maxThreads: 1024
+
+
+.. _man-configuration-all:
+
+All
+----
+
+====================== ===============================================  =============================================================================
+Name                   Default                                          Description
+====================== ===============================================  =============================================================================
+type                   default                                          - default
+                                                                        - simple
+maxThreads             1024                                             The maximum number of threads to use for requests.
+minThreads             8                                                The minimum number of threads to use for requests.
+maxQueuedRequests      1024                                             The maximum number of requests to queue before blocking
+                                                                        the acceptors.
+idleThreadTimeout      1 minute                                         The amount of time a worker thread can be idle before
+                                                                        being stopped.
+nofileSoftLimit        (none)                                           The number of open file descriptors before a soft error is issued.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+nofileHardLimit        (none)                                           The number of open file descriptors before a hard error is issued.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+gid                    (none)                                           The group ID to switch to once the connectors have started.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+uid                    (none)                                           The user ID to switch to once the connectors have started.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+user                   (none)                                           The username to switch to once the connectors have started.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+group                  (none)                                           The group to switch to once the connectors have started.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+umask                  (none)                                           The umask to switch to once the connectors have started.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+startsAsRoot           (none)                                           Whether or not the Dropwizard application is started as a root user.
+                                                                        Requires Jetty's ``libsetuid.so`` on ``java.library.path``.
+shutdownGracePeriod    30 seconds                                       The maximum time to wait for Jetty, and all Managed instances,
+                                                                        to cleanly shutdown before forcibly terminating them.
+allowedMethods         ``GET``, ``POST``, ``PUT``, ``DELETE``,          The set of allowed HTTP methods. Others will be rejected with a
+                       ``HEAD``, ``OPTIONS``, ``PATCH``                 405 Method Not Allowed response.
+====================== ===============================================  =============================================================================
+
+
+.. _man-configuration-gzip:
+
+GZip
+.....
+
+.. code-block:: yaml
+
+    server:
+      gzip: 
+        bufferSize: 8KiB
+
+
++----------------------+------------+---------------------------------------------------------------------------------------------------+ 
+|     Name             | Default    | Description                                                                                       | 
++======================+============+===================================================================================================+ 
+| enabled              | true       | If true, all requests with gzip in their Accept-Content-Encoding                                  | 
+|                      |            | headers will have their response entities encoded with gzip.                                      |
++----------------------+------------+---------------------------------------------------------------------------------------------------+
+| minimumEntitySize    | 256 bytes  | All response entities under this size are not compressed.                                         |
++----------------------+------------+---------------------------------------------------------------------------------------------------+
+| bufferSize           | 8KiB       | The size of the buffer to use when compressing.                                                   |
++----------------------+------------+---------------------------------------------------------------------------------------------------+
+| excludedUserAgents   | []         | The set of user agents to exclude from compression.                                               |
++----------------------+------------+---------------------------------------------------------------------------------------------------+
+| compressedMimeTypes  | []         | If specified, the set of mime types to compress.                                                  |
++----------------------+------------+---------------------------------------------------------------------------------------------------+
+
+
+.. _man-configuration-requestLog:
+
+Request Log
+...........
+
+.. code-block:: yaml
+
+    server:
+      requestLog: 
+        timeZone: UTC
+
+
+====================== ================ ===========
+Name                   Default          Description
+====================== ================ ===========
+timeZone               UTC              The time zone to which request timestamps will be converted.
+appenders              console appender The set of AppenderFactory appenders to which requests will be logged.
+                                        *TODO* See logging/appender refs for more info
+====================== ================ ===========
+
+
+.. _man-configuration-simple:
+
+Simple
+-------
+
+Extends the attributes that are available to :ref:`all servers <man-configuration-all>`
+
+.. code-block:: yaml
+
+    server:
+      type: simple
+      applicationContextPath: /application
+      adminContextPath: /admin
+      connector:
+        type: http
+        port: 8080
+
+
+
+========================  ===============   =====================================================================
+Name                      Default           Description
+========================  ===============   =====================================================================
+connector                 http connector    HttpConnectorFactory HTTP connector listening on port 8080.
+                                            The ConnectorFactory connector which will handle both application
+                                            and admin requests. TODO link to connector below.
+applicationContextPath    /application      The context path of the application servlets, including Jersey.
+adminContextPath          /admin            The context path of the admin servlets, including metrics and tasks.
+========================  ===============   =====================================================================
+
+
+.. _man-configuration-default:
+
+Default
+--------
+
+Extends the attributes that are available to :ref:`all servers <man-configuration-all>`
+
+.. code-block:: yaml
+
+    server:
+      adminMinThreads: 1
+      adminMaxThreads: 64
+      applicationConnectors:
+        - type: http
+          port: 8080
+        - type: https
+          port: 8443
+          keyStorePath: example.keystore
+          keyStorePassword: example
+          validateCerts: false
+      adminConnectors:
+        - type: http
+          port: 8081
+        - type: https
+          port: 8444
+          keyStorePath: example.keystore
+          keyStorePassword: example
+          validateCerts: false
+
+
+========================  =======================   =====================================================================
+Name                      Default                   Description
+========================  =======================   =====================================================================
+applicationConnectors     An `HTTP connector`_      A set of :ref:`connectors <man-configuration-connectors>` which will
+                          listening on port 8080.   handle application requests.
+adminConnectors           An `HTTP connector`_      An `HTTP connector`_ listening on port 8081.
+                          listening on port 8081.   A set of :ref:`connectors <man-configuration-connectors>` which will 
+                                                    handle admin requests.
+adminMinThreads           1                         The minimum number of threads to use for admin requests.
+adminMaxThreads           64                        The maximum number of threads to use for admin requests.
+========================  =======================   =====================================================================
+
+.. _`HTTP connector`:  https://github.com/dropwizard/dropwizard/blob/master/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpConnectorFactory.java
+
+.. _man-configuration-connectors:
+
+Connectors
+==========
+
+
+.. _man-configuration-http:
+
+HTTP
+------
+
+.. code-block:: yaml
+    
+    # Extending from the default server configuration
+    server:
+      applicationConnectors:
+        - type: http
+          port: 8080
+          bindHost: 127.0.0.1 # only bind to loopback
+          headerCacheSize: 512 bytes
+          outputBufferSize: 32KiB
+          maxRequestHeaderSize: 8KiB
+          maxResponseHeaderSize: 8KiB
+          inputBufferSize: 8KiB
+          idleTimeout: 30 seconds
+          minBufferPoolSize: 64 bytes
+          bufferPoolIncrement: 1KiB
+          maxBufferPoolSize: 64KiB
+          acceptorThreads: 1
+          selectorThreads: 2
+          acceptQueueSize: 1024
+          reuseAddress: true
+          soLingerTime: 345s
+          useServerHeader: false
+          useDateHeader: true
+          useForwardedHeaders: true
+
+
+======================== ==================  ======================================================================================
+Name                     Default             Description
+======================== ==================  ======================================================================================
+port                     8080                The TCP/IP port on which to listen for incoming connections.
+bindHost                 (none)              The hostname to bind to.
+headerCacheSize          512 bytes           The size of the header field cache.
+outputBufferSize         32KiB               The size of the buffer into which response content is aggregated before being sent to
+                                             the client. A larger buffer can improve performance by allowing a content producer
+                                             to run without blocking, however larger buffers consume more memory and may induce
+                                             some latency before a client starts processing the content.
+maxRequestHeaderSize     8KiB                The maximum size of a request header. Larger headers will allow for more and/or
+                                             larger cookies plus larger form content encoded  in a URL. However, larger headers
+                                             consume more memory and can make a server more vulnerable to denial of service
+                                             attacks.
+maxResponseHeaderSize    8KiB                The maximum size of a response header. Larger headers will allow for more and/or
+                                             larger cookies and longer HTTP headers (eg for redirection).  However, larger headers
+                                             will also consume more memory.
+inputBufferSize          8KiB                The size of the per-connection input buffer.
+idleTimeout              30 seconds          The maximum idle time for a connection, which roughly translates to the
+                                             `java.net.Socket#setSoTimeout(int)`_ call, although with NIO implementations
+                                             other mechanisms may be used to implement the timeout.
+                                             The max idle time is applied when waiting for a new message to be received on a connection
+                                             or when waiting for a new message to be sent on a connection.
+                                             This value is interpreted as the maximum time between some progress being made on the
+                                             connection. So if a single byte is read or written, then the timeout is reset.
+minBufferPoolSize        64 bytes            The minimum size of the buffer pool. 
+bufferPoolIncrement      1KiB                The increment by which the buffer pool should be increased.
+maxBufferPoolSize        64KiB               The maximum size of the buffer pool.
+acceptorThreads          # of CPUs/2         The number of worker threads dedicated to accepting connections.
+selectorThreads          # of CPUs           The number of worker threads dedicated to sending and receiving data.
+acceptQueueSize          (OS default)        The size of the TCP/IP accept queue for the listening socket.
+reuseAddress             true                Whether or not ``SO_REUSEADDR`` is enabled on the listening socket.
+soLingerTime             (disabled)          Enable/disable ``SO_LINGER`` with the specified linger time.
+useServerHeader          false               Whether or not to add the ``Server`` header to each response.
+useDateHeader            true                Whether or not to add the ``Date`` header to each response.
+useForwardedHeaders      true                Whether or not to look at ``X-Forwarded-*`` headers added by proxies. See
+                                             ``ForwardedRequestCustomize`` for details.
+======================== ==================  ======================================================================================
+
+.. _`java.net.Socket#setSoTimeout(int)`: http://docs.oracle.com/javase/7/docs/api/java/net/Socket.html#setSoTimeout(int)
+
+.. _man-configuration-https:
+
+HTTPS
+------
+
+Extends the attributes that are available to the :ref:`HTTP connector <man-configuration-http>`
+
+.. code-block:: yaml
+    
+    # Extending from the default server configuration
+    server:
+      applicationConnectors:
+        - type: https
+          port: 8443
+          ....
+          keyStorePath: /path/to/file
+          keyStorePassword: changeit
+          keyStoreType: JKS
+          keyStoreProvider: 
+          trustStorePath: /path/to/file
+          trustStorePassword: changeit
+          trustStoreType: JKS
+          trustStoreProvider: 
+          keyManagerPassword: changeit
+          needClientAuth: false
+          wantClientAuth: 
+          certAlias: <alias>
+          crlPath: /path/to/file
+          enableCRLDP: false
+          enableOCSP: false
+          maxCertPathLength: (unlimited)
+          ocspResponderUrl: (none)
+          jceProvider: (none)
+          validateCerts: true
+          validatePeers: true
+          supportedProtocols: SSLv3
+          supportedCipherSuites: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
+          allowRenegotiation: true
+          endpointIdentificationAlgorithm: (none)
+
+================================ ==================  ======================================================================================
+Name                             Default             Description
+================================ ==================  ======================================================================================
+keyStorePath                     REQUIRED            The path to the Java key store which contains the host certificate and private key.
+keyStorePassword                 REQUIRED            The password used to access the key store.
+keyStoreType                     JKS                 The type of key store (usually ``JKS``, ``PKCS12``, JCEKS``,
+                                                     ``Windows-MY``}, or ``Windows-ROOT``).
+keyStoreProvider                 (none)              The JCE provider to use to access the key store.
+trustStorePath                   (none)              The path to the Java key store which contains the CA certificates used to establish
+                                                     trust.
+trustStorePassword               (none)              The password used to access the trust store.
+trustStoreType                   JKS                 The type of trust store (usually ``JKS``, ``PKCS12``, ``JCEKS``,
+                                                     ``Windows-MY``, or ``Windows-ROOT``).
+trustStoreProvider               (none)              The JCE provider to use to access the trust store.
+keyManagerPassword               (none)              The password, if any, for the key manager.
+needClientAuth                   (none)              Whether or not client authentication is required.
+wantClientAuth                   (none)              Whether or not client authentication is requested.
+certAlias                        (none)              The alias of the certificate to use.
+crlPath                          (none)              The path to the file which contains the Certificate Revocation List.
+enableCRLDP                      false               Whether or not CRL Distribution Points (CRLDP) support is enabled.
+enableOCSP                       false               Whether or not On-Line Certificate Status Protocol (OCSP) support is enabled.
+maxCertPathLength                (unlimited)         The maximum certification path length.
+ocspResponderUrl                 (none)              The location of the OCSP responder.
+jceProvider                      (none)              The name of the JCE provider to use for cryptographic support.
+validateCerts                    true                Whether or not to validate TLS certificates before starting. If enabled, Dropwizard
+                                                     will refuse to start with expired or otherwise invalid certificates.
+validatePeers                    true                Whether or not to validate TLS peer certificates.
+supportedProtocols               (none)              A list of protocols (e.g., ``SSLv3``, ``TLSv1``) which are supported. All
+                                                     other protocols will be refused.
+supportedCipherSuites            (none)              A list of cipher suites (e.g., ``TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256``) which
+                                                     are supported. All other cipher suites will be refused
+excludedCipherSuites             (none)              A list of cipher suites (e.g., ``TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256``) which
+                                                     are excluded. These cipher suites will be refused and exclusion takes higher 
+                                                     precedence than inclusion, such that if a cipher suite is listed in 
+                                                     ``supportedCipherSuites`` and ``excludedCipherSuitse``, the cipher suite will be
+                                                     excluded. To verify that the proper cipher suites are being whitelisted and
+                                                     blacklisted, it is recommended to use the tool `sslyze`_.
+allowRenegotiation               true                Whether or not TLS renegotiation is allowed.
+endpointIdentificationAlgorithm  (none)              Which endpoint identification algorithm, if any, to use during the TLS handshake.
+================================ ==================  ======================================================================================
+
+.. _sslyze: https://github.com/iSECPartners/sslyze
+
+.. _man-configuration-spdy:
+
+SPDY
+------
+
+Extends the attributes that are available to the :ref:`HTTPS connector <man-configuration-https>`
+
+.. code-block:: yaml
+
+    server:
+      applicationConnectors:
+        - type: spdy3
+          port: 8445
+          keyStorePath: example.keystore
+          keyStorePassword: example
+          validateCerts: false
+
+
+====================== ===========  ===========
+Name                   Default      Description
+====================== ===========  ===========
+pushStrategy           (none)       The `push strategy`_ to use for server-initiated SPDY pushes.
+====================== ===========  ===========
+
+.. _`push strategy`: https://github.com/dropwizard/dropwizard/blob/master/dropwizard-spdy/src/main/java/io/dropwizard/spdy/PushStrategyFactory.java
+
+
+.. _man-configuration-logging:
+
+Logging
+=========
+
+.. code-block:: yaml
+
+    logging:
+      level: INFO
+      loggers:
+        io.dropwizard: INFO
+      appenders:
+        - type: console
+
+
+====================== ===========  ===========
+Name                   Default      Description
+====================== ===========  ===========
+level                  Level.INFO   Logback logging level
+loggers                (none)       
+appenders              (none)       one of console, file or syslog
+====================== ===========  ===========
+
+
+.. _man-configuration-logging-console:
+
+Console
+-------
+
+.. code-block:: yaml
+
+    logging:
+      level: INFO
+      appenders:
+        - type: console
+          threshold: ALL
+          timeZone: UTC
+          target: stdout
+          logFormat: # TODO
+
+
+====================== ===========  ===========
+Name                   Default      Description
+====================== ===========  ===========
+type                   REQUIRED     The appender type. Must be ``console``.
+threshold              ALL          The lowest level of events to print to the console.
+timeZone               UTC          The time zone to which event timestamps will be converted.
+target                 stdout       The name of the standard stream to which events will be written.
+                                    Can be ``stdout`` or ``stderr``.
+logFormat              default      The Logback pattern with which events will be formatted. See
+                                    the Logback_ documentation for details.
+====================== ===========  ===========
+
+.. _Logback: http://logback.qos.ch/manual/layouts.html#conversionWord
+
+
+.. _man-configuration-logging-file:
+
+File
+-------
+
+.. code-block:: yaml
+
+    logging:
+      level: INFO
+      appenders:
+        - type: file
+          currentLogFilename: /var/log/myapplication.log
+          threshold: ALL
+          archive: true
+          archivedLogFilenamePattern: /var/log/myapplication-%d.log
+          archivedFileCount: 5
+          timeZone: UTC
+          logFormat: # TODO
+
+
+============================ ===========  ==================================================================================================
+Name                         Default      Description
+============================ ===========  ==================================================================================================
+type                         REQUIRED     The appender type. Must be ``file``.
+currentLogFilename           REQUIRED     The filename where current events are logged.
+threshold                    ALL          The lowest level of events to write to the file.
+archive                      true         Whether or not to archive old events in separate files.
+archivedLogFilenamePattern   (none)       Required if ``archive`` is ``true``.
+                                          The filename pattern for archived files. ``%d`` is replaced with the date in ``yyyy-MM-dd`` form,
+                                          and the fact that it ends with ``.gz`` indicates the file will be gzipped as it's archived.                                
+                                          Likewise, filename patterns which end in ``.zip`` will be filled as they are archived.
+archivedFileCount            5            The number of archived files to keep. Must be between ``1`` and ``50``.
+timeZone                     UTC          The time zone to which event timestamps will be converted.
+logFormat                    default      The Logback pattern with which events will be formatted. See
+                                          the Logback_ documentation for details.
+============================ ===========  ==================================================================================================
+
+
+.. _man-configuration-logging-syslog:
+
+Syslog
+-------
+
+.. code-block:: yaml
+
+    logging:
+      level: INFO
+      appenders:
+        - type: syslog
+          host: localhost
+          port: 514
+          facility: local0
+          threshold: ALL
+          stackTracePrefix: \t
+          logFormat: # TODO
+
+
+============================ ===========  ==================================================================================================
+Name                         Default      Description
+============================ ===========  ==================================================================================================
+host                         localhost    The hostname of the syslog server.
+port                         514          The port on which the syslog server is listening.
+facility                     local0       The syslog facility to use. Can be either ``auth``, ``authpriv``,
+                                          ``daemon``, ``cron``, ``ftp``, ``lpr``, ``kern``, ``mail``,
+                                          ``news``, ``syslog``, ``user``, ``uucp``, ``local0``,
+                                          ``local1``, ``local2``, ``local3``, ``local4``, ``local5``,
+                                          ``local6``, or ``local7``.
+threshold                    ALL          The lowest level of events to write to the file.
+logFormat                    default      The Logback pattern with which events will be formatted. See
+                                          the Logback_ documentation for details.
+stackTracePrefix             \t           The prefix to use when writing stack trace lines (these are sent
+                                          to the syslog server separately from the main message)
+============================ ===========  ==================================================================================================
+
+
+.. _man-configuration-metrics:
+
+Metrics
+=========
+
+The metrics configuration has two fields; frequency and reporters.
+
+.. code-block:: yaml
+
+    metrics:
+      frequency: 1 second
+      reporters:
+        - type: <type>
+
+
+====================== ===========  ===========
+Name                   Default      Description
+====================== ===========  ===========
+frequency              1 second     The frequency to report metrics. Overridable per-reporter.
+reporters              (none)       A list of reporters to report metrics.
+====================== ===========  ===========
+
+
+.. _man-configuration-metrics-all:
+
+All Reporters
+-------------
+
+The following options are available for all metrics reporters.
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: <type>
+          durationUnit: milliseconds
+          rateUnit: seconds
+          excludes: (none)
+          includes: (all)
+          frequency: 1 second
+
+
+====================== =============  ===========
+Name                   Default        Description
+====================== =============  ===========
+durationUnit           milliseconds   The unit to report durations as. Overrides per-metric duration units.
+rateUnit               seconds        The unit to report rates as. Overrides per-metric rate units.
+excludes               (none)         Metrics to exclude from reports, by name. When defined, matching metrics will not be reported.
+includes               (all)          Metrics to include in reports, by name. When defined, only these metrics will be reported.
+frequency              (none)         The frequency to report metrics. Overrides the default.
+====================== =============  ===========
+
+
+.. _man-configuration-metrics-formatted:
+
+Formatted Reporters
+...................
+
+These options are available only to "formatted" reporters and extend the options available to :ref:`all reporters <man-configuration-metrics-all>`
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: <type>
+          locale: <system default>
+
+
+====================== ===============  ===========
+Name                   Default          Description
+====================== ===============  ===========
+locale                 System default   The Locale_ for formatting numbers, dates and times.
+====================== ===============  ===========
+
+.. _Locale: http://docs.oracle.com/javase/7/docs/api/java/util/Locale.html
+
+.. _man-configuration-metrics-console:
+
+Console Reporter
+----------------
+
+Reports metrics periodically to the console.
+
+Extends the attributes that are available to :ref:`formatted reporters <man-configuration-metrics-formatted>`
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: console
+          timeZone: UTC
+          output: stdout
+
+
+====================== ===============  ===========
+Name                   Default          Description
+====================== ===============  ===========
+timeZone               UTC              The timezone to display dates/times for.
+output                 stdout           The stream to write to. One of ``stdout`` or ``stderr``.
+====================== ===============  ===========
+
+
+.. _man-configuration-metrics-csv:
+
+CSV Reporter
+------------
+
+Reports metrics periodically to a CSV file.
+
+Extends the attributes that are available to :ref:`formatted reporters <man-configuration-metrics-formatted>`
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: csv
+          file: /path/to/file
+
+
+====================== ===============  ===========
+Name                   Default          Description
+====================== ===============  ===========
+file                   No default       The CSV file to write metrics to.
+====================== ===============  ===========
+
+
+.. _man-configuration-metrics-ganglia:
+
+Ganglia Reporter
+----------------
+
+Reports metrics periodically to Ganglia.
+
+Extends the attributes that are available to :ref:`all reporters <man-configuration-metrics-all>`
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: ganglia
+          host: localhost
+          port: 8649
+          mode: unicast
+          ttl: 1
+          uuid: (none)
+          spoof: localhost:8649
+          tmax: 60
+          dmax: 0
+
+
+====================== ===============  ====================================================================================================
+Name                   Default          Description
+====================== ===============  ====================================================================================================
+host                   localhost        The hostname (or group) of the Ganglia server(s) to report to.
+port                   8649             The port of the Ganglia server(s) to report to.
+mode                   unicast          The UDP addressing mode to announce the metrics with. One of ``unicast`` 
+                                        or ``multicast``.
+ttl                    1                The time-to-live of the UDP packets for the announced metrics.
+uuid                   (none)           The UUID to tag announced metrics with.
+spoof                  (none)           The hostname and port to use instead of this nodes for the announced metrics. 
+                                        In the format ``hostname:port``.
+tmax                   60               The tmax value to annouce metrics with.
+dmax                   0                The dmax value to announce metrics with.
+====================== ===============  ====================================================================================================
+
+
+.. _man-configuration-metrics-graphite:
+
+Graphite Reporter
+-----------------
+
+Reports metrics periodically to Graphite.
+
+Extends the attributes that are available to :ref:`all reporters <man-configuration-metrics-all>`
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: graphite
+          host: localhost
+          port: 8080
+          prefix: <prefix>
+
+
+====================== ===============  ====================================================================================================
+Name                   Default          Description
+====================== ===============  ====================================================================================================
+host                   localhost        The hostname of the Graphite server to report to.
+port                   8080             The port of the Graphite server to report to.
+prefix                 (none)           The prefix for Metric key names to report to Graphite.
+====================== ===============  ====================================================================================================
+
+
+.. _man-configuration-metrics-slf4j:
+
+SLF4J
+-----
+
+Reports metrics periodically by logging via SLF4J.
+
+Extends the attributes that are available to :ref:`all reporters <man-configuration-metrics-all>`
+
+See BaseReporterFactory_  and BaseFormattedReporterFactory_ for more options.
+
+.. _BaseReporterFactory:  https://github.com/dropwizard/dropwizard/blob/master/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseReporterFactory.java
+.. _BaseFormattedReporterFactory: https://github.com/dropwizard/dropwizard/blob/master/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseFormattedReporterFactory.java
+
+
+.. code-block:: yaml
+
+    metrics:
+      reporters:
+        - type: log
+          logger: metrics
+          markerName: <marker name>
+
+
+====================== ===============  ====================================================================================================
+Name                   Default          Description
+====================== ===============  ====================================================================================================
+logger                 metrics          The name of the logger to write metrics to.
+markerName             (none)           The name of the marker to mark logged metrics with.
+====================== ===============  ====================================================================================================
+
+
+.. _man-configuration-clients:
+
+Clients
+=========
+
+.. _man-configuration-clients-http:
+
+HttpClient
+-----
+
+See HttpClientConfiguration_  for more options.
+
+.. _HttpClientConfiguration:  https://github.com/dropwizard/dropwizard/blob/master/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientConfiguration.java
+
+.. code-block:: yaml
+
+    httpClient:
+      timeout: 500ms
+      connectionTimeout: 500ms
+      timeToLive: 1h
+      cookiesEnabled: false
+      maxConnections: 1024
+      maxConnectionsPerRoute: 1024
+      keepAlive: 0ms
+      retries: 0
+      userAgent: <application name> (<client name>)
+
+
+======================= ======================================  =============================================================================
+Name                    Default                                 Description
+======================= ======================================  =============================================================================
+timeout                 500 milliseconds                        The maximum idle time for a connection, once established.
+connectionTimeout       500 milliseconds                        The maximum time to wait for a connection to open.
+timeToLive              1 hour                                  The maximum time a pooled connection can stay idle (not leased to any thread)
+                                                                before it is shut down.
+cookiesEnabled          false                                   Whether or not to enable cookies.
+maxConnections          1024                                    The maximum number of concurrent open connections.
+maxConnectionsPerRoute  1024                                    The maximum number of concurrent open connections per route.
+keepAlive               0 milliseconds                          The maximum time a connection will be kept alive before it is reconnected. If set
+                                                                to 0, connections will be immediately closed after every request/response.
+retries                 0                                       The number of times to retry failed requests. Requests are only
+                                                                retried if they throw an exception other than ``InterruptedIOException``,
+                                                                ``UnknownHostException``, ``ConnectException``, or ``SSLException``.
+userAgent               ``applicationName`` (``clientName``)    The User-Agent to send with requests.
+======================= ======================================  =============================================================================
+
+
+.. _man-configuration-clients-jersey:
+
+JerseyClient
+-----
+
+Extends the attributes that are available to :ref:`http clients <man-configuration-clients-http>`
+
+See JerseyClientConfiguration_ and HttpClientConfiguration_ for more options.
+
+.. _JerseyClientConfiguration:  https://github.com/dropwizard/dropwizard/blob/master/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientConfiguration.java
+
+.. code-block:: yaml
+
+    jerseyClient:
+      minThreads: 1
+      maxThreads: 128
+      gzipEnabled: true
+      gzipEnabledForRequests: true
+
+
+======================= ==================  ===================================================================================================
+Name                    Default             Description
+======================= ==================  ===================================================================================================
+minThreads              1                   The minimum number of threads in the pool used for asynchronous requests.
+maxThreads              128                 The maximum number of threads in the pool used for asynchronous requests.
+gzipEnabled             true                Adds an Accept-Encoding: gzip header to all requests, and enables automatic gzip decoding of responses.
+gzipEnabledForRequests  true                Adds a Content-Encoding: gzip header to all requests, and enables automatic gzip encoding of requests.
+======================= ==================  ===================================================================================================
+
diff --git a/docs/source/manual/core.rst b/docs/source/manual/core.rst
new file mode 100644
index 0000000..7b32946
--- /dev/null
+++ b/docs/source/manual/core.rst
@@ -0,0 +1,1342 @@
+.. _man-core:
+
+###############
+Dropwizard Core
+###############
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-core`` module provides you with everything you'll need for most of your
+            applications.
+
+It includes:
+
+* Jetty, a high-performance HTTP server.
+* Jersey, a full-featured RESTful web framework.
+* Jackson, the best JSON library for the JVM.
+* Metrics, an excellent library for application metrics.
+* Guava, Google's excellent utility library.
+* Logback, the successor to Log4j, Java's most widely-used logging framework.
+* Hibernate Validator, the reference implementation of the Java Bean Validation standard.
+
+Dropwizard consists mostly of glue code to automatically connect and configure these components.
+
+.. _man-core-organization:
+
+Organizing Your Project
+=======================
+
+In general, we recommend you separate your projects into three Maven modules: ``project-api``,
+``project-client``, and ``project-application``.
+
+``project-api`` should contain your :ref:`man-core-representations`; ``project-client`` should use
+those classes and an :ref:`HTTP client <man-client>` to implement a full-fledged client for your
+application, and ``project-application`` should provide the actual application implementation, including
+:ref:`man-core-resources`.
+
+Our applications tend to look like this:
+
+* ``com.example.myapplication``:
+
+  * ``api``: :ref:`man-core-representations`.
+  * ``cli``: :ref:`man-core-commands`
+  * ``client``: :ref:`Client <man-client>` implementation for your application
+  * ``core``: Domain implementation
+  * ``jdbi``: :ref:`Database <man-jdbi>` access classes
+  * ``health``: :ref:`man-core-healthchecks`
+  * ``resources``: :ref:`man-core-resources`
+  * ``MyApplication``: The :ref:`application <man-core-application>` class
+  * ``MyApplicationConfiguration``: :ref:`configuration <man-core-configuration>` class
+
+.. _man-core-application:
+
+Application
+============
+
+The main entry point into a Dropwizard application is, unsurprisingly, the ``Application`` class. Each
+``Application`` has a **name**, which is mostly used to render the command-line interface. In the
+constructor of your ``Application`` you can add :ref:`man-core-bundles` and :ref:`man-core-commands` to
+your application.
+
+.. _man-core-configuration:
+
+Configuration
+=============
+
+Dropwizard provides a number of built-in configuration parameters. They are
+well documented in the `example project's configuration`__.
+
+.. __: https://github.com/dropwizard/dropwizard/blob/master/dropwizard-example/example.yml
+
+Each ``Application`` subclass has a single type parameter: that of its matching ``Configuration``
+subclass. These are usually at the root of your application's main package. For example, your User
+application would have two classes: ``UserApplicationConfiguration``, extending ``Configuration``, and
+``UserApplication``, extending ``Application<UserApplicationConfiguration>``.
+
+When your application runs :ref:`man-core-commands-configured` like the ``server`` command, Dropwizard
+parses the provided YAML configuration file and builds an instance of your application's configuration
+class by mapping YAML field names to object field names.
+
+.. note::
+
+    If your configuration file doesn't end in ``.yml`` or ``.yaml``, Dropwizard tries to parse it
+    as a JSON file.
+
+In order to keep your configuration file and class manageable, we recommend grouping related
+configuration parameters into independent configuration classes. If your application requires a set of
+configuration parameters in order to connect to a message queue, for example, we recommend that you
+create a new ``MessageQueueFactory`` class:
+
+.. code-block:: java
+
+    public class MessageQueueFactory {
+        @NotEmpty
+        private String host;
+
+        @Min(1)
+        @Max(65535)
+        private int port = 5672;
+
+        @JsonProperty
+        public String getHost() {
+            return host;
+        }
+
+        @JsonProperty
+        public void setHost(String host) {
+            this.host = host;
+        }
+
+        @JsonProperty
+        public int getPort() {
+            return port;
+        }
+
+        @JsonProperty
+        public void setPort(int port) {
+            this.port = port;
+        }
+
+        public MessageQueueClient build(Environment environment) {
+            MessageQueueClient client = new MessageQueueClient(getHost(), getPort());
+            environment.lifecycle().manage(new Managed() {
+                @Override
+                public void start() {
+                }
+
+                @Override
+                public void stop() {
+                    client.close();
+                }
+            };
+            return client;
+        }
+    }
+
+In this example our factory will automatically tie our ``MessageQueueClient`` connection to the
+lifecycle of our application's ``Environment``.
+
+Your main ``Configuration`` subclass can then include this as a member field:
+
+.. code-block:: java
+
+    public class ExampleApplicationConfiguration extends Configuration {
+        @Valid
+        @NotNull
+        private MessageQueueFactory messageQueue = new MessageQueueFactory();
+
+        @JsonProperty("messageQueue")
+        public MessageQueueFactory getMessageQueueFactory() {
+            return messageQueue;
+        }
+
+        @JsonProperty("messageQueue")
+        public void setMessageQueueFactory(MessageQueueFactory factory) {
+            this.messageQueue = factory;
+        }
+    }
+
+And your ``Application`` subclass can then use your factory to directly construct a client for the 
+message queue:
+
+.. code-block:: java
+
+    public void run(ExampleConfiguration configuration,
+                    Environment environment) {
+        MessageQueueClient messageQueue = configuration.getMessageQueueFactory().build(environment);
+    }
+
+Then, in your application's YAML file, you can use a nested ``messageQueue`` field:
+
+.. code-block:: java
+
+    messageQueue:
+      host: mq.example.com
+      port: 5673
+
+The ``@NotNull``, ``@NotEmpty``, ``@Min``, ``@Max``, and ``@Valid`` annotations are part of Dropwizard's
+:ref:`man-core-representations-validation` functionality. If your YAML configuration file's
+``messageQueue.host`` field was missing (or was a blank string), Dropwizard would refuse to start
+and would output an error message describing the issues.
+
+Once your application has parsed the YAML file and constructed its ``Configuration`` instance,
+Dropwizard then calls your ``Application`` subclass to initialize your application's ``Environment``.
+
+.. note::
+
+    You can override configuration settings by passing special Java system properties when starting
+    your application. Overrides must start with prefix ``dw.``, followed by the path to the
+    configuration value being overridden.
+
+    For example, to override the Logging level, you could start your application like this:
+
+    ``java -Ddw.logging.level=DEBUG server my-config.json``
+
+    This will work even if the configuration setting in question does not exist in your config file, in 
+    which case it will get added.
+
+    You can override configuration settings in arrays of objects like this:
+
+    ``java -Ddw.server.applicationConnectors[0].port=9090 server my-config.json``
+
+    You can override configuration settings in maps like this:
+
+    ``java -Ddw.database.properties.hibernate.hbm2ddl.auto=none server my-config.json``
+
+    You can also override a configuration setting that is an array of strings by using the ',' character
+    as an array element separator. For example, to override a configuration setting myapp.myserver.hosts
+    that is an array of strings in the configuration, you could start your service like this:
+    ``java -Ddw.myapp.myserver.hosts=server1,server2,server3 server my-config.json``
+
+    If you need to use the ',' character in one of the values, you can escape it by using '\,' instead.
+    
+    The array override facility only handles configuration elements that are arrays of simple strings. 
+    Also, the setting in question must already exist in your configuration file as an array; 
+    this mechanism will not work if the configuration key being overridden does not exist in your configuration 
+    file. If it does not exist or is not an array setting, it will get added as a simple string setting, including 
+    the ',' characters as part of the string.
+
+.. _man-core-environments:
+
+SSL
+---
+
+SSL support is built into Dropwizard. You will need to provide your own java
+keystore, which is outside the scope of this document (``keytool`` is the
+command you need). There is a test keystore you can use in the
+`Dropwizard example project`__.
+
+.. __: https://github.com/dropwizard/dropwizard/tree/master/dropwizard-example
+
+.. code-block:: yaml
+
+    server:
+      applicationConnectors:
+        - type: https
+          port: 8443
+          keyStorePath: example.keystore
+          keyStorePassword: example
+          validateCerts: false
+
+Bootstrapping
+=============
+
+Before a Dropwizard application can provide the command-line interface, parse a configuration file, or
+run as a server, it must first go through a bootstrapping phase. This phase corresponds to your
+``Application`` subclass's ``initialize`` method. You can add :ref:`man-core-bundles`,
+:ref:`man-core-commands`, or register Jackson modules to allow you to include custom types as part
+of your configuration class.
+
+Environments
+============
+
+A Dropwizard ``Environment`` consists of all the :ref:`man-core-resources`, servlets, filters,
+:ref:`man-core-healthchecks`, Jersey providers, :ref:`man-core-managed`, :ref:`man-core-tasks`, and
+Jersey properties which your application provides.
+
+Each ``Application`` subclass implements a ``run`` method. This is where you should be creating new
+resource instances, etc., and adding them to the given ``Environment`` class:
+
+.. code-block:: java
+
+    @Override
+    public void run(ExampleConfiguration config,
+                    Environment environment) {
+        // encapsulate complicated setup logic in factories
+        final Thingy thingy = config.getThingyFactory().build();
+
+        environment.jersey().register(new ThingyResource(thingy));
+        environment.healthChecks().register("thingy", new ThingyHealthCheck(thingy));
+    }
+
+It's important to keep the ``run`` method clean, so if creating an instance of something is
+complicated, like the ``Thingy`` class above, extract that logic into a factory.
+
+.. _man-core-healthchecks:
+
+Health Checks
+=============
+
+A health check is a runtime test which you can use to verify your application's behavior in its
+production environment. For example, you may want to ensure that your database client is connected
+to the database:
+
+.. code-block:: java
+
+    public class DatabaseHealthCheck extends HealthCheck {
+        private final Database database;
+
+        public DatabaseHealthCheck(Database database) {
+            this.database = database;
+        }
+
+        @Override
+        protected Result check() throws Exception {
+            if (database.isConnected()) {
+                return Result.healthy();
+            } else {
+                return Result.unhealthy("Cannot connect to " + database.getUrl());
+            }
+        }
+    }
+
+You can then add this health check to your application's environment:
+
+.. code-block:: java
+
+    environment.healthChecks().register("database", new DatabaseHealthCheck(database));
+
+By sending a ``GET`` request to ``/healthcheck`` on the admin port you can run these tests and view
+the results::
+
+    $ curl http://dw.example.com:8081/healthcheck
+    {"deadlocks":{"healthy":true},"database":{"healthy":true}}
+
+If all health checks report success, a ``200 OK`` is returned. If any fail, a
+``500 Internal Server Error`` is returned with the error messages and exception stack traces (if an
+exception was thrown).
+
+All Dropwizard applications ship with the ``deadlocks`` health check installed by default, which uses
+Java 1.6's built-in thread deadlock detection to determine if any threads are deadlocked.
+
+.. _man-core-managed:
+
+Managed Objects
+===============
+
+Most applications involve objects which need to be started and stopped: thread pools, database
+connections, etc. Dropwizard provides the ``Managed`` interface for this. You can either have the
+class in question implement the ``#start()`` and ``#stop()`` methods, or write a wrapper class which
+does so. Adding a ``Managed`` instance to your application's ``Environment`` ties that object's
+lifecycle to that of the application's HTTP server. Before the server starts, the ``#start()`` method is
+called. After the server has stopped (and after its graceful shutdown period) the ``#stop()`` method
+is called.
+
+For example, given a theoretical Riak__ client which needs to be started and stopped:
+
+.. __: http://riak.basho.com
+
+.. code-block:: java
+
+    public class RiakClientManager implements Managed {
+        private final RiakClient client;
+
+        public RiakClientManager(RiakClient client) {
+            this.client = client;
+        }
+
+        @Override
+        public void start() throws Exception {
+            client.start();
+        }
+
+        @Override
+        public void stop() throws Exception {
+            client.stop();
+        }
+    }
+
+.. code-block:: java
+
+    public class MyApplication extends Application<MyConfiguration>{
+        @Override
+        public void run(MyApplicationConfiguration configuration, Environment environment) {
+            RiakClient client = ...;
+            RiakClientManager riakClientManager = new RiakClientManager(client);
+            environment.lifecycle().manage(riakClientManager);
+        }
+    }
+
+If ``RiakClientManager#start()`` throws an exception--e.g., an error connecting to the server--your
+application will not start and a full exception will be logged. If ``RiakClientManager#stop()`` throws
+an exception, the exception will be logged but your application will still be able to shut down.
+
+It should be noted that ``Environment`` has built-in factory methods for ``ExecutorService`` and
+``ScheduledExecutorService`` instances which are managed. See ``LifecycleEnvironment#executorService``
+and ``LifecycleEnvironment#scheduledExecutorService`` for details.
+
+.. _man-core-bundles:
+
+Bundles
+=======
+
+A Dropwizard bundle is a reusable group of functionality, used to define blocks of an application's
+behavior. For example, ``AssetBundle`` provides a simple way to serve static assets from your
+application's ``src/main/resources/assets`` directory as files available from ``/assets/*`` in your
+application.
+
+Some bundles require configuration parameters. These bundles implement ``ConfiguredBundle`` and will
+require your application's ``Configuration`` subclass to implement a specific interface.
+
+Serving Assets
+--------------
+
+Either your application or your static assets can be served from the root path, but
+not both. The latter is useful when using Dropwizard to back a Javascript
+application. To enable it, move your application to a sub-URL.
+
+.. code-block:: yaml
+
+    server:
+      type: simple
+      applicationContextPath: /application/*  # Default value*
+
+Then use an extended ``AssetsBundle`` constructor to serve resources in the
+``assets`` folder from the root path. ``index.htm`` is served as the default
+page.
+
+.. code-block:: java
+
+    @Override
+    public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
+        bootstrap.addBundle(new AssetsBundle("/assets/", "/"));
+    }
+
+When an ``AssetBundle`` is added to the application, it is registered as a servlet
+using a default name of ``assets``. If the application needs to have multiple ``AssetBundle``
+instances, the extended constructor should be used to specify a unique name for the ``AssetBundle``.
+
+.. code-block:: java
+
+    @Override
+    public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
+        bootstrap.addBundle(new AssetsBundle("/assets/css", "/css", null, "css"));
+        bootstrap.addBundle(new AssetsBundle("/assets/js", "/js", null, "js"));
+        bootstrap.addBundle(new AssetsBundle("/assets/fonts", "/fonts", null, "fonts"));
+    }
+
+.. _man-core-commands:
+
+Commands
+========
+
+Commands are basic actions which Dropwizard runs based on the arguments provided on the command
+line. The built-in ``server`` command, for example, spins up an HTTP server and runs your application.
+Each ``Command`` subclass has a name and a set of command line options which Dropwizard will use to
+parse the given command line arguments.
+
+.. code-block:: java
+	
+    public class MyApplication extends Application<MyConfiguration>{
+        @Override
+        public void initialize(Bootstrap<DropwizardConfiguration> bootstrap) {
+            bootstrap.addCommand(new MyCommand());
+        }
+    }
+
+.. _man-core-commands-configured:
+
+Configured Commands
+-------------------
+
+Some commands require access to configuration parameters and should extend the ``ConfiguredCommand``
+class, using your application's ``Configuration`` class as its type parameter. Dropwizard will treat 
+the first argument on the command line as the path to a YAML configuration file, parse and validate it,
+and provide your command with an instance of the configuration class.
+
+.. _man-core-tasks:
+
+Tasks
+=====
+
+A ``Task`` is a run-time action your application provides access to on the administrative port via HTTP.
+All Dropwizard applications start with the ``gc`` task, which explicitly triggers the JVM's garbage
+collection. (This is useful, for example, for running full garbage collections during off-peak times
+or while the given application is out of rotation.) The execute method of a ``Task`` can be annotated
+with ``@Timed``, ``@Metered``, and ``@ExceptionMetered``. Dropwizard will automatically
+record runtime information about your tasks. Here's a basic task class:
+
+.. code-block:: java
+
+    public class TruncateDatabaseTask extends Task {
+        private final Database database;
+
+        public TruncateDatabaseTask(Database database) {
+            super('truncate');
+            this.database = database;
+        }
+
+          @Override
+        public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception {
+            this.database.truncate();
+        }
+    }
+
+You can then add this task to your application's environment:
+
+.. code-block:: java
+
+    environment.admin().addTask(new TruncateDatabaseTask(database));
+
+Running a task can be done by sending a ``POST`` request to ``/tasks/{task-name}`` on the admin
+port. For example::
+
+    $ curl -X POST http://dw.example.com:8081/tasks/gc
+    Running GC...
+    Done!
+
+.. _man-core-logging:
+
+Logging
+=======
+
+Dropwizard uses Logback_ for its logging backend. It provides an slf4j_ implementation, and even
+routes all ``java.util.logging``, Log4j, and Apache Commons Logging usage through Logback.
+
+.. _Logback: http://logback.qos.ch/
+.. _slf4j: http://www.slf4j.org/
+
+slf4j provides the following logging levels:
+
+``ERROR``
+  Error events that might still allow the application to continue running.
+``WARN``
+  Potentially harmful situations.
+``INFO``
+  Informational messages that highlight the progress of the application at coarse-grained level.
+``DEBUG``
+  Fine-grained informational events that are most useful to debug an application.
+``TRACE``
+  Finer-grained informational events than the ``DEBUG`` level.
+
+.. _man-core-logging-format:
+
+Log Format
+----------
+
+Dropwizard's log format has a few specific goals:
+
+* Be human readable.
+* Be machine parsable.
+* Be easy for sleepy ops folks to figure out why things are pear-shaped at 3:30AM using standard
+  UNIXy tools like ``tail`` and ``grep``.
+
+The logging output looks like this::
+
+    TRACE [2010-04-06 06:42:35,271] com.example.dw.Thing: Contemplating doing a thing.
+    DEBUG [2010-04-06 06:42:35,274] com.example.dw.Thing: About to do a thing.
+    INFO  [2010-04-06 06:42:35,274] com.example.dw.Thing: Doing a thing
+    WARN  [2010-04-06 06:42:35,275] com.example.dw.Thing: Doing a thing
+    ERROR [2010-04-06 06:42:35,275] com.example.dw.Thing: This may get ugly.
+    ! java.lang.RuntimeException: oh noes!
+    ! at com.example.dw.Thing.run(Thing.java:16)
+    !
+
+A few items of note:
+
+* All timestamps are in UTC and ISO 8601 format.
+* You can grep for messages of a specific level really easily::
+
+    tail -f dw.log | grep '^WARN'
+
+* You can grep for messages from a specific class or package really easily::
+
+    tail -f dw.log | grep 'com.example.dw.Thing'
+
+* You can even pull out full exception stack traces, plus the accompanying log message::
+
+    tail -f dw.log | grep -B 1 '^\!'
+
+* The `!` prefix does *not* apply to syslog appenders, as stack traces are sent separately from the main message.
+  Instead, `\t` is used (this is the default value of the `SyslogAppender` that comes with Logback). This can be
+  configured with the `stackTracePrefix` option when defining your appender.
+
+Configuration
+-------------
+
+You can specify a default logger level and even override the levels of
+other loggers in your YAML configuration file:
+
+.. code-block:: yaml
+
+    # Logging settings.
+    logging:
+
+      # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL.
+      level: INFO
+
+      # Logger-specific levels.
+      loggers:
+
+        # Overrides the level of com.example.dw.Thing and sets it to DEBUG.
+        "com.example.dw.Thing": DEBUG
+
+.. _man-core-logging-console:
+
+Console Logging
+---------------
+
+By default, Dropwizard applications log ``INFO`` and higher to ``STDOUT``. You can configure this by
+editing the ``logging`` section of your YAML configuration file:
+
+.. code-block:: yaml
+
+    logging:
+      appenders:
+        - type: console
+          threshold: WARN
+          target: stderr
+
+In the above, we're instead logging only ``WARN`` and ``ERROR`` messages to the ``STDERR`` device.
+
+.. _man-core-logging-file:
+
+File Logging
+------------
+
+Dropwizard can also log to an automatically rotated set of log files. This is the recommended
+configuration for your production environment:
+
+.. code-block:: yaml
+
+    logging:
+
+      appenders:
+        - type: file
+          # The file to which current statements will be logged.
+          currentLogFilename: ./logs/example.log
+
+          # When the log file rotates, the archived log will be renamed to this and gzipped. The
+          # %d is replaced with the previous day (yyyy-MM-dd). Custom rolling windows can be created
+          # by passing a SimpleDateFormat-compatible format as an argument: "%d{yyyy-MM-dd-hh}".
+          archivedLogFilenamePattern: ./logs/example-%d.log.gz
+
+          # The number of archived files to keep.
+          archivedFileCount: 5
+
+          # The timezone used to format dates. HINT: USE THE DEFAULT, UTC.
+          timeZone: UTC
+
+.. _man-core-logging-syslog:
+
+Syslog Logging
+--------------
+
+Finally, Dropwizard can also log statements to syslog.
+
+.. note::
+
+    Because Java doesn't use the native syslog bindings, your syslog server **must** have an open
+    network socket.
+
+.. code-block:: yaml
+
+    logging:
+
+      appenders:
+        - type: syslog
+          # The hostname of the syslog server to which statements will be sent.
+          # N.B.: If this is the local host, the local syslog instance will need to be configured to
+          # listen on an inet socket, not just a Unix socket.
+          host: localhost
+
+          # The syslog facility to which statements will be sent.
+          facility: local0
+
+You can combine any number of different ``appenders``, including multiple instances of the same 
+appender with different configurations:
+
+.. code-block:: yaml
+
+    logging:
+
+      # Permit DEBUG, INFO, WARN and ERROR messages to be logged by appenders.
+      level: DEBUG
+
+      appenders:
+        # Log warnings and errors to stderr
+        - type: console
+          threshold: WARN
+          target: stderr
+
+        # Log info, warnings and errors to our apps' main log.
+        # Rolled over daily and retained for 5 days.
+        - type: file
+          threshold: INFO
+          currentLogFilename: ./logs/example.log
+          archivedLogFilenamePattern: ./logs/example-%d.log.gz
+          archivedFileCount: 5
+
+        # Log debug messages, info, warnings and errors to our apps' debug log.
+        # Rolled over hourly and retained for 6 hours
+        - type: file
+          threshold: DEBUG
+          currentLogFilename: ./logs/debug.log
+          archivedLogFilenamePattern: ./logs/debug-%d{yyyy-MM-dd-hh}.log.gz
+          archivedFileCount: 6
+
+.. _man-core-testing-applications:
+
+Testing Applications
+====================
+
+All of Dropwizard's APIs are designed with testability in mind, so even your applications can have unit
+tests:
+
+.. code-block:: java
+
+    public class MyApplicationTest {
+        private final Environment environment = mock(Environment.class);
+        private final JerseyEnvironment jersey = mock(JerseyEnvironment.class);
+        private final MyApplication application = new MyApplication();
+        private final MyConfiguration config = new MyConfiguration();
+
+        @Before
+        public void setup() throws Exception {
+            config.setMyParam("yay");
+            when(environment.jersey()).thenReturn(jersey);
+        }
+
+        @Test
+        public void buildsAThingResource() throws Exception {
+            application.run(config, environment);
+
+            verify(jersey).register(any(ThingResource.class));
+        }
+    }
+
+We highly recommend Mockito_ for all your mocking needs.
+
+.. _Mockito: http://code.google.com/p/mockito/
+
+
+.. _man-core-banners:
+
+Banners
+=======
+
+We think applications should print out a big ASCII art banner on startup. Yours should, too. It's fun.
+Just add a ``banner.txt`` class to ``src/main/resources`` and it'll print it out when your application
+starts::
+
+    INFO  [2011-12-09 21:56:37,209] io.dropwizard.cli.ServerCommand: Starting hello-world
+                                                     dP
+                                                     88
+      .d8888b. dP.  .dP .d8888b. 88d8b.d8b. 88d888b. 88 .d8888b.
+      88ooood8  `8bd8'  88'  `88 88'`88'`88 88'  `88 88 88ooood8
+      88.  ...  .d88b.  88.  .88 88  88  88 88.  .88 88 88.  ...
+      `88888P' dP'  `dP `88888P8 dP  dP  dP 88Y888P' dP `88888P'
+                                            88
+                                            dP
+
+    INFO  [2011-12-09 21:56:37,214] org.eclipse.jetty.server.Server: jetty-7.6.0
+    ...
+
+We could probably make up an argument about why this is a serious devops best practice with high ROI
+and an Agile Tool, but honestly we just enjoy this.
+
+We recommend you use TAAG_ for all your ASCII art banner needs.
+
+.. _TAAG: http://patorjk.com/software/taag/
+
+.. _man-core-resources:
+
+Resources
+=========
+
+Unsurprisingly, most of your day-to-day work with a Dropwizard application will be in the resource
+classes, which model the resources exposed in your RESTful API. Dropwizard uses Jersey__ for this,
+so most of this section is just re-hashing or collecting various bits of Jersey documentation.
+
+.. __: http://jersey.java.net/
+
+Jersey is a framework for mapping various aspects of incoming HTTP requests to POJOs and then
+mapping various aspects of POJOs to outgoing HTTP responses. Here's a basic resource class:
+
+.. _man-core-resources-example:
+
+.. code-block:: java
+
+    @Path("/{user}/notifications")
+    @Produces(MediaType.APPLICATION_JSON)
+    @Consumes(MediaType.APPLICATION_JSON)
+    public class NotificationsResource {
+        private final NotificationStore store;
+
+        public NotificationsResource(NotificationStore store) {
+            this.store = store;
+        }
+
+        @GET
+        public NotificationList fetch(@PathParam("user") LongParam userId,
+                                      @QueryParam("count") @DefaultValue("20") IntParam count) {
+            final List<Notification> notifications = store.fetch(userId.get(), count.get());
+            if (notifications != null) {
+                return new NotificationList(userId, notifications);
+            }
+            throw new WebApplicationException(Status.NOT_FOUND);
+        }
+
+        @POST
+        public Response add(@PathParam("user") LongParam userId,
+                            @Valid Notification notification) {
+            final long id = store.add(userId.get(), notification);
+            return Response.created(UriBuilder.fromResource(NotificationResource.class)
+                                              .build(userId.get(), id))
+                           .build();
+        }
+    }
+
+This class provides a resource (a user's list of notifications) which responds to ``GET`` and
+``POST`` requests to ``/{user}/notifications``, providing and consuming ``application/json``
+representations. There's quite a lot of functionality on display here, and this section will
+explain in detail what's in play and how to use these features in your application.
+
+.. _man-core-resources-paths:
+
+Paths
+-----
+
+.. important::
+
+    Every resource class must have a ``@Path`` annotation.
+
+The ``@Path`` annotation isn't just a static string, it's a `URI Template`__. The ``{user}`` part
+denotes a named variable, and when the template matches a URI the value of that variable will be
+accessible via ``@PathParam``-annotated method parameters.
+
+.. __: http://tools.ietf.org/html/draft-gregorio-uritemplate-07
+
+For example, an incoming request for ``/1001/notifications`` would match the URI template, and the
+value ``"1001"`` would be available as the path parameter named ``user``.
+
+If your application doesn't have a resource class whose ``@Path`` URI template matches the URI of an
+incoming request, Jersey will automatically return a ``404 Not Found`` to the client.
+
+.. _man-core-resources-methods:
+
+Methods
+-------
+
+Methods on a resource class which accept incoming requests are annotated with the HTTP methods they
+handle: ``@GET``, ``@POST``, ``@PUT``, ``@DELETE``, ``@HEAD``, ``@OPTIONS``, ``@PATCH``.
+
+Support for arbitrary new methods can be added via the ``@HttpMethod`` annotation. They also must
+to be added to the :ref:`list of allowed methods <man-configuration-all>`. This means, by default,
+methods such as ``CONNECT`` and ``TRACE`` are blocked, and will return a ``405 Method Not Allowed``
+response.
+
+If a request comes in which matches a resource class's path but has a method which the class doesn't
+support, Jersey will automatically return a ``405 Method Not Allowed`` to the client.
+
+The return value of the method (in this case, a ``NotificationList`` instance) is then mapped to the
+:ref:`negotiated media type <man-core-resources-media-types>` this case, our resource only supports
+JSON, and so the ``NotificationList`` is serialized to JSON using Jackson.
+
+.. _man-core-resources-metrics:
+
+Metrics
+-------
+
+Every resource method can be annotated with ``@Timed``, ``@Metered``, and ``@ExceptionMetered``.
+Dropwizard augments Jersey to automatically record runtime information about your resource methods.
+
+
+.. _man-core-resources-parameters:
+
+Parameters
+----------
+
+The annotated methods on a resource class can accept parameters which are mapped to from aspects of
+the incoming request. The ``*Param`` annotations determine which part of the request the data is
+mapped, and the parameter *type* determines how the data is mapped.
+
+For example:
+
+* A ``@PathParam("user")``-annotated ``String`` takes the raw value from the ``user`` variable in
+  the matched URI template and passes it into the method as a ``String``.
+* A ``@QueryParam("count")``-annotated ``IntParam`` parameter takes the first ``count`` value from
+  the request's query string and passes it as a ``String`` to ``IntParam``'s constructor.
+  ``IntParam`` (and all other ``io.dropwizard.jersey.params.*`` classes) parses the string
+  as an ``Integer``, returning a ``400 Bad Request`` if the value is malformed.
+* A ``@FormParam("name")``-annotated ``Set<String>`` parameter takes all the ``name`` values from a
+  posted form and passes them to the method as a set of strings.
+
+What's noteworthy here is that you can actually encapsulate the vast majority of your validation
+logic using specialized parameter objects. See ``AbstractParam`` for details.
+
+.. _man-core-resources-request-entities:
+
+Request Entities
+----------------
+
+If you're handling request entities (e.g., an ``application/json`` object on a ``PUT`` request), you
+can model this as a parameter without a ``*Param`` annotation. In the
+:ref:`example code <man-core-resources-example>`, the ``add`` method provides a good example of
+this:
+
+.. code-block:: java
+    :emphasize-lines: 3
+
+    @POST
+    public Response add(@PathParam("user") LongParam userId,
+                        @Valid Notification notification) {
+        final long id = store.add(userId.get(), notification);
+        return Response.created(UriBuilder.fromResource(NotificationResource.class)
+                                          .build(userId.get(), id)
+                       .build();
+    }
+
+Jersey maps the request entity to any single, unbound parameter. In this case, because the resource
+is annotated with ``@Consumes(MediaType.APPLICATION_JSON)``, it uses the Dropwizard-provided Jackson
+support which, in addition to parsing the JSON and mapping it to an instance of ``Notification``,
+also runs that instance through Dropwizard's :ref:`man-core-representations-validation`.
+
+If the deserialized ``Notification`` isn't valid, Dropwizard returns a ``422 Unprocessable Entity``
+response to the client.
+
+.. note::
+
+    If your request entity parameter isn't annotated with ``@Valid``, it won't be validated.
+
+.. _man-core-resources-media-types:
+
+Media Types
+-----------
+
+Jersey also provides full content negotiation, so if your resource class consumes
+``application/json`` but the client sends a ``text/plain`` entity, Jersey will automatically reply
+with a ``406 Not Acceptable``. Jersey's even smart enough to use client-provided ``q``-values in
+their ``Accept`` headers to pick the best response content type based on what both the client and
+server will support.
+
+.. _man-core-resources-responses:
+
+Responses
+---------
+
+If your clients are expecting custom headers or additional information (or, if you simply desire an
+additional degree of control over your responses), you can return explicitly-built ``Response``
+objects:
+
+.. code-block:: java
+
+    return Response.noContent().language(Locale.GERMAN).build();
+
+
+In general, though, we recommend you return actual domain objects if at all possible. It makes
+:ref:`testing resources <man-core-resources-testing>` much easier.
+
+.. _man-core-resource-error-handling:
+
+Error Handling
+--------------
+
+If your resource class unintentionally throws an exception, Dropwizard will log that exception
+(including stack traces) and return a terse, safe ``text/plain`` ``500 Internal Server Error``
+response.
+
+If your resource class needs to return an error to the client (e.g., the requested record doesn't
+exist), you have two options: throw a subclass of ``Exception`` or restructure your method to
+return a ``Response``.
+
+If at all possible, prefer throwing ``Exception`` instances to returning
+``Response`` objects.
+
+If you throw a subclass of ``WebApplicationException`` jersey will map that to a defined response.
+
+If you want more control, you can also declare JerseyProviders in your Environment to map Exceptions
+to certain responses by calling ``JerseyEnvironment#register(Object)`` with an implementation of 
+javax.ws.rs.ext.ExceptionMapper.
+e.g. Your resource throws an InvalidArgumentException, but the response would be 400, bad request.
+
+
+.. _man-core-resources-uris:
+
+URIs
+----
+
+While Jersey doesn't quite have first-class support for hyperlink-driven applications, the provided
+``UriBuilder`` functionality does quite well.
+
+Rather than duplicate resource URIs, it's possible (and recommended!) to initialize a ``UriBuilder``
+with the path from the resource class itself:
+
+.. code-block:: java
+
+    UriBuilder.fromResource(UserResource.class).build(user.getId());
+
+.. _man-core-resources-testing:
+
+Testing
+-------
+
+As with just about everything in Dropwizard, we recommend you design your resources to be testable.
+Dependencies which aren't request-injected should be passed in via the constructor and assigned to
+``final`` fields.
+
+Testing, then, consists of creating an instance of your resource class and passing it a mock.
+(Again: Mockito_.)
+
+.. code-block:: java
+
+    public class NotificationsResourceTest {
+        private final NotificationStore store = mock(NotificationStore.class);
+        private final NotificationsResource resource = new NotificationsResource(store);
+
+        @Test
+        public void getsReturnNotifications() {
+            final List<Notification> notifications = mock(List.class);
+            when(store.fetch(1, 20)).thenReturn(notifications);
+
+            final NotificationList list = resource.fetch(new LongParam("1"), new IntParam("20"));
+
+            assertThat(list.getUserId(),
+                      is(1L));
+
+            assertThat(list.getNotifications(),
+                       is(notifications));
+        }
+    }
+
+Caching
+-------
+
+Adding a ``Cache-Control`` statement to your resource class is simple with Dropwizard:
+
+.. code-block:: java
+
+    @GET
+    @CacheControl(maxAge = 6, maxAgeUnit = TimeUnit.HOURS)
+    public String getCachableValue() {
+        return "yay";
+    }
+
+The ``@CacheControl`` annotation will take all of the parameters of the ``Cache-Control`` header.
+
+.. _man-core-representations:
+
+Representations
+===============
+
+Representation classes are classes which, when handled to various Jersey ``MessageBodyReader`` and
+``MessageBodyWriter`` providers, become the entities in your application's API. Dropwizard heavily
+favors JSON, but it's possible to map from any POJO to custom formats and back.
+
+.. _man-core-representations-basic:
+
+Basic JSON
+----------
+
+Jackson is awesome at converting regular POJOs to JSON and back. This file:
+
+.. code-block:: java
+
+    public class Notification {
+        private String text;
+
+        public Notification(String text) {
+            this.text = text;
+        }
+
+        @JsonProperty
+        public String getText() {
+            return text;
+        }
+
+        @JsonProperty
+        public void setText(String text) {
+            this.text = text;
+        }
+    }
+
+gets converted into this JSON:
+
+.. code-block:: javascript
+
+    {
+        "text": "hey it's the value of the text field"
+    }
+
+If, at some point, you need to change the JSON field name or the Java field without affecting the
+other, you can add an explicit field name to the ``@JsonProperty`` annotation.
+
+If you prefer immutable objects rather than JavaBeans, that's also doable:
+
+.. code-block:: java
+
+    public class Notification {
+        private final String text;
+
+        @JsonCreator
+        public Notification(@JsonProperty("text") String text) {
+            this.text = text;
+        }
+
+        @JsonProperty("text")
+        public String getText() {
+            return text;
+        }
+    }
+
+.. _man-core-representations-advanced:
+
+Advanced JSON
+-------------
+
+Not all JSON representations map nicely to the objects your application deals with, so it's sometimes
+necessary to use custom serializers and deserializers. Just annotate your object like this:
+
+.. code-block:: java
+
+    @JsonSerialize(using=FunkySerializer.class)
+    @JsonDeserialize(using=FunkyDeserializer.class)
+    public class Funky {
+        // ...
+    }
+
+Then make a ``FunkySerializer`` class which implements ``JsonSerializer<Funky>`` and a
+``FunkyDeserializer`` class which implements ``JsonDeserializer<Funky>``.
+
+.. _man-core-representations-advanced-snake-case:
+
+``snake_case``
+**************
+
+A common issue with JSON is the disagreement between ``camelCase`` and ``snake_case`` field names.
+Java and Javascript folks tend to like ``camelCase``; Ruby, Python, and Perl folks insist on
+``snake_case``. To make Dropwizard automatically convert field names to ``snake_case`` (and back),
+just annotate the class with ``@JsonSnakeCase``:
+
+.. code-block:: java
+
+    @JsonSnakeCase
+    public class Person {
+        private final String firstName;
+
+        @JsonCreator
+        public Person(@JsonProperty String firstName) {
+            this.firstName = firstName;
+        }
+
+        @JsonProperty
+        public String getFirstName() {
+            return firstName;
+        }
+    }
+
+This gets converted into this JSON:
+
+.. code-block:: javascript
+
+    {
+        "first_name": "Coda"
+    }
+
+.. _man-core-representations-validation:
+
+Validation
+----------
+
+Like :ref:`man-core-configuration`, you can add validation annotations to fields of your
+representation classes and validate them. If we're accepting client-provided ``Person`` objects, we
+probably want to ensure that the ``name`` field of the object isn't ``null`` or blank. We can do
+this as follows:
+
+.. code-block:: java
+
+    public class Person {
+
+        @NotEmpty // ensure that name isn't null or blank
+        private final String name;
+
+        @JsonCreator
+        public Person(@JsonProperty("name") String name) {
+            this.name = name;
+        }
+
+        @JsonProperty("name")
+        public String getName() {
+            return name;
+        }
+    }
+
+Then, in our resource class, we can add the ``@Valid`` annotation to the ``Person`` annotation:
+
+.. code-block:: java
+
+    @PUT
+    public Response replace(@Valid Person person) {
+        // ...
+    }
+
+If the ``name`` field is missing, Dropwizard will return a ``text/plain``
+``422 Unprocessable Entity`` response detailing the validation errors::
+
+    * name may not be empty
+
+.. _man-core-resources-validation-advanced:
+
+Advanced
+********
+
+More complex validations (for example, cross-field comparisons) are often hard to do using
+declarative annotations. As an emergency maneuver, add the ``@ValidationMethod`` to any
+``boolean``-returning method which begins with ``is``:
+
+.. code-block:: java
+
+    @ValidationMethod(message="may not be Coda")
+    public boolean isNotCoda() {
+        return !("Coda".equals(name));
+    }
+
+.. note::
+
+    Due to the rather daft JavaBeans conventions, the method must begin with ``is`` (e.g.,
+    ``#isValidPortRange()``. This is a limitation of Hibernate Validator, not Dropwizard.
+
+.. _man-core-representations-streaming:
+
+Streaming Output
+----------------
+
+If your application happens to return lots of information, you may get a big performance and efficiency
+bump by using streaming output. By returning an object which implements Jersey's ``StreamingOutput``
+interface, your method can stream the response entity in a chunk-encoded output stream. Otherwise,
+you'll need to fully construct your return value and *then* hand it off to be sent to the client.
+
+
+.. _man-core-representations-html:
+
+HTML Representations
+--------------------
+
+For generating HTML pages, check out Dropwizard's :ref:`views support <manual-views>`.
+
+.. _man-core-representations-custom:
+
+Custom Representations
+----------------------
+
+Sometimes, though, you've got some wacky output format you need to produce or consume and no amount
+of arguing will make JSON acceptable. That's unfortunate but OK. You can add support for arbitrary
+input and output formats by creating classes which implement Jersey's ``MessageBodyReader<T>`` and
+``MessageBodyWriter<T>`` interfaces. (Make sure they're annotated with ``@Provider`` and
+``@Produces("text/gibberish")`` or ``@Consumes("text/gibberish")``.) Once you're done, just add
+instances of them (or their classes if they depend on Jersey's ``@Context`` injection) to your
+application's ``Environment`` on initialization.
+
+.. _man-core-jersey-filters:
+
+Jersey filters
+--------------
+
+There might be cases when you want to filter out requests or modify them before they reach your Resources. Jersey
+provides you with the means to do so. If you want to stop the request from reaching your resources, throw a web-application
+``WebApplicationException``, if you want to modify the request or let it pass through the filter, return it.
+
+.. code-block:: java
+
+    public class DateNotSpecifiedFilter implements ContainerRequestFilter {
+
+        @Context ExtendedUriInfo extendedUriInfo;
+
+        @Override
+        public ContainerRequest filter(ContainerRequest request) {
+            boolean methodNeedsDateHeader = extendedUriInfo.getMatchedMethod().isAnnotationPresent(DateRequired.class);
+            String dateHeader = request.getHeaderValue(HttpHeaders.DATE);
+
+            if (methodNeedsDateHeader && dateHeader == null) {
+                Exception cause = new IllegalArgumentException("Date Header was not specified");
+                throw new WebApplicationException(cause, Response.Status.BAD_REQUEST);
+            } else {
+                return request;
+            }
+        }
+    }
+
+This example checks the request for the "Date" header, and denies the request if was ommitted and the method this request would call has a certain annotation present.
+You can then register this filter in your Application class, like so:
+
+.. code-block:: java
+
+    environment.jersey().getResourceConfig().getContainerRequestFilters().add(new DateNotSpecifiedFilter());
+
+
+.. _man-core-servlet-filters:
+
+Servlet filters
+---------------
+
+Another way to create filters is by creating servlet filters. They offer a way to to register filters that apply both to servlet requests as well as resource requests.
+Jetty comes with a few `bundled`_  filters which may already suit your needs. If you want to create your own filter,
+this example demonstrates a servlet filter analogous to the previous example:
+
+.. _bundled: http://www.eclipse.org/jetty/documentation/current/advanced-extras.html
+
+.. code-block:: java
+
+    public class DateNotSpecifiedServletFilter implements javax.servlet.Filter {
+        // Other methods in interface ommited for brevity
+
+        @Override
+        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+            if (request instanceof HttpServletRequest) {
+                String dateHeader = ((HttpServletRequest) request).getHeader(HttpHeaders.DATE);
+
+                if (dateHeader == null) {
+                    chain.doFilter(request, response); // This signals that the request should pass this filter
+                } else {
+                    HttpServletResponse httpResponse = (HttpServletResponse) response;
+                    httpResponse.setStatus(HttpStatus.BAD_REQUEST_400);
+                    httpResponse.getWriter().print("Date Header was not specified");
+                }
+            }
+        }
+    }
+
+
+This servlet filter can then be registered in your Application class by wrapping it in ``FilterHolder`` and adding it to the application context together with a
+specification for which paths this filter should active. Here's an example:
+
+.. code-block:: java
+
+        environment.servlets().addFilter("DateHeaderServletFilter", new DateHeaderServletFilter())
+                              .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*");
+.. _man-glue-detail:
+
+How it's glued together
+=======================
+
+When your application starts up, it will spin up a Jetty HTTP server, see ``DefaultServerFactory``.
+This server will have two handlers, one for your application port and the other for your admin port.
+The admin handler creates and registers the ``AdminServlet``. This has a handle to all of the
+application healthchecks and metrics via the ServletContext.
+
+The application port has an HttpServlet as well, this is composed of ``DropwizardResourceConfig``,
+which is an extension of Jersey's resource configuration that performs scanning to
+find root resource and provider classes. Ultimately when you call
+``env.jersey().register(new SomeResource())``,
+you are adding to the ``DropwizardResourceConfig``. This config is a jersey ``Application``, so all of
+your application resources are served from one ``Servlet``
+
+``DropwizardResourceConfig`` is where the various ResourceMethodDispatchAdapter are registered to
+enable the following functionality:
+
+    * Resource method requests with ``@Timed``, ``@Metered``, ``@ExceptionMetered`` are delegated to special dispatchers which decorate the metric telemetry
+    * Resources that return Guava Optional are unboxed. Present returns underlying type, and non present 404s
+    * Resource methods that are annotated with ``@CacheControl`` are delegated to a special dispatcher that decorates on the cache control headers
+    * Enables using Jackson to parse request entities into objects and generate response entities from objects, all while performing validation
+
+
diff --git a/docs/source/manual/example.rst b/docs/source/manual/example.rst
new file mode 100644
index 0000000..e08a393
--- /dev/null
+++ b/docs/source/manual/example.rst
@@ -0,0 +1,30 @@
+.. _man-example:
+
+################################
+Dropwizard Example, Step by Step
+################################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-example`` module provides you with a working example
+            of a dropwizard app
+
+
+* Open a terminal
+* Make sure you have maven installed
+* Make sure java home points at JDK 7
+* Make sure you have curl
+* mvn dependency:resolve
+* mvn clean compile install
+* mvn eclipse:eclipse -DdownloadSources=true
+* From eclipse, File --> Import --> Existing Project into workspace
+* java -jar ~/git/dropwizard/dropwizard-example/target/dropwizard-example-0.7.0-SNAPSHOT.jar db migrate example.yml
+* The above ran the liquibase migration in /src/main/resources/migrations.xml, creating the table schema
+* You can now start the app in your IDE by running ``java -jar ~/git/dropwizard/dropwizard-example/target/dropwizard-example-0.7.0-SNAPSHOT.jar db migrate example.yml``
+* Alternatively you can run this file in your IDE: ``com.example.helloworld.HelloWorldApplication server example.yml``
+* Insert a new person: ``curl -H "Content-Type: application/json" -X POST -d '{"fullName":"Coda Hale", "jobTitle" : "Chief Wizard" }' http://localhost:8080/people``
+* Retrieve that person: ``curl http://localhost:8080/people/1``
+* View the freemarker template: ``curl http://localhost:8080/people/1/view_freemarker``
+* View the mustache template: ``curl http://localhost:8080/people/1/view_mustache``
+
+
diff --git a/docs/source/manual/hibernate.rst b/docs/source/manual/hibernate.rst
new file mode 100644
index 0000000..fc1017f
--- /dev/null
+++ b/docs/source/manual/hibernate.rst
@@ -0,0 +1,168 @@
+.. _man-hibernate:
+
+####################
+Dropwizard Hibernate
+####################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-hibernate`` module provides you with managed access to Hibernate_, a
+            powerful, industry-standard object-relation mapper (ORM).
+
+.. _Hibernate: http://www.hibernate.org/
+
+Configuration
+=============
+
+To create a :ref:`managed <man-core-managed>`, instrumented ``SessionFactory`` instance, your
+:ref:`configuration class <man-core-configuration>` needs a ``DataSourceFactory`` instance:
+
+.. code-block:: java
+
+    public class ExampleConfiguration extends Configuration {
+        @Valid
+        @NotNull
+        @JsonProperty("database")
+        private DataSourceFactory database = new DataSourceFactory();
+
+        public DataSourceFactory getDataSourceFactory() {
+            return database;
+        }
+    }
+
+Then, add a ``HibernateBundle`` instance to your application class, specifying your entity classes
+and how to get a ``DataSourceFactory`` from your configuration subclass:
+
+.. code-block:: java
+
+    private final HibernateBundle<ExampleConfiguration> hibernate = new HibernateBundle<ExampleConfiguration>(Person.class) {
+        @Override
+        public DataSourceFactory getDataSourceFactory(ExampleConfiguration configuration) {
+            return configuration.getDataSourceFactory();
+        }
+    };
+
+    @Override
+    public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
+        bootstrap.addBundle(hibernate);
+    }
+
+    @Override
+    public void run(ExampleConfiguration config,
+                    Environment environment) throws ClassNotFoundException {
+        final UserDAO dao = new UserDAO(hibernate.getSessionFactory());
+        environment.jersey().register(new UserResource(dao));
+    }
+
+This will create a new :ref:`managed <man-core-managed>` connection pool to the database, a
+:ref:`health check <man-core-healthchecks>` for connectivity to the database, and a new
+``SessionFactory`` instance for you to use in your DAO classes.
+
+Your application's configuration file will then look like this:
+
+.. code-block:: yaml
+
+    database:
+      # the name of your JDBC driver
+      driverClass: org.postgresql.Driver
+
+      # the username
+      user: pg-user
+
+      # the password
+      password: iAMs00perSecrEET
+
+      # the JDBC URL
+      url: jdbc:postgresql://db.example.com/db-prod
+
+      # any properties specific to your JDBC driver:
+      properties:
+        charSet: UTF-8
+        hibernate.dialect: org.hibernate.dialect.PostgreSQLDialect
+
+      # the maximum amount of time to wait on an empty pool before throwing an exception
+      maxWaitForConnection: 1s
+
+      # the SQL query to run when validating a connection's liveness
+      validationQuery: "/* MyApplication Health Check */ SELECT 1"
+
+      # the minimum number of connections to keep open
+      minSize: 8
+
+      # the maximum number of connections to keep open
+      maxSize: 32
+
+      # whether or not idle connections should be validated
+      checkConnectionWhileIdle: false
+
+Usage
+=====
+
+Data Access Objects
+-------------------
+
+Dropwizard comes with ``AbstractDAO``, a minimal template for entity-specific DAO classes. It
+contains type-safe wrappers for most of ``SessionFactory``'s common operations:
+
+.. code-block:: java
+
+    public class PersonDAO extends AbstractDAO<Person> {
+        public PersonDAO(SessionFactory factory) {
+            super(factory);
+        }
+
+        public Person findById(Long id) {
+            return get(id);
+        }
+
+        public long create(Person person) {
+            return persist(person).getId();
+        }
+
+        public List<Person> findAll() {
+            return list(namedQuery("com.example.helloworld.core.Person.findAll"));
+        }
+    }
+
+Transactional Resource Methods
+------------------------------
+
+Dropwizard uses a declarative method of scoping transactional boundaries. Not all resource methods
+actually require database access, so the ``@UnitOfWork`` annotation is provided:
+
+.. code-block:: java
+
+    @GET
+    @Timed
+    @UnitOfWork
+    public Person findPerson(@PathParam("id") LongParam id) {
+        return dao.findById(id.get());
+    }
+
+This will automatically open a session, begin a transaction, call ``findByPerson``, commit the
+transaction, and finally close the session. If an exception is thrown, the transaction is rolled
+back.
+
+.. important:: The Hibernate session is closed **before** your resource method's return value (e.g.,
+               the ``Person`` from the database), which means your resource method (or DAO) is
+               responsible for initializing all lazily-loaded collections, etc., before returning.
+               Otherwise, you'll get a ``LazyInitializationException`` thrown in your template (or
+               ``null`` values produced by Jackson).
+
+Prepended Comments
+==================
+
+Dropwizard automatically configures Hibernate to prepend a comment describing the context of all
+queries:
+
+.. code-block:: sql
+
+    /* load com.example.helloworld.core.Person */
+    select
+        person0_.id as id0_0_,
+        person0_.fullName as fullName0_0_,
+        person0_.jobTitle as jobTitle0_0_
+    from people person0_
+    where person0_.id=?
+
+This will allow you to quickly determine the origin of any slow or misbehaving queries.
diff --git a/docs/source/manual/index.rst b/docs/source/manual/index.rst
new file mode 100644
index 0000000..b5b4c4c
--- /dev/null
+++ b/docs/source/manual/index.rst
@@ -0,0 +1,25 @@
+.. _manual-index:
+
+###########
+User Manual
+###########
+
+.. rubric:: This goal of this document is to provide you with all the information required to build,
+            organize, test, deploy, and maintain Dropwizard-based applications. If you're new to
+            Dropwizard, you should read the :ref:`getting-started` guide first.
+
+.. toctree::
+    :maxdepth: 1
+
+    core
+    client
+    jdbi
+    migrations
+    hibernate
+    auth
+    views
+    scala
+    testing
+    example
+    configuration
+
diff --git a/docs/source/manual/jdbi.rst b/docs/source/manual/jdbi.rst
new file mode 100644
index 0000000..9c89648
--- /dev/null
+++ b/docs/source/manual/jdbi.rst
@@ -0,0 +1,152 @@
+.. _man-jdbi:
+
+###############
+Dropwizard JDBI
+###############
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-jdbi`` module provides you with managed access to JDBI_, a flexible and
+            modular library for interacting with relational databases via SQL.
+
+.. _JDBI: http://jdbi.org/
+
+Configuration
+=============
+
+To create a :ref:`managed <man-core-managed>`, instrumented ``DBI`` instance, your
+:ref:`configuration class <man-core-configuration>` needs a ``DataSourceFactory`` instance:
+
+.. code-block:: java
+
+    public class ExampleConfiguration extends Configuration {
+        @Valid
+        @NotNull
+        @JsonProperty
+        private DataSourceFactory database = new DataSourceFactory();
+
+        public DataSourceFactory getDataSourceFactory() {
+            return database;
+        }
+    }
+
+Then, in your service's ``run`` method, create a new ``DBIFactory``:
+
+.. code-block:: java
+
+    @Override
+    public void run(ExampleConfiguration config,
+                    Environment environment) throws ClassNotFoundException {
+        final DBIFactory factory = new DBIFactory();
+        final DBI jdbi = factory.build(environment, config.getDataSourceFactory(), "postgresql");
+        final UserDAO dao = jdbi.onDemand(UserDAO.class);
+        environment.jersey().register(new UserResource(dao));
+    }
+
+This will create a new :ref:`managed <man-core-managed>` connection pool to the database, a
+:ref:`health check <man-core-healthchecks>` for connectivity to the database, and a new ``DBI``
+instance for you to use. Note the ``ClassNotFoundException`` is thrown by the ``DBIFactory`` class
+when the ``build`` method is unable to locate the JDBC driver class. This will cause the service to
+exit displaying the output of the exception.
+
+Your service's configuration file will then look like this:
+
+.. code-block:: yaml
+
+    database:
+      # the name of your JDBC driver
+      driverClass: org.postgresql.Driver
+
+      # the username
+      user: pg-user
+
+      # the password
+      password: iAMs00perSecrEET
+
+      # the JDBC URL
+      url: jdbc:postgresql://db.example.com/db-prod
+
+      # any properties specific to your JDBC driver:
+      properties:
+        charSet: UTF-8
+
+      # the maximum amount of time to wait on an empty pool before throwing an exception
+      maxWaitForConnection: 1s
+
+      # the SQL query to run when validating a connection's liveness
+      validationQuery: "/* MyService Health Check */ SELECT 1"
+
+      # the minimum number of connections to keep open
+      minSize: 8
+
+      # the maximum number of connections to keep open
+      maxSize: 32
+
+      # whether or not idle connections should be validated
+      checkConnectionWhileIdle: false
+
+      # the amount of time to sleep between runs of the idle connection validation, abandoned cleaner and idle pool resizing
+      evictionInterval: 10s
+
+      # the minimum amount of time an connection must sit idle in the pool before it is eligible for eviction
+      minIdleTime: 1 minute
+
+Usage
+=====
+
+We highly recommend you use JDBI's `SQL Objects API`_, which allows you to write DAO classes as
+interfaces:
+
+.. _SQL Objects API: http://jdbi.org/sql_object_overview/
+
+.. code-block:: java
+
+    public interface MyDAO {
+      @SqlUpdate("create table something (id int primary key, name varchar(100))")
+      void createSomethingTable();
+
+      @SqlUpdate("insert into something (id, name) values (:id, :name)")
+      void insert(@Bind("id") int id, @Bind("name") String name);
+
+      @SqlQuery("select name from something where id = :id")
+      String findNameById(@Bind("id") int id);
+    }
+
+    final MyDAO dao = database.onDemand(MyDAO.class);
+
+This ensures your DAO classes are trivially mockable, as well as encouraging you to extract mapping
+code (e.g., ``ResultSet`` -> domain objects) into testable, reusable classes.
+
+Exception Handling
+==================
+
+By adding the ``DBIExceptionsBundle`` to your :ref:`application <man-core-application>`, Dropwizard
+will automatically unwrap any thrown ``SQLException`` or ``DBIException`` instances.
+This is critical for debugging, since otherwise only the common wrapper exception's stack trace is
+logged.
+
+Prepended Comments
+==================
+
+If you're using JDBI's `SQL Objects API`_ (and you should be), ``dropwizard-jdbi`` will
+automatically prepend the SQL object's class and method name to the SQL query as an SQL comment:
+
+.. code-block:: sql
+
+    /* com.example.service.dao.UserDAO.findByName */
+    SELECT id, name, email
+    FROM users
+    WHERE name = 'Coda';
+
+This will allow you to quickly determine the origin of any slow or misbehaving queries.
+
+Guava Support
+=============
+
+``dropwizard-jdbi`` supports ``Optional<T>`` arguments and ``ImmutableList<T>`` and
+``ImmutableSet<T>`` query results.
+
+Joda Time Support
+=================
+``dropwizard-jdbi`` supports joda-time ``DateTime`` arguments and ``DateTime`` fields in query results.
+
diff --git a/docs/source/manual/migrations.rst b/docs/source/manual/migrations.rst
new file mode 100644
index 0000000..7b38961
--- /dev/null
+++ b/docs/source/manual/migrations.rst
@@ -0,0 +1,220 @@
+.. _man-migrations:
+
+#####################
+Dropwizard Migrations
+#####################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-migrations`` module provides you with a wrapper for Liquibase_ database
+            refactoring.
+
+.. _Liquibase: http://www.liquibase.org
+
+Configuration
+=============
+
+Like :ref:`man-jdbi`, your :ref:`configuration class <man-core-configuration>` needs a
+``DataSourceFactory`` instance:
+
+.. code-block:: java
+
+    public class ExampleConfiguration extends Configuration {
+        @Valid
+        @NotNull
+        @JsonProperty("database")
+        private DataSourceFactory database = new DataSourceFactory();
+
+        public DataSourceFactory getDataSourceFactory() {
+            return database;
+        }
+    }
+
+Adding The Bundle
+=================
+
+Then, in your application's ``initialize`` method, add a new ``MigrationsBundle`` subclass:
+
+.. code-block:: java
+
+    @Override
+    public void initialize(Bootstrap<ExampleConfiguration> bootstrap) {
+        bootstrap.addBundle(new MigrationsBundle<ExampleConfiguration>() {
+            @Override
+	        public DataSourceFactory getDataSourceFactory(ExampleConfiguration configuration) {
+	            return configuration.getDataSourceFactory();
+	        }
+        });
+    }
+
+Defining Migrations
+===================
+
+Your database migrations are stored in your Dropwizard project, in
+``src/main/resources/migrations.xml``. This file will be packaged with your application, allowing you to
+run migrations using your application's command-line interface.
+
+For example, to create a new ``people`` table, I might create an initial ``migrations.xml`` like
+this:
+
+.. code-block:: xml
+
+    <?xml version="1.0" encoding="UTF-8"?>
+
+    <databaseChangeLog
+            xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+             http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.0.xsd">
+
+        <changeSet id="1" author="codahale">
+            <createTable tableName="people">
+                <column name="id" type="bigint" autoIncrement="true">
+                    <constraints primaryKey="true" nullable="false"/>
+                </column>
+                <column name="fullName" type="varchar(255)">
+                    <constraints nullable="false"/>
+                </column>
+                <column name="jobTitle" type="varchar(255)"/>
+            </createTable>
+        </changeSet>
+    </databaseChangeLog>
+
+For more information on available database refactorings, check the Liquibase_ documentation.
+
+Checking Your Database's State
+==============================
+
+To check the state of your database, use the ``db status`` command:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db status helloworld.yml
+
+Dumping Your Schema
+===================
+
+If your database already has an existing schema and you'd like to pre-seed your ``migrations.xml``
+document, you can run the ``db dump`` command:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db dump helloworld.yml
+
+This will output a Liquibase_ change log with a change set capable of recreating your database.
+
+Tagging Your Schema
+===================
+
+To tag your schema at a particular point in time (e.g., to make rolling back easier), use the
+``db tag`` command:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db tag helloworld.yml 2012-10-08-pre-user-move
+
+Migrating Your Schema
+=====================
+
+To apply pending change sets to your database schema, run the ``db migrate`` command:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db migrate helloworld.yml
+
+.. warning::
+
+    This will potentially make irreversible changes to your database. Always check the pending DDL
+    scripts by using the ``--dry-run`` flag first. This will output the SQL to be run to stdout.
+
+.. note::
+
+    To apply only a specific number of pending change sets, use the ``--count`` flag.
+
+Rolling Back Your Schema
+========================
+
+To roll back change sets which have already been applied, run the ``db rollback`` command. You will
+need to specify either a **tag**, a **date**, or a **number of change sets** to roll back to:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db rollback helloworld.yml --tag 2012-10-08-pre-user-move
+
+.. warning::
+
+    This will potentially make irreversible changes to your database. Always check the pending DDL
+    scripts by using the ``--dry-run`` flag first. This will output the SQL to be run to stdout.
+
+Testing Migrations
+==================
+
+To verify that a set of pending change sets can be fully rolled back, use the ``db test`` command,
+which will migrate forward, roll back to the original state, then migrate forward again:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db test helloworld.yml
+
+.. warning::
+
+    Do not run this in production, for obvious reasons.
+
+Preparing A Rollback Script
+===========================
+
+To prepare a rollback script for pending change sets *before* they have been applied, use the
+``db prepare-rollback`` command:
+
+.. code-block:: text
+
+    java -jar hello-world.jar db prepare-rollback helloworld.yml
+
+This will output a DDL script to stdout capable of rolling back all unapplied change sets.
+
+Generating Documentation
+========================
+
+To generate HTML documentation on the current status of the database, use the ``db generate-docs``
+command:
+
+.. code-block:: text
+
+     java -jar hello-world.jar db generate-docs helloworld.yml ~/db-docs/
+
+Dropping All Objects
+====================
+
+To drop all objects in the database, use the ``db drop-all`` command:
+
+.. code-block:: text
+
+     java -jar hello-world.jar db drop-all --confirm-delete-everything helloworld.yml
+
+.. warning::
+
+    You need to specify the ``--confirm-delete-everything`` flag because this command **deletes
+    everything in the database**. Be sure you want to do that first.
+
+Fast-Forwarding Through A Change Set
+====================================
+
+To mark a pending change set as applied (e.g., after having backfilled your ``migrations.xml`` with
+``db dump``), use the ``db fast-forward`` command:
+
+.. code-block:: text
+
+     java -jar hello-world.jar db fast-forward helloworld.yml
+
+This will mark the next pending change set as applied. You can also use the ``--all`` flag to mark
+all pending change sets as applied.
+
+More Information
+================
+
+If you are using databases supporting multiple schemas like PostgreSQL, Oracle, or H2, you can use the
+optional ``--catalog`` and ``--schema`` arguments to specify the database catalog and schema used for the
+Liquibase commands.
+
+For more information on available commands, either use the ``db --help`` command, or for more
+detailed help on a specific command, use ``db <cmd> --help``.
diff --git a/docs/source/manual/scala.rst b/docs/source/manual/scala.rst
new file mode 100644
index 0000000..eed0da3
--- /dev/null
+++ b/docs/source/manual/scala.rst
@@ -0,0 +1,9 @@
+.. _manual-scala:
+
+##################
+Dropwizard & Scala
+##################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-scala`` module is now maintained and documented `elsewhere <https://github.com/bretthoerner/dropwizard-scala>`_.
diff --git a/docs/source/manual/testing.rst b/docs/source/manual/testing.rst
new file mode 100644
index 0000000..eacff35
--- /dev/null
+++ b/docs/source/manual/testing.rst
@@ -0,0 +1,239 @@
+.. _manual-testing:
+
+##################
+Testing Dropwizard
+##################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-testing`` module provides you with some handy classes for testing
+            your :ref:`representation classes <man-core-representations>`
+            and :ref:`resource classes <man-core-resources>`. It also provides a JUnit rule
+            for full-stack testing of your entire app.
+
+.. _man-testing-representations:
+
+Testing Representations
+=======================
+
+While Jackson's JSON support is powerful and fairly easy-to-use, you shouldn't just rely on
+eyeballing your representation classes to ensure you're actually producing the API you think you
+are. By using the helper methods in `FixtureHelpers` you can add unit tests for serializing and
+deserializing your representation classes to and from JSON.
+
+Let's assume we have a ``Person`` class which your API uses as both a request entity (e.g., when
+writing via a ``PUT`` request) and a response entity (e.g., when reading via a ``GET`` request):
+
+.. code-block:: java
+
+    public class Person {
+        @JsonProperty
+        private String name;
+
+        @JsonProperty
+        private String email;
+
+        private Person() {
+            // Jackson deserialization
+        }
+
+        public Person(String name, String email) {
+            this.name = name;
+            this.email = email;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getEmail() {
+            return email;
+        }
+
+        public void setEmail(String email) {
+            this.email = email;
+        }
+
+        // hashCode
+        // equals
+        // toString etc.
+    }
+
+.. _man-testing-representations-fixtures:
+
+Fixtures
+--------
+
+First, write out the exact JSON representation of a ``Person`` in the
+``src/test/resources/fixtures`` directory of your Dropwizard project as ``person.json``:
+
+.. code-block:: javascript
+
+    {
+        "name": "Luther Blissett",
+        "email": "lb at example.com"
+    }
+
+.. _man-testing-representations-serialization:
+
+Testing Serialization
+---------------------
+
+Next, write a test for serializing a ``Person`` instance to JSON:
+
+.. code-block:: java
+
+    import static io.dropwizard.testing.FixtureHelpers.*;
+    import static org.fest.assertions.api.Assertions.assertThat;
+    import io.dropwizard.jackson.Jackson;
+    import org.junit.Test;
+    import com.fasterxml.jackson.databind.ObjectMapper;
+
+    public class PersonTest {
+
+        private static final ObjectMapper MAPPER = Jackson.newObjectMapper();
+
+        @Test
+        public void serializesToJSON() throws Exception {
+            final Person person = new Person("Luther Blissett", "lb at example.com");
+            assertThat(MAPPER.writeValueAsString(person))
+                    .isEqualTo(fixture("fixtures/person.json"));
+        }
+    }
+
+This test uses `FEST matchers`_ and JUnit_ to test that when a ``Person`` instance is serialized
+via Jackson it matches the JSON in the fixture file. (The comparison is done via a normalized JSON
+string representation, so whitespace doesn't affect the results.)
+
+.. _FEST matchers: https://code.google.com/p/fest/
+.. _JUnit: http://www.junit.org/
+
+.. _man-testing-representations-deserialization:
+
+Testing Deserialization
+-----------------------
+
+Next, write a test for deserializing a ``Person`` instance from JSON:
+
+.. code-block:: java
+
+    import static io.dropwizard.testing.FixtureHelpers.*;
+    import static org.fest.assertions.api.Assertions.assertThat;
+    import io.dropwizard.jackson.Jackson;
+    import org.junit.Test;
+    import com.fasterxml.jackson.databind.ObjectMapper;
+
+    public class PersonTest {
+
+        private static final ObjectMapper MAPPER = Jackson.newObjectMapper();
+
+        @Test
+        public void deserializesFromJSON() throws Exception {
+            final Person person = new Person("Luther Blissett", "lb at example.com");
+            assertThat(MAPPER.readValue(fixture("fixtures/person.json"), Person.class))
+                    .isEqualTo(person);
+        }
+    }
+
+
+This test uses `FEST matchers`_ and JUnit_ to test that when a ``Person`` instance is
+deserialized via Jackson from the specified JSON fixture it matches the given object.
+
+.. _man-testing-resources:
+
+Testing Resources
+=================
+
+While many resource classes can be tested just by calling the methods on the class in a test, some
+resources lend themselves to a more full-stack approach. For these, use ``ResourceTestRule``, which
+loads a given resource instance in an in-memory Jersey server:
+
+.. _man-testing-resources-example:
+
+.. code-block:: java
+
+    import static org.fest.assertions.api.Assertions.assertThat;
+    import static org.mockito.Mockito.*;
+
+    public class PersonResourceTest {
+
+        private static final PeopleStore dao = mock(PeopleStore.class);
+
+        @ClassRule
+        public static final ResourceTestRule resources = ResourceTestRule.builder()
+                .addResource(new PersonResource(dao))
+                .build();
+
+        private final Person person = new Person("blah", "blah at example.com");
+
+        @Before
+        public void setup() {
+            when(dao.fetchPerson(eq("blah"))).thenReturn(person);
+            // we have to reset the mock after each test because of the
+            // @ClassRule, or use a @Rule as mentioned below.
+            reset(dao);
+        }
+
+        @Test
+        public void testGetPerson() {
+            assertThat(resources.client().resource("/person/blah").get(Person.class))
+                    .isEqualTo(person);
+            verify(dao).fetchPerson("blah");
+        }
+    }
+
+Instansiate a ``ResourceTestRule`` using its ``Builder`` and add the various resource instances you
+want to test via ``ResourceTestRule.Builder#addResource(Object)``. Use a ``@ClassRule`` annotation
+to have the rule wrap the entire test class or the ``@Rule`` annotation to have the rule wrap
+each test individually (make sure to remove static final modifier from ``resources``).
+
+In your tests, use ``#client()``, which returns a Jersey ``Client`` instance to talk to and test
+your instances.
+
+This doesn't require opening a port, but ``ResourceTestRule`` tests will perform all the serialization,
+deserialization, and validation that happens inside of the HTTP process.
+
+This also doesn't require a full integration test. In the above
+:ref:`example <man-testing-resources-example>`, a mocked ``PeopleStore`` is passed to the
+``PersonResource`` instance to isolate it from the database. Not only does this make the test much
+faster, but it allows your resource unit tests to test error conditions and edge cases much more
+easily.
+
+.. hint::
+
+    You can trust ``PeopleStore`` works because you've got working unit tests for it, right?
+
+Should you, at some point, grow tired of the near-infinite amount of debug logging produced by
+``ResourceTestRule`` you can use the ``java.util.logging`` API to silence the ``com.sun.jersey`` logger.
+
+Integrated Testing
+==================
+It can be useful to start up your entire app and hit it with real HTTP requests during testing. This can be
+achieved by adding ``DropwizardAppRule`` to your JUnit test class, which will start the app prior to any tests
+running and stop it again when they've completed (roughly equivalent to having used ``@BeforeClass`` and ``@AfterClass``).
+``DropwizardAppRule`` also exposes the app's ``Configuration``,
+``Environment`` and the app object itself so that these can be queried by the tests.
+
+.. code-block:: java
+
+    public class LoginAcceptanceTest {
+
+        @ClassRule
+        public static final DropwizardAppRule<TestConfiguration> RULE =
+                new DropwizardAppRule<TestConfiguration>(MyApp.class, resourceFilePath("my-app-config.yaml"));
+
+        @Test
+        public void loginHandlerRedirectsAfterPost() {
+            Client client = new Client();
+
+            ClientResponse response = client.resource(
+                    String.format("http://localhost:%d/login", RULE.getLocalPort()))
+                    .post(ClientResponse.class, loginForm());
+
+            assertThat(response.getStatus()).isEqualTo(302);
+        }
+    }
diff --git a/docs/source/manual/views.rst b/docs/source/manual/views.rst
new file mode 100644
index 0000000..be94988
--- /dev/null
+++ b/docs/source/manual/views.rst
@@ -0,0 +1,98 @@
+.. _manual-views:
+
+################
+Dropwizard Views
+################
+
+.. highlight:: text
+
+.. rubric:: The ``dropwizard-views-mustache`` & ``dropwizard-views-freemarker`` modules provides you with simple, fast HTML views using either FreeMarker_ or Mustache_.
+
+.. _FreeMarker: http://FreeMarker.sourceforge.net/
+.. _Mustache: http://mustache.github.com/mustache.5.html
+
+To enable views for your :ref:`Application <man-core-application>`, add the ``ViewBundle`` in the ``initialize`` method of your Application class:
+
+.. code-block:: java
+
+    public void initialize(Bootstrap<MyConfiguration> bootstrap) {
+        bootstrap.addBundle(new ViewBundle());
+    }
+
+Then, in your :ref:`resource method <man-core-resources>`, add a ``View`` class:
+
+.. code-block:: java
+
+    public class PersonView extends View {
+        private final Person person;
+
+        public PersonView(Person person) {
+            super("person.ftl");
+            this.person = person;
+        }
+
+        public Person getPerson() {
+            return person;
+        }
+    }
+
+``person.ftl`` is the path of the template relative to the class name. If this class was
+``com.example.service.PersonView``, Dropwizard would then look for the file
+``src/main/resources/com/example/service/person.ftl``.
+
+If your template ends with ``.ftl``, it'll be interpreted as a FreeMarker_ template. If it ends with
+``.mustache``, it'll be interpreted as a Mustache template.
+
+.. tip::
+
+    Dropwizard Views also support localized template files. It picks up the client's locale from
+    their ``Accept-Language``, so you can add a French template in ``person_fr.ftl`` or a Canadian
+    template in ``person_en_CA.ftl``.
+
+Your template file might look something like this:
+
+.. code-block:: html
+    :emphasize-lines: 1,5
+
+    <#-- @ftlvariable name="" type="com.example.views.PersonView" -->
+    <html>
+        <body>
+            <!-- calls getPerson().getName() and sanitizes it -->
+            <h1>Hello, ${person.name?html}!</h1>
+        </body>
+    </html>
+
+The ``@ftlvariable`` lets FreeMarker (and any FreeMarker IDE plugins you may be using) know that the
+root object is a ``com.example.views.PersonView`` instance. If you attempt to call a property which
+doesn't exist on ``PersonView`` -- ``getConnectionPool()``, for example -- it will flag that line in
+your IDE.
+
+Once you have your view and template, you can simply return an instance of your ``View`` subclass:
+
+.. code-block:: java
+
+    @Path("/people/{id}")
+    @Produces(MediaType.TEXT_HTML)
+    public class PersonResource {
+        private final PersonDAO dao;
+
+        public PersonResource(PersonDAO dao) {
+            this.dao = dao;
+        }
+
+        @GET
+        public PersonView getPerson(@PathParam("id") String id) {
+            return new PersonView(dao.find(id));
+        }
+    }
+
+.. tip::
+
+    Jackson can also serialize your views, allowing you to serve both ``text/html`` and
+    ``application/json`` with a single representation class.
+
+For more information on how to use FreeMarker, see the `FreeMarker`_ documentation.
+
+For more information on how to use Mustache, see the `Mustache`_ and `Mustache.java`_ documentation.
+
+ .. _Mustache.java: https://github.com/spullara/mustache.java
diff --git a/dropwizard-assets/pom.xml b/dropwizard-assets/pom.xml
new file mode 100644
index 0000000..809eb2e
--- /dev/null
+++ b/dropwizard-assets/pom.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-assets</artifactId>
+    <name>Dropwizard Asset Bundle</name>
+    
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-servlets</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-assets/src/main/java/io/dropwizard/assets/AssetsBundle.java b/dropwizard-assets/src/main/java/io/dropwizard/assets/AssetsBundle.java
new file mode 100644
index 0000000..95232d8
--- /dev/null
+++ b/dropwizard-assets/src/main/java/io/dropwizard/assets/AssetsBundle.java
@@ -0,0 +1,115 @@
+package io.dropwizard.assets;
+
+import com.google.common.base.Charsets;
+import io.dropwizard.Bundle;
+import io.dropwizard.servlets.assets.AssetServlet;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * A bundle for serving static asset files from the classpath.
+ */
+public class AssetsBundle implements Bundle {
+    private static final Logger LOGGER = LoggerFactory.getLogger(AssetsBundle.class);
+
+    private static final String DEFAULT_ASSETS_NAME = "assets";
+    private static final String DEFAULT_INDEX_FILE = "index.htm";
+    private static final String DEFAULT_PATH = "/assets";
+
+    private final String resourcePath;
+    private final String uriPath;
+    private final String indexFile;
+    private final String assetsName;
+
+    /**
+     * Creates a new AssetsBundle which serves up static assets from
+     * {@code src/main/resources/assets/*} as {@code /assets/*}.
+     *
+     * @see AssetsBundle#AssetsBundle(String, String, String)
+     */
+    public AssetsBundle() {
+        this(DEFAULT_PATH, DEFAULT_PATH, DEFAULT_INDEX_FILE, DEFAULT_ASSETS_NAME);
+    }
+
+    /**
+     * Creates a new AssetsBundle which will configure the application to serve the static files
+     * located in {@code src/main/resources/${path}} as {@code /${path}}. For example, given a
+     * {@code path} of {@code "/assets"}, {@code src/main/resources/assets/example.js} would be
+     * served up from {@code /assets/example.js}.
+     *
+     * @param path    the classpath and URI root of the static asset files
+     * @see AssetsBundle#AssetsBundle(String, String, String)
+     */
+    public AssetsBundle(String path) {
+        this(path, path, DEFAULT_INDEX_FILE, DEFAULT_ASSETS_NAME);
+    }
+
+    /**
+     * Creates a new AssetsBundle which will configure the application to serve the static files
+     * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. For example, given a
+     * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"},
+     * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}.
+     *
+     * @param resourcePath    the resource path (in the classpath) of the static asset files
+     * @param uriPath    the uri path for the static asset files
+     * @see AssetsBundle#AssetsBundle(String, String, String)
+     */
+    public AssetsBundle(String resourcePath, String uriPath) {
+        this(resourcePath, uriPath, DEFAULT_INDEX_FILE, DEFAULT_ASSETS_NAME);
+    }
+
+    /**
+     * Creates a new AssetsBundle which will configure the application to serve the static files
+     * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name is
+     * in ${uriPath}, ${indexFile} is appended before serving. For example, given a
+     * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"},
+     * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}.
+     *
+     * @param resourcePath        the resource path (in the classpath) of the static asset files
+     * @param uriPath             the uri path for the static asset files
+     * @param indexFile           the name of the index file to use
+     */
+    public AssetsBundle(String resourcePath, String uriPath, String indexFile) {
+        this(resourcePath, uriPath, indexFile, DEFAULT_ASSETS_NAME);
+    }
+
+    /**
+     * Creates a new AssetsBundle which will configure the application to serve the static files
+     * located in {@code src/main/resources/${resourcePath}} as {@code /${uriPath}}. If no file name is
+     * in ${uriPath}, ${indexFile} is appended before serving. For example, given a
+     * {@code resourcePath} of {@code "/assets"} and a uriPath of {@code "/js"},
+     * {@code src/main/resources/assets/example.js} would be served up from {@code /js/example.js}.
+     *
+     * @param resourcePath        the resource path (in the classpath) of the static asset files
+     * @param uriPath             the uri path for the static asset files
+     * @param indexFile           the name of the index file to use
+     * @param assetsName          the name of servlet mapping used for this assets bundle
+     */
+    public AssetsBundle(String resourcePath, String uriPath, String indexFile, String assetsName) {
+        checkArgument(resourcePath.startsWith("/"), "%s is not an absolute path", resourcePath);
+        checkArgument(!"/".equals(resourcePath), "%s is the classpath root", resourcePath);
+        this.resourcePath = resourcePath.endsWith("/") ? resourcePath : (resourcePath + '/');
+        this.uriPath = uriPath.endsWith("/") ? uriPath : (uriPath + '/');
+        this.indexFile = indexFile;
+        this.assetsName = assetsName;
+    }
+
+    @Override
+    public void initialize(Bootstrap<?> bootstrap) {
+        // nothing doing
+    }
+
+    @Override
+    public void run(Environment environment) {
+        LOGGER.info("Registering AssetBundle with name: {} for path {}", assetsName, uriPath + '*');
+        environment.servlets().addServlet(assetsName, createServlet()).addMapping(uriPath + '*');
+    }
+
+    private AssetServlet createServlet() {
+        return new AssetServlet(resourcePath, uriPath, indexFile, Charsets.UTF_8);
+    }
+}
diff --git a/dropwizard-assets/src/test/java/io/dropwizard/assets/AssetsBundleTest.java b/dropwizard-assets/src/test/java/io/dropwizard/assets/AssetsBundleTest.java
new file mode 100644
index 0000000..88df15c
--- /dev/null
+++ b/dropwizard-assets/src/test/java/io/dropwizard/assets/AssetsBundleTest.java
@@ -0,0 +1,151 @@
+package io.dropwizard.assets;
+
+import com.google.common.io.Resources;
+import io.dropwizard.jetty.setup.ServletEnvironment;
+import io.dropwizard.servlets.assets.AssetServlet;
+import io.dropwizard.servlets.assets.ResourceURL;
+import io.dropwizard.setup.Environment;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import javax.servlet.ServletRegistration;
+import java.net.URL;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class AssetsBundleTest {
+    private final ServletEnvironment servletEnvironment = mock(ServletEnvironment.class);
+    private final Environment environment = mock(Environment.class);
+
+    private AssetServlet servlet;
+    private String servletPath;
+
+    @Before
+    public void setUp() throws Exception {
+        when(environment.servlets()).thenReturn(servletEnvironment);
+    }
+
+    @Test
+    public void hasADefaultPath() throws Exception {
+        runBundle(new AssetsBundle());
+
+        assertThat(servletPath)
+                .isEqualTo("/assets/*");
+
+        assertThat(servlet.getIndexFile())
+                .isEqualTo("index.htm");
+
+        assertThat(servlet.getResourceURL())
+                .isEqualTo(normalize("assets"));
+
+        assertThat(servlet.getUriPath())
+                .isEqualTo("/assets");
+    }
+
+    @Test
+    public void canHaveCustomPaths() throws Exception {
+        runBundle(new AssetsBundle("/json"));
+
+        assertThat(servletPath)
+                .isEqualTo("/json/*");
+
+        assertThat(servlet.getIndexFile())
+                .isEqualTo("index.htm");
+
+        assertThat(servlet.getResourceURL())
+                .isEqualTo(normalize("json"));
+
+        assertThat(servlet.getUriPath())
+                .isEqualTo("/json");
+    }
+
+    @Test
+    public void canHaveDifferentUriAndResourcePaths() throws Exception {
+        runBundle(new AssetsBundle("/json", "/what"));
+
+        assertThat(servletPath)
+                .isEqualTo("/what/*");
+
+        assertThat(servlet.getIndexFile())
+                .isEqualTo("index.htm");
+
+        assertThat(servlet.getResourceURL())
+                .isEqualTo(normalize("json"));
+
+        assertThat(servlet.getUriPath())
+                .isEqualTo("/what");
+    }
+
+    @Test
+    public void canSupportDiffrentAssetsBundleName() throws Exception {
+        runBundle(new AssetsBundle("/json", "/what/new", "index.txt", "customAsset1"), "customAsset1");
+
+        assertThat(servletPath)
+                .isEqualTo("/what/new/*");
+
+        assertThat(servlet.getIndexFile())
+                .isEqualTo("index.txt");
+
+        assertThat(servlet.getResourceURL())
+                .isEqualTo(normalize("json"));
+
+        assertThat(servlet.getUriPath())
+                .isEqualTo("/what/new");
+
+        runBundle(new AssetsBundle("/json", "/what/old", "index.txt", "customAsset2"), "customAsset2");
+        assertThat(servletPath)
+                .isEqualTo("/what/old/*");
+
+        assertThat(servlet.getIndexFile())
+                .isEqualTo("index.txt");
+
+        assertThat(servlet.getResourceURL())
+                .isEqualTo(normalize("json"));
+
+        assertThat(servlet.getUriPath())
+                .isEqualTo("/what/old");
+    }
+
+    @Test
+    public void canHaveDifferentUriAndResourcePathsAndIndexFilename() throws Exception {
+        runBundle(new AssetsBundle("/json", "/what", "index.txt"));
+
+        assertThat(servletPath)
+                .isEqualTo("/what/*");
+
+        assertThat(servlet.getIndexFile())
+                .isEqualTo("index.txt");
+
+        assertThat(servlet.getResourceURL())
+                .isEqualTo(normalize("json"));
+
+        assertThat(servlet.getUriPath())
+                .isEqualTo("/what");
+    }
+
+    private URL normalize(String path) {
+        return ResourceURL.appendTrailingSlash(Resources.getResource(path));
+    }
+
+    private void runBundle(AssetsBundle bundle) {
+        runBundle(bundle, "assets");
+    }
+
+    private void runBundle(AssetsBundle bundle, String assetName) {
+        final ServletRegistration.Dynamic registration = mock(ServletRegistration.Dynamic.class);
+        when(servletEnvironment.addServlet(anyString(), any(AssetServlet.class))).thenReturn(registration);
+
+        bundle.run(environment);
+
+        final ArgumentCaptor<AssetServlet> servletCaptor = ArgumentCaptor.forClass(AssetServlet.class);
+        final ArgumentCaptor<String> pathCaptor = ArgumentCaptor.forClass(String.class);
+
+        verify(servletEnvironment).addServlet(eq(assetName), servletCaptor.capture());
+        verify(registration).addMapping(pathCaptor.capture());
+
+        this.servlet = servletCaptor.getValue();
+        this.servletPath = pathCaptor.getValue();
+    }
+}
diff --git a/dropwizard-assets/src/test/resources/assets/git-turd.txt b/dropwizard-assets/src/test/resources/assets/git-turd.txt
new file mode 100644
index 0000000..e69de29
diff --git a/dropwizard-assets/src/test/resources/json/git-turd.txt b/dropwizard-assets/src/test/resources/json/git-turd.txt
new file mode 100644
index 0000000..e69de29
diff --git a/dropwizard-auth/pom.xml b/dropwizard-auth/pom.xml
new file mode 100644
index 0000000..d388ffa
--- /dev/null
+++ b/dropwizard-auth/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-auth</artifactId>
+    <name>Dropwizard Authentication</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-inmemory</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/Auth.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/Auth.java
new file mode 100644
index 0000000..55f1102
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/Auth.java
@@ -0,0 +1,20 @@
+package io.dropwizard.auth;
+
+import java.lang.annotation.*;
+
+/**
+ * This annotation is used to inject authenticated principal objects into protected JAX-RS resource
+ * methods.
+ *
+ * @see Authenticator
+ */
+ at Documented
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target({ ElementType.PARAMETER, ElementType.FIELD })
+public @interface Auth {
+    /**
+     * If {@code true}, the request will not be processed in the absence of a valid principal. If
+     * {@code false}, {@code null} will be passed in as a principal. Defaults to {@code true}.
+     */
+    boolean required() default true;
+}
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthenticationException.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthenticationException.java
new file mode 100644
index 0000000..a1dd637
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthenticationException.java
@@ -0,0 +1,23 @@
+package io.dropwizard.auth;
+
+/**
+ * An exception thrown to indicate that an {@link Authenticator} is <b>unable</b> to check the
+ * validity of the given credentials.
+ * <p/>
+ * <b>DO NOT USE THIS TO INDICATE THAT THE CREDENTIALS ARE INVALID.</b>
+ */
+public class AuthenticationException extends Exception {
+    private static final long serialVersionUID = -5053567474138953905L;
+
+    public AuthenticationException(String message) {
+        super(message);
+    }
+
+    public AuthenticationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public AuthenticationException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/Authenticator.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/Authenticator.java
new file mode 100644
index 0000000..e64d35e
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/Authenticator.java
@@ -0,0 +1,26 @@
+package io.dropwizard.auth;
+
+import com.google.common.base.Optional;
+
+/**
+ * An interface for classes which authenticate user-provided credentials and return principal
+ * objects.
+ *
+ * @param <C> the type of credentials the authenticator can authenticate
+ * @param <P> the type of principals the authenticator returns
+ */
+public interface Authenticator<C, P> {
+    /**
+     * Given a set of user-provided credentials, return an optional principal.
+     * <p/>
+     * If the credentials are valid and map to a principal, returns an {@code Optional.of(p)}.
+     * <p/>
+     * If the credentials are invalid, returns an {@code Optional.absent()}.
+     *
+     * @param credentials a set of user-provided credentials
+     * @return either an authenticated principal or an absent optional
+     * @throws AuthenticationException if the credentials cannot be authenticated due to an
+     *                                 underlying error
+     */
+    Optional<P> authenticate(C credentials) throws AuthenticationException;
+}
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthenticator.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthenticator.java
new file mode 100644
index 0000000..2ec757f
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthenticator.java
@@ -0,0 +1,126 @@
+package io.dropwizard.auth;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.cache.*;
+import com.google.common.collect.Sets;
+
+import java.util.concurrent.ExecutionException;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+/**
+ * An {@link Authenticator} decorator which uses a Guava cache to temporarily cache credentials and
+ * their corresponding principals.
+ *
+ * @param <C> the type of credentials the authenticator can authenticate
+ * @param <P> the type of principals the authenticator returns
+ */
+public class CachingAuthenticator<C, P> implements Authenticator<C, P> {
+    private final Authenticator<C, P> underlying;
+    private final LoadingCache<C, Optional<P>> cache;
+    private final Meter cacheMisses;
+    private final Timer gets;
+
+    /**
+     * Creates a new cached authenticator.
+     *
+     * @param metricRegistry the application's registry of metrics
+     * @param authenticator  the underlying authenticator
+     * @param cacheSpec      a {@link CacheBuilderSpec}
+     */
+    public CachingAuthenticator(MetricRegistry metricRegistry,
+                                Authenticator<C, P> authenticator,
+                                CacheBuilderSpec cacheSpec) {
+        this(metricRegistry, authenticator, CacheBuilder.from(cacheSpec));
+    }
+
+    /**
+     * Creates a new cached authenticator.
+     *
+     * @param metricRegistry the application's registry of metrics
+     * @param authenticator  the underlying authenticator
+     * @param builder        a {@link CacheBuilder}
+     */
+    public CachingAuthenticator(MetricRegistry metricRegistry,
+                                Authenticator<C, P> authenticator,
+                                CacheBuilder<Object, Object> builder) {
+        this.underlying = authenticator;
+        this.cacheMisses = metricRegistry.meter(name(authenticator.getClass(), "cache-misses"));
+        this.gets = metricRegistry.timer(name(authenticator.getClass(), "gets"));
+        this.cache = builder.recordStats().build(new CacheLoader<C, Optional<P>>() {
+            @Override
+            public Optional<P> load(C key) throws Exception {
+                cacheMisses.mark();
+                return underlying.authenticate(key);
+            }
+        });
+    }
+
+    @Override
+    public Optional<P> authenticate(C credentials) throws AuthenticationException {
+        final Timer.Context context = gets.time();
+        try {
+            return cache.get(credentials);
+        } catch (ExecutionException e) {
+            throw new AuthenticationException(e);
+        } finally {
+            context.stop();
+        }
+    }
+
+    /**
+     * Discards any cached principal for the given credentials.
+     *
+     * @param credentials a set of credentials
+     */
+    public void invalidate(C credentials) {
+        cache.invalidate(credentials);
+    }
+
+    /**
+     * Discards any cached principal for the given collection of credentials.
+     *
+     * @param credentials a collection of credentials
+     */
+    public void invalidateAll(Iterable<C> credentials) {
+        cache.invalidateAll(credentials);
+    }
+
+    /**
+     * Discards any cached principal for the collection of credentials satisfying the given predicate.
+     *
+     * @param predicate a predicate to filter credentials
+     */
+    public void invalidateAll(Predicate<? super C> predicate) {
+    	cache.invalidateAll(Sets.filter(cache.asMap().keySet(), predicate));
+    }
+    
+    /**
+     * Discards all cached principals.
+     */
+    public void invalidateAll() {
+        cache.invalidateAll();
+    }
+
+    /**
+     * Returns the number of cached principals.
+     *
+     * @return the number of cached principals
+     */
+    public long size() {
+        return cache.size();
+    }
+
+    /**
+     * Returns a set of statistics about the cache contents and usage.
+     *
+     * @return a set of statistics about the cache contents and usage
+     */
+    public CacheStats stats() {
+        return cache.stats();
+    }
+}
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicAuthProvider.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicAuthProvider.java
new file mode 100644
index 0000000..ec51970
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicAuthProvider.java
@@ -0,0 +1,117 @@
+package io.dropwizard.auth.basic;
+
+import com.google.common.base.Optional;
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.api.model.Parameter;
+import com.sun.jersey.core.spi.component.ComponentContext;
+import com.sun.jersey.core.spi.component.ComponentScope;
+import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
+import com.sun.jersey.spi.inject.Injectable;
+import com.sun.jersey.spi.inject.InjectableProvider;
+import io.dropwizard.auth.Auth;
+import io.dropwizard.auth.AuthenticationException;
+import io.dropwizard.auth.Authenticator;
+import org.eclipse.jetty.util.B64Code;
+import org.eclipse.jetty.util.StringUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * A Jersey provider for Basic HTTP authentication.
+ *
+ * @param <T> the principal type.
+ */
+public class BasicAuthProvider<T> implements InjectableProvider<Auth, Parameter> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(BasicAuthProvider.class);
+
+    private static class BasicAuthInjectable<T> extends AbstractHttpContextInjectable<T> {
+        private static final String PREFIX = "Basic";
+        private static final String CHALLENGE_FORMAT = PREFIX + " realm=\"%s\"";
+
+        private final Authenticator<BasicCredentials, T> authenticator;
+        private final String realm;
+        private final boolean required;
+
+        private BasicAuthInjectable(Authenticator<BasicCredentials, T> authenticator,
+                                    String realm,
+                                    boolean required) {
+            this.authenticator = authenticator;
+            this.realm = realm;
+            this.required = required;
+        }
+
+        @Override
+        public T getValue(HttpContext c) {
+            final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
+            try {
+                if (header != null) {
+                    final int space = header.indexOf(' ');
+                    if (space > 0) {
+                        final String method = header.substring(0, space);
+                        if (PREFIX.equalsIgnoreCase(method)) {
+                            final String decoded = B64Code.decode(header.substring(space + 1),
+                                                                  StringUtil.__ISO_8859_1);
+                            final int i = decoded.indexOf(':');
+                            if (i > 0) {
+                                final String username = decoded.substring(0, i);
+                                final String password = decoded.substring(i + 1);
+                                final BasicCredentials credentials = new BasicCredentials(username,
+                                                                                          password);
+                                final Optional<T> result = authenticator.authenticate(credentials);
+                                if (result.isPresent()) {
+                                    return result.get();
+                                }
+                            }
+                        }
+                    }
+                }
+            } catch (IllegalArgumentException e) {
+                LOGGER.debug("Error decoding credentials", e);
+            } catch (AuthenticationException e) {
+                LOGGER.warn("Error authenticating credentials", e);
+                throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
+            }
+
+            if (required) {
+                final String challenge = String.format(CHALLENGE_FORMAT, realm);
+                throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
+                                                          .header(HttpHeaders.WWW_AUTHENTICATE,
+                                                                  challenge)
+                                                          .entity("Credentials are required to access this resource.")
+                                                          .type(MediaType.TEXT_PLAIN_TYPE)
+                                                          .build());
+            }
+            return null;
+        }
+    }
+
+    private final Authenticator<BasicCredentials, T> authenticator;
+    private final String realm;
+
+    /**
+     * Creates a new BasicAuthProvider with the given {@link Authenticator} and realm.
+     *
+     * @param authenticator the authenticator which will take the {@link BasicCredentials} and
+     *                      convert them into instances of {@code T}
+     * @param realm         the name of the authentication realm
+     */
+    public BasicAuthProvider(Authenticator<BasicCredentials, T> authenticator, String realm) {
+        this.authenticator = authenticator;
+        this.realm = realm;
+    }
+
+    @Override
+    public ComponentScope getScope() {
+        return ComponentScope.PerRequest;
+    }
+
+    @Override
+    public Injectable<?> getInjectable(ComponentContext ic, Auth a, Parameter c) {
+        return new BasicAuthInjectable<>(authenticator, realm, a.required());
+    }
+}
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicCredentials.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicCredentials.java
new file mode 100644
index 0000000..a860a40
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicCredentials.java
@@ -0,0 +1,70 @@
+package io.dropwizard.auth.basic;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Objects;
+
+import java.security.MessageDigest;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A set of user-provided Basic Authentication credentials, consisting of a username and a
+ * password.
+ */
+public class BasicCredentials {
+    private final String username;
+    private final String password;
+
+    /**
+     * Creates a new BasicCredentials with the given username and password.
+     *
+     * @param username the username
+     * @param password the password
+     */
+    public BasicCredentials(String username, String password) {
+        this.username = checkNotNull(username);
+        this.password = checkNotNull(password);
+    }
+
+    /**
+     * Returns the credentials' username.
+     *
+     * @return the credentials' username
+     */
+    public String getUsername() {
+        return username;
+    }
+
+    /**
+     * Returns the credentials' password.
+     *
+     * @return the credentials' password
+     */
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) { return true; }
+        if ((obj == null) || (getClass() != obj.getClass())) { return false; }
+        final BasicCredentials that = (BasicCredentials) obj;
+        // N.B.: Doing a constant-time comparison here to prevent timing attacks.
+        final byte[] thisBytes = password.getBytes(Charsets.UTF_8);
+        final byte[] thatBytes = that.password.getBytes(Charsets.UTF_8);
+        return username.equals(that.username) && MessageDigest.isEqual(thisBytes, thatBytes);
+    }
+
+    @Override
+    public int hashCode() {
+        return (31 * username.hashCode()) + password.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                      .add("username", username)
+                      .add("password", "**********")
+                      .toString();
+    }
+}
diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/oauth/OAuthProvider.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/oauth/OAuthProvider.java
new file mode 100644
index 0000000..765c1aa
--- /dev/null
+++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/oauth/OAuthProvider.java
@@ -0,0 +1,104 @@
+package io.dropwizard.auth.oauth;
+
+import com.google.common.base.Optional;
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.api.model.Parameter;
+import com.sun.jersey.core.spi.component.ComponentContext;
+import com.sun.jersey.core.spi.component.ComponentScope;
+import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
+import com.sun.jersey.spi.inject.Injectable;
+import com.sun.jersey.spi.inject.InjectableProvider;
+import io.dropwizard.auth.Auth;
+import io.dropwizard.auth.AuthenticationException;
+import io.dropwizard.auth.Authenticator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+/**
+ * A Jersey provider for OAuth2 bearer tokens.
+ *
+ * @param <T> the principal type
+ */
+public class OAuthProvider<T> implements InjectableProvider<Auth, Parameter> {
+    private static class OAuthInjectable<T> extends AbstractHttpContextInjectable<T> {
+        private static final Logger LOGGER = LoggerFactory.getLogger(OAuthInjectable.class);
+        private static final String CHALLENGE_FORMAT = "Bearer realm=\"%s\"";
+        private static final String PREFIX = "bearer";
+
+        private final Authenticator<String, T> authenticator;
+        private final String realm;
+        private final boolean required;
+
+        private OAuthInjectable(Authenticator<String, T> authenticator,
+                                String realm,
+                                boolean required) {
+            this.authenticator = authenticator;
+            this.realm = realm;
+            this.required = required;
+        }
+
+        @Override
+        public T getValue(HttpContext c) {
+            try {
+                final String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION);
+                if (header != null) {
+                    final int space = header.indexOf(' ');
+                    if (space > 0) {
+                        final String method = header.substring(0, space);
+                        if (PREFIX.equalsIgnoreCase(method)) {
+                            final String credentials = header.substring(space + 1);
+                            final Optional<T> result = authenticator.authenticate(credentials);
+                            if (result.isPresent()) {
+                                return result.get();
+                            }
+                        }
+                    }
+                }
+            } catch (AuthenticationException e) {
+                LOGGER.warn("Error authenticating credentials", e);
+                throw new WebApplicationException(Response.Status.INTERNAL_SERVER_ERROR);
+            }
+
+            if (required) {
+                final String challenge = String.format(CHALLENGE_FORMAT, realm);
+                throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED)
+                                                          .header(HttpHeaders.WWW_AUTHENTICATE,
+                                                                  challenge)
+                                                          .entity("Credentials are required to access this resource.")
+                                                          .type(MediaType.TEXT_PLAIN_TYPE)
+                                                          .build());
+            }
+            return null;
+        }
+    }
+
+    private final Authenticator<String, T> authenticator;
+    private final String realm;
+
+    /**
+     * Creates a new OAuthProvider with the given {@link Authenticator} and realm.
+     *
+     * @param authenticator the authenticator which will take the OAuth2 bearer token and convert
+     *                      them into instances of {@code T}
+     * @param realm         the name of the authentication realm
+     */
+    public OAuthProvider(Authenticator<String, T> authenticator, String realm) {
+        this.authenticator = authenticator;
+        this.realm = realm;
+    }
+
+    @Override
+    public ComponentScope getScope() {
+        return ComponentScope.PerRequest;
+    }
+
+    @Override
+    public Injectable<?> getInjectable(ComponentContext ic, Auth a, Parameter c) {
+        return new OAuthInjectable<>(authenticator, realm, a.required());
+    }
+}
diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthenticatorTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthenticatorTest.java
new file mode 100644
index 0000000..64d5e48
--- /dev/null
+++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthenticatorTest.java
@@ -0,0 +1,112 @@
+package io.dropwizard.auth;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.cache.CacheBuilderSpec;
+import com.google.common.cache.CacheStats;
+import com.google.common.collect.ImmutableSet;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class CachingAuthenticatorTest {
+    @SuppressWarnings("unchecked")
+    private final Authenticator<String, String> underlying = mock(Authenticator.class);
+    private final CachingAuthenticator<String, String> cached =
+            new CachingAuthenticator<>(new MetricRegistry(), underlying,
+                                       CacheBuilderSpec.parse("maximumSize=1"));
+
+    @Before
+    public void setUp() throws Exception {
+        when(underlying.authenticate(anyString())).thenReturn(Optional.of("principal"));
+    }
+
+    @Test
+    public void cachesTheFirstReturnedPrincipal() throws Exception {
+        assertThat(cached.authenticate("credentials"))
+                .isEqualTo(Optional.of("principal"));
+
+        assertThat(cached.authenticate("credentials"))
+                .isEqualTo(Optional.of("principal"));
+
+        verify(underlying, times(1)).authenticate("credentials");
+    }
+
+    @Test
+    public void respectsTheCacheConfiguration() throws Exception {
+        cached.authenticate("credentials1");
+        cached.authenticate("credentials2");
+        cached.authenticate("credentials1");
+
+        final InOrder inOrder = inOrder(underlying);
+        inOrder.verify(underlying, times(1)).authenticate("credentials1");
+        inOrder.verify(underlying, times(1)).authenticate("credentials2");
+        inOrder.verify(underlying, times(1)).authenticate("credentials1");
+    }
+
+    @Test
+    public void invalidatesSingleCredentials() throws Exception {
+        cached.authenticate("credentials");
+        cached.invalidate("credentials");
+        cached.authenticate("credentials");
+
+        verify(underlying, times(2)).authenticate("credentials");
+    }
+
+    @Test
+    public void invalidatesSetsOfCredentials() throws Exception {
+        cached.authenticate("credentials");
+        cached.invalidateAll(ImmutableSet.of("credentials"));
+        cached.authenticate("credentials");
+
+        verify(underlying, times(2)).authenticate("credentials");
+    }
+
+    @Test
+    public void invalidatesCredentialsMatchingGivenPredicate() throws Exception {
+    	Predicate<String> predicate = new Predicate<String>() {
+			@Override
+			public boolean apply(String c) {
+				return c.equals("credentials");
+			}
+		}; 
+    	
+    	cached.authenticate("credentials");
+    	cached.invalidateAll(predicate);
+    	cached.authenticate("credentials");
+    	
+    	verify(underlying, times(2)).authenticate("credentials");
+    }
+    
+    @Test
+    public void invalidatesAllCredentials() throws Exception {
+        cached.authenticate("credentials");
+        cached.invalidateAll();
+        cached.authenticate("credentials");
+
+        verify(underlying, times(2)).authenticate("credentials");
+    }
+
+    @Test
+    public void calculatesTheSizeOfTheCache() throws Exception {
+        cached.authenticate("credentials1");
+
+        assertThat(cached.size())
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void calculatesCacheStats() throws Exception {
+        cached.authenticate("credentials1");
+
+        final CacheStats stats = cached.stats();
+
+        assertThat(stats.loadCount())
+                .isEqualTo(1);
+    }
+}
diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicAuthProviderTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicAuthProviderTest.java
new file mode 100644
index 0000000..1811bd5
--- /dev/null
+++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicAuthProviderTest.java
@@ -0,0 +1,111 @@
+package io.dropwizard.auth.basic;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.base.Optional;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import io.dropwizard.auth.Auth;
+import io.dropwizard.auth.AuthenticationException;
+import io.dropwizard.auth.Authenticator;
+import io.dropwizard.jersey.DropwizardResourceConfig;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+
+public class BasicAuthProviderTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Path("/test/")
+    @Produces(MediaType.TEXT_PLAIN)
+    public static class ExampleResource {
+        @GET
+        public String show(@Auth String principal) {
+            return principal;
+        }
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        final DropwizardResourceConfig config = DropwizardResourceConfig.forTesting(new MetricRegistry());
+        final Authenticator<BasicCredentials, String> authenticator = new Authenticator<BasicCredentials, String>() {
+            @Override
+            public Optional<String> authenticate(BasicCredentials credentials) throws AuthenticationException {
+                if ("good-guy".equals(credentials.getUsername()) &&
+                        "secret".equals(credentials.getPassword())) {
+                    return Optional.of("good-guy");
+                }
+                if ("bad-guy".equals(credentials.getUsername())) {
+                    throw new AuthenticationException("CRAP");
+                }
+                return Optional.absent();
+            }
+        };
+        config.getSingletons().add(new BasicAuthProvider<>(authenticator, "realm"));
+        config.getSingletons().add(new ExampleResource());
+        return new LowLevelAppDescriptor.Builder(config).build();
+    }
+
+    @Test
+    public void respondsToMissingCredentialsWith401() throws Exception {
+        try {
+            client().resource("/test").get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(401);
+
+            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
+                    .containsOnly("Basic realm=\"realm\"");
+        }
+    }
+
+    @Test
+    public void transformsCredentialsToPrincipals() throws Exception {
+        assertThat(client().resource("/test")
+                           .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0")
+                           .get(String.class))
+                .isEqualTo("good-guy");
+    }
+
+    @Test
+    public void respondsToNonBasicCredentialsWith401() throws Exception {
+        try {
+            client().resource("/test")
+                    .header(HttpHeaders.AUTHORIZATION, "Derp Z29vZC1ndXk6c2VjcmV0")
+                    .get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(401);
+
+            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
+                    .containsOnly("Basic realm=\"realm\"");
+        }
+    }
+
+    @Test
+    public void respondsToExceptionsWith500() throws Exception {
+        try {
+            client().resource("/test")
+                    .header(HttpHeaders.AUTHORIZATION, "Basic YmFkLWd1eTpzZWNyZXQ=")
+                    .get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(500);
+        }
+    }
+}
diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCredentialsTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCredentialsTest.java
new file mode 100644
index 0000000..949b7ac
--- /dev/null
+++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCredentialsTest.java
@@ -0,0 +1,61 @@
+package io.dropwizard.auth.basic;
+
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class BasicCredentialsTest {
+    private final BasicCredentials credentials = new BasicCredentials("u", "p");
+
+    @Test
+    public void hasAUsername() throws Exception {
+        assertThat(credentials.getUsername())
+                .isEqualTo("u");
+    }
+
+    @Test
+    public void hasAPassword() throws Exception {
+        assertThat(credentials.getPassword())
+                .isEqualTo("p");
+    }
+
+    @Test
+    @SuppressWarnings({ "ObjectEqualsNull", "EqualsBetweenInconvertibleTypes", "LiteralAsArgToStringEquals" })
+    public void hasAWorkingEqualsMethod() throws Exception {
+        assertThat(credentials.equals(credentials))
+                .isTrue();
+
+        assertThat(credentials.equals(new BasicCredentials("u", "p")))
+                .isTrue();
+
+        assertThat(credentials.equals(null))
+                .isFalse();
+
+        assertThat(credentials.equals("string"))
+                .isFalse();
+
+        assertThat(credentials.equals(new BasicCredentials("u1", "p")))
+                .isFalse();
+
+        assertThat(credentials.equals(new BasicCredentials("u", "p1")))
+                .isFalse();
+    }
+
+    @Test
+    public void hasAWorkingHashCode() throws Exception {
+        assertThat(credentials.hashCode())
+                .isEqualTo(new BasicCredentials("u", "p").hashCode());
+
+        assertThat(credentials.hashCode())
+                .isNotEqualTo(new BasicCredentials("u1", "p").hashCode());
+
+        assertThat(credentials.hashCode())
+                .isNotEqualTo(new BasicCredentials("u", "p1").hashCode());
+    }
+
+    @Test
+    public void isHumanReadable() throws Exception {
+        assertThat(credentials.toString())
+                .isEqualTo("BasicCredentials{username=u, password=**********}");
+    }
+}
diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthProviderTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthProviderTest.java
new file mode 100644
index 0000000..4ce1d63
--- /dev/null
+++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthProviderTest.java
@@ -0,0 +1,109 @@
+package io.dropwizard.auth.oauth;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.base.Optional;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import io.dropwizard.auth.Auth;
+import io.dropwizard.auth.AuthenticationException;
+import io.dropwizard.auth.Authenticator;
+import io.dropwizard.jersey.DropwizardResourceConfig;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class OAuthProviderTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Path("/test/")
+    @Produces(MediaType.TEXT_PLAIN)
+    public static class ExampleResource {
+        @GET
+        public String show(@Auth String principal) {
+            return principal;
+        }
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        final DropwizardResourceConfig config = DropwizardResourceConfig.forTesting(new MetricRegistry());
+        final Authenticator<String, String> authenticator = new Authenticator<String, String>() {
+            @Override
+            public Optional<String> authenticate(String credentials) throws AuthenticationException {
+                if ("good-guy".equals(credentials)) {
+                    return Optional.of("good-guy");
+                }
+                if ("bad-guy".equals(credentials)) {
+                    throw new AuthenticationException("CRAP", new RuntimeException(""));
+                }
+                return Optional.absent();
+            }
+        };
+        config.getSingletons().add(new OAuthProvider<>(authenticator, "realm"));
+        config.getSingletons().add(new ExampleResource());
+        return new LowLevelAppDescriptor.Builder(config).build();
+    }
+
+    @Test
+    public void respondsToMissingCredentialsWith401() throws Exception {
+        try {
+            client().resource("/test").get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(401);
+
+            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
+                    .containsOnly("Bearer realm=\"realm\"");
+        }
+    }
+
+    @Test
+    public void transformsCredentialsToPrincipals() throws Exception {
+        assertThat(client().resource("/test")
+                           .header(HttpHeaders.AUTHORIZATION, "Bearer good-guy")
+                           .get(String.class))
+                .isEqualTo("good-guy");
+    }
+
+    @Test
+    public void respondsToNonBasicCredentialsWith401() throws Exception {
+        try {
+            client().resource("/test")
+                    .header(HttpHeaders.AUTHORIZATION, "Derp WHEE")
+                    .get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(401);
+
+            assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE))
+                    .containsOnly("Bearer realm=\"realm\"");
+        }
+    }
+
+    @Test
+    public void respondsToExceptionsWith500() throws Exception {
+        try {
+            client().resource("/test")
+                    .header(HttpHeaders.AUTHORIZATION, "Bearer bad-guy")
+                    .get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(500);
+        }
+    }
+}
diff --git a/dropwizard-auth/src/test/resources/logback-test.xml b/dropwizard-auth/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-auth/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-client/pom.xml b/dropwizard-client/pom.xml
new file mode 100644
index 0000000..b7abcbf
--- /dev/null
+++ b/dropwizard-client/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-client</artifactId>
+    <name>Dropwizard HTTP Client</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-client</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.contribs</groupId>
+            <artifactId>jersey-apache-client4</artifactId>
+            <version>${jersey.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.httpcomponents</groupId>
+                    <artifactId>httpclient</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.3.4</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>commons-logging</groupId>
+                    <artifactId>commons-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-httpclient</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.httpcomponents</groupId>
+                    <artifactId>httpclient</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientBuilder.java b/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientBuilder.java
new file mode 100644
index 0000000..e21e40d
--- /dev/null
+++ b/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientBuilder.java
@@ -0,0 +1,243 @@
+package io.dropwizard.client;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.httpclient.InstrumentedClientConnManager;
+import com.codahale.metrics.httpclient.InstrumentedHttpClient;
+import io.dropwizard.setup.Environment;
+import io.dropwizard.util.Duration;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.params.AllClientPNames;
+import org.apache.http.client.params.CookiePolicy;
+import org.apache.http.conn.DnsResolver;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.DefaultConnectionReuseStrategy;
+import org.apache.http.impl.NoConnectionReuseStrategy;
+import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
+import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
+import org.apache.http.impl.conn.SchemeRegistryFactory;
+import org.apache.http.impl.conn.SystemDefaultDnsResolver;
+import org.apache.http.params.BasicHttpParams;
+import org.apache.http.protocol.HttpContext;
+
+import java.io.IOException;
+
+/**
+ * A convenience class for building {@link HttpClient} instances.
+ * <p>
+ * Among other things,
+ * <ul>
+ *     <li>Disables stale connection checks</li>
+ *     <li>Disables Nagle's algorithm</li>
+ *     <li>Disables cookie management by default</li>
+ * </ul>
+ */
+public class HttpClientBuilder {
+    private static final HttpRequestRetryHandler NO_RETRIES = new HttpRequestRetryHandler() {
+        @Override
+        public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
+            return false;
+        }
+    };
+
+    private final MetricRegistry metricRegistry;
+    private String environmentName;
+    private HttpClientConfiguration configuration = new HttpClientConfiguration();
+    private DnsResolver resolver = new SystemDefaultDnsResolver();
+    private HttpRequestRetryHandler httpRequestRetryHandler;
+    private SchemeRegistry registry = SchemeRegistryFactory.createSystemDefault();
+    private CredentialsProvider credentialsProvider = null;
+
+    public HttpClientBuilder(MetricRegistry metricRegistry) {
+        this.metricRegistry = metricRegistry;
+    }
+
+    public HttpClientBuilder(Environment environment) {
+        this (environment.metrics());
+        name(environment.getName());
+    }
+
+    /**
+     * Use the given environment name. This is used in the user agent.
+     *
+     * @param environmentName  an environment name to use in the user agent.
+     * @return {@code this}
+     */
+    public HttpClientBuilder name(String environmentName) {
+        this.environmentName = environmentName;
+        return this;
+    }
+
+    /**
+     * Use the given {@link HttpClientConfiguration} instance.
+     *
+     * @param configuration    a {@link HttpClientConfiguration} instance
+     * @return {@code this}
+     */
+    public HttpClientBuilder using(HttpClientConfiguration configuration) {
+        this.configuration = configuration;
+        return this;
+    }
+
+    /**
+     * Use the given {@link DnsResolver} instance.
+     *
+     * @param resolver    a {@link DnsResolver} instance
+     * @return {@code this}
+     */
+    public HttpClientBuilder using(DnsResolver resolver) {
+        this.resolver = resolver;
+        return this;
+    }
+
+    /**
+     * Uses the {@link HttpRequestRetryHandler} for handling request retries.
+     *
+     * @param httpRequestRetryHandler an httpRequestRetryHandler
+     * @return {@code this}
+     */
+    public HttpClientBuilder using(HttpRequestRetryHandler httpRequestRetryHandler) {
+        this.httpRequestRetryHandler = httpRequestRetryHandler;
+        return this;
+    }
+    
+    /**
+     * Use the given {@link SchemeRegistry} instance.
+     *
+     * @param registry    a {@link SchemeRegistry} instance
+     * @return {@code this}
+     */
+    public HttpClientBuilder using(SchemeRegistry registry) {
+        this.registry = registry;
+        return this;
+    }
+
+    /**
+     * Use the given {@link CredentialsProvider} instance.
+     *
+     * @param credentialsProvider    a {@link CredentialsProvider} instance
+     * @return {@code this}
+     */
+    public HttpClientBuilder using(CredentialsProvider credentialsProvider) {
+        this.credentialsProvider = credentialsProvider;
+        return this;
+    }
+
+    /**
+     * Builds the {@link HttpClient}.
+     *
+     * @return an {@link HttpClient}
+     */
+    public HttpClient build(String name) {
+        final BasicHttpParams params = createHttpParams(name);
+        final InstrumentedClientConnManager manager = createConnectionManager(registry, name);
+        final InstrumentedHttpClient client = new InstrumentedHttpClient(metricRegistry, manager, params, name);
+        setStrategiesForClient(client);
+
+        return client;
+    }
+
+    /**
+     * Add strategies to client such as ConnectionReuseStrategy and KeepAliveStrategy Note that this
+     * method mutates the client object by setting the strategies
+     *
+     * @param client The InstrumentedHttpClient that should be configured with strategies
+     */
+    protected void setStrategiesForClient(InstrumentedHttpClient client) {
+        final long keepAlive = configuration.getKeepAlive().toMilliseconds();
+
+        // don't keep alive the HTTP connection and thus don't reuse the TCP socket
+        if (keepAlive == 0) {
+            client.setReuseStrategy(new NoConnectionReuseStrategy());
+        } else {
+            client.setReuseStrategy(new DefaultConnectionReuseStrategy());
+            // either keep alive based on response header Keep-Alive,
+            // or if the server can keep a persistent connection (-1), then override based on client's configuration
+            client.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy() {
+                @Override
+                public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
+                    final long duration = super.getKeepAliveDuration(response, context);
+                    return (duration == -1) ? keepAlive : duration;
+                }
+            });
+        }
+
+        if (configuration.getRetries() == 0) {
+            client.setHttpRequestRetryHandler(NO_RETRIES);
+        } else if (httpRequestRetryHandler != null) {
+            client.setHttpRequestRetryHandler(httpRequestRetryHandler);
+        } else {
+            client.setHttpRequestRetryHandler(new DefaultHttpRequestRetryHandler(configuration.getRetries(),
+                                                                                 false));
+        }
+
+        if (credentialsProvider != null) {
+            client.setCredentialsProvider(credentialsProvider);
+        }
+    }
+
+    /**
+     * Map the parameters in HttpClientConfiguration to a BasicHttpParams object
+     *
+     * @return a BasicHttpParams object from the HttpClientConfiguration
+     */
+    protected BasicHttpParams createHttpParams(String name) {
+        final BasicHttpParams params = new BasicHttpParams();
+
+        if (configuration.isCookiesEnabled()) {
+            params.setParameter(AllClientPNames.COOKIE_POLICY, CookiePolicy.BEST_MATCH);
+        } else {
+            params.setParameter(AllClientPNames.COOKIE_POLICY, CookiePolicy.IGNORE_COOKIES);
+        }
+
+        params.setParameter(AllClientPNames.USER_AGENT, createUserAgent(name));
+
+        final Integer timeout = (int) configuration.getTimeout().toMilliseconds();
+        params.setParameter(AllClientPNames.SO_TIMEOUT, timeout);
+
+        final Integer connectionTimeout = (int) configuration.getConnectionTimeout()
+                                                             .toMilliseconds();
+        params.setParameter(AllClientPNames.CONNECTION_TIMEOUT, connectionTimeout);
+
+        params.setParameter(AllClientPNames.TCP_NODELAY, Boolean.TRUE);
+        params.setParameter(AllClientPNames.STALE_CONNECTION_CHECK, Boolean.FALSE);
+
+        return params;
+    }
+
+    /**
+     * Create a user agent string using the configured user agent if defined, otherwise
+     * using a combination of the environment name and this client name
+     *
+     * @param name the name of this client
+     * @return the user agent string to be used by this client
+     */
+    protected String createUserAgent(String name) {
+        final String defaultUserAgent = environmentName == null ? name : String.format("%s (%s)", environmentName, name);
+        return configuration.getUserAgent().or(defaultUserAgent);
+    }
+
+    /**
+     * Create a InstrumentedClientConnManager based on the HttpClientConfiguration. It sets the
+     * maximum connections per route and the maximum total connections that the connection manager
+     * can create
+     *
+     * @param registry the SchemeRegistry
+     * @return a InstrumentedClientConnManger instance
+     */
+    protected InstrumentedClientConnManager createConnectionManager(SchemeRegistry registry, String name) {
+        final Duration ttl = configuration.getTimeToLive();
+        final InstrumentedClientConnManager manager =
+                new InstrumentedClientConnManager(metricRegistry,
+                                                  registry,
+                                                  ttl.getQuantity(),
+                                                  ttl.getUnit(),
+                                                  resolver,
+                                                  name);
+        manager.setDefaultMaxPerRoute(configuration.getMaxConnectionsPerRoute());
+        manager.setMaxTotal(configuration.getMaxConnections());
+        return manager;
+    }
+}
diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientConfiguration.java b/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientConfiguration.java
new file mode 100644
index 0000000..e44fb4c
--- /dev/null
+++ b/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientConfiguration.java
@@ -0,0 +1,137 @@
+package io.dropwizard.client;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Optional;
+import io.dropwizard.util.Duration;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+// TODO: 5/15/13 <coda> -- write tests for HttpClientConfiguration
+
+/**
+ * The configuration class used by {@link HttpClientBuilder}.
+ *
+ * @see <a href="http://www.dropwizard.io/manual/client/#configuration-defaults">Http Client Configuration</a>
+ */
+public class HttpClientConfiguration {
+    @NotNull
+    private Duration timeout = Duration.milliseconds(500);
+
+    @NotNull
+    private Duration connectionTimeout = Duration.milliseconds(500);
+
+    @NotNull
+    private Duration timeToLive = Duration.hours(1);
+
+    private boolean cookiesEnabled = false;
+
+    @Min(1)
+    @Max(Integer.MAX_VALUE)
+    private int maxConnections = 1024;
+
+    @Min(1)
+    @Max(Integer.MAX_VALUE)
+    private int maxConnectionsPerRoute = 1024;
+
+    @NotNull
+    private Duration keepAlive = Duration.milliseconds(0);
+
+    @Min(0)
+    @Max(1000)
+    private int retries = 0;
+
+    @NotNull
+    private Optional<String> userAgent = Optional.absent();
+
+    @JsonProperty
+    public Duration getKeepAlive() {
+        return keepAlive;
+    }
+
+    @JsonProperty
+    public void setKeepAlive(Duration keepAlive) {
+        this.keepAlive = keepAlive;
+    }
+
+    @JsonProperty
+    public int getMaxConnectionsPerRoute() {
+        return maxConnectionsPerRoute;
+    }
+
+    @JsonProperty
+    public void setMaxConnectionsPerRoute(int maxConnectionsPerRoute) {
+        this.maxConnectionsPerRoute = maxConnectionsPerRoute;
+    }
+
+    @JsonProperty
+    public Duration getTimeout() {
+        return timeout;
+    }
+
+    @JsonProperty
+    public Duration getConnectionTimeout() {
+        return connectionTimeout;
+    }
+
+    @JsonProperty
+    public Duration getTimeToLive() {
+        return timeToLive;
+    }
+
+    @JsonProperty
+    public boolean isCookiesEnabled() {
+        return cookiesEnabled;
+    }
+
+    @JsonProperty
+    public void setTimeout(Duration duration) {
+        this.timeout = duration;
+    }
+
+    @JsonProperty
+    public void setConnectionTimeout(Duration duration) {
+        this.connectionTimeout = duration;
+    }
+
+    @JsonProperty
+    public void setTimeToLive(Duration timeToLive) {
+        this.timeToLive = timeToLive;
+    }
+
+    @JsonProperty
+    public void setCookiesEnabled(boolean enabled) {
+        this.cookiesEnabled = enabled;
+    }
+
+    @JsonProperty
+    public int getMaxConnections() {
+        return maxConnections;
+    }
+
+    @JsonProperty
+    public void setMaxConnections(int maxConnections) {
+        this.maxConnections = maxConnections;
+    }
+
+    @JsonProperty
+    public int getRetries() {
+        return retries;
+    }
+
+    @JsonProperty
+    public void setRetries(int retries) {
+        this.retries = retries;
+    }
+
+    @JsonProperty
+    public Optional<String> getUserAgent() {
+        return userAgent;
+    }
+
+    @JsonProperty
+    public void setUserAgent(Optional<String> userAgent) {
+        this.userAgent = userAgent;
+    }
+}
diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientBuilder.java b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientBuilder.java
new file mode 100644
index 0000000..db48199
--- /dev/null
+++ b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientBuilder.java
@@ -0,0 +1,247 @@
+package io.dropwizard.client;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+import io.dropwizard.setup.Environment;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.conn.DnsResolver;
+import org.apache.http.conn.scheme.SchemeRegistry;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter;
+import com.sun.jersey.client.apache4.ApacheHttpClient4;
+import com.sun.jersey.client.apache4.ApacheHttpClient4Handler;
+import com.sun.jersey.client.apache4.config.ApacheHttpClient4Config;
+import com.sun.jersey.client.apache4.config.DefaultApacheHttpClient4Config;
+
+/**
+ * A convenience class for building {@link Client} instances.
+ * <p/>
+ * Among other things,
+ *
+ * <ul>
+ *     <li>Backed by Apache HttpClient</li>
+ *     <li>Disables stale connection checks</li>
+ *     <li>Disables Nagle's algorithm</li>
+ *     <li>Disables cookie management by default</li>
+ * </ul>
+ *
+ * @see HttpClientBuilder
+ */
+public class JerseyClientBuilder {
+    private final HttpClientBuilder builder;
+    private final List<Object> singletons = Lists.newArrayList();
+    private final List<Class<?>> providers = Lists.newArrayList();
+    private final Map<String, Boolean> features = Maps.newLinkedHashMap();
+    private final Map<String, Object> properties = Maps.newLinkedHashMap();
+
+    private JerseyClientConfiguration configuration = new JerseyClientConfiguration();
+    private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+    private Environment environment;
+    private ObjectMapper objectMapper;
+    private ExecutorService executorService;
+
+    public JerseyClientBuilder(Environment environment) {
+        this.builder = new HttpClientBuilder(environment);
+        this.environment = environment;
+    }
+
+    public JerseyClientBuilder(MetricRegistry metricRegistry) {
+        this.builder = new HttpClientBuilder(metricRegistry);
+    }
+
+    /**
+     * Adds the given object as a Jersey provider.
+     *
+     * @param provider a Jersey provider
+     * @return {@code this}
+     */
+    public JerseyClientBuilder withProvider(Object provider) {
+        singletons.add(checkNotNull(provider));
+        return this;
+    }
+
+    /**
+     * Adds the given class as a Jersey provider. <p/><b>N.B.:</b> This class must either have a
+     * no-args constructor or use Jersey's built-in dependency injection.
+     *
+     * @param klass a Jersey provider class
+     * @return {@code this}
+     */
+    public JerseyClientBuilder withProvider(Class<?> klass) {
+        providers.add(checkNotNull(klass));
+        return this;
+    }
+
+    /**
+     * Sets the state of the given Jersey feature.
+     *
+     * @param featureName  the name of the Jersey feature
+     * @param featureState the state of the Jersey feature
+     * @return {@code this}
+     */
+    @SuppressWarnings("UnusedDeclaration") // basically impossible to test
+    public JerseyClientBuilder withFeature(String featureName, boolean featureState) {
+        features.put(featureName, featureState);
+        return this;
+    }
+
+    /**
+     * Sets the state of the given Jersey property.
+     *
+     * @param propertyName  the name of the Jersey property
+     * @param propertyValue the state of the Jersey property
+     * @return {@code this}
+     */
+    public JerseyClientBuilder withProperty(String propertyName, Object propertyValue) {
+        properties.put(propertyName, propertyValue);
+        return this;
+    }
+    
+    /**
+     * Uses the {@link org.apache.http.client.HttpRequestRetryHandler} for handling request retries.
+     *
+     * @param httpRequestRetryHandler a HttpRequestRetryHandler
+     * @return {@code this}
+     */
+    public JerseyClientBuilder using(HttpRequestRetryHandler httpRequestRetryHandler) {
+        builder.using(httpRequestRetryHandler);
+        return this;
+    }
+
+    /**
+     * Uses the given {@link JerseyClientConfiguration}.
+     *
+     * @param configuration a configuration object
+     * @return {@code this}
+     */
+    public JerseyClientBuilder using(JerseyClientConfiguration configuration) {
+        this.configuration = configuration;
+        builder.using(configuration);
+        return this;
+    }
+
+    /**
+     * Uses the given {@link Environment}.
+     *
+     * @param environment a Dropwizard {@link Environment}
+     * @return {@code this}
+     * @see #using(java.util.concurrent.ExecutorService, com.fasterxml.jackson.databind.ObjectMapper)
+     */
+    public JerseyClientBuilder using(Environment environment) {
+        this.environment = environment;
+        return this;
+    }
+
+    /**
+     * Use the given {@link DnsResolver} instance.
+     *
+     * @param resolver a {@link DnsResolver} instance
+     * @return {@code this}
+     */
+    public JerseyClientBuilder using(DnsResolver resolver) {
+        builder.using(resolver);
+        return this;
+    }
+
+    /**
+     * Use the given {@link SchemeRegistry} instance.
+     *
+     * @param registry a {@link SchemeRegistry} instance
+     * @return {@code this}
+     */
+    public JerseyClientBuilder using(SchemeRegistry registry) {
+        builder.using(registry);
+        return this;
+    }
+
+    /**
+     * Use the given {@link Validator} instance.
+     *
+     * @param validator a {@link Validator} instance
+     * @return {@code this}
+     */
+    public JerseyClientBuilder using(Validator validator) {
+        this.validator = validator;
+        return this;
+    }
+
+    /**
+     * Uses the given {@link ExecutorService} and {@link ObjectMapper}.
+     *
+     * @param executorService a thread pool
+     * @param objectMapper    an object mapper
+     * @return {@code this}
+     * @see #using(io.dropwizard.setup.Environment)
+     */
+    public JerseyClientBuilder using(ExecutorService executorService, ObjectMapper objectMapper) {
+        this.executorService = executorService;
+        this.objectMapper = objectMapper;
+        return this;
+    }
+
+    /**
+     * Builds the {@link Client} instance.
+     *
+     * @return a fully-configured {@link Client}
+     */
+    public Client build(String name) {
+        if ((environment == null) && (executorService == null) && (objectMapper == null)) {
+            throw new IllegalStateException("Must have either an environment or both " +
+                                                    "an executor service and an object mapper");
+        }
+
+        if (environment == null) {
+            return build(executorService, objectMapper, validator, name);
+        }
+
+        return build(environment.lifecycle()
+                                .executorService("jersey-client-" + name + "-%d")
+                                .minThreads(configuration.getMinThreads())
+                                .maxThreads(configuration.getMaxThreads())
+                                .build(),
+                     environment.getObjectMapper(),
+                     environment.getValidator(),
+                     name);
+    }
+
+    private Client build(ExecutorService threadPool,
+                         ObjectMapper objectMapper,
+                         Validator validator,
+                         String name) {
+        final Client client = new ApacheHttpClient4(buildHandler(name), buildConfig(objectMapper));
+        client.setExecutorService(threadPool);
+
+        if (configuration.isGzipEnabled()) {
+            client.addFilter(new GZIPContentEncodingFilter(configuration.isGzipEnabledForRequests()));
+        }
+
+        return client;
+    }
+
+    private ApacheHttpClient4Handler buildHandler(String name) {
+        return new ApacheHttpClient4Handler(builder.build(name), null, true);
+    }
+
+    private ApacheHttpClient4Config buildConfig(ObjectMapper objectMapper) {
+        final ApacheHttpClient4Config config = new DefaultApacheHttpClient4Config();
+        config.getSingletons().addAll(singletons);
+        config.getSingletons().add(new JacksonMessageBodyProvider(objectMapper, validator));
+        config.getClasses().addAll(providers);
+        config.getFeatures().putAll(features);
+        config.getProperties().putAll(properties);
+        return config;
+    }
+}
diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientConfiguration.java b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientConfiguration.java
new file mode 100644
index 0000000..7f6efe4
--- /dev/null
+++ b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientConfiguration.java
@@ -0,0 +1,83 @@
+package io.dropwizard.client;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.dropwizard.validation.ValidationMethod;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+
+// TODO: 5/15/13 <coda> -- write tests for JerseyClientConfiguration
+
+/**
+ * The configuration class used by {@link JerseyClientBuilder}. Extends
+ * {@link HttpClientConfiguration}.
+ *
+ * @see HttpClientConfiguration
+ * @see <a href="http://www.dropwizard.io/manual/client/#man-client-jersey-config">Jersey Client Configuration</a>
+ */
+public class JerseyClientConfiguration extends HttpClientConfiguration {
+    @Min(1)
+    @Max(16 * 1024)
+    private int minThreads = 1;
+
+    @Min(1)
+    @Max(16 * 1024)
+    private int maxThreads = 128;
+
+    private boolean gzipEnabled = true;
+
+    private boolean gzipEnabledForRequests = true;
+
+    @JsonProperty
+    public int getMinThreads() {
+        return minThreads;
+    }
+
+    @JsonProperty
+    public void setMinThreads(int minThreads) {
+        this.minThreads = minThreads;
+    }
+
+    @JsonProperty
+    public int getMaxThreads() {
+        return maxThreads;
+    }
+
+    @JsonProperty
+    public void setMaxThreads(int maxThreads) {
+        this.maxThreads = maxThreads;
+    }
+
+    @JsonProperty
+    public boolean isGzipEnabled() {
+        return gzipEnabled;
+    }
+
+    @JsonProperty
+    public void setGzipEnabled(boolean enabled) {
+        this.gzipEnabled = enabled;
+    }
+
+    @JsonProperty
+    public boolean isGzipEnabledForRequests() {
+        return gzipEnabledForRequests;
+    }
+
+    @JsonProperty
+    public void setGzipEnabledForRequests(boolean enabled) {
+        this.gzipEnabledForRequests = enabled;
+    }
+
+    @JsonIgnore
+    @ValidationMethod(message = ".minThreads must be less than or equal to maxThreads")
+    public boolean isThreadPoolSizedCorrectly() {
+        return minThreads <= maxThreads;
+    }
+
+    @JsonIgnore
+    @ValidationMethod(message = ".gzipEnabledForRequests requires gzipEnabled set to true")
+    public boolean isCompressionConfigurationValid() {
+        return !gzipEnabledForRequests || gzipEnabled;
+    }
+}
diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/HttpClientBuilderTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/HttpClientBuilderTest.java
new file mode 100644
index 0000000..01d2b95
--- /dev/null
+++ b/dropwizard-client/src/test/java/io/dropwizard/client/HttpClientBuilderTest.java
@@ -0,0 +1,269 @@
+package io.dropwizard.client;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.util.Duration;
+import org.apache.http.Header;
+import org.apache.http.HeaderIterator;
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.HttpRequestRetryHandler;
+import org.apache.http.client.params.AllClientPNames;
+import org.apache.http.client.params.CookiePolicy;
+import org.apache.http.conn.DnsResolver;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.impl.DefaultConnectionReuseStrategy;
+import org.apache.http.impl.NoConnectionReuseStrategy;
+import org.apache.http.impl.client.AbstractHttpClient;
+import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
+import org.apache.http.impl.conn.PoolingClientConnectionManager;
+import org.apache.http.impl.conn.SchemeRegistryFactory;
+import org.apache.http.impl.conn.SystemDefaultDnsResolver;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.message.BasicListHeaderIterator;
+import org.apache.http.protocol.HTTP;
+import org.apache.http.protocol.HttpContext;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class HttpClientBuilderTest {
+    private final HttpClientConfiguration configuration = new HttpClientConfiguration();
+    private final DnsResolver resolver = mock(DnsResolver.class);
+    private final HttpClientBuilder builder = new HttpClientBuilder(new MetricRegistry());
+    private final SchemeRegistry registry = new SchemeRegistry();
+
+    @Test
+    public void setsTheMaximumConnectionPoolSize() throws Exception {
+        configuration.setMaxConnections(412);
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+        final PoolingClientConnectionManager connectionManager = (PoolingClientConnectionManager) client.getConnectionManager();
+
+        assertThat(connectionManager.getMaxTotal())
+                .isEqualTo(412);
+    }
+
+    @Test
+    public void setsTheMaximumRoutePoolSize() throws Exception {
+        configuration.setMaxConnectionsPerRoute(413);
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+        final PoolingClientConnectionManager connectionManager = (PoolingClientConnectionManager) client
+                .getConnectionManager();
+
+        assertThat(connectionManager.getDefaultMaxPerRoute())
+                .isEqualTo(413);
+    }
+
+    @Test
+    public void setsTheUserAgent() {
+        configuration.setUserAgent(Optional.of("qwerty"));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+        assertThat(client.getParams().getParameter(AllClientPNames.USER_AGENT))
+                .isEqualTo("qwerty");
+    }
+
+    @Test
+    public void canUseACustomDnsResolver() throws Exception {
+        // Yes, this is gross. Thanks, Apache!
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(resolver).build("test");
+        final Field field = PoolingClientConnectionManager.class.getDeclaredField("dnsResolver");
+        field.setAccessible(true);
+
+        assertThat(field.get(client.getConnectionManager()))
+                .isEqualTo(resolver);
+    }
+
+    @Test
+    public void usesASystemDnsResolverByDefault() throws Exception {
+        // Yes, this is gross. Thanks, Apache!
+        final AbstractHttpClient client = (AbstractHttpClient) builder.build("test");
+        final Field field = PoolingClientConnectionManager.class.getDeclaredField("dnsResolver");
+        field.setAccessible(true);
+
+        assertThat(field.get(client.getConnectionManager()))
+                .isInstanceOf(SystemDefaultDnsResolver.class);
+    }
+
+    @Test
+    public void doesNotReuseConnectionsIfKeepAliveIsZero() throws Exception {
+        configuration.setConnectionTimeout(Duration.seconds(0));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getConnectionReuseStrategy())
+                .isInstanceOf(NoConnectionReuseStrategy.class);
+    }
+
+    @Test
+    public void reusesConnectionsIfKeepAliveIsNonZero() throws Exception {
+        configuration.setKeepAlive(Duration.seconds(1));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getConnectionReuseStrategy())
+                .isInstanceOf(DefaultConnectionReuseStrategy.class);
+    }
+
+    @Test
+    public void usesKeepAliveForPersistentConnections() throws Exception {
+        configuration.setKeepAlive(Duration.seconds(1));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        final DefaultConnectionKeepAliveStrategy strategy = (DefaultConnectionKeepAliveStrategy) client.getConnectionKeepAliveStrategy();
+
+        final HttpResponse response = mock(HttpResponse.class);
+        when(response.headerIterator(HTTP.CONN_KEEP_ALIVE)).thenReturn(mock(HeaderIterator.class));
+
+        final HttpContext context = mock(HttpContext.class);
+
+        assertThat(strategy.getKeepAliveDuration(response, context))
+                .isEqualTo(1000);
+    }
+
+    @Test
+    public void usesDefaultForNonPersistentConnections() throws Exception {
+        configuration.setKeepAlive(Duration.seconds(1));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        final DefaultConnectionKeepAliveStrategy strategy = (DefaultConnectionKeepAliveStrategy) client
+                .getConnectionKeepAliveStrategy();
+
+        final HttpResponse response = mock(HttpResponse.class);
+
+        final HeaderIterator iterator = new BasicListHeaderIterator(
+                ImmutableList.<Header>of(new BasicHeader(HttpHeaders.CONNECTION, "timeout=50")),
+                HttpHeaders.CONNECTION
+        );
+
+        when(response.headerIterator(HTTP.CONN_KEEP_ALIVE)).thenReturn(iterator);
+
+        final HttpContext context = mock(HttpContext.class);
+
+        assertThat(strategy.getKeepAliveDuration(response, context))
+                .isEqualTo(50000);
+    }
+
+    @Test
+    public void ignoresCookiesByDefault() throws Exception {
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getParams().getParameter(AllClientPNames.COOKIE_POLICY))
+                .isEqualTo(CookiePolicy.IGNORE_COOKIES);
+    }
+
+    @Test
+    public void usesBestMatchCookiePolicyIfCookiesAreEnabled() throws Exception {
+        configuration.setCookiesEnabled(true);
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getParams().getParameter(AllClientPNames.COOKIE_POLICY))
+                .isEqualTo(CookiePolicy.BEST_MATCH);
+    }
+
+    @Test
+    public void setsTheSocketTimeout() throws Exception {
+        configuration.setTimeout(Duration.milliseconds(500));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getParams().getIntParameter(AllClientPNames.SO_TIMEOUT, -1))
+                .isEqualTo(500);
+    }
+
+    @Test
+    public void setsTheConnectTimeout() throws Exception {
+        configuration.setConnectionTimeout(Duration.milliseconds(500));
+
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getParams().getIntParameter(AllClientPNames.CONNECTION_TIMEOUT, -1))
+                .isEqualTo(500);
+    }
+
+    @Test
+    public void disablesNaglesAlgorithm() throws Exception {
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getParams().getBooleanParameter(AllClientPNames.TCP_NODELAY, false))
+                .isTrue();
+    }
+
+    @Test
+    public void disablesStaleConnectionCheck() throws Exception {
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getParams().getBooleanParameter(AllClientPNames.STALE_CONNECTION_CHECK, true))
+                .isFalse();
+    }
+
+    @Test
+    public void usesTheDefaultSchemeRegistry() throws Exception {
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(configuration).build("test");
+
+        assertThat(client.getConnectionManager().getSchemeRegistry().getSchemeNames())
+                .isEqualTo(SchemeRegistryFactory.createSystemDefault().getSchemeNames());
+    }
+
+    @Test
+    public void usesACustomSchemeRegistry() throws Exception {
+        final AbstractHttpClient client = (AbstractHttpClient) builder.using(registry).build("test");
+
+        assertThat(client.getConnectionManager().getSchemeRegistry())
+                .isEqualTo(registry);
+    }
+    
+    @Test
+    public void usesACustomHttpRequestRetryHandler() throws Exception {
+        HttpRequestRetryHandler customHandler = new HttpRequestRetryHandler() {
+            @Override
+            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {
+                return false;
+            }
+        };
+        HttpClientConfiguration config = new HttpClientConfiguration();
+        config.setRetries(1);
+        AbstractHttpClient client = (AbstractHttpClient) builder.using(config).using(customHandler).build("test");
+        
+        assertThat(client.getHttpRequestRetryHandler()).isEqualTo(customHandler);
+    }
+
+    @Test
+    public void usesCredentialsProvider() throws Exception {
+        CredentialsProvider credentialsProvider = new CredentialsProvider() {
+            @Override
+            public void setCredentials(AuthScope authscope, Credentials credentials) {
+
+            }
+
+            @Override
+            public Credentials getCredentials(AuthScope authscope) {
+                return null;
+            }
+
+            @Override
+            public void clear() {
+
+            }
+        };
+        HttpClientConfiguration config = new HttpClientConfiguration();
+        config.setRetries(1);
+        AbstractHttpClient client = (AbstractHttpClient) builder.using(config).using(credentialsProvider).build("test");
+
+        assertThat(client.getCredentialsProvider()).isEqualTo(credentialsProvider);
+    }
+}
diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientBuilderTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientBuilderTest.java
new file mode 100644
index 0000000..f8f3523
--- /dev/null
+++ b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientBuilderTest.java
@@ -0,0 +1,200 @@
+package io.dropwizard.client;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.filter.GZIPContentEncodingFilter;
+import com.sun.jersey.client.apache4.ApacheHttpClient4;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import io.dropwizard.setup.Environment;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.MessageBodyReader;
+import javax.ws.rs.ext.Provider;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.concurrent.ExecutorService;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.mockito.Mockito.*;
+
+public class JerseyClientBuilderTest {
+    @Provider
+    @Consumes(MediaType.APPLICATION_SVG_XML)
+    public static class FakeMessageBodyReader implements MessageBodyReader<JerseyClientBuilderTest> {
+        @Override
+        public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+            return JerseyClientBuilderTest.class.isAssignableFrom(type);
+        }
+
+        @Override
+        public JerseyClientBuilderTest readFrom(Class<JerseyClientBuilderTest> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException {
+            return null;
+        }
+    }
+
+    private static final Annotation[] NO_ANNOTATIONS = new Annotation[0];
+
+    private final JerseyClientBuilder builder = new JerseyClientBuilder(new MetricRegistry());
+    private final LifecycleEnvironment lifecycleEnvironment = spy(new LifecycleEnvironment());
+    private final Environment environment = mock(Environment.class);
+    private final ExecutorService executorService = mock(ExecutorService.class);
+    private final ObjectMapper objectMapper = mock(ObjectMapper.class);
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Before
+    public void setUp() throws Exception {
+        when(environment.lifecycle()).thenReturn(lifecycleEnvironment);
+        when(environment.getObjectMapper()).thenReturn(objectMapper);
+        when(environment.getValidator()).thenReturn(validator);
+    }
+
+    @Test
+    public void throwsAnExceptionWithoutAnEnvironmentOrAThreadPoolAndObjectMapper() throws Exception {
+        try {
+            builder.build("test");
+            failBecauseExceptionWasNotThrown(IllegalStateException.class);
+        } catch (IllegalStateException e) {
+            assertThat(e.getMessage())
+                    .isEqualTo("Must have either an environment or both an executor service and an object mapper");
+        }
+    }
+
+    @Test
+    public void buildsAnApache4BasedClient() throws Exception {
+        final Client client = builder.using(executorService, objectMapper).build("test");
+
+        assertThat(client)
+                .isInstanceOf(ApacheHttpClient4.class);
+    }
+
+    @Test
+    public void includesJerseyProperties() throws Exception {
+        final ApacheHttpClient4 client = (ApacheHttpClient4) builder.withProperty("poop", true)
+                                                                    .using(executorService,
+                                                                           objectMapper)
+                                                                    .build("test");
+
+        assertThat(client.getProperties().get("poop"))
+                .isEqualTo(Boolean.TRUE);
+    }
+
+    @Test
+    public void includesJerseyProviderSingletons() throws Exception {
+        final FakeMessageBodyReader provider = new FakeMessageBodyReader();
+        final ApacheHttpClient4 client = (ApacheHttpClient4) builder.withProvider(provider)
+                                                                    .using(executorService,
+                                                                           objectMapper)
+                                                                    .build("test");
+
+        assertThat(client.getProviders()
+                         .getMessageBodyReader(JerseyClientBuilderTest.class,
+                                               null,
+                                               NO_ANNOTATIONS,
+                                               MediaType.APPLICATION_SVG_XML_TYPE))
+                .isSameAs(provider);
+    }
+
+    @Test
+    public void includesJerseyProviderClasses() throws Exception {
+        final ApacheHttpClient4 client = (ApacheHttpClient4) builder.withProvider(FakeMessageBodyReader.class)
+                                                                    .using(executorService,
+                                                                           objectMapper)
+                                                                    .build("test");
+
+        assertThat(client.getProviders()
+                         .getMessageBodyReader(JerseyClientBuilderTest.class,
+                                               null,
+                                               NO_ANNOTATIONS,
+                                               MediaType.APPLICATION_SVG_XML_TYPE))
+                .isInstanceOf(FakeMessageBodyReader.class);
+    }
+
+    @Test
+    public void usesTheObjectMapperForJson() throws Exception {
+        final Client client = builder.using(executorService, objectMapper).build("test");
+
+        final MessageBodyReader<Object> reader = client.getProviders()
+                                                       .getMessageBodyReader(Object.class,
+                                                                             null,
+                                                                             NO_ANNOTATIONS,
+                                                                             MediaType.APPLICATION_JSON_TYPE);
+
+        assertThat(reader)
+                .isInstanceOf(JacksonMessageBodyProvider.class);
+        assertThat(((JacksonMessageBodyProvider) reader).getObjectMapper())
+                .isEqualTo(objectMapper);
+    }
+
+    @Test
+    public void usesTheGivenThreadPool() throws Exception {
+        final ApacheHttpClient4 client = (ApacheHttpClient4) builder.using(executorService, objectMapper).build("test");
+
+        assertThat(client.getExecutorService())
+                .isEqualTo(executorService);
+    }
+
+    @Test
+    public void addBidirectionalGzipSupportIfEnabled() throws Exception {
+        final JerseyClientConfiguration configuration = new JerseyClientConfiguration();
+        configuration.setGzipEnabled(true);
+
+        final ApacheHttpClient4 client = (ApacheHttpClient4) builder.using(configuration)
+                                                                    .using(executorService,
+                                                                           objectMapper).build("test");
+        assertThat(client.getHeadHandler())
+                .isInstanceOf(GZIPContentEncodingFilter.class);
+    }
+
+    @Test
+    public void disablesGzipSupportIfDisabled() throws Exception {
+        final JerseyClientConfiguration configuration = new JerseyClientConfiguration();
+        configuration.setGzipEnabled(false);
+
+        final ApacheHttpClient4 client = (ApacheHttpClient4) builder.using(configuration)
+                                                                    .using(executorService,
+                                                                           objectMapper).build("test");
+
+        assertThat(client.getHeadHandler())
+                .isNotInstanceOf(GZIPContentEncodingFilter.class);
+    }
+
+    @Test
+    public void usesAnObjectMapperFromTheEnvironment() throws Exception {
+        final Client client = builder.using(environment).build("test");
+
+        final MessageBodyReader<Object> reader = client.getProviders()
+                                                       .getMessageBodyReader(Object.class,
+                                                                             null,
+                                                                             NO_ANNOTATIONS,
+                                                                             MediaType.APPLICATION_JSON_TYPE);
+
+        assertThat(reader)
+                .isInstanceOf(JacksonMessageBodyProvider.class);
+        assertThat(((JacksonMessageBodyProvider) reader).getObjectMapper())
+                .isEqualTo(objectMapper);
+    }
+
+    @Test
+    public void usesAnExecutorServiceFromTheEnvironment() throws Exception {
+        final JerseyClientConfiguration configuration = new JerseyClientConfiguration();
+        configuration.setMinThreads(7);
+        configuration.setMaxThreads(532);
+
+        builder.using(configuration)
+               .using(environment).build("test");
+
+        verify(lifecycleEnvironment).executorService("jersey-client-test-%d");
+    }
+}
diff --git a/dropwizard-client/src/test/resources/logback-test.xml b/dropwizard-client/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-client/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-configuration/pom.xml b/dropwizard-configuration/pom.xml
new file mode 100644
index 0000000..b492f19
--- /dev/null
+++ b/dropwizard-configuration/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-configuration</artifactId>
+    <name>Dropwizard Configuration Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jackson</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-validation</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-yaml</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.6</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationException.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationException.java
new file mode 100644
index 0000000..3816615
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationException.java
@@ -0,0 +1,54 @@
+package io.dropwizard.configuration;
+
+import java.util.Collection;
+
+/**
+ * Base class for problems with a Configuration object.
+ * <p/>
+ * Refer to the implementations for different classes of problems:
+ * <ul>
+ *     <li>Parsing errors: {@link ConfigurationParsingException}</li>
+ *     <li>Validation errors: {@link ConfigurationValidationException}</li>
+ * </ul>
+ */
+public abstract class ConfigurationException extends Exception {
+    protected static final String NEWLINE = String.format("%n");
+
+    private final Collection<String> errors;
+
+    /**
+     * Creates a new ConfigurationException for the given path with the given errors.
+     *
+     * @param path      the bad configuration path
+     * @param errors    the errors in the path
+     */
+    public ConfigurationException(String path, Collection<String> errors) {
+        super(formatMessage(path, errors));
+        this.errors = errors;
+    }
+
+    /**
+     * Creates a new ConfigurationException for the given path with the given errors and cause.
+     *
+     * @param path      the bad configuration path
+     * @param errors    the errors in the path
+     * @param cause     the cause of the error(s)
+     */
+    public ConfigurationException(String path, Collection<String> errors, Throwable cause) {
+        super(formatMessage(path, errors), cause);
+        this.errors = errors;
+    }
+
+    public Collection<String> getErrors() {
+        return errors;
+    }
+
+    protected static String formatMessage(String file, Collection<String> errors) {
+        final StringBuilder msg = new StringBuilder(file);
+        msg.append(errors.size() == 1 ? " has an error:" : " has the following errors:").append(NEWLINE);
+        for (String error : errors) {
+            msg.append("  * ").append(error).append(NEWLINE);
+        }
+        return msg.toString();
+    }
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactory.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactory.java
new file mode 100644
index 0000000..480346f
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactory.java
@@ -0,0 +1,242 @@
+package io.dropwizard.configuration;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.exc.InvalidFormatException;
+import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.databind.node.TextNode;
+import com.fasterxml.jackson.databind.node.TreeTraversingParser;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import com.fasterxml.jackson.dataformat.yaml.snakeyaml.error.MarkedYAMLException;
+import com.fasterxml.jackson.dataformat.yaml.snakeyaml.error.YAMLException;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validator;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.*;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * A factory class for loading YAML configuration files, binding them to configuration objects, and
+ * and validating their constraints. Allows for overriding configuration parameters from system
+ * properties.
+ *
+ * @param <T> the type of the configuration objects to produce
+ */
+public class ConfigurationFactory<T> {
+    private final Class<T> klass;
+    private final String propertyPrefix;
+    private final ObjectMapper mapper;
+    private final Validator validator;
+    private final YAMLFactory yamlFactory;
+
+    /**
+     * Creates a new configuration factory for the given class.
+     *
+     * @param klass          the configuration class
+     * @param validator      the validator to use
+     * @param objectMapper   the Jackson {@link ObjectMapper} to use
+     * @param propertyPrefix the system property name prefix used by overrides
+     */
+    public ConfigurationFactory(Class<T> klass,
+                                Validator validator,
+                                ObjectMapper objectMapper,
+                                String propertyPrefix) {
+        this.klass = klass;
+        this.propertyPrefix = propertyPrefix.endsWith(".") ? propertyPrefix : propertyPrefix + '.';
+        this.mapper = objectMapper.copy();
+        mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        this.validator = validator;
+        this.yamlFactory = new YAMLFactory();
+    }
+
+    /**
+     * Loads, parses, binds, and validates a configuration object.
+     *
+     * @param provider the provider to to use for reading configuration files
+     * @param path     the path of the configuration file
+     * @return a validated configuration object
+     * @throws IOException            if there is an error reading the file
+     * @throws ConfigurationException if there is an error parsing or validating the file
+     */
+    public T build(ConfigurationSourceProvider provider, String path) throws IOException, ConfigurationException {
+        try (InputStream input = provider.open(checkNotNull(path))) {
+            final JsonNode node = mapper.readTree(yamlFactory.createParser(input));
+            return build(node, path);
+        } catch (YAMLException e) {
+            ConfigurationParsingException.Builder builder = ConfigurationParsingException
+                    .builder("Malformed YAML")
+                    .setCause(e)
+                    .setDetail(e.getMessage());
+
+            if (e instanceof MarkedYAMLException) {
+                builder.setLocation(((MarkedYAMLException) e).getProblemMark());
+            }
+
+            throw builder.build(path);
+        }
+    }
+
+    /**
+     * Loads, parses, binds, and validates a configuration object from a file.
+     *
+     * @param file the path of the configuration file
+     * @return a validated configuration object
+     * @throws IOException            if there is an error reading the file
+     * @throws ConfigurationException if there is an error parsing or validating the file
+     */
+    public T build(File file) throws IOException, ConfigurationException {
+        return build(new FileConfigurationSourceProvider(), file.toString());
+    }
+
+    /**
+     * Loads, parses, binds, and validates a configuration object from an empty document.
+     *
+     * @return a validated configuration object
+     * @throws IOException            if there is an error reading the file
+     * @throws ConfigurationException if there is an error parsing or validating the file
+     */
+    public T build() throws IOException, ConfigurationException {
+        return build(JsonNodeFactory.instance.objectNode(), "default configuration");
+    }
+
+    private T build(JsonNode node, String path) throws IOException, ConfigurationException {
+        for (Map.Entry<Object, Object> pref : System.getProperties().entrySet()) {
+            final String prefName = (String) pref.getKey();
+            if (prefName.startsWith(propertyPrefix)) {
+                final String configName = prefName.substring(propertyPrefix.length());
+                addOverride(node, configName, System.getProperty(prefName));
+            }
+        }
+
+        try {
+            final T config = mapper.readValue(new TreeTraversingParser(node), klass);
+            validate(path, config);
+            return config;
+        } catch (UnrecognizedPropertyException e) {
+            Collection<Object> knownProperties = e.getKnownPropertyIds();
+            List<String> properties = new ArrayList<>(knownProperties.size());
+            for (Object property : knownProperties) {
+                properties.add(property.toString());
+            }
+            throw ConfigurationParsingException.builder("Unrecognized field")
+                    .setFieldPath(e.getPath())
+                    .setLocation(e.getLocation())
+                    .addSuggestions(properties)
+                    .setSuggestionBase(e.getPropertyName())
+                    .setCause(e)
+                    .build(path);
+        } catch (InvalidFormatException e) {
+            String sourceType = e.getValue().getClass().getSimpleName();
+            String targetType = e.getTargetType().getSimpleName();
+            throw ConfigurationParsingException.builder("Incorrect type of value")
+                    .setDetail("is of type: " + sourceType + ", expected: " + targetType)
+                    .setLocation(e.getLocation())
+                    .setFieldPath(e.getPath())
+                    .setCause(e)
+                    .build(path);
+        } catch (JsonMappingException e) {
+            throw ConfigurationParsingException.builder("Failed to parse configuration")
+                    .setDetail(e.getMessage())
+                    .setFieldPath(e.getPath())
+                    .setLocation(e.getLocation())
+                    .setCause(e)
+                    .build(path);
+        }
+    }
+
+    private void addOverride(JsonNode root, String name, String value) {
+        JsonNode node = root;
+        final Iterable<String> split = Splitter.on('.').trimResults().split(name);
+        final String[] parts = Iterables.toArray(split, String.class);
+
+        for(int i = 0; i < parts.length; i++) {
+            String key = parts[i];
+
+            if (!(node instanceof ObjectNode)) {
+                throw new IllegalArgumentException("Unable to override " + name + "; it's not a valid path.");
+            }
+            final ObjectNode obj = (ObjectNode) node;
+
+            final String remainingPath = Joiner.on('.').join(Arrays.copyOfRange(parts, i, parts.length));
+            if (obj.has(remainingPath) && !remainingPath.equals(key)) {
+                if (obj.get(remainingPath).isValueNode()) {
+                    obj.put(remainingPath, value);
+                    return;
+                }
+            }
+
+            JsonNode child;
+            final boolean moreParts = i < parts.length - 1;
+
+            if (key.matches(".+\\[\\d+\\]$")) {
+                final int s = key.indexOf('[');
+                final int index = Integer.parseInt(key.substring(s + 1, key.length() - 1));
+                key = key.substring(0, s);
+                child = obj.get(key);
+                if (child == null) {
+                    throw new IllegalArgumentException("Unable to override " + name + "; node with index not found.");
+                }
+                if (!child.isArray()) {
+                    throw new IllegalArgumentException("Unable to override " + name + "; node with index is not an array.");
+                }
+                else if (index >= child.size()) {
+                    throw new ArrayIndexOutOfBoundsException("Unable to override " + name + "; index is greater than size of array.");
+                }
+                if (moreParts) {
+                    child = child.get(index);
+                    node = child;
+                }
+                else {
+                    ArrayNode array = (ArrayNode)child;
+                    array.set(index, TextNode.valueOf(value));
+                    return;
+                }
+            }
+            else if (moreParts) {
+                child = obj.get(key);
+                if (child == null) {
+                    child = obj.objectNode();
+                    obj.put(key, child);
+                }
+                if (child.isArray()) {
+                    throw new IllegalArgumentException("Unable to override " + name + "; target is an array but no index specified");
+                }
+                node = child;
+            }
+
+            if (!moreParts) {
+                if (node.get(key) != null && node.get(key).isArray()) {
+                    ArrayNode arrayNode = (ArrayNode) obj.get(key);
+                    arrayNode.removeAll();
+                    Pattern escapedComma = Pattern.compile("\\\\,");
+                    for (String val : Splitter.on(Pattern.compile("(?<!\\\\),")).trimResults().split(value)) {
+                        arrayNode.add(escapedComma.matcher(val).replaceAll(","));
+                    }
+                }
+                else {
+                    obj.put(key, value);
+                }
+            }
+        }
+    }
+
+    private void validate(String path, T config) throws ConfigurationValidationException {
+        final Set<ConstraintViolation<T>> violations = validator.validate(config);
+        if (!violations.isEmpty()) {
+            throw new ConfigurationValidationException(path, violations);
+        }
+    }
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactoryFactory.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactoryFactory.java
new file mode 100644
index 0000000..ec0eeef
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactoryFactory.java
@@ -0,0 +1,12 @@
+package io.dropwizard.configuration;
+
+import javax.validation.Validator;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public interface ConfigurationFactoryFactory<T> {
+    public ConfigurationFactory<T> create(Class<T> klass,
+            Validator validator,
+            ObjectMapper objectMapper,
+            String propertyPrefix);
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationParsingException.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationParsingException.java
new file mode 100644
index 0000000..bc9d0e4
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationParsingException.java
@@ -0,0 +1,377 @@
+package io.dropwizard.configuration;
+
+import com.fasterxml.jackson.core.JsonLocation;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.dataformat.yaml.snakeyaml.error.Mark;
+import com.google.common.collect.ImmutableSet;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
+import java.util.*;
+
+/**
+ * A {@link ConfigurationException} for errors parsing a configuration file.
+ */
+public class ConfigurationParsingException extends ConfigurationException {
+
+    static class Builder {
+        private static final int MAX_SUGGESTIONS = 5;
+
+        private String summary;
+        private String detail = "";
+        private List<JsonMappingException.Reference> fieldPath = Collections.emptyList();
+        private int line = -1;
+        private int column = -1;
+        private Exception cause = null;
+        private List<String> suggestions = new ArrayList<>();
+        private String suggestionBase = null;
+        private boolean suggestionsSorted = false;
+
+        Builder(String summary) {
+            this.summary = summary;
+        }
+
+        /**
+         * Returns a brief message summarizing the error.
+         *
+         * @return a brief message summarizing the error.
+         */
+        public String getSummary() {
+            return summary.trim();
+        }
+
+        /**
+         * Returns a detailed description of the error.
+         *
+         * @return a detailed description of the error or the empty String if there is none.
+         */
+        public String getDetail() {
+            return detail.trim();
+        }
+
+        /**
+         * Determines if a detailed description of the error has been set.
+         *
+         * @return true if there is a detailed description of the error; false if there is not.
+         */
+        public boolean hasDetail() {
+            return detail != null && !detail.isEmpty();
+        }
+
+        /**
+         * Returns the path to the problematic JSON field, if there is one.
+         *
+         * @return a {@link List} with each element in the path in order, beginning at the root; or
+         *         an empty list if there is no JSON field in the context of this error.
+         */
+        public List<JsonMappingException.Reference> getFieldPath() {
+            return fieldPath;
+        }
+
+        /**
+         * Determines if the path to a JSON field has been set.
+         *
+         * @return true if the path to a JSON field has been set for the error; false if no path has
+         *         yet been set.
+         */
+        public boolean hasFieldPath() {
+            return fieldPath != null && !fieldPath.isEmpty();
+        }
+
+        /**
+         * Returns the line number of the source of the problem.
+         * <p/>
+         * Note: the line number is indexed from zero.
+         *
+         * @return the line number of the source of the problem, or -1 if unknown.
+         */
+        public int getLine() {
+            return line;
+        }
+
+        /**
+         * Returns the column number of the source of the problem.
+         * <p/>
+         * Note: the column number is indexed from zero.
+         *
+         * @return the column number of the source of the problem, or -1 if unknown.
+         */
+        public int getColumn() {
+            return column;
+        }
+
+        /**
+         * Determines if a location (line and column numbers) have been set.
+         *
+         * @return true if both a line and column number has been set; false if only one or neither
+         *         have been set.
+         */
+        public boolean hasLocation() {
+            return line > -1 && column > -1;
+        }
+
+        /**
+         * Returns a list of suggestions.
+         * <p/>
+         * If a {@link #getSuggestionBase() suggestion-base} has been set, the suggestions will be
+         * sorted according to the suggestion-base such that suggestions close to the base appear
+         * first in the list.
+         *
+         * @return a list of suggestions, or the empty list if there are no suggestions available.
+         */
+        public List<String> getSuggestions() {
+
+            if (suggestionsSorted || !hasSuggestionBase()) {
+                return suggestions;
+            }
+
+            Collections.sort(suggestions, new LevenshteinComparator(getSuggestionBase()));
+            suggestionsSorted = true;
+
+            return suggestions;
+        }
+
+        /**
+         * Determines whether suggestions are available.
+         *
+         * @return true if suggestions are available; false if they are not.
+         */
+        public boolean hasSuggestions() {
+            return suggestions != null && !suggestions.isEmpty();
+        }
+
+        /**
+         * Returns the base for ordering suggestions.
+         * <p/>
+         * Suggestions will be ordered such that suggestions closer to the base will appear first.
+         *
+         * @return the base for suggestions.
+         */
+        public String getSuggestionBase() {
+            return suggestionBase;
+        }
+
+        /**
+         * Determines whether a suggestion base is available.
+         * <p/>
+         * If no base is available, suggestions will not be sorted.
+         *
+         * @return true if a base is available for suggestions; false if there is none.
+         */
+        public boolean hasSuggestionBase() {
+            return suggestionBase != null && !suggestionBase.isEmpty();
+        }
+
+        /**
+         * Returns the {@link Exception} that encapsulates the problem itself.
+         *
+         * @return an Exception representing the cause of the problem, or null if there is none.
+         */
+        public Exception getCause() {
+            return cause;
+        }
+
+        /**
+         * Determines whether a cause has been set.
+         *
+         * @return true if there is a cause; false if there is none.
+         */
+        public boolean hasCause() {
+            return cause != null;
+        }
+
+        Builder setCause(Exception cause) {
+            this.cause = cause;
+            return this;
+        }
+
+        Builder setDetail(String detail) {
+            this.detail = detail;
+            return this;
+        }
+
+        Builder setFieldPath(List<JsonMappingException.Reference> fieldPath) {
+            this.fieldPath = fieldPath;
+            return this;
+        }
+
+        Builder setLocation(JsonLocation location) {
+            return location == null
+                    ? this
+                    : setLocation(location.getLineNr(), location.getColumnNr());
+        }
+
+        Builder setLocation(Mark mark) {
+            return mark == null
+                    ? this
+                    : setLocation(mark.getLine(), mark.getColumn());
+        }
+
+        Builder setLocation(int line, int column) {
+            this.line = line;
+            this.column = column;
+            return this;
+        }
+
+        Builder addSuggestion(String suggestion) {
+            this.suggestionsSorted = false;
+            this.suggestions.add(suggestion);
+            return this;
+        }
+
+        Builder addSuggestions(Collection<String> suggestions) {
+            this.suggestionsSorted = false;
+            this.suggestions.addAll(suggestions);
+            return this;
+        }
+
+        Builder setSuggestionBase(String base) {
+            this.suggestionBase = base;
+            this.suggestionsSorted = false;
+            return this;
+        }
+
+        ConfigurationParsingException build(String path) {
+            StringBuilder sb = new StringBuilder(getSummary());
+            if (hasFieldPath()) {
+                sb.append(" at: ").append(buildPath(getFieldPath()));
+            } else if (hasLocation()) {
+                sb.append(" at line: ").append(getLine() + 1)
+                        .append(", column: ").append(getColumn() + 1);
+            }
+
+            if (hasDetail()) {
+                sb.append("; ").append(getDetail());
+            }
+
+            if (hasSuggestions()) {
+                List<String> suggestions = getSuggestions();
+                sb.append(NEWLINE).append("    Did you mean?:").append(NEWLINE);
+                Iterator<String> it = suggestions.iterator();
+                int i = 0;
+                while (it.hasNext() && i < MAX_SUGGESTIONS) {
+                    sb.append("      - ").append(it.next());
+                    i++;
+                    if (it.hasNext()) {
+                        sb.append(NEWLINE);
+                    }
+                }
+
+                int total = suggestions.size();
+                if (i < total) {
+                    sb.append("        [").append(total - i).append(" more]");
+                }
+            }
+
+            return hasCause()
+                    ? new ConfigurationParsingException(path, sb.toString(), getCause())
+                    : new ConfigurationParsingException(path, sb.toString());
+        }
+
+        private String buildPath(Iterable<JsonMappingException.Reference> path) {
+            StringBuilder sb = new StringBuilder();
+            if (path != null) {
+                Iterator<JsonMappingException.Reference> it = path.iterator();
+                while (it.hasNext()) {
+                    JsonMappingException.Reference reference = it.next();
+                    String name = reference.getFieldName();
+
+                    // append either the field name or list index
+                    if (name == null) {
+                        sb.append('[').append(reference.getIndex()).append(']');
+                    } else {
+                        sb.append(name);
+                    }
+
+                    if (it.hasNext()) {
+                        sb.append('.');
+                    }
+                }
+            }
+            return sb.toString();
+        }
+
+        private static class LevenshteinComparator implements Comparator<String>, Serializable {
+
+            private String base;
+
+            public LevenshteinComparator(String base) {
+                this.base = base;
+            }
+
+            /**
+             * Compares two Strings with respect to the base String, by Levenshtein distance.
+             * <p/>
+             * The input that is the closest match to the base String will sort before the other.
+             *
+             * @param a an input to compare relative to the base.
+             * @param b an input to compare relative to the base.
+             *
+             * @return -1 if {@code a} is closer to the base than {@code b}; 1 if {@code b} is
+             *         closer to the base than {@code a}; 0 if both {@code a} and {@code b} are
+             *         equally close to the base.
+             */
+            @Override
+            public int compare(String a, String b) {
+
+                // shortcuts
+                if (a.equals(b)) {
+                    return 0; // comparing the same value; don't bother
+                } else if (a.equals(base)) {
+                    return -1; // a is equal to the base, so it's always first
+                } else if (b.equals(base)) {
+                    return 1; // b is equal to the base, so it's always first
+                }
+
+                // determine which of the two is closer to the base and order it first
+                return StringUtils.getLevenshteinDistance(a, base) < StringUtils.getLevenshteinDistance(b, base)
+                        ? -1
+                        : 1;
+            }
+
+            private void writeObject(ObjectOutputStream stream) throws IOException {
+                stream.defaultWriteObject();
+            }
+
+            private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+                stream.defaultReadObject();
+            }
+        }
+    }
+
+    /**
+     * Create a mutable {@link Builder} to incrementally build a {@link ConfigurationParsingException}.
+     *
+     * @param brief the brief summary of the error.
+     *
+     * @return a mutable builder to incrementally build a {@link ConfigurationParsingException}.
+     */
+    static Builder builder(String brief) {
+        return new Builder(brief);
+    }
+
+    /**
+     * Creates a new ConfigurationParsingException for the given path with the given error.
+     *
+     * @param path   the bad configuration path
+     * @param msg    the full error message
+     */
+    private ConfigurationParsingException(String path, String msg) {
+        super(path, ImmutableSet.of(msg));
+    }
+
+    /**
+     * Creates a new ConfigurationParsingException for the given path with the given error.
+     *
+     * @param path   the bad configuration path
+     * @param msg    the full error message
+     * @param cause  the cause of the parsing error.
+     */
+    private ConfigurationParsingException(String path, String msg, Throwable cause) {
+        super(path, ImmutableSet.of(msg), cause);
+    }
+
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationSourceProvider.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationSourceProvider.java
new file mode 100644
index 0000000..adfe1d3
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationSourceProvider.java
@@ -0,0 +1,21 @@
+package io.dropwizard.configuration;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * An interface for objects that can create an {@link InputStream} to represent the application
+ * configuration.
+ */
+public interface ConfigurationSourceProvider {
+    /**
+     * Returns an {@link InputStream} that contains the source of the configuration for the
+     * application. The caller is responsible for closing the result.
+     *
+     * @param path the path to the configuration
+     * @return an {@link InputStream}
+     * @throws IOException if there is an error reading the data at {@code path}
+     */
+    public InputStream open(String path) throws IOException;
+
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationValidationException.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationValidationException.java
new file mode 100644
index 0000000..f725f31
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationValidationException.java
@@ -0,0 +1,36 @@
+package io.dropwizard.configuration;
+
+import com.google.common.collect.ImmutableSet;
+import io.dropwizard.validation.ConstraintViolations;
+
+import javax.validation.ConstraintViolation;
+import java.util.Set;
+
+/**
+ * An exception thrown where there is an error validating a configuration object.
+ */
+public class ConfigurationValidationException extends ConfigurationException {
+    private static final long serialVersionUID = 5325162099634227047L;
+
+    private final ImmutableSet<ConstraintViolation<?>> constraintViolations;
+
+    /**
+     * Creates a new ConfigurationException for the given path with the given errors.
+     *
+     * @param path      the bad configuration path
+     * @param errors    the errors in the path
+     */
+    public <T> ConfigurationValidationException(String path, Set<ConstraintViolation<T>> errors) {
+        super(path, ConstraintViolations.format(errors));
+        this.constraintViolations = ConstraintViolations.copyOf(errors);
+    }
+
+    /**
+     * Returns the set of constraint violations in the configuration.
+     *
+     * @return the set of constraint violations
+     */
+    public ImmutableSet<ConstraintViolation<?>> getConstraintViolations() {
+        return constraintViolations;
+    }
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/DefaultConfigurationFactoryFactory.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/DefaultConfigurationFactoryFactory.java
new file mode 100644
index 0000000..d3a4838
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/DefaultConfigurationFactoryFactory.java
@@ -0,0 +1,16 @@
+package io.dropwizard.configuration;
+
+import javax.validation.Validator;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class DefaultConfigurationFactoryFactory<T> implements ConfigurationFactoryFactory<T> {
+    @Override
+    public ConfigurationFactory<T> create(
+            Class<T>     klass,
+            Validator    validator, 
+            ObjectMapper objectMapper,
+            String       propertyPrefix) {
+        return new ConfigurationFactory<>(klass, validator, objectMapper, propertyPrefix);
+    }
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/FileConfigurationSourceProvider.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/FileConfigurationSourceProvider.java
new file mode 100644
index 0000000..bc0baff
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/FileConfigurationSourceProvider.java
@@ -0,0 +1,19 @@
+package io.dropwizard.configuration;
+
+import java.io.*;
+
+/**
+ * An implementation of {@link ConfigurationSourceProvider} that reads the configuration from the
+ * local file system.
+ */
+public class FileConfigurationSourceProvider implements ConfigurationSourceProvider {
+    @Override
+    public InputStream open(String path) throws IOException {
+        final File file = new File(path);
+        if (!file.exists()) {
+            throw new FileNotFoundException("File " + file + " not found");
+        }
+
+        return new FileInputStream(file);
+    }
+}
diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/UrlConfigurationSourceProvider.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/UrlConfigurationSourceProvider.java
new file mode 100644
index 0000000..991109c
--- /dev/null
+++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/UrlConfigurationSourceProvider.java
@@ -0,0 +1,16 @@
+package io.dropwizard.configuration;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+
+/**
+ * An implementation of {@link ConfigurationSourceProvider} that reads the configuration from a
+ * {@link URL}.
+ */
+public class UrlConfigurationSourceProvider implements ConfigurationSourceProvider {
+    @Override
+    public InputStream open(String path) throws IOException {
+        return new URL(path).openStream();
+    }
+}
diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryFactoryTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryFactoryTest.java
new file mode 100644
index 0000000..92423be
--- /dev/null
+++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryFactoryTest.java
@@ -0,0 +1,36 @@
+package io.dropwizard.configuration;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import io.dropwizard.configuration.ConfigurationFactoryTest.Example;
+import io.dropwizard.jackson.Jackson;
+
+import java.io.File;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.io.Resources;
+
+
+public class ConfigurationFactoryFactoryTest {
+        
+    private final ConfigurationFactoryFactory<Example> factoryFactory = new DefaultConfigurationFactoryFactory<Example>();
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+    private File validFile;    
+    
+    @Before
+    public void setUp() throws Exception {
+        this.validFile = new File(Resources.getResource("factory-test-valid.yml").toURI());
+    }
+            
+     @Test
+     public void createDefaultFactory() throws Exception {
+         ConfigurationFactory<Example> factory = factoryFactory.create(Example.class, validator, Jackson.newObjectMapper(), "dw");         
+         final Example example = factory.build(validFile);
+         assertThat(example.getName())
+                 .isEqualTo("Coda Hale");
+     }
+}
diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryTest.java
new file mode 100644
index 0000000..3a89837
--- /dev/null
+++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryTest.java
@@ -0,0 +1,272 @@
+package io.dropwizard.configuration;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.io.Resources;
+import io.dropwizard.jackson.Jackson;
+import org.fest.assertions.data.MapEntry;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import java.io.File;
+import java.util.*;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class ConfigurationFactoryTest {
+
+    @SuppressWarnings("UnusedDeclaration")
+    public static class ExampleServer {
+
+        @JsonProperty
+        private int port = 8000;
+
+        public int getPort() {
+            return port;
+        }
+
+    }
+
+    @SuppressWarnings("UnusedDeclaration")
+    public static class Example {
+
+        @NotNull
+        @Pattern(regexp = "[\\w]+[\\s]+[\\w]+([\\s][\\w]+)?")
+        private String name;
+
+        @JsonProperty
+        private int age = 1;
+        
+        List<String> type;
+
+        @JsonProperty
+        private Map<String, String> properties = Maps.newLinkedHashMap();
+
+        @JsonProperty
+        private List<ExampleServer> servers = Lists.newArrayList();
+
+        public String getName() {
+            return name;
+        }
+
+        public List<String> getType() {
+            return type;
+        }
+
+        public Map<String, String> getProperties() {
+            return properties;
+        }
+
+        public List<ExampleServer> getServers() {
+            return servers;
+        }
+
+    }
+
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+    private final ConfigurationFactory<Example> factory =
+            new ConfigurationFactory<>(Example.class, validator, Jackson.newObjectMapper(), "dw");
+    private File malformedFile;
+    private File invalidFile;
+    private File validFile;
+
+    @After
+    public void resetConfigOverrides() {
+        for (Enumeration<?> props = System.getProperties().propertyNames(); props.hasMoreElements();) {
+            String keyString = (String) props.nextElement();
+            if (keyString.startsWith("dw.")) {
+                System.clearProperty(keyString);
+            }
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        this.malformedFile = new File(Resources.getResource("factory-test-malformed.yml").toURI());
+        this.invalidFile = new File(Resources.getResource("factory-test-invalid.yml").toURI());
+        this.validFile = new File(Resources.getResource("factory-test-valid.yml").toURI());
+    }
+
+    @Test
+    public void loadsValidConfigFiles() throws Exception {
+        final Example example = factory.build(validFile);
+
+        assertThat(example.getName())
+                .isEqualTo("Coda Hale");
+
+        assertThat(example.getType().get(0))
+                .isEqualTo("coder");
+        assertThat(example.getType().get(1))
+                .isEqualTo("wizard");
+
+        assertThat(example.getProperties())
+                .contains(MapEntry.entry("debug", "true"),
+                        MapEntry.entry("settings.enabled", "false"));
+
+        assertThat(example.getServers())
+                .hasSize(3);
+        assertThat(example.getServers().get(0).getPort())
+                .isEqualTo(8080);
+
+    }
+    
+    @Test
+    public void handlesSimpleOverride() throws Exception {
+        System.setProperty("dw.name", "Coda Hale Overridden");
+        final Example example = factory.build(validFile);
+        assertThat(example.getName())
+            .isEqualTo("Coda Hale Overridden");
+    }
+    
+    @Test
+    public void handlesArrayOverride() throws Exception {
+        System.setProperty("dw.type", "coder,wizard,overridden");
+        final Example example = factory.build(validFile);
+        assertThat(example.getType().get(2))
+                .isEqualTo("overridden");
+        assertThat(example.getType().size())
+                .isEqualTo(3);
+    }
+
+    @Test
+    public void handlesArrayOverrideEscaped() throws Exception {
+        System.setProperty("dw.type", "coder,wizard,overr\\,idden");
+        final Example example = factory.build(validFile);
+        assertThat(example.getType().get(2))
+                .isEqualTo("overr,idden");
+        assertThat(example.getType().size())
+                .isEqualTo(3);
+    }
+
+    @Test
+    public void handlesSingleElementArrayOverride() throws Exception {
+        System.setProperty("dw.type", "overridden");
+        final Example example = factory.build(validFile);
+        assertThat(example.getType().get(0))
+                .isEqualTo("overridden");
+        assertThat(example.getType().size())
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void overridesArrayWithIndices() throws Exception {
+        System.setProperty("dw.type[1]", "overridden");
+        final Example example = factory.build(validFile);
+
+        assertThat(example.getType().get(0))
+                .isEqualTo("coder");
+        assertThat(example.getType().get(1))
+                .isEqualTo("overridden");
+    }
+
+    @Test
+    public void overridesArrayWithIndicesReverse() throws Exception {
+        System.setProperty("dw.type[0]", "overridden");
+        final Example example = factory.build(validFile);
+
+        assertThat(example.getType().get(0))
+                .isEqualTo("overridden");
+        assertThat(example.getType().get(1))
+                .isEqualTo("wizard");
+    }
+
+    @Test
+    public void overridesArrayPropertiesWithIndices() throws Exception {
+        System.setProperty("dw.servers[0].port", "7000");
+        System.setProperty("dw.servers[2].port", "9000");
+        final Example example = factory.build(validFile);
+
+        assertThat(example.getServers())
+                .hasSize(3);
+        assertThat(example.getServers().get(0).getPort())
+                .isEqualTo(7000);
+        assertThat(example.getServers().get(2).getPort())
+                .isEqualTo(9000);
+    }
+
+    @Test
+    public void overrideMapProperty() throws Exception {
+        System.setProperty("dw.properties.settings.enabled", "true");
+        final Example example = factory.build(validFile);
+        assertThat(example.getProperties())
+                .contains(MapEntry.entry("debug", "true"),
+                        MapEntry.entry("settings.enabled", "true"));
+    }
+
+    @Test
+    public void throwsAnExceptionOnUnexpectedArrayOverride() throws Exception {
+        System.setProperty("dw.servers.port", "9000");
+        try {
+            factory.build(validFile);
+            failBecauseExceptionWasNotThrown(IllegalArgumentException.class);
+        } catch (IllegalArgumentException e) {
+            assertThat(e.getMessage())
+                    .containsOnlyOnce("target is an array but no index specified");
+        }
+    }
+
+    @Test(expected = ConfigurationParsingException.class)
+    public void throwsAnExceptionOnArrayOverrideWithInvalidType() throws Exception {
+        System.setProperty("dw.servers", "one,two");
+
+        factory.build(validFile);
+        failBecauseExceptionWasNotThrown(ConfigurationParsingException.class);
+    }
+
+    @Test
+    public void throwsAnExceptionOnOverrideArrayIndexOutOfBounds() throws Exception {
+        System.setProperty("dw.type[2]", "invalid");
+        try {
+            factory.build(validFile);
+            failBecauseExceptionWasNotThrown(ArrayIndexOutOfBoundsException.class);
+        } catch (ArrayIndexOutOfBoundsException e) {
+            assertThat(e.getMessage())
+                    .containsOnlyOnce("index is greater than size of array");
+        }
+    }
+
+    @Test
+    public void throwsAnExceptionOnOverrideArrayPropertyIndexOutOfBounds() throws Exception {
+        System.setProperty("dw.servers[4].port", "9000");
+        try {
+            factory.build(validFile);
+            failBecauseExceptionWasNotThrown(ArrayIndexOutOfBoundsException.class);
+        } catch (ArrayIndexOutOfBoundsException e) {
+            assertThat(e.getMessage())
+                    .containsOnlyOnce("index is greater than size of array");
+        }
+    }
+
+    @Test
+    public void throwsAnExceptionOnMalformedFiles() throws Exception {
+        try {
+            factory.build(malformedFile);
+            failBecauseExceptionWasNotThrown(ConfigurationParsingException.class);
+        } catch (ConfigurationParsingException e) {
+            assertThat(e.getMessage())
+                    .containsOnlyOnce(" * Failed to parse configuration; Can not instantiate");
+        }
+    }
+
+    @Test
+    public void throwsAnExceptionOnInvalidFiles() throws Exception {
+        try {
+            factory.build(invalidFile);
+            failBecauseExceptionWasNotThrown(ConfigurationValidationException.class);
+        } catch (ConfigurationValidationException e) {
+            if ("en".equals(Locale.getDefault().getLanguage())) {
+                assertThat(e.getMessage())
+                        .endsWith(String.format(
+                                "factory-test-invalid.yml has an error:%n" +
+                                        "  * name must match \"[\\w]+[\\s]+[\\w]+([\\s][\\w]+)?\" (was Boop)%n"));
+            }
+        }
+    }
+}
diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationValidationExceptionTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationValidationExceptionTest.java
new file mode 100644
index 0000000..e5561b8
--- /dev/null
+++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationValidationExceptionTest.java
@@ -0,0 +1,48 @@
+package io.dropwizard.configuration;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.validation.constraints.NotNull;
+import java.util.Locale;
+import java.util.Set;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assume.assumeThat;
+
+public class ConfigurationValidationExceptionTest {
+    private static class Example {
+        @NotNull
+        String woo;
+    }
+
+    private ConfigurationValidationException e;
+
+    @Before
+    public void setUp() throws Exception {
+        assumeThat(Locale.getDefault().getLanguage(), is("en"));
+
+        final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        final Set<ConstraintViolation<Example>> violations = validator.validate(new Example());
+        this.e = new ConfigurationValidationException("config.yml", violations);
+    }
+
+    @Test
+    public void formatsTheViolationsIntoAHumanReadableMessage() throws Exception {
+        assertThat(e.getMessage())
+                .isEqualTo(String.format(
+                        "config.yml has an error:%n" +
+                                "  * woo may not be null (was null)%n"
+                ));
+    }
+
+    @Test
+    public void retainsTheSetOfExceptions() throws Exception {
+        assertThat(e.getConstraintViolations())
+                .isNotEmpty();
+    }
+}
diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/FileConfigurationSourceProviderTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/FileConfigurationSourceProviderTest.java
new file mode 100644
index 0000000..47966c6
--- /dev/null
+++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/FileConfigurationSourceProviderTest.java
@@ -0,0 +1,22 @@
+package io.dropwizard.configuration;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Resources;
+import org.junit.Test;
+
+import java.io.InputStream;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class FileConfigurationSourceProviderTest {
+    private final ConfigurationSourceProvider provider = new FileConfigurationSourceProvider();
+
+    @Test
+    public void readsFileContents() throws Exception {
+        try (InputStream input = provider.open(Resources.getResource("example.txt").getFile())) {
+            assertThat(new String(ByteStreams.toByteArray(input), Charsets.UTF_8).trim())
+                    .isEqualTo("whee");
+        }
+    }
+}
diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/UrlConfigurationSourceProviderTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/UrlConfigurationSourceProviderTest.java
new file mode 100644
index 0000000..a003aca
--- /dev/null
+++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/UrlConfigurationSourceProviderTest.java
@@ -0,0 +1,22 @@
+package io.dropwizard.configuration;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.ByteStreams;
+import com.google.common.io.Resources;
+import org.junit.Test;
+
+import java.io.InputStream;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class UrlConfigurationSourceProviderTest {
+    private final ConfigurationSourceProvider provider = new UrlConfigurationSourceProvider();
+
+    @Test
+    public void readsFileContents() throws Exception {
+        try (InputStream input = provider.open(Resources.getResource("example.txt").toString())) {
+            assertThat(new String(ByteStreams.toByteArray(input), Charsets.UTF_8).trim())
+                    .isEqualTo("whee");
+        }
+    }
+}
diff --git a/dropwizard-configuration/src/test/resources/example.txt b/dropwizard-configuration/src/test/resources/example.txt
new file mode 100644
index 0000000..c6284d2
--- /dev/null
+++ b/dropwizard-configuration/src/test/resources/example.txt
@@ -0,0 +1 @@
+whee
diff --git a/dropwizard-configuration/src/test/resources/factory-test-invalid.yml b/dropwizard-configuration/src/test/resources/factory-test-invalid.yml
new file mode 100644
index 0000000..2f6b712
--- /dev/null
+++ b/dropwizard-configuration/src/test/resources/factory-test-invalid.yml
@@ -0,0 +1 @@
+name: Boop
diff --git a/dropwizard-configuration/src/test/resources/factory-test-malformed.yml b/dropwizard-configuration/src/test/resources/factory-test-malformed.yml
new file mode 100644
index 0000000..8804ba7
--- /dev/null
+++ b/dropwizard-configuration/src/test/resources/factory-test-malformed.yml
@@ -0,0 +1 @@
+j&&&&
diff --git a/dropwizard-configuration/src/test/resources/factory-test-valid.yml b/dropwizard-configuration/src/test/resources/factory-test-valid.yml
new file mode 100644
index 0000000..28b5938
--- /dev/null
+++ b/dropwizard-configuration/src/test/resources/factory-test-valid.yml
@@ -0,0 +1,11 @@
+name: Coda Hale
+type:
+    - coder
+    - wizard
+properties:
+  debug: true
+  settings.enabled: false
+servers:
+  - port: 8080
+  - port: 8081
+  - port: 8082
\ No newline at end of file
diff --git a/dropwizard-configuration/src/test/resources/logback-test.xml b/dropwizard-configuration/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-configuration/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-core/pom.xml b/dropwizard-core/pom.xml
new file mode 100644
index 0000000..10c66c0
--- /dev/null
+++ b/dropwizard-core/pom.xml
@@ -0,0 +1,163 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-core</artifactId>
+    <name>Dropwizard</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-util</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jackson</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-validation</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-configuration</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-logging</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-metrics</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jersey</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-servlets</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jetty</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-lifecycle</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-jvm</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-servlets</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-healthchecks</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>net.sourceforge.argparse4j</groupId>
+            <artifactId>argparse4j</artifactId>
+            <version>0.4.3</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.orbit</groupId>
+            <artifactId>javax.servlet</artifactId>
+            <version>${servlet.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.toolchain.setuid</groupId>
+            <artifactId>jetty-setuid-java</artifactId>
+            <version>1.0.2</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-util</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-server</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-inmemory</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-core/src/main/java/io/dropwizard/Application.java b/dropwizard-core/src/main/java/io/dropwizard/Application.java
new file mode 100644
index 0000000..cbbaef9
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/Application.java
@@ -0,0 +1,77 @@
+package io.dropwizard;
+
+import io.dropwizard.cli.CheckCommand;
+import io.dropwizard.cli.Cli;
+import io.dropwizard.cli.ServerCommand;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import io.dropwizard.util.Generics;
+import io.dropwizard.util.JarLocation;
+
+/**
+ * The base class for Dropwizard applications.
+ *
+ * @param <T> the type of configuration class for this application
+ */
+public abstract class Application<T extends Configuration> {
+    static {
+        // make sure spinning up Hibernate Validator doesn't yell at us
+        LoggingFactory.bootstrap();
+    }
+
+    /**
+     * Returns the {@link Class} of the configuration class type parameter.
+     *
+     * @return the configuration class
+     * @see Generics#getTypeParameter(Class, Class)
+     */
+    public final Class<T> getConfigurationClass() {
+        return Generics.getTypeParameter(getClass(), Configuration.class);
+    }
+
+    /**
+     * Returns the name of the application.
+     *
+     * @return the application's name
+     */
+    public String getName() {
+        return getClass().getSimpleName();
+    }
+
+    /**
+     * Initializes the application bootstrap.
+     *
+     * @param bootstrap the application bootstrap
+     */
+    public abstract void initialize(Bootstrap<T> bootstrap);
+
+    /**
+     * When the application runs, this is called after the {@link Bundle}s are run. Override it to add
+     * providers, resources, etc. for your application.
+     *
+     * @param configuration the parsed {@link Configuration} object
+     * @param environment   the application's {@link Environment}
+     * @throws Exception if something goes wrong
+     */
+    public abstract void run(T configuration, Environment environment) throws Exception;
+
+    /**
+     * Parses command-line arguments and runs the application. Call this method from a {@code public
+     * static void main} entry point in your application.
+     *
+     * @param arguments the command-line arguments
+     * @throws Exception if something goes wrong
+     */
+    public final void run(String[] arguments) throws Exception {
+        final Bootstrap<T> bootstrap = new Bootstrap<>(this);
+        bootstrap.addCommand(new ServerCommand<>(this));
+        bootstrap.addCommand(new CheckCommand<>(this));
+        initialize(bootstrap);
+        final Cli cli = new Cli(new JarLocation(getClass()), bootstrap, System.out, System.err);
+        if (!cli.run(arguments)) {
+            // only exit if there's an error running the command
+            System.exit(1);
+        }
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/Bundle.java b/dropwizard-core/src/main/java/io/dropwizard/Bundle.java
new file mode 100644
index 0000000..ffa6b6d
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/Bundle.java
@@ -0,0 +1,23 @@
+package io.dropwizard;
+
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+/**
+ * A reusable bundle of functionality, used to define blocks of application behavior.
+ */
+public interface Bundle {
+    /**
+     * Initializes the application bootstrap.
+     *
+     * @param bootstrap the application bootstrap
+     */
+    void initialize(Bootstrap<?> bootstrap);
+
+    /**
+     * Initializes the application environment.
+     *
+     * @param environment the application environment
+     */
+    void run(Environment environment);
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/Configuration.java b/dropwizard-core/src/main/java/io/dropwizard/Configuration.java
new file mode 100644
index 0000000..42fc4db
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/Configuration.java
@@ -0,0 +1,127 @@
+package io.dropwizard;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Objects;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.metrics.MetricsFactory;
+import io.dropwizard.server.DefaultServerFactory;
+import io.dropwizard.server.ServerFactory;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+/**
+ * An object representation of the YAML configuration file. Extend this with your own configuration
+ * properties, and they'll be parsed from the YAML file as well.
+ * <p/>
+ * For example, given a YAML file with this:
+ * <pre>
+ * name: "Random Person"
+ * age: 43
+ * # ... etc ...
+ * </pre>
+ * And a configuration like this:
+ * <pre>
+ * public class ExampleConfiguration extends Configuration {
+ *     \@NotNull
+ *     private String name;
+ *
+ *     \@Min(1)
+ *     \@Max(120)
+ *     private int age;
+ *
+ *     \@JsonProperty
+ *     public String getName() {
+ *         return name;
+ *     }
+ *
+ *     \@JsonProperty
+ *     public void setName(String name) {
+ *         this.name = name;
+ *     }
+ *
+ *     \@JsonProperty
+ *     public int getAge() {
+ *         return age;
+ *     }
+ *
+ *     \@JsonProperty
+ *     public void setAge(int age) {
+ *         this.age = age;
+ *     }
+ * }
+ * </pre>
+ * <p/>
+ * Dropwizard will parse the given YAML file and provide an {@code ExampleConfiguration} instance
+ * to your application whose {@code getName()} method will return {@code "Random Person"} and whose
+ * {@code getAge()} method will return {@code 43}.
+ *
+ * @see <a href="http://www.yaml.org/YAML_for_ruby.html">YAML Cookbook</a>
+ */
+public class Configuration {
+    @Valid
+    @NotNull
+    private ServerFactory server = new DefaultServerFactory();
+
+    @Valid
+    @NotNull
+    private LoggingFactory logging = new LoggingFactory();
+
+    @Valid
+    @NotNull
+    private MetricsFactory metrics = new MetricsFactory();
+
+    /**
+     * Returns the server-specific section of the configuration file.
+     *
+     * @return server-specific configuration parameters
+     */
+    @JsonProperty("server")
+    public ServerFactory getServerFactory() {
+        return server;
+    }
+
+    /**
+     * Sets the HTTP-specific section of the configuration file.
+     */
+    @JsonProperty("server")
+    public void setServerFactory(ServerFactory factory) {
+        this.server = factory;
+    }
+
+    /**
+     * Returns the logging-specific section of the configuration file.
+     *
+     * @return logging-specific configuration parameters
+     */
+    @JsonProperty("logging")
+    public LoggingFactory getLoggingFactory() {
+        return logging;
+    }
+
+    /**
+     * Sets the logging-specific section of the configuration file.
+     */
+    @JsonProperty("logging")
+    public void setLoggingFactory(LoggingFactory factory) {
+        this.logging = factory;
+    }
+
+    @JsonProperty("metrics")
+    public MetricsFactory getMetricsFactory() {
+        return metrics;
+    }
+
+    @JsonProperty("metrics")
+    public void setMetricsFactory(MetricsFactory metrics) {
+        this.metrics = metrics;
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this)
+                      .add("server", server)
+                      .add("logging", logging)
+                      .toString();
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/ConfiguredBundle.java b/dropwizard-core/src/main/java/io/dropwizard/ConfiguredBundle.java
new file mode 100644
index 0000000..fb626a2
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/ConfiguredBundle.java
@@ -0,0 +1,28 @@
+package io.dropwizard;
+
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+/**
+ * A reusable bundle of functionality, used to define blocks of application behavior that are
+ * conditional on configuration parameters.
+ *
+ * @param <T>    the required configuration interface
+ */
+public interface ConfiguredBundle<T> {
+    /**
+     * Initializes the environment.
+     *
+     * @param configuration    the configuration object
+     * @param environment      the application's {@link Environment}
+     * @throws Exception if something goes wrong
+     */
+    void run(T configuration, Environment environment) throws Exception;
+
+    /**
+     * Initializes the application bootstrap.
+     *
+     * @param bootstrap the application bootstrap
+     */
+    void initialize(Bootstrap<?> bootstrap);
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/cli/CheckCommand.java b/dropwizard-core/src/main/java/io/dropwizard/cli/CheckCommand.java
new file mode 100644
index 0000000..54f9b01
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/cli/CheckCommand.java
@@ -0,0 +1,40 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.setup.Bootstrap;
+import net.sourceforge.argparse4j.inf.Namespace;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Parses and validates the application's configuration.
+ *
+ * @param <T> the {@link Configuration} subclass which is loaded from the configuration file
+ */
+public class CheckCommand<T extends Configuration> extends ConfiguredCommand<T> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(CheckCommand.class);
+
+    private final Class<T> configurationClass;
+
+    public CheckCommand(Application<T> application) {
+        super("check", "Parses and validates the configuration file");
+        this.configurationClass = application.getConfigurationClass();
+    }
+
+    /*
+     * Since we don't subclass CheckCommand, we need a concrete reference to the configuration
+     * class.
+     */
+    @Override
+    protected Class<T> getConfigurationClass() {
+        return configurationClass;
+    }
+
+    @Override
+    protected void run(Bootstrap<T> bootstrap,
+                       Namespace namespace,
+                       T configuration) throws Exception {
+        LOGGER.info("Configuration is OK");
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/cli/Cli.java b/dropwizard-core/src/main/java/io/dropwizard/cli/Cli.java
new file mode 100644
index 0000000..e2ea292
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/cli/Cli.java
@@ -0,0 +1,150 @@
+package io.dropwizard.cli;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Maps;
+import io.dropwizard.configuration.ConfigurationException;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.util.JarLocation;
+import net.sourceforge.argparse4j.ArgumentParsers;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.*;
+
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.SortedMap;
+
+/**
+ * The command-line runner for Dropwizard application.
+ */
+public class Cli {
+    private static final String COMMAND_NAME_ATTR = "command";
+    // assume -h if no arguments are given
+    private static final String[][] HELP = {{}, {"-h"}, {"--help"}};
+    private static final String[][] VERSION = {{"-v"}, {"--version"}};
+
+    private final PrintWriter stdOut;
+    private final PrintWriter stdErr;
+    private final SortedMap<String, Command> commands;
+    private final Bootstrap<?> bootstrap;
+    private final ArgumentParser parser;
+
+    /**
+     * Create a new CLI interface for a application and its bootstrapped environment.
+     *
+     * @param location     the location of the application
+     * @param bootstrap    the bootstrap for the application
+     * @param stdOut       standard out
+     * @param stdErr       standard err
+     */
+    public Cli(JarLocation location, Bootstrap<?> bootstrap, OutputStream stdOut, OutputStream stdErr) {
+        this.stdOut = new PrintWriter(new OutputStreamWriter(stdOut, Charsets.UTF_8), true);
+        this.stdErr = new PrintWriter(new OutputStreamWriter(stdErr, Charsets.UTF_8), true);
+        this.commands = Maps.newTreeMap();
+        this.parser = buildParser(location);
+        this.bootstrap = bootstrap;
+        for (Command command : bootstrap.getCommands()) {
+            addCommand(command);
+        }
+    }
+
+    /**
+     * Runs the command line interface given some arguments.
+     *
+     * @param arguments the command line arguments
+     * @return whether or not the command successfully executed
+     * @throws Exception if something goes wrong
+     */
+    public boolean run(String... arguments) throws Exception {
+        try {
+            if (isFlag(HELP, arguments)) {
+                parser.printHelp(stdOut);
+            } else if (isFlag(VERSION, arguments)) {
+                parser.printVersion(stdOut);
+            } else {
+                final Namespace namespace = parser.parseArgs(arguments);
+                if (namespace.get("is-help") == null) {
+                    final Command command = commands.get(namespace.getString(COMMAND_NAME_ATTR));
+                    command.run(bootstrap, namespace);
+                }
+            }
+            return true;
+        } catch (ArgumentParserException e) {
+            // TODO: 5/25/13 <coda> -- make ArgumentParser#handleError not depend on System.err
+            stdErr.println(e.getMessage());
+            e.getParser().printHelp(stdErr);
+            return false;
+        } catch (ConfigurationException e) {
+            // TODO: 7/26/13 <ntelford> -- as above, this probably shouldn't depend on System.err
+            stdErr.println(e.getMessage());
+            return false;
+        }
+    }
+
+    private boolean isFlag(String[][] flags, String[] arguments) {
+        for (String[] cmd : flags) {
+            if (Arrays.equals(arguments, cmd)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private ArgumentParser buildParser(JarLocation location) {
+        final String usage = "java -jar " + location;
+        final ArgumentParser p = ArgumentParsers.newArgumentParser(usage, false);
+        p.version(location.getVersion().or(
+                "No application version detected. Add a Implementation-Version " +
+                        "entry to your JAR's manifest to enable this."));
+        addHelp(p);
+        p.addArgument("-v", "--version")
+         .action(Arguments.help()) // never gets called; intercepted in #run
+         .help("show the application version and exit");
+        return p;
+    }
+
+    private void addHelp(ArgumentParser p) {
+        p.addArgument("-h", "--help")
+         .action(new SafeHelpAction(stdOut))
+         .help("show this help message and exit")
+         .setDefault(Arguments.SUPPRESS);
+    }
+
+    private void addCommand(Command command) {
+        commands.put(command.getName(), command);
+        parser.addSubparsers().help("available commands");
+        final Subparser subparser = parser.addSubparsers().addParser(command.getName(), false);
+        command.configure(subparser);
+        addHelp(subparser);
+        subparser.description(command.getDescription())
+                 .setDefault(COMMAND_NAME_ATTR, command.getName())
+                 .defaultHelp(true);
+    }
+
+    private static class SafeHelpAction implements ArgumentAction {
+        private final PrintWriter out;
+
+        SafeHelpAction(PrintWriter out) {
+            this.out = out;
+        }
+
+        @Override
+        public void run(ArgumentParser parser, Argument arg,
+                        Map<String, Object> attrs, String flag, Object value)
+                throws ArgumentParserException {
+            parser.printHelp(out);
+            attrs.put("is-help", Boolean.TRUE);
+        }
+
+        @Override
+        public boolean consumeArgument() {
+            return false;
+        }
+
+        @Override
+        public void onAttach(Argument arg) {
+        }
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/cli/Command.java b/dropwizard-core/src/main/java/io/dropwizard/cli/Command.java
new file mode 100644
index 0000000..d10db1d
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/cli/Command.java
@@ -0,0 +1,58 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.setup.Bootstrap;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+/**
+ * A basic CLI command.
+ */
+public abstract class Command {
+    private final String name;
+    private final String description;
+
+    /**
+     * Create a new command with the given name and description.
+     *
+     * @param name        the name of the command, used for command line invocation
+     * @param description a description of the command's purpose
+     */
+    protected Command(String name, String description) {
+        this.name = name;
+        this.description = description;
+    }
+
+    /**
+     * Returns the command's name.
+     *
+     * @return the command's name
+     */
+    public final String getName() {
+        return name;
+    }
+
+    /**
+     * Returns the command's description.
+     *
+     * @return the command's description
+     */
+    public final String getDescription() {
+        return description;
+    }
+
+    /**
+     * Configure the command's {@link Subparser}.
+     *
+     * @param subparser the {@link Subparser} specific to the command
+     */
+    public abstract void configure(Subparser subparser);
+
+    /**
+     * Executes when the user runs this specific command.
+     *
+     * @param bootstrap the bootstrap bootstrap
+     * @param namespace the parsed command line namespace
+     * @throws Exception if something goes wrong
+     */
+    public abstract void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception;
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/cli/ConfiguredCommand.java b/dropwizard-core/src/main/java/io/dropwizard/cli/ConfiguredCommand.java
new file mode 100644
index 0000000..585ee43
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/cli/ConfiguredCommand.java
@@ -0,0 +1,118 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.configuration.ConfigurationException;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.configuration.ConfigurationFactoryFactory;
+import io.dropwizard.configuration.ConfigurationSourceProvider;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.util.Generics;
+
+import java.io.IOException;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * A command whose first parameter is the location of a YAML configuration file. That file is parsed
+ * into an instance of a {@link Configuration} subclass, which is then validated. If the
+ * configuration is valid, the command is run.
+ *
+ * @param <T> the {@link Configuration} subclass which is loaded from the configuration file
+ * @see Configuration
+ */
+public abstract class ConfiguredCommand<T extends Configuration> extends Command {
+    private boolean asynchronous;
+
+    private T configuration;
+
+    protected ConfiguredCommand(String name, String description) {
+        super(name, description);
+        this.asynchronous = false;
+    }
+
+    /**
+     * Returns the {@link Class} of the configuration type.
+     *
+     * @return the {@link Class} of the configuration type
+     */
+    protected Class<T> getConfigurationClass() {
+        return Generics.getTypeParameter(getClass(), Configuration.class);
+    }
+
+    /**
+     * Configure the command's {@link Subparser}. <p><strong> N.B.: if you override this method, you
+     * <em>must</em> call {@code super.override(subparser)} in order to preserve the configuration
+     * file parameter in the subparser. </strong></p>
+     *
+     * @param subparser the {@link Subparser} specific to the command
+     */
+    @Override
+    public void configure(Subparser subparser) {
+        subparser.addArgument("file").nargs("?").help("application configuration file");
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public final void run(Bootstrap<?> bootstrap, Namespace namespace) throws Exception {
+        configuration = parseConfiguration(((Bootstrap<T>)bootstrap).getConfigurationFactoryFactory(),
+                                           bootstrap.getConfigurationSourceProvider(),
+                                           bootstrap.getValidatorFactory().getValidator(),
+                                           namespace.getString("file"),
+                                           getConfigurationClass(),
+                                           bootstrap.getObjectMapper());
+
+        try {
+            if (configuration != null) {
+                configuration.getLoggingFactory().configure(bootstrap.getMetricRegistry(),
+                                                            bootstrap.getApplication().getName());
+            }
+
+            run((Bootstrap<T>) bootstrap, namespace, configuration);
+        } finally {
+            if (!asynchronous) {
+                cleanup();
+            }
+        }
+    }
+
+    protected void cleanupAsynchronously() {
+        this.asynchronous = true;
+    }
+
+    protected void cleanup() {
+        if (configuration != null) {
+            configuration.getLoggingFactory().stop();
+        }
+    }
+
+    /**
+     * Runs the command with the given {@link Bootstrap} and {@link Configuration}.
+     *
+     * @param bootstrap     the bootstrap bootstrap
+     * @param namespace     the parsed command line namespace
+     * @param configuration the configuration object
+     * @throws Exception if something goes wrong
+     */
+    protected abstract void run(Bootstrap<T> bootstrap,
+                                Namespace namespace,
+                                T configuration) throws Exception;
+
+    private T parseConfiguration(ConfigurationFactoryFactory<T> configurationFactoryFactory,
+                                 ConfigurationSourceProvider provider,
+                                 Validator validator,
+                                 String path,
+                                 Class<T> klass,
+                                 ObjectMapper objectMapper) throws IOException, ConfigurationException {
+        final ConfigurationFactory<T> configurationFactory = configurationFactoryFactory.create(klass, validator, objectMapper, "dw");
+        if (path != null) {
+            return configurationFactory.build(provider, path);
+        }
+        return configurationFactory.build();
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/cli/EnvironmentCommand.java b/dropwizard-core/src/main/java/io/dropwizard/cli/EnvironmentCommand.java
new file mode 100644
index 0000000..5cb5a2a
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/cli/EnvironmentCommand.java
@@ -0,0 +1,55 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import net.sourceforge.argparse4j.inf.Namespace;
+
+import javax.validation.Validation;
+
+/**
+ * A command which executes with a configured {@link Environment}.
+ *
+ * @param <T> the {@link Configuration} subclass which is loaded from the configuration file
+ * @see Configuration
+ */
+public abstract class EnvironmentCommand<T extends Configuration> extends ConfiguredCommand<T> {
+    private final Application<T> application;
+
+    /**
+     * Creates a new environment command.
+     *
+     * @param application     the application providing this command
+     * @param name        the name of the command, used for command line invocation
+     * @param description a description of the command's purpose
+     */
+    protected EnvironmentCommand(Application<T> application, String name, String description) {
+        super(name, description);
+        this.application = application;
+    }
+
+    @Override
+    protected final void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
+        final Environment environment = new Environment(bootstrap.getApplication().getName(),
+                                                        bootstrap.getObjectMapper(),
+                                                        bootstrap.getValidatorFactory().getValidator(),
+                                                        bootstrap.getMetricRegistry(),
+                                                        bootstrap.getClassLoader());
+        configuration.getMetricsFactory().configure(environment.lifecycle(),
+                                                    bootstrap.getMetricRegistry());
+        bootstrap.run(configuration, environment);
+        application.run(configuration, environment);
+        run(environment, namespace, configuration);
+    }
+
+    /**
+     * Runs the command with the given {@link Environment} and {@link Configuration}.
+     *
+     * @param environment   the configured environment
+     * @param namespace     the parsed command line namespace
+     * @param configuration the configuration object
+     * @throws Exception if something goes wrong
+     */
+    protected abstract void run(Environment environment, Namespace namespace, T configuration) throws Exception;
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/cli/ServerCommand.java b/dropwizard-core/src/main/java/io/dropwizard/cli/ServerCommand.java
new file mode 100644
index 0000000..1a9631d
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/cli/ServerCommand.java
@@ -0,0 +1,58 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.setup.Environment;
+import net.sourceforge.argparse4j.inf.Namespace;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Runs a application as an HTTP server.
+ *
+ * @param <T> the {@link Configuration} subclass which is loaded from the configuration file
+ */
+public class ServerCommand<T extends Configuration> extends EnvironmentCommand<T> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServerCommand.class);
+
+    private final Class<T> configurationClass;
+
+    public ServerCommand(Application<T> application) {
+        super(application, "server", "Runs the Dropwizard application as an HTTP server");
+        this.configurationClass = application.getConfigurationClass();
+    }
+
+    /*
+     * Since we don't subclass ServerCommand, we need a concrete reference to the configuration
+     * class.
+     */
+    @Override
+    protected Class<T> getConfigurationClass() {
+        return configurationClass;
+    }
+
+    @Override
+    protected void run(Environment environment, Namespace namespace, T configuration) throws Exception {
+        final Server server = configuration.getServerFactory().build(environment);
+        try {
+            server.addLifeCycleListener(new LifeCycleListener());
+            cleanupAsynchronously();
+            server.start();
+        } catch (Exception e) {
+            LOGGER.error("Unable to start server, shutting down", e);
+            server.stop();
+            cleanup();
+            throw e;
+        }
+    }
+
+    private class LifeCycleListener extends AbstractLifeCycle.AbstractLifeCycleListener {
+        @Override
+        public void lifeCycleStopped(LifeCycle event) {
+            cleanup();
+        }
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/errors/EarlyEofExceptionMapper.java b/dropwizard-core/src/main/java/io/dropwizard/errors/EarlyEofExceptionMapper.java
new file mode 100644
index 0000000..02a64dc
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/errors/EarlyEofExceptionMapper.java
@@ -0,0 +1,29 @@
+package io.dropwizard.errors;
+
+import org.eclipse.jetty.io.EofException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+/**
+* This class is intended to catch Early EOF errors that occur when the client disconnects while the server is reading
+* from the input stream.
+*
+* We catch the org.ecplise.jetty.io.EofException rather than the more generic java.io.EOFException to ensure that we're
+* only catching jetty server based errors where the client disconnects, as specified by {@link EofException}.
+*/
+ at Provider
+public class EarlyEofExceptionMapper implements ExceptionMapper<EofException> {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(EarlyEofExceptionMapper.class);
+
+    @Override
+    public Response toResponse(EofException e) {
+        LOGGER.debug("EOF Exception encountered - client disconnected during stream processing.", e);
+
+        return Response.status(Response.Status.BAD_REQUEST).build();
+    }
+}
\ No newline at end of file
diff --git a/dropwizard-core/src/main/java/io/dropwizard/server/AbstractServerFactory.java b/dropwizard-core/src/main/java/io/dropwizard/server/AbstractServerFactory.java
new file mode 100644
index 0000000..b19209a
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/server/AbstractServerFactory.java
@@ -0,0 +1,542 @@
+package io.dropwizard.server;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.codahale.metrics.jetty9.InstrumentedHandler;
+import com.codahale.metrics.jetty9.InstrumentedQueuedThreadPool;
+import com.codahale.metrics.servlets.AdminServlet;
+import com.codahale.metrics.servlets.HealthCheckServlet;
+import com.codahale.metrics.servlets.MetricsServlet;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import com.google.common.io.Resources;
+import com.sun.jersey.spi.container.servlet.ServletContainer;
+import io.dropwizard.jersey.filter.AllowedMethodsFilter;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+import io.dropwizard.jersey.setup.JerseyEnvironment;
+import io.dropwizard.jetty.GzipFilterFactory;
+import io.dropwizard.jetty.MutableServletContextHandler;
+import io.dropwizard.jetty.NonblockingServletHolder;
+import io.dropwizard.jetty.RequestLogFactory;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import io.dropwizard.servlets.ThreadNameFilter;
+import io.dropwizard.util.Duration;
+import io.dropwizard.validation.MinDuration;
+import io.dropwizard.validation.ValidationMethod;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.ErrorHandler;
+import org.eclipse.jetty.server.handler.RequestLogHandler;
+import org.eclipse.jetty.server.handler.StatisticsHandler;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.setuid.RLimit;
+import org.eclipse.jetty.setuid.SetUIDListener;
+import org.eclipse.jetty.util.BlockingArrayQueue;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+import javax.servlet.DispatcherType;
+import javax.servlet.Servlet;
+import javax.validation.Valid;
+import javax.validation.Validator;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.io.IOException;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.regex.Pattern;
+
+// TODO: 5/15/13 <coda> -- add tests for AbstractServerFactory
+
+/**
+ * A base class for {@link ServerFactory} implementations.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code requestLog}</td>
+ *         <td></td>
+ *         <td>The {@link RequestLogFactory request log} configuration.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code gzip}</td>
+ *         <td></td>
+ *         <td>The {@link GzipFilterFactory GZIP} configuration.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxThreads}</td>
+ *         <td>1024</td>
+ *         <td>The maximum number of threads to use for requests.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code minThreads}</td>
+ *         <td>8</td>
+ *         <td>The minimum number of threads to use for requests.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxQueuedRequests}</td>
+ *         <td>1024</td>
+ *         <td>The maximum number of requests to queue before blocking the acceptors.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code idleThreadTimeout}</td>
+ *         <td>1 minute</td>
+ *         <td>The amount of time a worker thread can be idle before being stopped.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code nofileSoftLimit}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The number of open file descriptors before a soft error is issued. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code nofileHardLimit}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The number of open file descriptors before a hard error is issued. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code gid}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The group ID to switch to once the connectors have started. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code uid}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The user ID to switch to once the connectors have started. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code user}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The username to switch to once the connectors have started. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code group}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The group to switch to once the connectors have started. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code umask}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The umask to switch to once the connectors have started. <b>Requires Jetty's
+ *             {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code startsAsRoot}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             Whether or not the Dropwizard application is started as a root user. <b>Requires
+ *             Jetty's {@code libsetuid.so} on {@code java.library.path}.</b>
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code shutdownGracePeriod}</td>
+ *         <td>30 seconds</td>
+ *         <td>
+ *             The maximum time to wait for Jetty, and all Managed instances, to cleanly shutdown
+ *             before forcibly terminating them.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code allowedMethods}</td>
+ *         <td>GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH</td>
+ *         <td>
+ *             The set of allowed HTTP methods. Others will be rejected with a
+ *             405 Method Not Allowed response.
+ *         </td>
+ *     </tr>
+ * </table>
+ *
+ * @see DefaultServerFactory
+ * @see SimpleServerFactory
+ */
+public abstract class AbstractServerFactory implements ServerFactory {
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServerFactory.class);
+    private static final Pattern WINDOWS_NEWLINE = Pattern.compile("\\r\\n?");
+
+    @Valid
+    @NotNull
+    private RequestLogFactory requestLog = new RequestLogFactory();
+
+    @Valid
+    @NotNull
+    private GzipFilterFactory gzip = new GzipFilterFactory();
+
+    @Min(2)
+    private int maxThreads = 1024;
+
+    @Min(1)
+    private int minThreads = 8;
+
+    private int maxQueuedRequests = 1024;
+
+    @MinDuration(1)
+    private Duration idleThreadTimeout = Duration.minutes(1);
+
+    @Min(1)
+    private Integer nofileSoftLimit;
+
+    @Min(1)
+    private Integer nofileHardLimit;
+
+    private Integer gid;
+
+    private Integer uid;
+
+    private String user;
+
+    private String group;
+
+    private String umask;
+
+    private Boolean startsAsRoot;
+
+    private Duration shutdownGracePeriod = Duration.seconds(30);
+
+    @NotNull
+    private Set<String> allowedMethods = AllowedMethodsFilter.DEFAULT_ALLOWED_METHODS;
+
+    @JsonIgnore
+    @ValidationMethod(message = "must have a smaller minThreads than maxThreads")
+    public boolean isThreadPoolSizedCorrectly() {
+        return minThreads <= maxThreads;
+    }
+
+    @JsonProperty("requestLog")
+    public RequestLogFactory getRequestLogFactory() {
+        return requestLog;
+    }
+
+    @JsonProperty("requestLog")
+    public void setRequestLogFactory(RequestLogFactory requestLog) {
+        this.requestLog = requestLog;
+    }
+
+    @JsonProperty("gzip")
+    public GzipFilterFactory getGzipFilterFactory() {
+        return gzip;
+    }
+
+    @JsonProperty("gzip")
+    public void setGzipFilterFactory(GzipFilterFactory gzip) {
+        this.gzip = gzip;
+    }
+
+    @JsonProperty
+    public int getMaxThreads() {
+        return maxThreads;
+    }
+
+    @JsonProperty
+    public void setMaxThreads(int count) {
+        this.maxThreads = count;
+    }
+
+    @JsonProperty
+    public int getMinThreads() {
+        return minThreads;
+    }
+
+    @JsonProperty
+    public void setMinThreads(int count) {
+        this.minThreads = count;
+    }
+
+    @JsonProperty
+    public int getMaxQueuedRequests() {
+        return maxQueuedRequests;
+    }
+
+    @JsonProperty
+    public void setMaxQueuedRequests(int maxQueuedRequests) {
+        this.maxQueuedRequests = maxQueuedRequests;
+    }
+
+    @JsonProperty
+    public Duration getIdleThreadTimeout() {
+        return idleThreadTimeout;
+    }
+
+    @JsonProperty
+    public void setIdleThreadTimeout(Duration idleThreadTimeout) {
+        this.idleThreadTimeout = idleThreadTimeout;
+    }
+
+    @JsonProperty
+    public Integer getNofileSoftLimit() {
+        return nofileSoftLimit;
+    }
+
+    @JsonProperty
+    public void setNofileSoftLimit(Integer nofileSoftLimit) {
+        this.nofileSoftLimit = nofileSoftLimit;
+    }
+
+    @JsonProperty
+    public Integer getNofileHardLimit() {
+        return nofileHardLimit;
+    }
+
+    @JsonProperty
+    public void setNofileHardLimit(Integer nofileHardLimit) {
+        this.nofileHardLimit = nofileHardLimit;
+    }
+
+    @JsonProperty
+    public Integer getGid() {
+        return gid;
+    }
+
+    @JsonProperty
+    public void setGid(Integer gid) {
+        this.gid = gid;
+    }
+
+    @JsonProperty
+    public Integer getUid() {
+        return uid;
+    }
+
+    @JsonProperty
+    public void setUid(Integer uid) {
+        this.uid = uid;
+    }
+
+    @JsonProperty
+    public String getUser() {
+        return user;
+    }
+
+    @JsonProperty
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    @JsonProperty
+    public String getGroup() {
+        return group;
+    }
+
+    @JsonProperty
+    public void setGroup(String group) {
+        this.group = group;
+    }
+
+    @JsonProperty
+    public String getUmask() {
+        return umask;
+    }
+
+    @JsonProperty
+    public void setUmask(String umask) {
+        this.umask = umask;
+    }
+
+    @JsonProperty
+    public Boolean getStartsAsRoot() {
+        return startsAsRoot;
+    }
+
+    @JsonProperty
+    public void setStartsAsRoot(Boolean startsAsRoot) {
+        this.startsAsRoot = startsAsRoot;
+    }
+
+    @JsonProperty
+    public Duration getShutdownGracePeriod() {
+        return shutdownGracePeriod;
+    }
+
+    @JsonProperty
+    public void setShutdownGracePeriod(Duration shutdownGracePeriod) {
+        this.shutdownGracePeriod = shutdownGracePeriod;
+    }
+
+    @JsonProperty
+    public Set<String> getAllowedMethods() {
+        return allowedMethods;
+    }
+
+    @JsonProperty
+    public void setAllowedMethods(Set<String> allowedMethods) {
+        this.allowedMethods = allowedMethods;
+    }
+
+    protected Handler createAdminServlet(Server server,
+                                         MutableServletContextHandler handler,
+                                         MetricRegistry metrics,
+                                         HealthCheckRegistry healthChecks) {
+        configureSessionsAndSecurity(handler, server);
+        handler.setServer(server);  
+        handler.getServletContext().setAttribute(MetricsServlet.METRICS_REGISTRY, metrics);
+        handler.getServletContext().setAttribute(HealthCheckServlet.HEALTH_CHECK_REGISTRY, healthChecks);
+        handler.addServlet(new NonblockingServletHolder(new AdminServlet()), "/*");
+        handler.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
+                .setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, Joiner.on(',').join(allowedMethods));
+        return handler;
+    }
+
+    private void configureSessionsAndSecurity(MutableServletContextHandler handler, Server server) {
+        if (handler.isSecurityEnabled()) {
+            handler.getSecurityHandler().setServer(server);
+        }
+        if (handler.isSessionsEnabled()) {
+            handler.getSessionHandler().setServer(server);
+        }
+    }
+
+    protected Handler createAppServlet(Server server,
+                                       JerseyEnvironment jersey,
+                                       ObjectMapper objectMapper,
+                                       Validator validator,
+                                       MutableServletContextHandler handler,
+                                       @Nullable ServletContainer jerseyContainer,
+                                       MetricRegistry metricRegistry) {
+        configureSessionsAndSecurity(handler, server);
+        handler.addFilter(AllowedMethodsFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST))
+                .setInitParameter(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, Joiner.on(',').join(allowedMethods));
+        handler.addFilter(ThreadNameFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST));
+        if (gzip.isEnabled()) {
+            final FilterHolder holder = new FilterHolder(gzip.build());
+            handler.addFilter(holder, "/*", EnumSet.allOf(DispatcherType.class));
+        }
+        if (jerseyContainer != null) {
+            jersey.register(new JacksonMessageBodyProvider(objectMapper, validator));
+            handler.addServlet(new NonblockingServletHolder(jerseyContainer), jersey.getUrlPattern());
+        }
+        final InstrumentedHandler instrumented = new InstrumentedHandler(metricRegistry);
+        instrumented.setServer(server);
+        instrumented.setHandler(handler);
+        return instrumented;
+    }
+
+    protected ThreadPool createThreadPool(MetricRegistry metricRegistry) {
+        final BlockingQueue<Runnable> queue = new BlockingArrayQueue<>(minThreads, maxThreads, maxQueuedRequests);
+        final InstrumentedQueuedThreadPool threadPool =
+                new InstrumentedQueuedThreadPool(metricRegistry, maxThreads, minThreads,
+                                                 (int) idleThreadTimeout.toMilliseconds(), queue);
+        threadPool.setName("dw");
+        return threadPool;
+    }
+
+    protected Server buildServer(LifecycleEnvironment lifecycle,
+                                 ThreadPool threadPool) {
+        final Server server = new Server(threadPool);
+        server.addLifeCycleListener(buildSetUIDListener());
+        lifecycle.attach(server);
+        final ErrorHandler errorHandler = new ErrorHandler();
+        errorHandler.setServer(server);
+        errorHandler.setShowStacks(false);
+        server.addBean(errorHandler);
+        server.setStopAtShutdown(true);
+        server.setStopTimeout(shutdownGracePeriod.toMilliseconds());
+        return server;
+    }
+
+    protected SetUIDListener buildSetUIDListener() {
+        final SetUIDListener listener = new SetUIDListener();
+
+        if (startsAsRoot != null) {
+            listener.setStartServerAsPrivileged(startsAsRoot);
+        }
+
+        if (gid != null) {
+            listener.setGid(gid);
+        }
+
+        if (uid != null) {
+            listener.setUid(uid);
+        }
+
+        if (user != null) {
+            listener.setUsername(user);
+        }
+
+        if (group != null) {
+            listener.setGroupname(group);
+        }
+
+        if (nofileHardLimit != null || nofileSoftLimit != null) {
+            final RLimit rlimit = new RLimit();
+            if (nofileHardLimit != null) {
+                rlimit.setHard(nofileHardLimit);
+            }
+
+            if (nofileSoftLimit != null) {
+                rlimit.setSoft(nofileSoftLimit);
+            }
+
+            listener.setRLimitNoFiles(rlimit);
+        }
+
+        if (umask != null) {
+            listener.setUmaskOctal(umask);
+        }
+
+        return listener;
+    }
+
+    protected Handler addRequestLog(Server server, Handler handler, String name) {
+        if (requestLog.isEnabled()) {
+            final RequestLogHandler requestLogHandler = new RequestLogHandler();
+            requestLogHandler.setRequestLog(requestLog.build(name));
+            // server should own the request log's lifecycle since it's already started,
+            // the handler might not become managed in case of an error which would leave
+            // the request log stranded
+            server.addBean(requestLogHandler.getRequestLog(), true);
+            requestLogHandler.setHandler(handler);
+            return requestLogHandler;
+        }
+        return handler;
+    }
+
+    protected Handler addStatsHandler(Handler handler) {
+        // Graceful shutdown is implemented via the statistics handler,
+        // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=420142
+        StatisticsHandler statisticsHandler = new StatisticsHandler();
+        statisticsHandler.setHandler(handler);
+        return statisticsHandler;
+    }
+
+    protected void printBanner(String name) {
+        try {
+            final String banner = WINDOWS_NEWLINE.matcher(Resources.toString(Resources.getResource("banner.txt"),
+                                                                             Charsets.UTF_8))
+                                                 .replaceAll("\n")
+                                                 .replace("\n", String.format("%n"));
+            LOGGER.info(String.format("Starting {}%n{}"), name, banner);
+        } catch (IllegalArgumentException | IOException ignored) {
+            // don't display the banner if there isn't one
+            LOGGER.info("Starting {}", name);
+        }
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/server/DefaultServerFactory.java b/dropwizard-core/src/main/java/io/dropwizard/server/DefaultServerFactory.java
new file mode 100644
index 0000000..76f9663
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/server/DefaultServerFactory.java
@@ -0,0 +1,196 @@
+package io.dropwizard.server;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import io.dropwizard.jetty.ConnectorFactory;
+import io.dropwizard.jetty.HttpConnectorFactory;
+import io.dropwizard.jetty.RoutingHandler;
+import io.dropwizard.setup.Environment;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.thread.QueuedThreadPool;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+import java.util.Map;
+
+// TODO: 5/15/13 <coda> -- add tests for DefaultServerFactory
+
+/**
+ * The default implementation of {@link ServerFactory}, which allows for multiple sets of
+ * application and admin connectors, all running on separate ports. Admin connectors use a separate
+ * thread pool to keep the control and data planes separate(ish).
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code applicationConnectors}</td>
+ *         <td>An {@link HttpConnectorFactory HTTP connector} listening on port 8080.</td>
+ *         <td>A set of {@link ConnectorFactory connectors} which will handle application requests.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code adminConnectors}</td>
+ *         <td>An {@link HttpConnectorFactory HTTP connector} listening on port 8081.</td>
+ *         <td>A set of {@link ConnectorFactory connectors} which will handle admin requests.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code adminMaxThreads}</td>
+ *         <td>64</td>
+ *         <td>The maximum number of threads to use for admin requests.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code adminMinThreads}</td>
+ *         <td>1</td>
+ *         <td>The minimum number of threads to use for admin requests.</td>
+ *     </tr>
+ * </table>
+ * <p/>
+ * For more configuration parameters, see {@link AbstractServerFactory}.
+ *
+ * @see ServerFactory
+ * @see AbstractServerFactory
+ */
+ at JsonTypeName("default")
+public class DefaultServerFactory extends AbstractServerFactory {
+    @Valid
+    @NotNull
+    private List<ConnectorFactory> applicationConnectors =
+            Lists.newArrayList(HttpConnectorFactory.application());
+
+    @Valid
+    @NotNull
+    private List<ConnectorFactory> adminConnectors =
+            Lists.newArrayList(HttpConnectorFactory.admin());
+
+    @Min(2)
+    private int adminMaxThreads = 64;
+
+    @Min(1)
+    private int adminMinThreads = 1;
+
+    @JsonProperty
+    public List<ConnectorFactory> getApplicationConnectors() {
+        return applicationConnectors;
+    }
+
+    @JsonProperty
+    public void setApplicationConnectors(List<ConnectorFactory> connectors) {
+        this.applicationConnectors = connectors;
+    }
+
+    @JsonProperty
+    public List<ConnectorFactory> getAdminConnectors() {
+        return adminConnectors;
+    }
+
+    @JsonProperty
+    public void setAdminConnectors(List<ConnectorFactory> connectors) {
+        this.adminConnectors = connectors;
+    }
+
+    @JsonProperty
+    public int getAdminMaxThreads() {
+        return adminMaxThreads;
+    }
+
+    @JsonProperty
+    public void setAdminMaxThreads(int adminMaxThreads) {
+        this.adminMaxThreads = adminMaxThreads;
+    }
+
+    @JsonProperty
+    public int getAdminMinThreads() {
+        return adminMinThreads;
+    }
+
+    @JsonProperty
+    public void setAdminMinThreads(int adminMinThreads) {
+        this.adminMinThreads = adminMinThreads;
+    }
+
+    @Override
+    public Server build(Environment environment) {
+        printBanner(environment.getName());
+        final ThreadPool threadPool = createThreadPool(environment.metrics());
+        final Server server = buildServer(environment.lifecycle(), threadPool);
+        final Handler applicationHandler = createAppServlet(server,
+                                                            environment.jersey(),
+                                                            environment.getObjectMapper(),
+                                                            environment.getValidator(),
+                                                            environment.getApplicationContext(),
+                                                            environment.getJerseyServletContainer(),
+                                                            environment.metrics());
+        final Handler adminHandler = createAdminServlet(server,
+                                                        environment.getAdminContext(),
+                                                        environment.metrics(),
+                                                        environment.healthChecks());
+        final RoutingHandler routingHandler = buildRoutingHandler(environment.metrics(),
+                                                                  server,
+                                                                  applicationHandler,
+                                                                  adminHandler);
+        server.setHandler(addStatsHandler(addRequestLog(server, routingHandler, environment.getName())));
+        return server;
+    }
+
+    private RoutingHandler buildRoutingHandler(MetricRegistry metricRegistry,
+                                               Server server,
+                                               Handler applicationHandler,
+                                               Handler adminHandler) {
+        final List<Connector> appConnectors = buildAppConnectors(metricRegistry, server);
+
+        final List<Connector> adConnectors = buildAdminConnectors(metricRegistry, server);
+
+        final Map<Connector, Handler> handlers = Maps.newLinkedHashMap();
+
+        for (Connector connector : appConnectors) {
+            server.addConnector(connector);
+            handlers.put(connector, applicationHandler);
+        }
+
+        for (Connector connector : adConnectors) {
+            server.addConnector(connector);
+            handlers.put(connector, adminHandler);
+        }
+
+        return new RoutingHandler(handlers);
+    }
+
+    private List<Connector> buildAdminConnectors(MetricRegistry metricRegistry, Server server) {
+        // threadpool is shared between all the connectors, so it should be managed by the server instead of the
+        // individual connectors
+        final QueuedThreadPool threadPool = new QueuedThreadPool(adminMaxThreads, adminMinThreads);
+        threadPool.setName("dw-admin");
+        server.addBean(threadPool);
+
+        final List<Connector> connectors = Lists.newArrayList();
+        for (ConnectorFactory factory : adminConnectors) {
+            Connector connector = factory.build(server, metricRegistry, "admin", threadPool);
+            if (connector instanceof ContainerLifeCycle) {
+                ((ContainerLifeCycle) connector).unmanage(threadPool);
+            }
+            connectors.add(connector);
+        }
+        return connectors;
+    }
+
+    private List<Connector> buildAppConnectors(MetricRegistry metricRegistry, Server server) {
+        final List<Connector> connectors = Lists.newArrayList();
+        for (ConnectorFactory factory : applicationConnectors) {
+            connectors.add(factory.build(server, metricRegistry, "application", null));
+        }
+        return connectors;
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/server/ServerFactory.java b/dropwizard-core/src/main/java/io/dropwizard/server/ServerFactory.java
new file mode 100644
index 0000000..c5ba098
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/server/ServerFactory.java
@@ -0,0 +1,22 @@
+package io.dropwizard.server;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.dropwizard.jackson.Discoverable;
+import io.dropwizard.setup.Environment;
+import org.eclipse.jetty.server.Server;
+
+/**
+ * A factory for building {@link Server} instances for Dropwizard applications.
+ *
+ * @see DefaultServerFactory
+ */
+ at JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = DefaultServerFactory.class)
+public interface ServerFactory extends Discoverable {
+    /**
+     * Build a server for the given Dropwizard application.
+     *
+     * @param environment the application's environment
+     * @return a {@link Server} running the Dropwizard application
+     */
+    Server build(Environment environment);
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/server/SimpleServerFactory.java b/dropwizard-core/src/main/java/io/dropwizard/server/SimpleServerFactory.java
new file mode 100644
index 0000000..1f0f549
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/server/SimpleServerFactory.java
@@ -0,0 +1,133 @@
+package io.dropwizard.server;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.common.collect.ImmutableMap;
+import io.dropwizard.jetty.ConnectorFactory;
+import io.dropwizard.jetty.ContextRoutingHandler;
+import io.dropwizard.jetty.HttpConnectorFactory;
+import io.dropwizard.setup.Environment;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+// TODO: 5/15/13 <coda> -- add tests for SimpleServerFactory
+
+/**
+ * A single-connector implementation of {@link ServerFactory}, suitable for PaaS deployments
+ * (e.g., Heroku) where applications are limited to a single, runtime-defined port. A startup script
+ * can override the port via {@code -Ddw.server.connector.port=$PORT}.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code connector}</td>
+ *         <td>An {@link HttpConnectorFactory HTTP connector} listening on port {@code 8080}.</td>
+ *         <td>The {@link ConnectorFactory connector} which will handle both application and admin requests.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code applicationContextPath}</td>
+ *         <td>{@code /application}</td>
+ *         <td>The context path of the application servlets, including Jersey.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code adminContextPath}</td>
+ *         <td>{@code /admin}</td>
+ *         <td>The context path of the admin servlets, including metrics and tasks.</td>
+ *     </tr>
+ * </table>
+ * <p/>
+ * For more configuration parameters, see {@link AbstractServerFactory}.
+ *
+ * @see ServerFactory
+ * @see AbstractServerFactory
+ */
+ at JsonTypeName("simple")
+public class SimpleServerFactory extends AbstractServerFactory {
+    @Valid
+    @NotNull
+    private ConnectorFactory connector = HttpConnectorFactory.application();
+
+    @NotEmpty
+    private String applicationContextPath = "/application";
+
+    @NotEmpty
+    private String adminContextPath = "/admin";
+
+    @JsonProperty
+    public ConnectorFactory getConnector() {
+        return connector;
+    }
+
+    @JsonProperty
+    public void setConnector(ConnectorFactory factory) {
+        this.connector = factory;
+    }
+
+    @JsonProperty
+    public String getApplicationContextPath() {
+        return applicationContextPath;
+    }
+
+    @JsonProperty
+    public void setApplicationContextPath(String contextPath) {
+        this.applicationContextPath = contextPath;
+    }
+
+    @JsonProperty
+    public String getAdminContextPath() {
+        return adminContextPath;
+    }
+
+    @JsonProperty
+    public void setAdminContextPath(String contextPath) {
+        this.adminContextPath = contextPath;
+    }
+
+    @Override
+    public Server build(Environment environment) {
+        printBanner(environment.getName());
+        final ThreadPool threadPool = createThreadPool(environment.metrics());
+        final Server server = buildServer(environment.lifecycle(), threadPool);
+
+        environment.getApplicationContext().setContextPath(applicationContextPath);
+        final Handler applicationHandler = createAppServlet(server,
+                                                            environment.jersey(),
+                                                            environment.getObjectMapper(),
+                                                            environment.getValidator(),
+                                                            environment.getApplicationContext(),
+                                                            environment.getJerseyServletContainer(),
+                                                            environment.metrics());
+
+        environment.getAdminContext().setContextPath(adminContextPath);
+        final Handler adminHandler = createAdminServlet(server,
+                                                        environment.getAdminContext(),
+                                                        environment.metrics(),
+                                                        environment.healthChecks());
+
+        final Connector conn = connector.build(server,
+                                               environment.metrics(),
+                                               environment.getName(),
+                                               null);
+
+        server.addConnector(conn);
+
+        final ContextRoutingHandler routingHandler = new ContextRoutingHandler(ImmutableMap.of(
+                applicationContextPath, applicationHandler,
+                adminContextPath, adminHandler
+        ));
+        server.setHandler(addStatsHandler(addRequestLog(server, routingHandler, environment.getName())));
+
+        return server;
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/setup/AdminEnvironment.java b/dropwizard-core/src/main/java/io/dropwizard/setup/AdminEnvironment.java
new file mode 100644
index 0000000..24e4e0e
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/setup/AdminEnvironment.java
@@ -0,0 +1,88 @@
+package io.dropwizard.setup;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.codahale.metrics.health.jvm.ThreadDeadlockHealthCheck;
+import io.dropwizard.jetty.MutableServletContextHandler;
+import io.dropwizard.jetty.setup.ServletEnvironment;
+import io.dropwizard.servlets.tasks.GarbageCollectionTask;
+import io.dropwizard.servlets.tasks.Task;
+import io.dropwizard.servlets.tasks.TaskServlet;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * The administrative environment of a Dropwizard application.
+ */
+public class AdminEnvironment extends ServletEnvironment {
+    private static final Logger LOGGER = LoggerFactory.getLogger(AdminEnvironment.class);
+
+    private final HealthCheckRegistry healthChecks;
+    private final TaskServlet tasks;
+
+    /**
+     * Creates a new {@link AdminEnvironment}.
+     *
+     * @param handler      a servlet context handler
+     * @param healthChecks a health check registry
+     */
+    public AdminEnvironment(MutableServletContextHandler handler,
+                            HealthCheckRegistry healthChecks, MetricRegistry metricRegistry) {
+        super(handler);
+        this.healthChecks = healthChecks;
+        this.healthChecks.register("deadlocks", new ThreadDeadlockHealthCheck());
+        this.tasks = new TaskServlet(metricRegistry);
+        tasks.add(new GarbageCollectionTask());
+        addServlet("tasks", tasks).addMapping("/tasks/*");
+        handler.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener() {
+            @Override
+            public void lifeCycleStarting(LifeCycle event) {
+                logTasks();
+                logHealthChecks();
+            }
+        });
+    }
+
+    /**
+     * Adds the given task to the set of tasks exposed via the admin interface.
+     *
+     * @param task a task
+     */
+    public void addTask(Task task) {
+        tasks.add(checkNotNull(task));
+    }
+
+    private void logTasks() {
+        final StringBuilder stringBuilder = new StringBuilder(1024).append(String.format("%n%n"));
+
+        for (Task task : tasks.getTasks()) {
+            stringBuilder.append(String.format("    %-7s /tasks/%s (%s)%n",
+                                               "POST",
+                                               task.getName(),
+                                               task.getClass().getCanonicalName()));
+        }
+
+        LOGGER.info("tasks = {}", stringBuilder.toString());
+    }
+
+    private void logHealthChecks() {
+        if (healthChecks.getNames().size() <= 1) {
+            LOGGER.warn(String.format(
+                    "%n" +
+                            "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!%n" +
+                            "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!%n" +
+                            "!    THIS APPLICATION HAS NO HEALTHCHECKS. THIS MEANS YOU WILL NEVER KNOW      !%n" +
+                            "!     IF IT DIES IN PRODUCTION, WHICH MEANS YOU WILL NEVER KNOW IF YOU'RE      !%n" +
+                            "!    LETTING YOUR USERS DOWN. YOU SHOULD ADD A HEALTHCHECK FOR EACH OF YOUR    !%n" +
+                            "!         APPLICATION'S DEPENDENCIES WHICH FULLY (BUT LIGHTLY) TESTS IT.       !%n" +
+                            "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!%n" +
+                            "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
+            ));
+        }
+        LOGGER.debug("health checks = {}", healthChecks.getNames());
+    }
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/setup/Bootstrap.java b/dropwizard-core/src/main/java/io/dropwizard/setup/Bootstrap.java
new file mode 100644
index 0000000..0151fa5
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/setup/Bootstrap.java
@@ -0,0 +1,208 @@
+package io.dropwizard.setup;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.codahale.metrics.JmxReporter;
+import io.dropwizard.Application;
+import io.dropwizard.Bundle;
+import io.dropwizard.Configuration;
+import io.dropwizard.ConfiguredBundle;
+import io.dropwizard.cli.Command;
+import io.dropwizard.cli.ConfiguredCommand;
+import io.dropwizard.configuration.ConfigurationFactoryFactory;
+import io.dropwizard.configuration.ConfigurationSourceProvider;
+import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
+import io.dropwizard.configuration.FileConfigurationSourceProvider;
+import io.dropwizard.jackson.Jackson;
+
+import java.lang.management.ManagementFactory;
+import java.util.List;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jvm.BufferPoolMetricSet;
+import com.codahale.metrics.jvm.GarbageCollectorMetricSet;
+import com.codahale.metrics.jvm.MemoryUsageGaugeSet;
+import com.codahale.metrics.jvm.ThreadStatesGaugeSet;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import io.dropwizard.validation.valuehandling.OptionalValidatedValueUnwrapper;
+import org.hibernate.validator.HibernateValidator;
+
+import javax.validation.Validation;
+import javax.validation.ValidatorFactory;
+
+/**
+ * The pre-start application environment, containing everything required to bootstrap a Dropwizard
+ * command.
+ *
+ * @param <T> the configuration type
+ */
+public class Bootstrap<T extends Configuration> {
+    private final Application<T> application;
+    private final ObjectMapper objectMapper;
+    private final List<Bundle> bundles;
+    private final List<ConfiguredBundle<? super T>> configuredBundles;
+    private final List<Command> commands;
+    private final MetricRegistry metricRegistry;
+    private final ValidatorFactory validatorFactory;
+
+    private ConfigurationSourceProvider configurationSourceProvider;
+    private ClassLoader classLoader;
+    private ConfigurationFactoryFactory<T> configurationFactoryFactory;
+
+    /**
+     * Creates a new {@link Bootstrap} for the given application.
+     *
+     * @param application a Dropwizard {@link Application}
+     */
+    public Bootstrap(Application<T> application) {
+        this.application = application;
+        this.objectMapper = Jackson.newObjectMapper();
+        this.bundles = Lists.newArrayList();
+        this.configuredBundles = Lists.newArrayList();
+        this.commands = Lists.newArrayList();
+        this.metricRegistry = new MetricRegistry();
+        this.validatorFactory = Validation
+                .byProvider(HibernateValidator.class)
+                .configure()
+                .addValidatedValueHandler(new OptionalValidatedValueUnwrapper())
+                .buildValidatorFactory();
+        getMetricRegistry().register("jvm.buffers", new BufferPoolMetricSet(ManagementFactory
+                                                                               .getPlatformMBeanServer()));
+        getMetricRegistry().register("jvm.gc", new GarbageCollectorMetricSet());
+        getMetricRegistry().register("jvm.memory", new MemoryUsageGaugeSet());
+        getMetricRegistry().register("jvm.threads", new ThreadStatesGaugeSet());
+
+        JmxReporter.forRegistry(getMetricRegistry()).build().start();
+
+        this.configurationSourceProvider = new FileConfigurationSourceProvider();
+        this.classLoader = Thread.currentThread().getContextClassLoader();
+        this.configurationFactoryFactory = new DefaultConfigurationFactoryFactory<T>();
+    }
+
+    /**
+     * Returns the bootstrap's {@link Application}.
+     */
+    public Application<T> getApplication() {
+        return application;
+    }
+
+    /**
+     * Returns the bootstrap's {@link ConfigurationSourceProvider}.
+     */
+    public ConfigurationSourceProvider getConfigurationSourceProvider() {
+        return configurationSourceProvider;
+    }
+
+    /**
+     * Sets the bootstrap's {@link ConfigurationSourceProvider}.
+     */
+    public void setConfigurationSourceProvider(ConfigurationSourceProvider provider) {
+        this.configurationSourceProvider = checkNotNull(provider);
+    }
+
+    /**
+     * Returns the bootstrap's class loader.
+     */
+    public ClassLoader getClassLoader() {
+        return classLoader;
+    }
+
+    /**
+     * Sets the bootstrap's class loader.
+     */
+    public void setClassLoader(ClassLoader classLoader) {
+        this.classLoader = classLoader;
+    }
+
+    /**
+     * Adds the given bundle to the bootstrap.
+     *
+     * @param bundle a {@link Bundle}
+     */
+    public void addBundle(Bundle bundle) {
+        bundle.initialize(this);
+        bundles.add(bundle);
+    }
+
+    /**
+     * Adds the given bundle to the bootstrap.
+     *
+     * @param bundle a {@link ConfiguredBundle}
+     */
+    public void addBundle(ConfiguredBundle<? super T> bundle) {
+        bundle.initialize(this);
+        configuredBundles.add(bundle);
+    }
+
+    /**
+     * Adds the given command to the bootstrap.
+     *
+     * @param command a {@link Command}
+     */
+    public void addCommand(Command command) {
+        commands.add(command);
+    }
+
+    /**
+     * Adds the given command to the bootstrap.
+     *
+     * @param command a {@link ConfiguredCommand}
+     */
+    public void addCommand(ConfiguredCommand<T> command) {
+        commands.add(command);
+    }
+
+    /**
+     * Returns the bootstrap's {@link ObjectMapper}.
+     */
+    public ObjectMapper getObjectMapper() {
+        return objectMapper;
+    }
+
+    /**
+     * Runs the bootstrap's bundles with the given configuration and environment.
+     *
+     * @param configuration the parsed configuration
+     * @param environment   the application environment
+     * @throws Exception if a bundle throws an exception
+     */
+    public void run(T configuration, Environment environment) throws Exception {
+        for (Bundle bundle : bundles) {
+            bundle.run(environment);
+        }
+        for (ConfiguredBundle<? super T> bundle : configuredBundles) {
+            bundle.run(configuration, environment);
+        }
+    }
+
+    /**
+     * Returns the application's commands.
+     */
+    public ImmutableList<Command> getCommands() {
+        return ImmutableList.copyOf(commands);
+    }
+
+    /**
+     * Returns the application's metrics.
+     */
+    public MetricRegistry getMetricRegistry() {
+        return metricRegistry;
+    }
+
+    /**
+     * Returns the application's validator factory.
+     */
+    public ValidatorFactory getValidatorFactory() {
+        return validatorFactory;
+    }
+
+    public ConfigurationFactoryFactory<T> getConfigurationFactoryFactory() {
+        return configurationFactoryFactory;
+    }
+
+    public void setConfigurationFactoryFactory(ConfigurationFactoryFactory<T> configurationFactoryFactory) {
+        this.configurationFactoryFactory = configurationFactoryFactory;
+    }   
+}
diff --git a/dropwizard-core/src/main/java/io/dropwizard/setup/Environment.java b/dropwizard-core/src/main/java/io/dropwizard/setup/Environment.java
new file mode 100755
index 0000000..e28f540
--- /dev/null
+++ b/dropwizard-core/src/main/java/io/dropwizard/setup/Environment.java
@@ -0,0 +1,165 @@
+package io.dropwizard.setup;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.jersey.spi.container.servlet.ServletContainer;
+import io.dropwizard.errors.EarlyEofExceptionMapper;
+import io.dropwizard.jersey.DropwizardResourceConfig;
+import io.dropwizard.jersey.setup.JerseyContainerHolder;
+import io.dropwizard.jersey.setup.JerseyEnvironment;
+import io.dropwizard.jetty.MutableServletContextHandler;
+import io.dropwizard.jetty.setup.ServletEnvironment;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+
+import javax.validation.Validator;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+// TODO: 5/15/13 <coda> -- add tests for Environment
+
+/**
+ * A Dropwizard application's environment.
+ */
+public class Environment {
+    private final String name;
+    private final MetricRegistry metricRegistry;
+    private final HealthCheckRegistry healthCheckRegistry;
+
+    private final ObjectMapper objectMapper;
+    private Validator validator;
+
+    private final JerseyContainerHolder jerseyServletContainer;
+    private final JerseyEnvironment jerseyEnvironment;
+
+    private final MutableServletContextHandler servletContext;
+    private final ServletEnvironment servletEnvironment;
+
+    private final LifecycleEnvironment lifecycleEnvironment;
+
+    private final MutableServletContextHandler adminContext;
+    private final AdminEnvironment adminEnvironment;
+
+    /**
+     * Creates a new environment.
+     *
+     * @param name                the name of the application
+     * @param objectMapper the {@link ObjectMapper} for the application
+     */
+    public Environment(String name,
+                       ObjectMapper objectMapper,
+                       Validator validator,
+                       MetricRegistry metricRegistry,
+                       ClassLoader classLoader) {
+        this.name = name;
+        this.objectMapper = objectMapper;
+        this.metricRegistry = metricRegistry;
+        this.healthCheckRegistry = new HealthCheckRegistry();
+        this.validator = validator;
+
+        this.servletContext = new MutableServletContextHandler();
+        servletContext.setClassLoader(classLoader);
+        this.servletEnvironment = new ServletEnvironment(servletContext);
+
+        this.adminContext = new MutableServletContextHandler();
+        adminContext.setClassLoader(classLoader);
+        this.adminEnvironment = new AdminEnvironment(adminContext, healthCheckRegistry, metricRegistry);
+
+        this.lifecycleEnvironment = new LifecycleEnvironment();
+
+        final DropwizardResourceConfig jerseyConfig = new DropwizardResourceConfig(metricRegistry);
+
+        jerseyConfig.getSingletons().add(new EarlyEofExceptionMapper());
+
+        this.jerseyServletContainer = new JerseyContainerHolder(new ServletContainer(jerseyConfig));
+        this.jerseyEnvironment = new JerseyEnvironment(jerseyServletContainer, jerseyConfig);
+    }
+
+    /**
+     * Returns the application's {@link JerseyEnvironment}.
+     */
+    public JerseyEnvironment jersey() {
+        return jerseyEnvironment;
+    }
+
+    /**
+     * Returns the application's {@link AdminEnvironment}.
+     */
+    public AdminEnvironment admin() {
+        return adminEnvironment;
+    }
+
+    /**
+     * Returns the application's {@link LifecycleEnvironment}.
+     */
+    public LifecycleEnvironment lifecycle() {
+        return lifecycleEnvironment;
+    }
+
+    /**
+     * Returns the application's {@link ServletEnvironment}.
+     */
+    public ServletEnvironment servlets() {
+        return servletEnvironment;
+    }
+
+    /**
+     * Returns the application's {@link ObjectMapper}.
+     */
+    public ObjectMapper getObjectMapper() {
+        return objectMapper;
+    }
+
+    /**
+     * Returns the application's name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Returns the application's {@link Validator}.
+     */
+    public Validator getValidator() {
+        return validator;
+    }
+
+    /**
+     * Sets the application's {@link Validator}.
+     */
+    public void setValidator(Validator validator) {
+        this.validator = checkNotNull(validator);
+    }
+
+    /**
+     * Returns the application's {@link MetricRegistry}.
+     */
+    public MetricRegistry metrics() {
+        return metricRegistry;
+    }
+
+    /**
+     * Returns the application's {@link HealthCheckRegistry}.
+     */
+    public HealthCheckRegistry healthChecks() {
+        return healthCheckRegistry;
+    }
+
+    /*
+    * Internal Accessors
+    */
+
+    // TODO: 5/4/13 <coda> -- figure out how to make these accessors not a public API
+
+    public MutableServletContextHandler getApplicationContext() {
+        return servletContext;
+    }
+
+    public ServletContainer getJerseyServletContainer() {
+        return jerseyServletContainer.getContainer();
+    }
+
+    public MutableServletContextHandler getAdminContext() {
+        return adminContext;
+    }
+}
diff --git a/dropwizard-core/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/dropwizard-core/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
new file mode 100644
index 0000000..ae5cedd
--- /dev/null
+++ b/dropwizard-core/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
@@ -0,0 +1 @@
+io.dropwizard.server.ServerFactory
diff --git a/dropwizard-core/src/main/resources/META-INF/services/io.dropwizard.server.ServerFactory b/dropwizard-core/src/main/resources/META-INF/services/io.dropwizard.server.ServerFactory
new file mode 100644
index 0000000..a779ed4
--- /dev/null
+++ b/dropwizard-core/src/main/resources/META-INF/services/io.dropwizard.server.ServerFactory
@@ -0,0 +1,2 @@
+io.dropwizard.server.DefaultServerFactory
+io.dropwizard.server.SimpleServerFactory
diff --git a/dropwizard-core/src/test/java/io/dropwizard/ApplicationTest.java b/dropwizard-core/src/test/java/io/dropwizard/ApplicationTest.java
new file mode 100644
index 0000000..0e7328a
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/ApplicationTest.java
@@ -0,0 +1,58 @@
+package io.dropwizard;
+
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class ApplicationTest {
+    private static class FakeConfiguration extends Configuration {}
+
+    private static class FakeApplication extends Application<FakeConfiguration> {
+        @Override
+        public void initialize(Bootstrap<FakeConfiguration> bootstrap) {}
+
+        @Override
+        public void run(FakeConfiguration configuration, Environment environment) {}
+    }
+
+    private static class PoserApplication extends FakeApplication {}
+
+    private static class WrapperApplication<C extends FakeConfiguration> extends Application<C> {
+        private final Application<C> application;
+
+        private WrapperApplication(Application<C> application) {
+            this.application = application;
+        }
+
+        @Override
+        public void initialize(Bootstrap<C> bootstrap) {
+            this.application.initialize(bootstrap);
+        }
+
+        @Override
+        public void run(C configuration, Environment environment) throws Exception {
+            this.application.run(configuration, environment);
+        }
+    }
+
+    @Test
+    public void hasAReferenceToItsTypeParameter() throws Exception {
+        assertThat(new FakeApplication().getConfigurationClass())
+                .isSameAs(FakeConfiguration.class);
+    }
+
+    @Test
+    public void canDetermineConfiguration() throws Exception {
+        assertThat(new PoserApplication().getConfigurationClass())
+                .isSameAs(FakeConfiguration.class);
+    }
+
+    @Test
+    public void canDetermineWrappedConfiguration() throws Exception {
+        final PoserApplication application = new PoserApplication();
+        assertThat(new WrapperApplication<>(application).getConfigurationClass())
+                .isSameAs(FakeConfiguration.class);
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/ConfigurationTest.java b/dropwizard-core/src/test/java/io/dropwizard/ConfigurationTest.java
new file mode 100644
index 0000000..07e39af
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/ConfigurationTest.java
@@ -0,0 +1,45 @@
+package io.dropwizard;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.sun.jersey.spi.service.ServiceFinder;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jetty.ConnectorFactory;
+import io.dropwizard.logging.AppenderFactory;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class ConfigurationTest {
+    private final Configuration configuration = new Configuration();
+
+    @Test
+    public void hasAnHttpConfiguration() throws Exception {
+        assertThat(configuration.getServerFactory())
+                .isNotNull();
+    }
+
+    @Test
+    public void hasALoggingConfiguration() throws Exception {
+        assertThat(configuration.getLoggingFactory())
+                .isNotNull();
+    }
+
+    @Test
+    public void ensureConfigSerializable() throws Exception {
+        final ObjectMapper mapper = Jackson.newObjectMapper();
+        mapper.getSubtypeResolver()
+              .registerSubtypes(ServiceFinder.find(AppenderFactory.class).toClassArray());
+        mapper.getSubtypeResolver()
+              .registerSubtypes(ServiceFinder.find(ConnectorFactory.class).toClassArray());
+
+        // Issue-96: some types were not serializable
+        final String json = mapper.writeValueAsString(configuration);
+        assertThat(json)
+                .isNotNull();
+
+        // and as an added bonus, let's see we can also read it back:
+        final Configuration cfg = mapper.readValue(json, Configuration.class);
+        assertThat(cfg)
+                .isNotNull();
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/cli/CheckCommandTest.java b/dropwizard-core/src/test/java/io/dropwizard/cli/CheckCommandTest.java
new file mode 100644
index 0000000..5aca8de
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/cli/CheckCommandTest.java
@@ -0,0 +1,51 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import net.sourceforge.argparse4j.inf.Namespace;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class CheckCommandTest {
+    private static class MyApplication extends Application<Configuration> {
+        @Override
+        public void initialize(Bootstrap<Configuration> bootstrap) {
+        }
+
+        @Override
+        public void run(Configuration configuration, Environment environment) throws Exception {
+        }
+    }
+
+    private final MyApplication application = new MyApplication();
+    private final CheckCommand<Configuration> command = new CheckCommand<>(application);
+
+    @SuppressWarnings("unchecked")
+    private final Bootstrap<Configuration> bootstrap = mock(Bootstrap.class);
+    private final Namespace namespace = mock(Namespace.class);
+    private final Configuration configuration = mock(Configuration.class);
+
+    @Test
+    public void hasAName() throws Exception {
+        assertThat(command.getName())
+                .isEqualTo("check");
+    }
+
+    @Test
+    public void hasADescription() throws Exception {
+        assertThat(command.getDescription())
+                .isEqualTo("Parses and validates the configuration file");
+    }
+
+    @Test
+    public void doesNotInteractWithAnything() throws Exception {
+        command.run(bootstrap, namespace, configuration);
+
+        verifyZeroInteractions(bootstrap, namespace, configuration);
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/cli/CliTest.java b/dropwizard-core/src/test/java/io/dropwizard/cli/CliTest.java
new file mode 100644
index 0000000..6c11609
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/cli/CliTest.java
@@ -0,0 +1,280 @@
+package io.dropwizard.cli;
+
+import com.google.common.base.Optional;
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import io.dropwizard.util.JarLocation;
+import net.sourceforge.argparse4j.inf.Namespace;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.ByteArrayOutputStream;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class CliTest {
+    private final JarLocation location = mock(JarLocation.class);
+    @SuppressWarnings("unchecked")
+    private final Application<Configuration> app = new Application<Configuration>() {
+        @Override
+        public void initialize(Bootstrap<Configuration> bootstrap) {
+        }
+
+        @Override
+        public void run(Configuration configuration, Environment environment) throws Exception {
+        }
+    };
+    private final Bootstrap<Configuration> bootstrap = new Bootstrap<>(app);
+    private final ByteArrayOutputStream stdOut = new ByteArrayOutputStream();
+    private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream();
+    private final CheckCommand<Configuration> command = spy(new CheckCommand<>(app));
+    private Cli cli;
+
+    @Before
+    @SuppressWarnings("unchecked")
+    public void setUp() throws Exception {
+        when(location.toString()).thenReturn("dw-thing.jar");
+        when(location.getVersion()).thenReturn(Optional.of("1.0.0"));
+        bootstrap.addCommand(command);
+
+        doNothing().when(command).run(any(Bootstrap.class), any(Namespace.class), any(Configuration.class));
+
+        this.cli = new Cli(location, bootstrap, stdOut, stdErr);
+    }
+
+    @Test
+    public void handlesShortVersionCommands() throws Exception {
+        assertThat(cli.run("-v"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format("1.0.0%n"));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+    }
+
+    @Test
+    public void handlesLongVersionCommands() throws Exception {
+        assertThat(cli.run("--version"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format("1.0.0%n"));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+    }
+
+    @Test
+    public void handlesMissingVersions() throws Exception {
+        when(location.getVersion()).thenReturn(Optional.<String>absent());
+        final Cli newCli = new Cli(location, bootstrap, stdOut, stdErr);
+
+        assertThat(newCli.run("--version"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format("No application version detected. Add a Implementation-Version entry to your JAR's manifest to enable this.%n"));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+    }
+
+    @Test
+    public void handlesZeroArgumentsAsHelpCommand() throws Exception {
+        assertThat(cli.run())
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format(
+                        "usage: java -jar dw-thing.jar [-h] [-v] {check} ...%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  {check}                available commands%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n" +
+                                "  -v, --version          show the application version and exit%n"
+                ));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+    }
+
+    @Test
+    public void handlesShortHelpCommands() throws Exception {
+        assertThat(cli.run("-h"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format(
+                        "usage: java -jar dw-thing.jar [-h] [-v] {check} ...%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  {check}                available commands%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n" +
+                                "  -v, --version          show the application version and exit%n"
+                ));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+    }
+
+    @Test
+    public void handlesLongHelpCommands() throws Exception {
+        assertThat(cli.run("--help"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format(
+                        "usage: java -jar dw-thing.jar [-h] [-v] {check} ...%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  {check}                available commands%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n" +
+                                "  -v, --version          show the application version and exit%n"
+                ));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void handlesShortHelpSubcommands() throws Exception {
+        assertThat(cli.run("check", "-h"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format(
+                        "usage: java -jar dw-thing.jar check [-h] [file]%n" +
+                                "%n" +
+                                "Parses and validates the configuration file%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  file                   application configuration file%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n"
+                ));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+
+        verify(command, never()).run(any(Bootstrap.class), any(Namespace.class), any(Configuration.class));
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void handlesLongHelpSubcommands() throws Exception {
+        assertThat(cli.run("check", "--help"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEqualTo(String.format(
+                        "usage: java -jar dw-thing.jar check [-h] [file]%n" +
+                                "%n" +
+                                "Parses and validates the configuration file%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  file                   application configuration file%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n"
+                ));
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+
+        verify(command, never()).run(any(Bootstrap.class), any(Namespace.class), any(Configuration.class));
+    }
+
+    @Test
+    public void rejectsBadCommandFlags() throws Exception {
+        assertThat(cli.run("--yes"))
+                .isFalse();
+
+        assertThat(stdOut.toString())
+                .isEmpty();
+
+        assertThat(stdErr.toString())
+                .isEqualTo(String.format(
+                        "unrecognized arguments: '--yes'%n" +
+                                "usage: java -jar dw-thing.jar [-h] [-v] {check} ...%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  {check}                available commands%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n" +
+                                "  -v, --version          show the application version and exit%n"
+                ));
+    }
+
+    @Test
+    public void rejectsBadSubcommandFlags() throws Exception {
+        assertThat(cli.run("check", "--yes"))
+                .isFalse();
+
+        assertThat(stdOut.toString())
+                .isEmpty();
+
+        assertThat(stdErr.toString())
+                .isEqualTo(String.format(
+                        "unrecognized arguments: '--yes'%n" +
+                                "usage: java -jar dw-thing.jar check [-h] [file]%n" +
+                                "%n" +
+                                "Parses and validates the configuration file%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  file                   application configuration file%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n"
+                ));
+    }
+
+    @Test
+    public void rejectsBadSubcommands() throws Exception {
+        assertThat(cli.run("plop"))
+                .isFalse();
+
+        assertThat(stdOut.toString())
+                .isEmpty();
+
+        assertThat(stdErr.toString())
+                .isEqualTo(String.format(
+                        "invalid choice: 'plop' (choose from 'check')%n" +
+                                "usage: java -jar dw-thing.jar [-h] [-v] {check} ...%n" +
+                                "%n" +
+                                "positional arguments:%n" +
+                                "  {check}                available commands%n" +
+                                "%n" +
+                                "optional arguments:%n" +
+                                "  -h, --help             show this help message and exit%n" +
+                                "  -v, --version          show the application version and exit%n"
+                ));
+    }
+
+    @Test
+    public void runsCommands() throws Exception {
+        assertThat(cli.run("check"))
+                .isTrue();
+
+        assertThat(stdOut.toString())
+                .isEmpty();
+
+        assertThat(stdErr.toString())
+                .isEmpty();
+
+        verify(command).run(eq(bootstrap), any(Namespace.class), any(Configuration.class));
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/cli/ConfiguredCommandTest.java b/dropwizard-core/src/test/java/io/dropwizard/cli/ConfiguredCommandTest.java
new file mode 100644
index 0000000..51c9b58
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/cli/ConfiguredCommandTest.java
@@ -0,0 +1,67 @@
+package io.dropwizard.cli;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.configuration.ConfigurationFactoryFactory;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+import javax.validation.Validator;
+
+import net.sourceforge.argparse4j.inf.Namespace;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+
+public class ConfiguredCommandTest {
+    private static class TestCommand extends ConfiguredCommand<Configuration> {
+        protected TestCommand() {
+            super("test", "test");
+        }
+
+        @Override
+        protected void run(Bootstrap<Configuration> bootstrap, Namespace namespace, Configuration configuration) throws Exception {
+            
+        }        
+    }
+    
+    private static class MyApplication extends Application<Configuration> {
+        @Override
+        public void initialize(Bootstrap<Configuration> bootstrap) {
+        }
+
+        @Override
+        public void run(Configuration configuration, Environment environment) throws Exception {
+        }
+    }
+    
+    private final MyApplication application = new MyApplication();
+    private final TestCommand command = new TestCommand();
+    private final Bootstrap<Configuration> bootstrap = new Bootstrap<>(application);
+    private final Namespace namespace = mock(Namespace.class);
+    
+    @SuppressWarnings("unchecked")
+    @Test
+    public void canUseCustomConfigurationFactory() throws Exception {
+
+        ConfigurationFactory<Configuration> factory = Mockito.mock(ConfigurationFactory.class);
+        when(factory.build()).thenReturn(null);
+        
+
+        ConfigurationFactoryFactory<Configuration> factoryFactory = Mockito.mock(ConfigurationFactoryFactory.class);
+        when(factoryFactory.create(any(Class.class), any(Validator.class), any(ObjectMapper.class), any(String.class))).thenReturn(factory);       
+        bootstrap.setConfigurationFactoryFactory(factoryFactory);
+        
+        command.run(bootstrap, namespace);
+        
+        Mockito.verify(factoryFactory).create(any(Class.class), any(Validator.class), any(ObjectMapper.class), any(String.class));
+        Mockito.verify(factory).build();
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/cli/ServerCommandTest.java b/dropwizard-core/src/test/java/io/dropwizard/cli/ServerCommandTest.java
new file mode 100644
index 0000000..dfc7f13
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/cli/ServerCommandTest.java
@@ -0,0 +1,99 @@
+package io.dropwizard.cli;
+
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.server.ServerFactory;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import net.sourceforge.argparse4j.inf.Namespace;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ServerCommandTest {
+    private static class MyApplication extends Application<Configuration> {
+        @Override
+        public void initialize(Bootstrap<Configuration> bootstrap) {
+        }
+
+        @Override
+        public void run(Configuration configuration, Environment environment) throws Exception {
+        }
+    }
+
+    private final MyApplication application = new MyApplication();
+    private final ServerCommand<Configuration> command = new ServerCommand<>(application);
+    private final Server server = new Server(0);
+
+    private final Environment environment = mock(Environment.class);
+    private final Namespace namespace = mock(Namespace.class);
+    private final ServerFactory serverFactory = mock(ServerFactory.class);
+    private final Configuration configuration = mock(Configuration.class);
+
+    @Before
+    public void setUp() throws Exception {
+        when(serverFactory.build(environment)).thenReturn(server);
+        when(configuration.getServerFactory()).thenReturn(serverFactory);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        server.stop();
+    }
+
+    @Test
+    public void hasAName() throws Exception {
+        assertThat(command.getName())
+                .isEqualTo("server");
+    }
+
+    @Test
+    public void hasADescription() throws Exception {
+        assertThat(command.getDescription())
+                .isEqualTo("Runs the Dropwizard application as an HTTP server");
+    }
+
+    @Test
+    public void hasTheApplicationsConfigurationClass() throws Exception {
+        assertThat(command.getConfigurationClass())
+                .isEqualTo(application.getConfigurationClass());
+    }
+
+    @Test
+    public void buildsAndRunsAConfiguredServer() throws Exception {
+        command.run(environment, namespace, configuration);
+
+        assertThat(server.isStarted())
+                .isTrue();
+    }
+
+    @Test
+    public void stopsAServerIfThereIsAnErrorStartingIt() throws Exception {
+        server.addBean(new AbstractLifeCycle() {
+            @Override
+            protected void doStart() throws Exception {
+                throw new IOException("oh crap");
+            }
+        });
+
+        try {
+            command.run(environment, namespace, configuration);
+            failBecauseExceptionWasNotThrown(IOException.class);
+        } catch (IOException e) {
+            assertThat(e.getMessage())
+                    .isEqualTo("oh crap");
+        }
+
+        assertThat(server.isStarted())
+                .isFalse();
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/errors/EarlyEofExceptionMapperTest.java b/dropwizard-core/src/test/java/io/dropwizard/errors/EarlyEofExceptionMapperTest.java
new file mode 100644
index 0000000..152734c
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/errors/EarlyEofExceptionMapperTest.java
@@ -0,0 +1,18 @@
+package io.dropwizard.errors;
+
+import org.eclipse.jetty.io.EofException;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.ws.rs.core.Response;
+
+public class EarlyEofExceptionMapperTest {
+
+    private final EarlyEofExceptionMapper mapper = new EarlyEofExceptionMapper();
+
+    @Test
+    public void testToReponse() {
+        final Response reponse = mapper.toResponse(new EofException());
+        Assert.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), reponse.getStatus());
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/server/DefaultServerFactoryTest.java b/dropwizard-core/src/test/java/io/dropwizard/server/DefaultServerFactoryTest.java
new file mode 100644
index 0000000..3c380e2
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/server/DefaultServerFactoryTest.java
@@ -0,0 +1,182 @@
+package io.dropwizard.server;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jetty.HttpConnectorFactory;
+import io.dropwizard.logging.ConsoleAppenderFactory;
+import io.dropwizard.logging.FileAppenderFactory;
+import io.dropwizard.logging.SyslogAppenderFactory;
+import io.dropwizard.setup.Environment;
+
+import java.io.File;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+
+import org.eclipse.jetty.server.AbstractNetworkConnector;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.NetworkConnector;
+import org.eclipse.jetty.server.Server;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.CharStreams;
+import com.google.common.io.Resources;
+
+public class DefaultServerFactoryTest {
+    private DefaultServerFactory http;
+
+    @Before
+    public void setUp() throws Exception {
+        final ObjectMapper objectMapper = Jackson.newObjectMapper();
+        objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class,
+                                                           FileAppenderFactory.class,
+                                                           SyslogAppenderFactory.class,
+                                                           HttpConnectorFactory.class);
+
+        this.http = new ConfigurationFactory<>(DefaultServerFactory.class,
+                                               Validation.buildDefaultValidatorFactory()
+                                                                 .getValidator(),
+                                               objectMapper, "dw")
+                .build(new File(Resources.getResource("yaml/server.yml").toURI()));
+    }
+
+    @Test
+    public void loadsGzipConfig() throws Exception {
+        assertThat(http.getGzipFilterFactory().isEnabled())
+                .isFalse();
+    }
+
+    @Test
+    public void hasAMaximumNumberOfThreads() throws Exception {
+        assertThat(http.getMaxThreads())
+                .isEqualTo(101);
+    }
+
+    @Test
+    public void hasAMinimumNumberOfThreads() throws Exception {
+        assertThat(http.getMinThreads())
+                .isEqualTo(89);
+    }
+
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(DefaultServerFactory.class);
+    }
+
+    @Test
+    public void testGracefulShutdown() throws Exception {
+        ObjectMapper objectMapper = Jackson.newObjectMapper();
+        Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+        MetricRegistry metricRegistry = new MetricRegistry();
+        Environment environment = new Environment("test", objectMapper, validator, metricRegistry,
+                ClassLoader.getSystemClassLoader());
+
+        CountDownLatch requestReceived = new CountDownLatch(1);
+        CountDownLatch shutdownInvoked = new CountDownLatch(1);
+
+        environment.jersey().register(new TestResource(requestReceived, shutdownInvoked));
+
+        final ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
+        final Server server = http.build(environment);
+        
+        ((AbstractNetworkConnector)server.getConnectors()[0]).setPort(0);
+
+        ScheduledFuture<Void> cleanup = executor.schedule(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                if (!server.isStopped()) {
+                    server.stop();
+                }
+                executor.shutdownNow();
+                return null;
+            }
+        }, 5, TimeUnit.SECONDS);
+
+
+        server.start();
+
+        final int port = ((AbstractNetworkConnector) server.getConnectors()[0]).getLocalPort();
+
+        Future<String> futureResult = executor.submit(new Callable<String>() {
+            @Override
+            public String call() throws Exception {
+                URL url = new URL("http://localhost:" + port + "/test");
+                URLConnection connection = url.openConnection();
+                connection.connect();
+                return CharStreams.toString(new InputStreamReader(connection.getInputStream()));
+            }
+        });
+
+        requestReceived.await();
+
+        Future<Void> serverStopped = executor.submit(new Callable<Void>() {
+            @Override
+            public Void call() throws Exception {
+                server.stop();
+                return null;
+            }
+        });
+
+        Connector[] connectors = server.getConnectors();
+        assertThat(connectors).isNotEmpty();
+        assertThat(connectors[0]).isInstanceOf(NetworkConnector.class);
+        NetworkConnector connector = (NetworkConnector) connectors[0];
+
+        // wait for server to close the connectors
+        while (true) {
+            if (!connector.isOpen()) {
+                shutdownInvoked.countDown();
+                break;
+            }
+            Thread.sleep(5);
+        }
+
+        String result = futureResult.get();
+        assertThat(result).isEqualTo("test");
+
+        serverStopped.get();
+
+        // cancel the cleanup future since everything succeeded
+        cleanup.cancel(false);
+        executor.shutdownNow();
+    }
+
+    @Path("/test")
+    @Produces("text/plain")
+    public static class TestResource {
+
+        private final CountDownLatch requestReceived;
+        private final CountDownLatch shutdownInvoked;
+
+        public TestResource(CountDownLatch requestReceived, CountDownLatch shutdownInvoked) {
+            this.requestReceived = requestReceived;
+            this.shutdownInvoked = shutdownInvoked;
+        }
+
+        @GET
+        public String get() throws Exception {
+            requestReceived.countDown();
+            shutdownInvoked.await();
+            return "test";
+        }
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/server/SimpleServerFactoryTest.java b/dropwizard-core/src/test/java/io/dropwizard/server/SimpleServerFactoryTest.java
new file mode 100644
index 0000000..cf46a7b
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/server/SimpleServerFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.server;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class SimpleServerFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(SimpleServerFactory.class);
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/setup/AdminEnvironmentTest.java b/dropwizard-core/src/test/java/io/dropwizard/setup/AdminEnvironmentTest.java
new file mode 100644
index 0000000..5442b37
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/setup/AdminEnvironmentTest.java
@@ -0,0 +1,38 @@
+package io.dropwizard.setup;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.google.common.collect.ImmutableMultimap;
+import io.dropwizard.jetty.MutableServletContextHandler;
+import io.dropwizard.servlets.tasks.Task;
+import org.junit.Test;
+
+import javax.servlet.ServletRegistration;
+import java.io.PrintWriter;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class AdminEnvironmentTest {
+    private final MutableServletContextHandler handler = new MutableServletContextHandler();
+    private final HealthCheckRegistry healthCheckRegistry = new HealthCheckRegistry();
+    private final MetricRegistry metricRegistry = new MetricRegistry();
+    private final AdminEnvironment env = new AdminEnvironment(handler, healthCheckRegistry, metricRegistry);
+
+    @Test
+    public void addsATaskServlet() throws Exception {
+        final Task task = new Task("thing") {
+            @Override
+            public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception {
+            }
+        };
+        env.addTask(task);
+
+        handler.start();
+
+        final ServletRegistration registration = handler.getServletHandler()
+                                                        .getServletContext()
+                                                        .getServletRegistration("tasks");
+        assertThat(registration.getMappings())
+                .containsOnly("/tasks/*");
+    }
+}
diff --git a/dropwizard-core/src/test/java/io/dropwizard/setup/BootstrapTest.java b/dropwizard-core/src/test/java/io/dropwizard/setup/BootstrapTest.java
new file mode 100644
index 0000000..4748dd5
--- /dev/null
+++ b/dropwizard-core/src/test/java/io/dropwizard/setup/BootstrapTest.java
@@ -0,0 +1,74 @@
+package io.dropwizard.setup;
+
+import com.codahale.metrics.MetricRegistry;
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.configuration.DefaultConfigurationFactoryFactory;
+import io.dropwizard.configuration.FileConfigurationSourceProvider;
+
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class BootstrapTest {
+    private final Application<Configuration> application = new Application<Configuration>() {
+        @Override
+        public void initialize(Bootstrap<Configuration> bootstrap) {
+        }
+
+        @Override
+        public void run(Configuration configuration, Environment environment) throws Exception {
+        }
+    };
+    private final Bootstrap<Configuration> bootstrap = new Bootstrap<Configuration>(application);
+
+    @Test
+    public void hasAnApplication() throws Exception {
+        assertThat(bootstrap.getApplication())
+                .isEqualTo(application);
+    }
+
+    @Test
+    public void hasAnObjectMapper() throws Exception {
+        assertThat(bootstrap.getObjectMapper())
+                .isNotNull();
+    }
+
+    @Test
+    public void defaultsToUsingFilesForConfiguration() throws Exception {
+        assertThat(bootstrap.getConfigurationSourceProvider())
+                .isInstanceOfAny(FileConfigurationSourceProvider.class);
+    }
+
+    @Test
+    public void defaultsToUsingTheDefaultClassLoader() throws Exception {
+        assertThat(bootstrap.getClassLoader())
+                .isEqualTo(Thread.currentThread().getContextClassLoader());
+    }
+
+    @Test
+    public void comesWithJvmInstrumentation() throws Exception {
+        assertThat(bootstrap.getMetricRegistry().getNames())
+                .contains("jvm.buffers.mapped.capacity", "jvm.threads.count", "jvm.memory.heap.usage");
+    }
+    
+    @Test
+    public void defaultsToDefaultConfigurationFactoryFactory() throws Exception {
+        assertThat(bootstrap.getConfigurationFactoryFactory())
+                .isInstanceOf(DefaultConfigurationFactoryFactory.class);
+    }
+    
+    @Test
+    public void testBYOMetrics() {
+        final MetricRegistry newRegistry = new MetricRegistry();
+        Bootstrap<Configuration> newBootstrap = new Bootstrap<Configuration>(application) {
+            @Override
+            public MetricRegistry getMetricRegistry() {
+                return super.getMetricRegistry();
+            }
+        };
+        
+        assertThat(newBootstrap.getMetricRegistry().getNames())
+                .contains("jvm.buffers.mapped.capacity", "jvm.threads.count", "jvm.memory.heap.usage");
+    }
+}
diff --git a/dropwizard-core/src/test/resources/logback-test.xml b/dropwizard-core/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-core/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-core/src/test/resources/yaml/server.yml b/dropwizard-core/src/test/resources/yaml/server.yml
new file mode 100644
index 0000000..272c858
--- /dev/null
+++ b/dropwizard-core/src/test/resources/yaml/server.yml
@@ -0,0 +1,22 @@
+requestLog:
+  appenders:
+    - type: console
+    - type: file
+      currentLogFilename: ./logs/requests.log
+      archivedLogFilenamePattern: ./logs/requests-%d.log.gz
+      archivedFileCount: 5
+gzip:
+  enabled: false
+applicationConnectors:
+  - type: http
+    port: 0
+    bindHost: "localhost"
+    acceptorThreads: 2
+    acceptQueueSize: 100
+    reuseAddress: false
+    soLingerTime: 2s
+    useServerHeader: true
+    useDateHeader: false
+    useForwardedHeaders: false
+minThreads: 89
+maxThreads: 101
diff --git a/dropwizard-db/pom.xml b/dropwizard-db/pom.xml
new file mode 100644
index 0000000..6a69a48
--- /dev/null
+++ b/dropwizard-db/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-db</artifactId>
+    <name>Dropwizard Database Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.tomcat</groupId>
+            <artifactId>tomcat-jdbc</artifactId>
+            <version>7.0.50</version>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>${h2.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-db/src/main/java/io/dropwizard/db/DataSourceFactory.java b/dropwizard-db/src/main/java/io/dropwizard/db/DataSourceFactory.java
new file mode 100644
index 0000000..548479c
--- /dev/null
+++ b/dropwizard-db/src/main/java/io/dropwizard/db/DataSourceFactory.java
@@ -0,0 +1,723 @@
+package io.dropwizard.db;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Optional;
+import com.google.common.collect.Maps;
+import io.dropwizard.util.Duration;
+import io.dropwizard.validation.MinDuration;
+import io.dropwizard.validation.ValidationMethod;
+import org.apache.tomcat.jdbc.pool.PoolProperties;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.sql.Connection;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A factory for pooled {@link ManagedDataSource}s.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code driverClass}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>The full name of the JDBC driver class.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code url}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>The URL of the server.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code user}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>The username used to connect to the server.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code password}</td>
+ *         <td>none</td>
+ *         <td>The password used to connect to the server.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code abandonWhenPercentageFull}</td>
+ *         <td>0</td>
+ *         <td>
+ *             Connections that have been abandoned (timed out) won't get closed and reported up
+ *             unless the number of connections in use are above the percentage defined by
+ *             {@code abandonWhenPercentageFull}. The value should be between 0-100.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code alternateUsernamesAllowed}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             Set to true if the call
+ *             {@link javax.sql.DataSource#getConnection(String, String) getConnection(username,password)}
+ *             is allowed. This is used for when the pool is used by an application accessing
+ *             multiple schemas. There is a performance impact turning this option on, even when not
+ *             used.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code commitOnReturn}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             Set to true if you want the connection pool to commit any pending transaction when a
+ *             connection is returned.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code autoCommitByDefault}</td>
+ *         <td>JDBC driver's default</td>
+ *         <td>The default auto-commit state of the connections.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code readOnlyByDefault}</td>
+ *         <td>JDBC driver's default</td>
+ *         <td>The default read-only state of the connections.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code properties}</td>
+ *         <td>none</td>
+ *         <td>Any additional JDBC driver parameters.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code defaultCatalog}</td>
+ *         <td>none</td>
+ *         <td>The default catalog to use for the connections.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code defaultTransactionIsolation}</td>
+ *         <td>JDBC driver default</td>
+ *         <td>
+ *             The default transaction isolation to use for the connections. Can be one of
+ *             {@code none}, {@code default}, {@code read-uncommitted}, {@code read-committed},
+ *             {@code repeatable-read}, or {@code serializable}.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code useFairQueue}</td>
+ *         <td>{@code true}</td>
+ *         <td>
+ *             If {@code true}, calls to {@code getConnection} are handled in a FIFO manner.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code initialSize}</td>
+ *         <td>10</td>
+ *         <td>
+ *             The initial size of the connection pool.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code minSize}</td>
+ *         <td>10</td>
+ *         <td>
+ *             The minimum size of the connection pool.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxSize}</td>
+ *         <td>10</td>
+ *         <td>
+ *             The maximum size of the connection pool.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code initializationQuery}</td>
+ *         <td>none</td>
+ *         <td>
+ *             A custom query to be run when a connection is first created.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code logAbandonedConnections}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             If {@code true}, logs stack traces of abandoned connections.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code logValidationErrors}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             If {@code true}, logs errors when connections fail validation.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxConnectionAge}</td>
+ *         <td>none</td>
+ *         <td>
+ *             If set, connections which have been open for longer than {@code maxConnectionAge} are
+ *             closed when returned.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxWaitForConnection}</td>
+ *         <td>30 seconds</td>
+ *         <td>
+ *             If a request for a connection is blocked for longer than this period, an exception
+ *             will be thrown.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code minIdleTime}</td>
+ *         <td>1 minute</td>
+ *         <td>
+ *             The minimum amount of time an connection must sit idle in the pool before it is
+ *             eligible for eviction.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code validationQuery}</td>
+ *         <td>{@code SELECT 1}</td>
+ *         <td>
+ *             The SQL query that will be used to validate connections from this pool before
+ *             returning them to the caller or pool. If specified, this query does not have to
+ *             return any data, it just can't throw a SQLException.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code checkConnectionWhileIdle}</td>
+ *         <td>{@code true}</td>
+ *         <td>
+ *             Set to true if query validation should take place while the connection is idle.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code checkConnectionOnBorrow}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             Whether or not connections will be validated before being borrowed from the pool. If
+ *             the connection fails to validate, it will be dropped from the pool, and another will
+ *             be borrowed.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code checkConnectionOnConnect}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             Whether or not connections will be validated before being added to the pool. If the
+ *             connection fails to validate, it won't be added to the pool.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code checkConnectionOnReturn}</td>
+ *         <td>{@code false}</td>
+ *         <td>
+ *             Whether or not connections will be validated after being returned to the pool. If
+ *             the connection fails to validate, it will be dropped from the pool.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code autoCommentsEnabled}</td>
+ *         <td>{@code true}</td>
+ *         <td>
+ *             Whether or not ORMs should automatically add comments.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code evictionInterval}</td>
+ *         <td>5 seconds</td>
+ *         <td>
+ *             The amount of time to sleep between runs of the idle connection validation, abandoned
+ *             cleaner and idle pool resizing.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code validationInterval}</td>
+ *         <td>30 seconds</td>
+ *         <td>
+ *             To avoid excess validation, only run validation once every interval.
+ *         </td>
+ *     </tr>
+ * </table>
+ */
+public class DataSourceFactory {
+    @SuppressWarnings("UnusedDeclaration")
+    public enum TransactionIsolation {
+        NONE(Connection.TRANSACTION_NONE),
+        DEFAULT(org.apache.tomcat.jdbc.pool.DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION),
+        READ_UNCOMMITTED(Connection.TRANSACTION_READ_UNCOMMITTED),
+        READ_COMMITTED(Connection.TRANSACTION_READ_COMMITTED),
+        REPEATABLE_READ(Connection.TRANSACTION_REPEATABLE_READ),
+        SERIALIZABLE(Connection.TRANSACTION_SERIALIZABLE);
+
+        private final int value;
+
+        private TransactionIsolation(int value) {
+            this.value = value;
+        }
+
+        public int get() {
+            return value;
+        }
+    }
+
+    @NotNull
+    private String driverClass = null;
+
+    @Min(0)
+    @Max(100)
+    private int abandonWhenPercentageFull = 0;
+
+    private boolean alternateUsernamesAllowed = false;
+
+    private boolean commitOnReturn = false;
+
+    private Boolean autoCommitByDefault;
+
+    private Boolean readOnlyByDefault;
+
+    @NotNull
+    private String user = null;
+
+    private String password = "";
+
+    @NotNull
+    private String url = null;
+
+    @NotNull
+    private Map<String, String> properties = Maps.newLinkedHashMap();
+
+    private String defaultCatalog;
+
+    @NotNull
+    private TransactionIsolation defaultTransactionIsolation = TransactionIsolation.DEFAULT;
+
+    private boolean useFairQueue = true;
+
+    @Min(1)
+    private int initialSize = 10;
+
+    @Min(1)
+    private int minSize = 10;
+
+    @Min(1)
+    private int maxSize = 100;
+
+    private String initializationQuery;
+
+    private boolean logAbandonedConnections = false;
+
+    private boolean logValidationErrors = false;
+
+    @MinDuration(value = 1, unit = TimeUnit.SECONDS)
+    private Duration maxConnectionAge;
+
+    @NotNull
+    @MinDuration(value = 1, unit = TimeUnit.SECONDS)
+    private Duration maxWaitForConnection = Duration.seconds(30);
+
+    @NotNull
+    @MinDuration(value = 1, unit = TimeUnit.SECONDS)
+    private Duration minIdleTime = Duration.minutes(1);
+
+    @NotNull
+    private String validationQuery = "/* Health Check */ SELECT 1";
+
+    private boolean checkConnectionWhileIdle = true;
+
+    private boolean checkConnectionOnBorrow = false;
+
+    private boolean checkConnectionOnConnect = true;
+
+    private boolean checkConnectionOnReturn = false;
+
+    private boolean autoCommentsEnabled = true;
+
+    @NotNull
+    @MinDuration(1)
+    private Duration evictionInterval = Duration.seconds(5);
+
+    @NotNull
+    @MinDuration(1)
+    private Duration validationInterval = Duration.seconds(30);
+
+    @JsonProperty
+    public boolean isAutoCommentsEnabled() {
+        return autoCommentsEnabled;
+    }
+
+    @JsonProperty
+    public void setAutoCommentsEnabled(boolean autoCommentsEnabled) {
+        this.autoCommentsEnabled = autoCommentsEnabled;
+    }
+
+    @JsonProperty
+    public String getDriverClass() {
+        return driverClass;
+    }
+
+    @JsonProperty
+    public void setDriverClass(String driverClass) {
+        this.driverClass = driverClass;
+    }
+
+    @JsonProperty
+    public String getUser() {
+        return user;
+    }
+
+    @JsonProperty
+    public void setUser(String user) {
+        this.user = user;
+    }
+
+    @JsonProperty
+    public String getPassword() {
+        return password;
+    }
+
+    @JsonProperty
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    @JsonProperty
+    public String getUrl() {
+        return url;
+    }
+
+    @JsonProperty
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    @JsonProperty
+    public Map<String, String> getProperties() {
+        return properties;
+    }
+
+    @JsonProperty
+    public void setProperties(Map<String, String> properties) {
+        this.properties = properties;
+    }
+
+    @JsonProperty
+    public Duration getMaxWaitForConnection() {
+        return maxWaitForConnection;
+    }
+
+    @JsonProperty
+    public void setMaxWaitForConnection(Duration maxWaitForConnection) {
+        this.maxWaitForConnection = maxWaitForConnection;
+    }
+
+    @JsonProperty
+    public String getValidationQuery() {
+        return validationQuery;
+    }
+
+    @JsonProperty
+    public void setValidationQuery(String validationQuery) {
+        this.validationQuery = validationQuery;
+    }
+
+    @JsonProperty
+    public int getMinSize() {
+        return minSize;
+    }
+
+    @JsonProperty
+    public void setMinSize(int minSize) {
+        this.minSize = minSize;
+    }
+
+    @JsonProperty
+    public int getMaxSize() {
+        return maxSize;
+    }
+
+    @JsonProperty
+    public void setMaxSize(int maxSize) {
+        this.maxSize = maxSize;
+    }
+
+    @JsonProperty
+    public boolean getCheckConnectionWhileIdle() {
+        return checkConnectionWhileIdle;
+    }
+
+    @JsonProperty
+    public void setCheckConnectionWhileIdle(boolean checkConnectionWhileIdle) {
+        this.checkConnectionWhileIdle = checkConnectionWhileIdle;
+    }
+
+    @Deprecated
+    @JsonProperty
+    public boolean isDefaultReadOnly() {
+        return Boolean.TRUE.equals(readOnlyByDefault);
+    }
+
+    @Deprecated
+    @JsonProperty
+    public void setDefaultReadOnly(boolean defaultReadOnly) {
+        readOnlyByDefault = Boolean.valueOf(defaultReadOnly);
+    }
+
+    @JsonIgnore
+    @ValidationMethod(message = ".minSize must be less than or equal to maxSize")
+    public boolean isMinSizeLessThanMaxSize() {
+        return minSize <= maxSize;
+    }
+
+    @JsonIgnore
+    @ValidationMethod(message = ".initialSize must be less than or equal to maxSize")
+    public boolean isInitialSizeLessThanMaxSize() {
+        return initialSize <= maxSize;
+    }
+
+    @JsonIgnore
+    @ValidationMethod(message = ".initialSize must be greater than or equal to minSize")
+    public boolean isInitialSizeGreaterThanMinSize() {
+        return minSize <= initialSize;
+    }
+
+    @JsonProperty
+    public int getAbandonWhenPercentageFull() {
+        return abandonWhenPercentageFull;
+    }
+
+    @JsonProperty
+    public void setAbandonWhenPercentageFull(int percentage) {
+        this.abandonWhenPercentageFull = percentage;
+    }
+
+    @JsonProperty
+    public boolean isAlternateUsernamesAllowed() {
+        return alternateUsernamesAllowed;
+    }
+
+    @JsonProperty
+    public void setAlternateUsernamesAllowed(boolean allow) {
+        this.alternateUsernamesAllowed = allow;
+    }
+
+    @JsonProperty
+    public boolean getCommitOnReturn() {
+        return commitOnReturn;
+    }
+
+    @JsonProperty
+    public void setCommitOnReturn(boolean commitOnReturn) {
+        this.commitOnReturn = commitOnReturn;
+    }
+
+    @JsonProperty
+    public Boolean getAutoCommitByDefault() {
+        return autoCommitByDefault;
+    }
+
+    @JsonProperty
+    public void setAutoCommitByDefault(Boolean autoCommit) {
+        this.autoCommitByDefault = autoCommit;
+    }
+
+    @JsonProperty
+    public String getDefaultCatalog() {
+        return defaultCatalog;
+    }
+
+    @JsonProperty
+    public void setDefaultCatalog(String defaultCatalog) {
+        this.defaultCatalog = defaultCatalog;
+    }
+
+    @JsonProperty
+    public Boolean getReadOnlyByDefault() {
+        return readOnlyByDefault;
+    }
+
+    @JsonProperty
+    public void setReadOnlyByDefault(Boolean readOnlyByDefault) {
+        this.readOnlyByDefault = readOnlyByDefault;
+    }
+
+    @JsonProperty
+    public TransactionIsolation getDefaultTransactionIsolation() {
+        return defaultTransactionIsolation;
+    }
+
+    @JsonProperty
+    public void setDefaultTransactionIsolation(TransactionIsolation isolation) {
+        this.defaultTransactionIsolation = isolation;
+    }
+
+    @JsonProperty
+    public boolean getUseFairQueue() {
+        return useFairQueue;
+    }
+
+    @JsonProperty
+    public void setUseFairQueue(boolean fair) {
+        this.useFairQueue = fair;
+    }
+
+    @JsonProperty
+    public int getInitialSize() {
+        return initialSize;
+    }
+
+    @JsonProperty
+    public void setInitialSize(int initialSize) {
+        this.initialSize = initialSize;
+    }
+
+    @JsonProperty
+    public String getInitializationQuery() {
+        return initializationQuery;
+    }
+
+    @JsonProperty
+    public void setInitializationQuery(String query) {
+        this.initializationQuery = query;
+    }
+
+    @JsonProperty
+    public boolean getLogAbandonedConnections() {
+        return logAbandonedConnections;
+    }
+
+    @JsonProperty
+    public void setLogAbandonedConnections(boolean log) {
+        this.logAbandonedConnections = log;
+    }
+
+    @JsonProperty
+    public boolean getLogValidationErrors() {
+        return logValidationErrors;
+    }
+
+    @JsonProperty
+    public void setLogValidationErrors(boolean log) {
+        this.logValidationErrors = log;
+    }
+
+    @JsonProperty
+    public Optional<Duration> getMaxConnectionAge() {
+        return Optional.fromNullable(maxConnectionAge);
+    }
+
+    @JsonProperty
+    public void setMaxConnectionAge(Duration age) {
+        this.maxConnectionAge = age;
+    }
+
+    @JsonProperty
+    public Duration getMinIdleTime() {
+        return minIdleTime;
+    }
+
+    @JsonProperty
+    public void setMinIdleTime(Duration time) {
+        this.minIdleTime = time;
+    }
+
+    @JsonProperty
+    public boolean getCheckConnectionOnBorrow() {
+        return checkConnectionOnBorrow;
+    }
+
+    @JsonProperty
+    public void setCheckConnectionOnBorrow(boolean checkConnectionOnBorrow) {
+        this.checkConnectionOnBorrow = checkConnectionOnBorrow;
+    }
+
+    @JsonProperty
+    public boolean getCheckConnectionOnConnect() {
+        return checkConnectionOnConnect;
+    }
+
+    @JsonProperty
+    public void setCheckConnectionOnConnect(boolean checkConnectionOnConnect) {
+        this.checkConnectionOnConnect = checkConnectionOnConnect;
+    }
+
+    @JsonProperty
+    public boolean getCheckConnectionOnReturn() {
+        return checkConnectionOnReturn;
+    }
+
+    @JsonProperty
+    public void setCheckConnectionOnReturn(boolean checkConnectionOnReturn) {
+        this.checkConnectionOnReturn = checkConnectionOnReturn;
+    }
+
+    @JsonProperty
+    public Duration getEvictionInterval() {
+        return evictionInterval;
+    }
+
+    @JsonProperty
+    public void setEvictionInterval(Duration interval) {
+        this.evictionInterval = interval;
+    }
+
+    @JsonProperty
+    public Duration getValidationInterval() {
+        return validationInterval;
+    }
+
+    @JsonProperty
+    public void setValidationInterval(Duration validationInterval) {
+        this.validationInterval = validationInterval;
+    }
+
+    public ManagedDataSource build(MetricRegistry metricRegistry,
+                                   String name) throws ClassNotFoundException {
+        final Properties properties = new Properties();
+        for (Map.Entry<String, String> property : this.properties.entrySet()) {
+            properties.setProperty(property.getKey(), property.getValue());
+        }
+
+        final PoolProperties poolConfig = new PoolProperties();
+        poolConfig.setAbandonWhenPercentageFull(abandonWhenPercentageFull);
+        poolConfig.setAlternateUsernameAllowed(alternateUsernamesAllowed);
+        poolConfig.setCommitOnReturn(commitOnReturn);
+        poolConfig.setDbProperties(properties);
+        poolConfig.setDefaultAutoCommit(autoCommitByDefault);
+        poolConfig.setDefaultCatalog(defaultCatalog);
+        poolConfig.setDefaultReadOnly(readOnlyByDefault);
+        poolConfig.setDefaultTransactionIsolation(defaultTransactionIsolation.get());
+        poolConfig.setDriverClassName(driverClass);
+        poolConfig.setFairQueue(useFairQueue);
+        poolConfig.setInitialSize(initialSize);
+        poolConfig.setInitSQL(initializationQuery);
+        poolConfig.setLogAbandoned(logAbandonedConnections);
+        poolConfig.setLogValidationErrors(logValidationErrors);
+        poolConfig.setMaxActive(maxSize);
+        poolConfig.setMaxIdle(maxSize);
+        poolConfig.setMinIdle(minSize);
+
+        if (maxConnectionAge != null) {
+            poolConfig.setMaxAge(maxConnectionAge.toMilliseconds());
+        }
+
+        poolConfig.setMaxWait((int) maxWaitForConnection.toMilliseconds());
+        poolConfig.setMinEvictableIdleTimeMillis((int) minIdleTime.toMilliseconds());
+        poolConfig.setName(name);
+        poolConfig.setUrl(url);
+        poolConfig.setUsername(user);
+        poolConfig.setPassword(password);
+        poolConfig.setTestWhileIdle(checkConnectionWhileIdle);
+        poolConfig.setValidationQuery(validationQuery);
+        poolConfig.setTestOnBorrow(checkConnectionOnBorrow);
+        poolConfig.setTestOnConnect(checkConnectionOnConnect);
+        poolConfig.setTestOnReturn(checkConnectionOnReturn);
+        poolConfig.setTimeBetweenEvictionRunsMillis((int) evictionInterval.toMilliseconds());
+        poolConfig.setValidationInterval(validationInterval.toMilliseconds());
+
+        return new ManagedPooledDataSource(poolConfig, metricRegistry);
+    }
+}
diff --git a/dropwizard-db/src/main/java/io/dropwizard/db/DatabaseConfiguration.java b/dropwizard-db/src/main/java/io/dropwizard/db/DatabaseConfiguration.java
new file mode 100644
index 0000000..bc8aa31
--- /dev/null
+++ b/dropwizard-db/src/main/java/io/dropwizard/db/DatabaseConfiguration.java
@@ -0,0 +1,7 @@
+package io.dropwizard.db;
+
+import io.dropwizard.Configuration;
+
+public interface DatabaseConfiguration<T extends Configuration> {
+    DataSourceFactory getDataSourceFactory(T configuration);
+}
diff --git a/dropwizard-db/src/main/java/io/dropwizard/db/ManagedDataSource.java b/dropwizard-db/src/main/java/io/dropwizard/db/ManagedDataSource.java
new file mode 100644
index 0000000..348dcc5
--- /dev/null
+++ b/dropwizard-db/src/main/java/io/dropwizard/db/ManagedDataSource.java
@@ -0,0 +1,9 @@
+package io.dropwizard.db;
+
+import io.dropwizard.lifecycle.Managed;
+
+import javax.sql.DataSource;
+
+public interface ManagedDataSource extends DataSource, Managed {
+
+}
diff --git a/dropwizard-db/src/main/java/io/dropwizard/db/ManagedPooledDataSource.java b/dropwizard-db/src/main/java/io/dropwizard/db/ManagedPooledDataSource.java
new file mode 100644
index 0000000..d0f7c68
--- /dev/null
+++ b/dropwizard-db/src/main/java/io/dropwizard/db/ManagedPooledDataSource.java
@@ -0,0 +1,61 @@
+package io.dropwizard.db;
+
+import com.codahale.metrics.Gauge;
+import com.codahale.metrics.MetricRegistry;
+import org.apache.tomcat.jdbc.pool.ConnectionPool;
+import org.apache.tomcat.jdbc.pool.DataSourceProxy;
+import org.apache.tomcat.jdbc.pool.PoolConfiguration;
+
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.logging.Logger;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+/**
+ * A {@link ManagedDataSource} which is backed by a Tomcat pooled {@link javax.sql.DataSource}.
+ */
+public class ManagedPooledDataSource extends DataSourceProxy implements ManagedDataSource {
+    private final MetricRegistry metricRegistry;
+
+    /**
+     * Create a new data source with the given connection pool configuration.
+     *
+     * @param config the connection pool configuration
+     */
+    public ManagedPooledDataSource(PoolConfiguration config, MetricRegistry metricRegistry) {
+        super(config);
+        this.metricRegistry = metricRegistry;
+    }
+
+    // JDK6 has JDBC 4.0 which doesn't have this -- don't add @Override
+    @SuppressWarnings("override")
+    public Logger getParentLogger() throws SQLFeatureNotSupportedException {
+        throw new SQLFeatureNotSupportedException("Doesn't use java.util.logging");
+    }
+
+    @Override
+    public void start() throws Exception {
+        final ConnectionPool connectionPool = createPool();
+        metricRegistry.register(name(getClass(), connectionPool.getName(), "active"),
+                                new Gauge<Integer>() {
+                                    @Override
+                                    public Integer getValue() {
+                                        return connectionPool.getActive();
+                                    }
+                                });
+
+        metricRegistry.register(name(getClass(), connectionPool.getName(), "idle"),
+                                new Gauge<Integer>() {
+
+                                    @Override
+                                    public Integer getValue() {
+                                        return connectionPool.getIdle();
+                                    }
+                                });
+    }
+
+    @Override
+    public void stop() throws Exception {
+        close();
+    }
+}
diff --git a/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceFactoryTest.java b/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceFactoryTest.java
new file mode 100644
index 0000000..48a6a47
--- /dev/null
+++ b/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceFactoryTest.java
@@ -0,0 +1,49 @@
+package io.dropwizard.db;
+
+import com.codahale.metrics.MetricRegistry;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class DataSourceFactoryTest {
+    private final MetricRegistry metricRegistry = new MetricRegistry();
+    private final DataSourceFactory factory = new DataSourceFactory();
+
+    private ManagedDataSource dataSource;
+
+    @Before
+    public void setUp() throws Exception {
+        factory.setUrl("jdbc:h2:mem:DbTest-" + System.currentTimeMillis());
+        factory.setUser("sa");
+        factory.setDriverClass("org.h2.Driver");
+        factory.setValidationQuery("SELECT 1");
+
+        this.dataSource = factory.build(metricRegistry, "test");
+        dataSource.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        dataSource.stop();
+    }
+
+    @Test
+    public void buildsAConnectionPoolToTheDatabase() throws Exception {
+        try (Connection connection = dataSource.getConnection()) {
+            try (PreparedStatement statement = connection.prepareStatement(
+                    "select 1")) {
+                try (ResultSet set = statement.executeQuery()) {
+                    while (set.next()) {
+                        assertThat(set.getInt(1)).isEqualTo(1);
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/dropwizard-db/src/test/java/io/dropwizard/db/ManagedPooledDataSourceTest.java b/dropwizard-db/src/test/java/io/dropwizard/db/ManagedPooledDataSourceTest.java
new file mode 100644
index 0000000..3951cd5
--- /dev/null
+++ b/dropwizard-db/src/test/java/io/dropwizard/db/ManagedPooledDataSourceTest.java
@@ -0,0 +1,26 @@
+package io.dropwizard.db;
+
+import com.codahale.metrics.MetricRegistry;
+import org.apache.tomcat.jdbc.pool.PoolProperties;
+import org.junit.Test;
+
+import java.sql.SQLFeatureNotSupportedException;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class ManagedPooledDataSourceTest {
+    private final PoolProperties config = new PoolProperties();
+    private final MetricRegistry metricRegistry = new MetricRegistry();
+    private final ManagedPooledDataSource dataSource = new ManagedPooledDataSource(config, metricRegistry);
+
+    @Test
+    public void hasNoParentLogger() throws Exception {
+        try {
+            dataSource.getParentLogger();
+            failBecauseExceptionWasNotThrown(SQLFeatureNotSupportedException.class);
+        } catch (SQLFeatureNotSupportedException e) {
+            assertThat((Object) e).isInstanceOf(SQLFeatureNotSupportedException.class);
+        }
+    }
+}
diff --git a/dropwizard-db/src/test/resources/logback-test.xml b/dropwizard-db/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-db/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-example/README.md b/dropwizard-example/README.md
new file mode 100644
index 0000000..8e15abc
--- /dev/null
+++ b/dropwizard-example/README.md
@@ -0,0 +1,51 @@
+# Introduction
+
+The drop wizard example application was developed to, as its name implies, provide examples of some of the features
+present in drop wizard.
+
+# Overview
+
+Included with this application is an example of the optional db API module. The examples provided illustrate a few of
+the features available in [JDBI](http://jdbi.org), along with demonstrating how these are used from within dropwizard.
+
+This database example is comprised of the following classes.
+
+* The `PersonDAO` illustrates using the [SQL Object Queries](http://jdbi.org/sql_object_api_queries/) and string template
+features in JDBI.
+
+* The `PeopleDAO.sql.stg` stores all the SQL statements for use in the `PersonDAO`, note this is located in the
+src/resources under the same path as the `PersonDAO` class file.
+
+* `migrations.xml` illustrates the usage of `dropwizard-migrations` which can create your database prior to running
+your application for the first time.
+
+* The `PersonResource` and `PeopleResource` are the REST resource which use the PersonDAO to retrieve data from the database, note the injection
+of the PersonDAO in their constructors.
+
+As with all the modules the db example is wired up in the `initialize` function of the `HelloWorldApplication`.
+
+# Running The Application
+
+To test the example application run the following commands.
+
+* To package the example run.
+
+        mvn package
+
+* To setup the h2 database run.
+
+        java -jar target/dropwizard-example-0.7.1-SNAPSHOT.jar db migrate example.yml
+
+* To run the server run.
+
+        java -jar target/dropwizard-example-0.7.1-SNAPSHOT.jar server example.yml
+
+* To hit the Hello World example (hit refresh a few times).
+
+	http://localhost:8080/hello-world
+
+* To post data into the application.
+
+	curl -H "Content-Type: application/json" -X POST -d '{"fullName":"Other Person","jobTitle":"Other Title"}' http://localhost:8080/people
+	
+	open http://localhost:8080/people
diff --git a/dropwizard-example/example.keystore b/dropwizard-example/example.keystore
new file mode 100644
index 0000000..2d05be4
Binary files /dev/null and b/dropwizard-example/example.keystore differ
diff --git a/dropwizard-example/example.yml b/dropwizard-example/example.yml
new file mode 100644
index 0000000..ca329df
--- /dev/null
+++ b/dropwizard-example/example.yml
@@ -0,0 +1,68 @@
+template: Hello, %s!
+
+defaultName: Stranger
+
+# Database settings.
+database:
+
+  # the name of your JDBC driver
+  driverClass: org.h2.Driver
+
+  # the username
+  user: sa
+
+  # the password
+  password: sa
+
+  # the JDBC URL
+  url: jdbc:h2:./target/example
+
+# use the simple server factory if you only want to run on a single port
+#server:
+#  type: simple
+#  connector:
+#    type: http
+#    port: 8080
+
+server:
+#  softNofileLimit: 1000
+#  hardNofileLimit: 1000
+  applicationConnectors:
+    - type: http
+      port: 8080
+    - type: https
+      port: 8443
+      keyStorePath: example.keystore
+      keyStorePassword: example
+      validateCerts: false
+# this requires the npn-boot library on the JVM's boot classpath
+#    - type: spdy3
+#      port: 8445
+#      keyStorePath: example.keystore
+#      keyStorePassword: example
+#      validateCerts: false
+  adminConnectors:
+    - type: http
+      port: 8081
+    - type: https
+      port: 8444
+      keyStorePath: example.keystore
+      keyStorePassword: example
+      validateCerts: false
+
+# Logging settings.
+logging:
+
+  # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL.
+  level: INFO
+
+  # Logger-specific levels.
+  loggers:
+
+    # Sets the level for 'com.example.app' to DEBUG.
+    com.example.app: DEBUG
+
+    org.hibernate.SQL: ALL
+
+  appenders:
+    - type: console
diff --git a/dropwizard-example/pom.xml b/dropwizard-example/pom.xml
new file mode 100644
index 0000000..b4c5251
--- /dev/null
+++ b/dropwizard-example/pom.xml
@@ -0,0 +1,192 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <prerequisites>
+        <maven>3.0.0</maven>
+    </prerequisites>
+
+    <groupId>io.dropwizard</groupId>
+    <artifactId>dropwizard-example</artifactId>
+    <version>0.7.1</version>
+    <name>Dropwizard Example Application</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+    </properties>
+
+    <repositories>
+        <repository>
+            <id>sonatype-nexus-snapshots</id>
+            <name>Sonatype Nexus Snapshots</name>
+            <url>http://oss.sonatype.org/content/repositories/snapshots</url>
+        </repository>
+    </repositories>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-auth</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-assets</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-spdy</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-hibernate</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-migrations</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-views-freemarker</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-views-mustache</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>1.4.178</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-testing</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>1.3.1</version>
+                <executions>
+                    <execution>
+                        <id>enforce</id>
+                        <configuration>
+                            <rules>
+                                <DependencyConvergence />
+                            </rules>
+                        </configuration>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                    <encoding>UTF-8</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.2.1</version>
+                <executions>
+                    <execution>
+                        <id>attach-sources</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>2.2</version>
+                <configuration>
+                    <createDependencyReducedPom>true</createDependencyReducedPom>
+                    <filters>
+                        <filter>
+                            <artifact>*:*</artifact>
+                            <excludes>
+                                <exclude>META-INF/*.SF</exclude>
+                                <exclude>META-INF/*.DSA</exclude>
+                                <exclude>META-INF/*.RSA</exclude>
+                            </excludes>
+                        </filter>
+                    </filters>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <transformers>
+                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>com.example.helloworld.HelloWorldApplication</mainClass>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <!-- You'll probably want to remove this for your project. I'm just using it here
+                     so that dropwizard-example doesn't get deployed as a library. -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-deploy-plugin</artifactId>
+                <version>2.7</version>
+                <configuration>
+                    <skip>true</skip>
+                </configuration>
+            </plugin>
+            <plugin>
+                <!-- You'll probably want to remove this for your project. I'm just using it here
+                     so that dropwizard-example site doesn't get staged and deployed. -->
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>3.3</version>
+                <configuration>
+                    <skip>true</skip>
+                    <skipDeploy>true</skipDeploy>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldApplication.java b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldApplication.java
new file mode 100644
index 0000000..9e0dfc4
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldApplication.java
@@ -0,0 +1,68 @@
+package com.example.helloworld;
+
+import com.example.helloworld.auth.ExampleAuthenticator;
+import com.example.helloworld.cli.RenderCommand;
+import com.example.helloworld.core.Person;
+import com.example.helloworld.core.Template;
+import com.example.helloworld.db.PersonDAO;
+import com.example.helloworld.health.TemplateHealthCheck;
+import com.example.helloworld.resources.*;
+import io.dropwizard.Application;
+import io.dropwizard.assets.AssetsBundle;
+import io.dropwizard.auth.basic.BasicAuthProvider;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.hibernate.HibernateBundle;
+import io.dropwizard.migrations.MigrationsBundle;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import io.dropwizard.views.ViewBundle;
+
+public class HelloWorldApplication extends Application<HelloWorldConfiguration> {
+    public static void main(String[] args) throws Exception {
+        new HelloWorldApplication().run(args);
+    }
+
+    private final HibernateBundle<HelloWorldConfiguration> hibernateBundle =
+            new HibernateBundle<HelloWorldConfiguration>(Person.class) {
+                @Override
+                public DataSourceFactory getDataSourceFactory(HelloWorldConfiguration configuration) {
+                    return configuration.getDataSourceFactory();
+                }
+            };
+
+    @Override
+    public String getName() {
+        return "hello-world";
+    }
+
+    @Override
+    public void initialize(Bootstrap<HelloWorldConfiguration> bootstrap) {
+        bootstrap.addCommand(new RenderCommand());
+        bootstrap.addBundle(new AssetsBundle());
+        bootstrap.addBundle(new MigrationsBundle<HelloWorldConfiguration>() {
+            @Override
+            public DataSourceFactory getDataSourceFactory(HelloWorldConfiguration configuration) {
+                return configuration.getDataSourceFactory();
+            }
+        });
+        bootstrap.addBundle(hibernateBundle);
+        bootstrap.addBundle(new ViewBundle());
+    }
+
+    @Override
+    public void run(HelloWorldConfiguration configuration,
+                    Environment environment) throws ClassNotFoundException {
+        final PersonDAO dao = new PersonDAO(hibernateBundle.getSessionFactory());
+        final Template template = configuration.buildTemplate();
+
+        environment.healthChecks().register("template", new TemplateHealthCheck(template));
+
+        environment.jersey().register(new BasicAuthProvider<>(new ExampleAuthenticator(),
+                                                              "SUPER SECRET STUFF"));
+        environment.jersey().register(new HelloWorldResource(template));
+        environment.jersey().register(new ViewResource());
+        environment.jersey().register(new ProtectedResource());
+        environment.jersey().register(new PeopleResource(dao));
+        environment.jersey().register(new PersonResource(dao));
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java
new file mode 100644
index 0000000..d6c4f56
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java
@@ -0,0 +1,56 @@
+package com.example.helloworld;
+
+import com.example.helloworld.core.Template;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DataSourceFactory;
+import org.hibernate.validator.constraints.NotEmpty;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+public class HelloWorldConfiguration extends Configuration {
+    @NotEmpty
+    private String template;
+
+    @NotEmpty
+    private String defaultName = "Stranger";
+
+    @Valid
+    @NotNull
+    private DataSourceFactory database = new DataSourceFactory();
+
+    @JsonProperty
+    public String getTemplate() {
+        return template;
+    }
+
+    @JsonProperty
+    public void setTemplate(String template) {
+        this.template = template;
+    }
+
+    @JsonProperty
+    public String getDefaultName() {
+        return defaultName;
+    }
+
+    @JsonProperty
+    public void setDefaultName(String defaultName) {
+        this.defaultName = defaultName;
+    }
+
+    public Template buildTemplate() {
+        return new Template(template, defaultName);
+    }
+
+    @JsonProperty("database")
+    public DataSourceFactory getDataSourceFactory() {
+        return database;
+    }
+
+    @JsonProperty("database")
+    public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
+        this.database = dataSourceFactory;
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthenticator.java b/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthenticator.java
new file mode 100644
index 0000000..2957cb9
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthenticator.java
@@ -0,0 +1,17 @@
+package com.example.helloworld.auth;
+
+import com.example.helloworld.core.User;
+import com.google.common.base.Optional;
+import io.dropwizard.auth.AuthenticationException;
+import io.dropwizard.auth.Authenticator;
+import io.dropwizard.auth.basic.BasicCredentials;
+
+public class ExampleAuthenticator implements Authenticator<BasicCredentials, User> {
+    @Override
+    public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
+        if ("secret".equals(credentials.getPassword())) {
+            return Optional.of(new User(credentials.getUsername()));
+        }
+        return Optional.absent();
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/cli/RenderCommand.java b/dropwizard-example/src/main/java/com/example/helloworld/cli/RenderCommand.java
new file mode 100644
index 0000000..ed3f1dc
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/cli/RenderCommand.java
@@ -0,0 +1,48 @@
+package com.example.helloworld.cli;
+
+import com.example.helloworld.HelloWorldConfiguration;
+import com.example.helloworld.core.Template;
+import com.google.common.base.Optional;
+import io.dropwizard.cli.ConfiguredCommand;
+import io.dropwizard.setup.Bootstrap;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class RenderCommand extends ConfiguredCommand<HelloWorldConfiguration> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(RenderCommand.class);
+
+    public RenderCommand() {
+        super("render", "Render the template data to console");
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+        subparser.addArgument("-i", "--include-default")
+                 .action(Arguments.storeTrue())
+                 .dest("include-default")
+                 .help("Also render the template with the default name");
+        subparser.addArgument("names").nargs("*");
+    }
+
+    @Override
+    protected void run(Bootstrap<HelloWorldConfiguration> bootstrap,
+                       Namespace namespace,
+                       HelloWorldConfiguration configuration) throws Exception {
+        final Template template = configuration.buildTemplate();
+
+        if (namespace.getBoolean("include-default")) {
+            LOGGER.info("DEFAULT => {}", template.render(Optional.<String>absent()));
+        }
+
+        for (String name : namespace.<String>getList("names")) {
+            for (int i = 0; i < 1000; i++) {
+                LOGGER.info("{} => {}", name, template.render(Optional.of(name)));
+                Thread.sleep(1000);
+            }
+        }
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/core/Person.java b/dropwizard-example/src/main/java/com/example/helloworld/core/Person.java
new file mode 100644
index 0000000..0a5d27f
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/core/Person.java
@@ -0,0 +1,51 @@
+package com.example.helloworld.core;
+
+import javax.persistence.*;
+
+ at Entity
+ at Table(name = "people")
+ at NamedQueries({
+    @NamedQuery(
+        name = "com.example.helloworld.core.Person.findAll",
+        query = "SELECT p FROM Person p"
+    ),
+    @NamedQuery(
+        name = "com.example.helloworld.core.Person.findById",
+        query = "SELECT p FROM Person p WHERE p.id = :id"
+    )
+})
+public class Person {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private long id;
+
+    @Column(name = "fullName", nullable = false)
+    private String fullName;
+
+    @Column(name = "jobTitle", nullable = false)
+    private String jobTitle;
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+
+    public String getFullName() {
+        return fullName;
+    }
+
+    public void setFullName(String fullName) {
+        this.fullName = fullName;
+    }
+
+    public String getJobTitle() {
+        return jobTitle;
+    }
+
+    public void setJobTitle(String jobTitle) {
+        this.jobTitle = jobTitle;
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/core/Saying.java b/dropwizard-example/src/main/java/com/example/helloworld/core/Saying.java
new file mode 100644
index 0000000..dc176f8
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/core/Saying.java
@@ -0,0 +1,30 @@
+package com.example.helloworld.core;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.hibernate.validator.constraints.Length;
+
+public class Saying {
+    private long id;
+
+    @Length(max = 3)
+    private String content;
+
+    public Saying() {
+        // Jackson deserialization
+    }
+
+    public Saying(long id, String content) {
+        this.id = id;
+        this.content = content;
+    }
+
+    @JsonProperty
+    public long getId() {
+        return id;
+    }
+
+    @JsonProperty
+    public String getContent() {
+        return content;
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/core/Template.java b/dropwizard-example/src/main/java/com/example/helloworld/core/Template.java
new file mode 100644
index 0000000..897260b
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/core/Template.java
@@ -0,0 +1,19 @@
+package com.example.helloworld.core;
+
+import com.google.common.base.Optional;
+
+import static java.lang.String.format;
+
+public class Template {
+    private final String content;
+    private final String defaultName;
+
+    public Template(String content, String defaultName) {
+        this.content = content;
+        this.defaultName = defaultName;
+    }
+    
+    public String render(Optional<String> name) {
+        return format(content, name.or(defaultName));
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/core/User.java b/dropwizard-example/src/main/java/com/example/helloworld/core/User.java
new file mode 100644
index 0000000..14c49f6
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/core/User.java
@@ -0,0 +1,13 @@
+package com.example.helloworld.core;
+
+public class User {
+    private final String name;
+
+    public User(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/db/PersonDAO.java b/dropwizard-example/src/main/java/com/example/helloworld/db/PersonDAO.java
new file mode 100644
index 0000000..3e2fecc
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/db/PersonDAO.java
@@ -0,0 +1,26 @@
+package com.example.helloworld.db;
+
+import com.example.helloworld.core.Person;
+import com.google.common.base.Optional;
+import io.dropwizard.hibernate.AbstractDAO;
+import org.hibernate.SessionFactory;
+
+import java.util.List;
+
+public class PersonDAO extends AbstractDAO<Person> {
+    public PersonDAO(SessionFactory factory) {
+        super(factory);
+    }
+
+    public Optional<Person> findById(Long id) {
+        return Optional.fromNullable(get(id));
+    }
+
+    public Person create(Person person) {
+        return persist(person);
+    }
+
+    public List<Person> findAll() {
+        return list(namedQuery("com.example.helloworld.core.Person.findAll"));
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/health/TemplateHealthCheck.java b/dropwizard-example/src/main/java/com/example/helloworld/health/TemplateHealthCheck.java
new file mode 100644
index 0000000..84aaae3
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/health/TemplateHealthCheck.java
@@ -0,0 +1,20 @@
+package com.example.helloworld.health;
+
+import com.codahale.metrics.health.HealthCheck;
+import com.example.helloworld.core.Template;
+import com.google.common.base.Optional;
+
+public class TemplateHealthCheck extends HealthCheck {
+    private final Template template;
+
+    public TemplateHealthCheck(Template template) {
+        this.template = template;
+    }
+
+    @Override
+    protected Result check() throws Exception {
+        template.render(Optional.of("woo"));
+        template.render(Optional.<String>absent());
+        return Result.healthy();
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/HelloWorldResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/HelloWorldResource.java
new file mode 100644
index 0000000..2973219
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/HelloWorldResource.java
@@ -0,0 +1,41 @@
+package com.example.helloworld.resources;
+
+import com.codahale.metrics.annotation.Timed;
+import com.example.helloworld.core.Saying;
+import com.example.helloworld.core.Template;
+import com.google.common.base.Optional;
+import io.dropwizard.jersey.caching.CacheControl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.validation.Valid;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+ at Path("/hello-world")
+ at Produces(MediaType.APPLICATION_JSON)
+public class HelloWorldResource {
+    private static final Logger LOGGER = LoggerFactory.getLogger(HelloWorldResource.class);
+
+    private final Template template;
+    private final AtomicLong counter;
+
+    public HelloWorldResource(Template template) {
+        this.template = template;
+        this.counter = new AtomicLong();
+    }
+
+    @GET
+    @Timed(name = "get-requests")
+    @CacheControl(maxAge = 1, maxAgeUnit = TimeUnit.DAYS)
+    public Saying sayHello(@QueryParam("name") Optional<String> name) {
+        return new Saying(counter.incrementAndGet(), template.render(name));
+    }
+
+    @POST
+    public void receiveHello(@Valid Saying saying) {
+        LOGGER.info("Received a saying: {}", saying);
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/PeopleResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/PeopleResource.java
new file mode 100644
index 0000000..40ad6d2
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/PeopleResource.java
@@ -0,0 +1,36 @@
+package com.example.helloworld.resources;
+
+import com.example.helloworld.core.Person;
+import com.example.helloworld.db.PersonDAO;
+import io.dropwizard.hibernate.UnitOfWork;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.List;
+
+ at Path("/people")
+ at Produces(MediaType.APPLICATION_JSON)
+public class PeopleResource {
+
+    private final PersonDAO peopleDAO;
+
+    public PeopleResource(PersonDAO peopleDAO) {
+        this.peopleDAO = peopleDAO;
+    }
+
+    @POST
+    @UnitOfWork
+    public Person createPerson(Person person) {
+        return peopleDAO.create(person);
+    }
+
+    @GET
+    @UnitOfWork
+    public List<Person> listPeople() {
+        return peopleDAO.findAll();
+    }
+
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/PersonResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/PersonResource.java
new file mode 100644
index 0000000..afc722d
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/PersonResource.java
@@ -0,0 +1,56 @@
+package com.example.helloworld.resources;
+
+import com.example.helloworld.core.Person;
+import com.example.helloworld.db.PersonDAO;
+import com.example.helloworld.views.PersonView;
+import com.google.common.base.Optional;
+import com.sun.jersey.api.NotFoundException;
+import io.dropwizard.hibernate.UnitOfWork;
+import io.dropwizard.jersey.params.LongParam;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+ at Path("/people/{personId}")
+ at Produces(MediaType.APPLICATION_JSON)
+public class PersonResource {
+
+    private final PersonDAO peopleDAO;
+
+    public PersonResource(PersonDAO peopleDAO) {
+        this.peopleDAO = peopleDAO;
+    }
+
+    @GET
+    @UnitOfWork
+    public Person getPerson(@PathParam("personId") LongParam personId) {
+        return findSafely(personId.get());
+    }
+
+	private Person findSafely(long personId) {
+		final Optional<Person> person = peopleDAO.findById(personId);
+        if (!person.isPresent()) {
+            throw new NotFoundException("No such user.");
+        }
+		return person.get();
+	}
+
+    @GET
+    @Path("/view_freemarker")
+    @UnitOfWork
+    @Produces(MediaType.TEXT_HTML)
+    public PersonView getPersonViewFreemarker(@PathParam("personId") LongParam personId) {
+        return new PersonView(PersonView.Template.FREEMARKER, findSafely(personId.get()));
+    }
+    
+    @GET
+    @Path("/view_mustache")
+    @UnitOfWork
+    @Produces(MediaType.TEXT_HTML)
+    public PersonView getPersonViewMustache(@PathParam("personId") LongParam personId) {
+    	return new PersonView(PersonView.Template.MUSTACHE, findSafely(personId.get()));    
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedResource.java
new file mode 100644
index 0000000..2617800
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedResource.java
@@ -0,0 +1,18 @@
+package com.example.helloworld.resources;
+
+import com.example.helloworld.core.User;
+import io.dropwizard.auth.Auth;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+ at Path("/protected")
+ at Produces(MediaType.TEXT_PLAIN)
+public class ProtectedResource {
+    @GET
+    public String showSecret(@Auth User user) {
+        return String.format("Hey there, %s. You know the secret!", user.getName());
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/ViewResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/ViewResource.java
new file mode 100644
index 0000000..37ab976
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/ViewResource.java
@@ -0,0 +1,43 @@
+package com.example.helloworld.resources;
+
+import com.google.common.base.Charsets;
+import io.dropwizard.views.View;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+
+ at Path("/views")
+public class ViewResource {
+    @GET
+    @Produces("text/html;charset=UTF-8")
+    @Path("/utf8.ftl")
+    public View freemarkerUTF8() {
+        return new View("/views/ftl/utf8.ftl", Charsets.UTF_8) {
+        };
+    }
+
+    @GET
+    @Produces("text/html;charset=ISO-8859-1")
+    @Path("/iso88591.ftl")
+    public View freemarkerISO88591() {
+        return new View("/views/ftl/iso88591.ftl", Charsets.ISO_8859_1) {
+        };
+    }
+
+    @GET
+    @Produces("text/html;charset=UTF-8")
+    @Path("/utf8.mustache")
+    public View mustacheUTF8() {
+        return new View("/views/mustache/utf8.mustache", Charsets.UTF_8) {
+        };
+    }
+
+    @GET
+    @Produces("text/html;charset=ISO-8859-1")
+    @Path("/iso88591.mustache")
+    public View mustacheISO88591() {
+        return new View("/views/mustache/iso88591.mustache", Charsets.ISO_8859_1) {
+        };
+    }
+}
diff --git a/dropwizard-example/src/main/java/com/example/helloworld/views/PersonView.java b/dropwizard-example/src/main/java/com/example/helloworld/views/PersonView.java
new file mode 100644
index 0000000..1e68326
--- /dev/null
+++ b/dropwizard-example/src/main/java/com/example/helloworld/views/PersonView.java
@@ -0,0 +1,31 @@
+package com.example.helloworld.views;
+
+import com.example.helloworld.core.Person;
+
+import io.dropwizard.views.View;
+
+public class PersonView extends View {
+    private final Person person;
+    public enum Template{
+    	FREEMARKER("freemarker/person.ftl"),
+    	MUSTACHE("mustache/person.mustache");
+    	
+    	private String templateName;
+    	private Template(String templateName){
+    		this.templateName = templateName;
+    	}
+    	
+    	public String getTemplateName(){
+    		return templateName;
+    	}
+    }
+
+    public PersonView(PersonView.Template template, Person person) {
+        super(template.getTemplateName());
+        this.person = person;
+    }
+
+    public Person getPerson() {
+        return person;
+    }
+}
\ No newline at end of file
diff --git a/dropwizard-example/src/main/resources/assets/example.txt b/dropwizard-example/src/main/resources/assets/example.txt
new file mode 100644
index 0000000..e71db1d
--- /dev/null
+++ b/dropwizard-example/src/main/resources/assets/example.txt
@@ -0,0 +1 @@
+Hello, I'm an example static asset file.
diff --git a/dropwizard-example/src/main/resources/banner.txt b/dropwizard-example/src/main/resources/banner.txt
new file mode 100644
index 0000000..1cbdb95
--- /dev/null
+++ b/dropwizard-example/src/main/resources/banner.txt
@@ -0,0 +1,8 @@
+                           web-scale hello world dP for the web
+                                                 88
+  .d8888b. dP.  .dP .d8888b. 88d8b.d8b. 88d888b. 88 .d8888b.
+  88ooood8  `8bd8'  88'  `88 88'`88'`88 88'  `88 88 88ooood8
+  88.  ...  .d88b.  88.  .88 88  88  88 88.  .88 88 88.  ...
+  `88888P' dP'  `dP `88888P8 dP  dP  dP 88Y888P' dP `88888P'
+                                        88
+                                        dP
diff --git a/dropwizard-example/src/main/resources/com/example/helloworld/views/freemarker/person.ftl b/dropwizard-example/src/main/resources/com/example/helloworld/views/freemarker/person.ftl
new file mode 100644
index 0000000..64d7da3
--- /dev/null
+++ b/dropwizard-example/src/main/resources/com/example/helloworld/views/freemarker/person.ftl
@@ -0,0 +1,7 @@
+<#-- @ftlvariable name="" type="com.example.views.PersonView" -->
+<html>
+    <body>
+        <!-- calls getPerson().getFullName() and sanitizes it -->
+        <h1>Hello, ${person.fullName?html}!</h1>
+    </body>
+</html>
\ No newline at end of file
diff --git a/dropwizard-example/src/main/resources/com/example/helloworld/views/mustache/person.mustache b/dropwizard-example/src/main/resources/com/example/helloworld/views/mustache/person.mustache
new file mode 100644
index 0000000..ddddf8d
--- /dev/null
+++ b/dropwizard-example/src/main/resources/com/example/helloworld/views/mustache/person.mustache
@@ -0,0 +1,5 @@
+<html>
+    <body>
+        <h1>Hello {{person.fullName}}</h1>
+    </body>
+</html>
\ No newline at end of file
diff --git a/dropwizard-example/src/main/resources/migrations.xml b/dropwizard-example/src/main/resources/migrations.xml
new file mode 100644
index 0000000..4ec4964
--- /dev/null
+++ b/dropwizard-example/src/main/resources/migrations.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<databaseChangeLog
+        xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
+         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
+
+    <changeSet id="1" author="codahale">
+        <createTable tableName="people">
+            <column name="id" type="bigint" autoIncrement="true">
+                <constraints primaryKey="true" nullable="false"/>
+            </column>
+            <column name="fullName" type="varchar(255)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="jobTitle" type="varchar(255)"/>
+        </createTable>
+    </changeSet>
+</databaseChangeLog>
diff --git a/dropwizard-example/src/main/resources/views/ftl/iso88591.ftl b/dropwizard-example/src/main/resources/views/ftl/iso88591.ftl
new file mode 100644
index 0000000..be7e5c9
--- /dev/null
+++ b/dropwizard-example/src/main/resources/views/ftl/iso88591.ftl
@@ -0,0 +1,10 @@
+<html>
+<body>
+
+<h1>This is an example of a view containing ISO-8859-1 characters</h1>
+
+���������������������������
+
+</body>
+</html>
+
diff --git a/dropwizard-example/src/main/resources/views/ftl/utf8.ftl b/dropwizard-example/src/main/resources/views/ftl/utf8.ftl
new file mode 100644
index 0000000..86d499e
--- /dev/null
+++ b/dropwizard-example/src/main/resources/views/ftl/utf8.ftl
@@ -0,0 +1,9 @@
+<html>
+<body>
+
+<h1>This is an example of a view containing UTF-8 characters</h1>
+
+€€€€€€€€€€€€€€€€€€
+
+</body>
+</html>
\ No newline at end of file
diff --git a/dropwizard-example/src/main/resources/views/mustache/iso88591.mustache b/dropwizard-example/src/main/resources/views/mustache/iso88591.mustache
new file mode 100644
index 0000000..be7e5c9
--- /dev/null
+++ b/dropwizard-example/src/main/resources/views/mustache/iso88591.mustache
@@ -0,0 +1,10 @@
+<html>
+<body>
+
+<h1>This is an example of a view containing ISO-8859-1 characters</h1>
+
+���������������������������
+
+</body>
+</html>
+
diff --git a/dropwizard-example/src/main/resources/views/mustache/utf8.mustache b/dropwizard-example/src/main/resources/views/mustache/utf8.mustache
new file mode 100644
index 0000000..86d499e
--- /dev/null
+++ b/dropwizard-example/src/main/resources/views/mustache/utf8.mustache
@@ -0,0 +1,9 @@
+<html>
+<body>
+
+<h1>This is an example of a view containing UTF-8 characters</h1>
+
+€€€€€€€€€€€€€€€€€€
+
+</body>
+</html>
\ No newline at end of file
diff --git a/dropwizard-forms/pom.xml b/dropwizard-forms/pom.xml
new file mode 100644
index 0000000..a1bd431
--- /dev/null
+++ b/dropwizard-forms/pom.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-forms</artifactId>
+    <packaging>pom</packaging>
+    <name>Dropwizard Multipart Form Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.sun.jersey.contribs</groupId>
+            <artifactId>jersey-multipart</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-hibernate/pom.xml b/dropwizard-hibernate/pom.xml
new file mode 100644
index 0000000..08eecb0
--- /dev/null
+++ b/dropwizard-hibernate/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-hibernate</artifactId>
+    <name>Dropwizard Hibernate Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-db</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-hibernate4</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jadira.usertype</groupId>
+            <artifactId>usertype.core</artifactId>
+            <version>3.0.0.GA</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.hibernate</groupId>
+                    <artifactId>hibernate-entitymanager</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.joda</groupId>
+                    <artifactId>joda-money</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-core</artifactId>
+            <version>4.3.5.Final</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.jboss.logging</groupId>
+                    <artifactId>jboss-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <!-- we need HSQL because it handles time zones, H2 totally doesn't -->
+        <dependency>
+            <groupId>org.hsqldb</groupId>
+            <artifactId>hsqldb</artifactId>
+            <version>2.3.2</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-inmemory</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/AbstractDAO.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/AbstractDAO.java
new file mode 100644
index 0000000..65eba86
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/AbstractDAO.java
@@ -0,0 +1,168 @@
+package io.dropwizard.hibernate;
+
+import io.dropwizard.util.Generics;
+import org.hibernate.*;
+
+import java.io.Serializable;
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * An abstract base class for Hibernate DAO classes.
+ *
+ * @param <E> the class which this DAO manages
+ */
+public class AbstractDAO<E> {
+    private final SessionFactory sessionFactory;
+    private final Class<?> entityClass;
+
+    /**
+     * Creates a new DAO with a given session provider.
+     *
+     * @param sessionFactory    a session provider
+     */
+    public AbstractDAO(SessionFactory sessionFactory) {
+        this.sessionFactory = checkNotNull(sessionFactory);
+        this.entityClass = Generics.getTypeParameter(getClass());
+    }
+
+    /**
+     * Returns the current {@link Session}.
+     *
+     * @return the current session
+     */
+    protected Session currentSession() {
+        return sessionFactory.getCurrentSession();
+    }
+
+    /**
+     * Creates a new {@link Criteria} query for {@code <E>}.
+     *
+     * @return a new {@link Criteria} query
+     * @see Session#createCriteria(Class)
+     */
+    protected Criteria criteria() {
+        return currentSession().createCriteria(entityClass);
+    }
+
+    /**
+     * Returns a named {@link Query}.
+     *
+     * @param queryName the name of the query
+     * @return the named query
+     * @see Session#getNamedQuery(String)
+     */
+    protected Query namedQuery(String queryName) throws HibernateException {
+        return currentSession().getNamedQuery(checkNotNull(queryName));
+    }
+
+    /**
+     * Returns the entity class managed by this DAO.
+     *
+     * @return the entity class managed by this DAO
+     */
+    @SuppressWarnings("unchecked")
+    public Class<E> getEntityClass() {
+        return (Class<E>) entityClass;
+    }
+
+    /**
+     * Convenience method to return a single instance that matches the criteria, or null if the
+     * criteria returns no results.
+     *
+     * @param criteria the {@link Criteria} query to run
+     * @return the single result or {@code null}
+     * @throws HibernateException if there is more than one matching result
+     * @see Criteria#uniqueResult()
+     */
+    @SuppressWarnings("unchecked")
+    protected E uniqueResult(Criteria criteria) throws HibernateException {
+        return (E) checkNotNull(criteria).uniqueResult();
+    }
+
+    /**
+     * Convenience method to return a single instance that matches the query, or null if the query
+     * returns no results.
+     *
+     * @param query the query to run
+     * @return the single result or {@code null}
+     * @throws HibernateException if there is more than one matching result
+     * @see Query#uniqueResult()
+     */
+    @SuppressWarnings("unchecked")
+    protected E uniqueResult(Query query) throws HibernateException {
+        return (E) checkNotNull(query).uniqueResult();
+    }
+
+    /**
+     * Get the results of a {@link Criteria} query.
+     *
+     * @param criteria the {@link Criteria} query to run
+     * @return the list of matched query results
+     * @see Criteria#list()
+     */
+    @SuppressWarnings("unchecked")
+    protected List<E> list(Criteria criteria) throws HibernateException {
+        return checkNotNull(criteria).list();
+    }
+
+    /**
+     * Get the results of a query.
+     *
+     * @param query the query to run
+     * @return the list of matched query results
+     * @see Query#list()
+     */
+    @SuppressWarnings("unchecked")
+    protected List<E> list(Query query) throws HibernateException {
+        return checkNotNull(query).list();
+    }
+
+    /**
+     * Return the persistent instance of {@code <E>} with the given identifier, or {@code null} if
+     * there is no such persistent instance. (If the instance, or a proxy for the instance, is
+     * already associated with the session, return that instance or proxy.)
+     *
+     * @param id an identifier
+     * @return a persistent instance or {@code null}
+     * @throws HibernateException
+     * @see Session#get(Class, Serializable)
+     */
+    @SuppressWarnings("unchecked")
+    protected E get(Serializable id) {
+        return (E) currentSession().get(entityClass, checkNotNull(id));
+    }
+
+    /**
+     * Either save or update the given instance, depending upon resolution of the unsaved-value
+     * checks (see the manual for discussion of unsaved-value checking).
+     * <p/>
+     * This operation cascades to associated instances if the association is mapped with
+     * <tt>cascade="save-update"</tt>.
+     *
+     * @param entity a transient or detached instance containing new or updated state
+     * @throws HibernateException
+     * @see Session#saveOrUpdate(Object)
+     */
+    protected E persist(E entity) throws HibernateException {
+        currentSession().saveOrUpdate(checkNotNull(entity));
+        return entity;
+    }
+
+    /**
+     * Force initialization of a proxy or persistent collection.
+     * <p/>
+     * Note: This only ensures initialization of a proxy object or collection;
+     * it is not guaranteed that the elements INSIDE the collection will be initialized/materialized.
+     *
+     * @param proxy a persistable object, proxy, persistent collection or {@code null}
+     * @throws HibernateException if we can't initialize the proxy at this time, eg. the {@link Session} was closed
+     */
+    protected <T> T initialize(T proxy) throws HibernateException {
+        if (!Hibernate.isInitialized(proxy)) {
+            Hibernate.initialize(proxy);
+        }
+        return proxy;
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/HibernateBundle.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/HibernateBundle.java
new file mode 100644
index 0000000..1c3132b
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/HibernateBundle.java
@@ -0,0 +1,51 @@
+package io.dropwizard.hibernate;
+
+import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.Configuration;
+import io.dropwizard.ConfiguredBundle;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.db.DatabaseConfiguration;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import org.hibernate.SessionFactory;
+
+public abstract class HibernateBundle<T extends Configuration> implements ConfiguredBundle<T>, DatabaseConfiguration<T> {
+    private SessionFactory sessionFactory;
+
+    private final ImmutableList<Class<?>> entities;
+    private final SessionFactoryFactory sessionFactoryFactory;
+
+    protected HibernateBundle(Class<?> entity, Class<?>... entities) {
+        this(ImmutableList.<Class<?>>builder().add(entity).add(entities).build(),
+             new SessionFactoryFactory());
+    }
+
+    protected HibernateBundle(ImmutableList<Class<?>> entities,
+                              SessionFactoryFactory sessionFactoryFactory) {
+        this.entities = entities;
+        this.sessionFactoryFactory = sessionFactoryFactory;
+    }
+
+    @Override
+    public final void initialize(Bootstrap<?> bootstrap) {
+        bootstrap.getObjectMapper().registerModule(new Hibernate4Module());
+    }
+
+    @Override
+    public final void run(T configuration, Environment environment) throws Exception {
+        final DataSourceFactory dbConfig = getDataSourceFactory(configuration);
+        this.sessionFactory = sessionFactoryFactory.build(this, environment, dbConfig, entities);
+        environment.jersey().register(new UnitOfWorkResourceMethodDispatchAdapter(sessionFactory));
+        environment.healthChecks().register("hibernate",
+                                            new SessionFactoryHealthCheck(sessionFactory,
+                                                                          dbConfig.getValidationQuery()));
+    }
+
+    public SessionFactory getSessionFactory() {
+        return sessionFactory;
+    }
+
+    protected void configure(org.hibernate.cfg.Configuration configuration) {
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/ScanningHibernateBundle.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/ScanningHibernateBundle.java
new file mode 100644
index 0000000..16074a8
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/ScanningHibernateBundle.java
@@ -0,0 +1,52 @@
+package io.dropwizard.hibernate;
+
+import javax.persistence.Entity;
+
+import io.dropwizard.Configuration;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableList.Builder;
+import com.sun.jersey.core.spi.scanning.PackageNamesScanner;
+import com.sun.jersey.spi.scanning.AnnotationScannerListener;
+
+/**
+ * Extension of HibernateBundle that scans given package for entites instead of giving them by hand.
+ *
+ */
+public abstract class ScanningHibernateBundle<T extends Configuration> extends HibernateBundle<T> {
+
+    /**
+     *
+     * @param pckg string with package containing Hibernate entities (classes annotated with Hibernate @Entity annotation)
+     *          e.g. com.codahale.fake.db.directory.entities
+     */
+    protected ScanningHibernateBundle(String pckg) {
+        this(pckg, new SessionFactoryFactory());
+    }
+
+    protected ScanningHibernateBundle(String pckg, SessionFactoryFactory sessionFactoryFactory) {
+        super(findEntityClassesFromDirectory(pckg), sessionFactoryFactory);
+    }
+
+    /**
+     * Method scanning given directory for classes containing Hibernate @Entity annotation
+     *
+     * @param pckg string with package containing Hibernate entities (classes annotated with @Entity annotation)
+     *          e.g. com.codahale.fake.db.directory.entities
+     * @return ImmutableList with classes from given directory annotated with Hibernate @Entity annotation
+     */
+    public static ImmutableList<Class<?>> findEntityClassesFromDirectory(String pckg) {
+        Builder<Class<?>> builder = ImmutableList.<Class<?>>builder();
+
+        PackageNamesScanner scanner = new PackageNamesScanner(new String[] {pckg});
+
+        @SuppressWarnings("unchecked")
+        final AnnotationScannerListener asl = new AnnotationScannerListener(Entity.class);
+        scanner.scan(asl);
+
+        for (Class<?> clazz : asl.getAnnotatedClasses()) {
+            builder.add(clazz);
+        }
+
+        return builder.build();
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryFactory.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryFactory.java
new file mode 100644
index 0000000..a5ee7d0
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryFactory.java
@@ -0,0 +1,97 @@
+package io.dropwizard.hibernate;
+
+import com.google.common.collect.Sets;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.db.ManagedDataSource;
+import io.dropwizard.setup.Environment;
+import org.hibernate.SessionFactory;
+import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
+import org.hibernate.cfg.AvailableSettings;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl;
+import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
+import org.hibernate.service.ServiceRegistry;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.sql.DataSource;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedSet;
+
+public class SessionFactoryFactory {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SessionFactoryFactory.class);
+
+    public SessionFactory build(HibernateBundle<?> bundle,
+                                Environment environment,
+                                DataSourceFactory dbConfig,
+                                List<Class<?>> entities) throws ClassNotFoundException {
+        final ManagedDataSource dataSource = dbConfig.build(environment.metrics(), "hibernate");
+        return build(bundle, environment, dbConfig, dataSource, entities);
+    }
+
+    public SessionFactory build(HibernateBundle<?> bundle,
+                                Environment environment,
+                                DataSourceFactory dbConfig,
+                                ManagedDataSource dataSource,
+                                List<Class<?>> entities) throws ClassNotFoundException {
+        final ConnectionProvider provider = buildConnectionProvider(dataSource,
+                                                                    dbConfig.getProperties());
+        final SessionFactory factory = buildSessionFactory(bundle,
+                                                           dbConfig,
+                                                           provider,
+                                                           dbConfig.getProperties(),
+                                                           entities);
+        final SessionFactoryManager managedFactory = new SessionFactoryManager(factory, dataSource);
+        environment.lifecycle().manage(managedFactory);
+        return factory;
+    }
+
+    private ConnectionProvider buildConnectionProvider(DataSource dataSource,
+                                                       Map<String, String> properties) {
+        final DatasourceConnectionProviderImpl connectionProvider = new DatasourceConnectionProviderImpl();
+        connectionProvider.setDataSource(dataSource);
+        connectionProvider.configure(properties);
+        return connectionProvider;
+    }
+
+    private SessionFactory buildSessionFactory(HibernateBundle<?> bundle,
+                                               DataSourceFactory dbConfig,
+                                               ConnectionProvider connectionProvider,
+                                               Map<String, String> properties,
+                                               List<Class<?>> entities) {
+        final Configuration configuration = new Configuration();
+        configuration.setProperty(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, "managed");
+        configuration.setProperty(AvailableSettings.USE_SQL_COMMENTS, Boolean.toString(dbConfig.isAutoCommentsEnabled()));
+        configuration.setProperty(AvailableSettings.USE_GET_GENERATED_KEYS, "true");
+        configuration.setProperty(AvailableSettings.GENERATE_STATISTICS, "true");
+        configuration.setProperty(AvailableSettings.USE_REFLECTION_OPTIMIZER, "true");
+        configuration.setProperty(AvailableSettings.ORDER_UPDATES, "true");
+        configuration.setProperty(AvailableSettings.ORDER_INSERTS, "true");
+        configuration.setProperty(AvailableSettings.USE_NEW_ID_GENERATOR_MAPPINGS, "true");
+        configuration.setProperty("jadira.usertype.autoRegisterUserTypes", "true");
+        for (Map.Entry<String, String> property : properties.entrySet()) {
+            configuration.setProperty(property.getKey(), property.getValue());
+        }
+
+        addAnnotatedClasses(configuration, entities);
+        bundle.configure(configuration);
+
+        final ServiceRegistry registry = new StandardServiceRegistryBuilder()
+                .addService(ConnectionProvider.class, connectionProvider)
+                .applySettings(properties)
+                .build();
+
+        return configuration.buildSessionFactory(registry);
+    }
+
+    private void addAnnotatedClasses(Configuration configuration,
+                                     Iterable<Class<?>> entities) {
+        final SortedSet<String> entityClasses = Sets.newTreeSet();
+        for (Class<?> klass : entities) {
+            configuration.addAnnotatedClass(klass);
+            entityClasses.add(klass.getCanonicalName());
+        }
+        LOGGER.info("Entity classes: {}", entityClasses);
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryHealthCheck.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryHealthCheck.java
new file mode 100644
index 0000000..03bfcbe
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryHealthCheck.java
@@ -0,0 +1,45 @@
+package io.dropwizard.hibernate;
+
+import com.codahale.metrics.health.HealthCheck;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.Transaction;
+
+public class SessionFactoryHealthCheck extends HealthCheck {
+    private final SessionFactory sessionFactory;
+    private final String validationQuery;
+
+    public SessionFactoryHealthCheck(SessionFactory sessionFactory,
+                                     String validationQuery) {
+        this.sessionFactory = sessionFactory;
+        this.validationQuery = validationQuery;
+    }
+
+    public SessionFactory getSessionFactory() {
+        return sessionFactory;
+    }
+
+    public String getValidationQuery() {
+        return validationQuery;
+    }
+
+    @Override
+    protected Result check() throws Exception {
+        final Session session = sessionFactory.openSession();
+        try {
+            final Transaction txn = session.beginTransaction();
+            try {
+                session.createSQLQuery(validationQuery).list();
+                txn.commit();
+            } catch (Exception e) {
+                if (txn.isActive()) {
+                    txn.rollback();
+                }
+                throw e;
+            }
+        } finally {
+            session.close();
+        }
+        return Result.healthy();
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryManager.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryManager.java
new file mode 100644
index 0000000..4d85455
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryManager.java
@@ -0,0 +1,25 @@
+package io.dropwizard.hibernate;
+
+import io.dropwizard.db.ManagedDataSource;
+import io.dropwizard.lifecycle.Managed;
+import org.hibernate.SessionFactory;
+
+public class SessionFactoryManager implements Managed {
+    private final SessionFactory factory;
+    private final ManagedDataSource dataSource;
+
+    public SessionFactoryManager(SessionFactory factory, ManagedDataSource dataSource) {
+        this.factory = factory;
+        this.dataSource = dataSource;
+    }
+
+    @Override
+    public void start() throws Exception {
+    }
+
+    @Override
+    public void stop() throws Exception {
+        factory.close();
+        dataSource.stop();
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWork.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWork.java
new file mode 100644
index 0000000..c7015f5
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWork.java
@@ -0,0 +1,50 @@
+package io.dropwizard.hibernate;
+
+import org.hibernate.CacheMode;
+import org.hibernate.FlushMode;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * When annotating a Jersey resource method, wraps the method in a Hibernate session.
+ *
+ * @see UnitOfWorkRequestDispatcher
+ */
+ at Target(METHOD)
+ at Retention(RUNTIME)
+ at Documented
+public @interface UnitOfWork {
+    /**
+     * If {@code true}, the Hibernate session will default to loading read-only entities.
+     *
+     * @see org.hibernate.Session#setDefaultReadOnly(boolean)
+     */
+    boolean readOnly() default false;
+
+    /**
+     * If {@code true}, a transaction will be automatically started before the resource method is
+     * invoked, committed if the method returned, and rolled back if an exception was thrown.
+     */
+    boolean transactional() default true;
+
+    /**
+     * The {@link CacheMode} for the session.
+     *
+     * @see CacheMode
+     * @see org.hibernate.Session#setCacheMode(CacheMode)
+     */
+    CacheMode cacheMode() default CacheMode.NORMAL;
+
+    /**
+     * The {@link FlushMode} for the session.
+     *
+     * @see FlushMode
+     * @see org.hibernate.Session#setFlushMode(org.hibernate.FlushMode)
+     */
+    FlushMode flushMode() default FlushMode.AUTO;
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkRequestDispatcher.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkRequestDispatcher.java
new file mode 100644
index 0000000..d9bae75
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkRequestDispatcher.java
@@ -0,0 +1,89 @@
+package io.dropwizard.hibernate;
+
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.spi.dispatch.RequestDispatcher;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.Transaction;
+import org.hibernate.context.internal.ManagedSessionContext;
+
+public class UnitOfWorkRequestDispatcher implements RequestDispatcher {
+    private final UnitOfWork unitOfWork;
+    private final RequestDispatcher dispatcher;
+    private final SessionFactory sessionFactory;
+
+    public UnitOfWorkRequestDispatcher(UnitOfWork unitOfWork,
+                                       RequestDispatcher dispatcher,
+                                       SessionFactory sessionFactory) {
+        this.unitOfWork = unitOfWork;
+        this.dispatcher = dispatcher;
+        this.sessionFactory = sessionFactory;
+    }
+
+    public UnitOfWork getUnitOfWork() {
+        return unitOfWork;
+    }
+
+    public RequestDispatcher getDispatcher() {
+        return dispatcher;
+    }
+
+    public SessionFactory getSessionFactory() {
+        return sessionFactory;
+    }
+
+    @Override
+    public void dispatch(Object resource, HttpContext context) {
+        final Session session = sessionFactory.openSession();
+        try {
+            configureSession(session);
+            ManagedSessionContext.bind(session);
+            beginTransaction(session);
+            try {
+                dispatcher.dispatch(resource, context);
+                commitTransaction(session);
+            } catch (Exception e) {
+                rollbackTransaction(session);
+                this.<RuntimeException>rethrow(e);
+            }
+        } finally {
+            session.close();
+            ManagedSessionContext.unbind(sessionFactory);
+        }
+    }
+
+    private void beginTransaction(Session session) {
+        if (unitOfWork.transactional()) {
+            session.beginTransaction();
+        }
+    }
+
+    private void configureSession(Session session) {
+        session.setDefaultReadOnly(unitOfWork.readOnly());
+        session.setCacheMode(unitOfWork.cacheMode());
+        session.setFlushMode(unitOfWork.flushMode());
+    }
+
+    private void rollbackTransaction(Session session) {
+        if (unitOfWork.transactional()) {
+            final Transaction txn = session.getTransaction();
+            if (txn != null && txn.isActive()) {
+                txn.rollback();
+            }
+        }
+    }
+
+    private void commitTransaction(Session session) {
+        if (unitOfWork.transactional()) {
+            final Transaction txn = session.getTransaction();
+            if (txn != null && txn.isActive()) {
+                txn.commit();
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private <E extends Exception> void rethrow(Exception e) throws E {
+        throw (E) e;
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchAdapter.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchAdapter.java
new file mode 100644
index 0000000..d7d2e69
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchAdapter.java
@@ -0,0 +1,25 @@
+package io.dropwizard.hibernate;
+
+import com.sun.jersey.spi.container.ResourceMethodDispatchAdapter;
+import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
+import org.hibernate.SessionFactory;
+
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class UnitOfWorkResourceMethodDispatchAdapter implements ResourceMethodDispatchAdapter {
+    private final SessionFactory sessionFactory;
+
+    public UnitOfWorkResourceMethodDispatchAdapter(SessionFactory sessionFactory) {
+        this.sessionFactory = sessionFactory;
+    }
+
+    public SessionFactory getSessionFactory() {
+        return sessionFactory;
+    }
+
+    @Override
+    public ResourceMethodDispatchProvider adapt(ResourceMethodDispatchProvider provider) {
+        return new UnitOfWorkResourceMethodDispatchProvider(provider, sessionFactory);
+    }
+}
diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchProvider.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchProvider.java
new file mode 100644
index 0000000..f7dd5de
--- /dev/null
+++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchProvider.java
@@ -0,0 +1,35 @@
+package io.dropwizard.hibernate;
+
+import com.sun.jersey.api.model.AbstractResourceMethod;
+import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
+import com.sun.jersey.spi.dispatch.RequestDispatcher;
+import org.hibernate.SessionFactory;
+
+public class UnitOfWorkResourceMethodDispatchProvider implements ResourceMethodDispatchProvider {
+    private final ResourceMethodDispatchProvider provider;
+    private final SessionFactory sessionFactory;
+
+    public UnitOfWorkResourceMethodDispatchProvider(ResourceMethodDispatchProvider provider,
+                                                    SessionFactory sessionFactory) {
+        this.provider = provider;
+        this.sessionFactory = sessionFactory;
+    }
+
+    public ResourceMethodDispatchProvider getProvider() {
+        return provider;
+    }
+
+    public SessionFactory getSessionFactory() {
+        return sessionFactory;
+    }
+
+    @Override
+    public RequestDispatcher create(AbstractResourceMethod abstractResourceMethod) {
+        final RequestDispatcher dispatcher = provider.create(abstractResourceMethod);
+        final UnitOfWork unitOfWork = abstractResourceMethod.getMethod().getAnnotation(UnitOfWork.class);
+        if (unitOfWork != null) {
+            return new UnitOfWorkRequestDispatcher(unitOfWork, dispatcher, sessionFactory);
+        }
+        return dispatcher;
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/AbstractDAOTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/AbstractDAOTest.java
new file mode 100644
index 0000000..c6fffc7
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/AbstractDAOTest.java
@@ -0,0 +1,182 @@
+package io.dropwizard.hibernate;
+
+import com.google.common.collect.ImmutableList;
+import org.hibernate.*;
+import org.hibernate.proxy.HibernateProxy;
+import org.hibernate.proxy.LazyInitializer;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.Serializable;
+import java.util.List;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+public class AbstractDAOTest {
+    private static class MockDAO extends AbstractDAO<String> {
+        MockDAO(SessionFactory factory) {
+            super(factory);
+        }
+
+        @Override
+        public Session currentSession() {
+            return super.currentSession();
+        }
+
+        @Override
+        public Criteria criteria() {
+            return super.criteria();
+        }
+
+        @Override
+        public Query namedQuery(String queryName) throws HibernateException {
+            return super.namedQuery(queryName);
+        }
+
+        @Override
+        public Class<String> getEntityClass() {
+            return super.getEntityClass();
+        }
+
+        @Override
+        public String uniqueResult(Criteria criteria) throws HibernateException {
+            return super.uniqueResult(criteria);
+        }
+
+        @Override
+        public String uniqueResult(Query query) throws HibernateException {
+            return super.uniqueResult(query);
+        }
+
+        @Override
+        public List<String> list(Criteria criteria) throws HibernateException {
+            return super.list(criteria);
+        }
+
+        @Override
+        public List<String> list(Query query) throws HibernateException {
+            return super.list(query);
+        }
+
+        @Override
+        public String get(Serializable id) {
+            return super.get(id);
+        }
+
+        @Override
+        public String persist(String entity) throws HibernateException {
+            return super.persist(entity);
+        }
+
+        @Override
+        public <T> T initialize(T proxy) {
+            return super.initialize(proxy);
+        }
+    }
+
+    private final SessionFactory factory = mock(SessionFactory.class);
+    private final Criteria criteria = mock(Criteria.class);
+    private final Query query = mock(Query.class);
+    private final Session session = mock(Session.class);
+    private final MockDAO dao = new MockDAO(factory);
+
+    @Before
+    public void setup() throws Exception {
+        when(factory.getCurrentSession()).thenReturn(session);
+        when(session.createCriteria(String.class)).thenReturn(criteria);
+        when(session.getNamedQuery(anyString())).thenReturn(query);
+    }
+
+    @Test
+    public void getsASessionFromTheSessionFactory() throws Exception {
+        assertThat(dao.currentSession())
+                .isSameAs(session);
+    }
+
+    @Test
+    public void hasAnEntityClass() throws Exception {
+        assertThat(dao.getEntityClass())
+                .isEqualTo(String.class);
+    }
+
+    @Test
+    public void getsNamedQueries() throws Exception {
+        assertThat(dao.namedQuery("query-name"))
+                .isEqualTo(query);
+
+        verify(session).getNamedQuery("query-name");
+    }
+
+    @Test
+    public void createsNewCriteriaQueries() throws Exception {
+        assertThat(dao.criteria())
+                .isEqualTo(criteria);
+
+        verify(session).createCriteria(String.class);
+    }
+
+    @Test
+    public void returnsUniqueResultsFromCriteriaQueries() throws Exception {
+        when(criteria.uniqueResult()).thenReturn("woo");
+
+        assertThat(dao.uniqueResult(criteria))
+                .isEqualTo("woo");
+    }
+
+    @Test
+    public void returnsUniqueResultsFromQueries() throws Exception {
+        when(query.uniqueResult()).thenReturn("woo");
+
+        assertThat(dao.uniqueResult(query))
+                .isEqualTo("woo");
+    }
+
+    @Test
+    public void returnsUniqueListsFromCriteriaQueries() throws Exception {
+        when(criteria.list()).thenReturn(ImmutableList.of("woo"));
+
+        assertThat(dao.list(criteria))
+                .containsOnly("woo");
+    }
+
+
+    @Test
+    public void returnsUniqueListsFromQueries() throws Exception {
+        when(query.list()).thenReturn(ImmutableList.of("woo"));
+
+        assertThat(dao.list(query))
+                .containsOnly("woo");
+    }
+
+    @Test
+    public void getsEntitiesById() throws Exception {
+        when(session.get(String.class, 200)).thenReturn("woo!");
+
+        assertThat(dao.get(200))
+                .isEqualTo("woo!");
+
+        verify(session).get(String.class, 200);
+    }
+
+    @Test
+    public void persistsEntities() throws Exception {
+        assertThat(dao.persist("woo"))
+                .isEqualTo("woo");
+
+        verify(session).saveOrUpdate("woo");
+    }
+
+    @Test
+    public void initializesProxies() throws Exception {
+        final LazyInitializer initializer = mock(LazyInitializer.class);
+        when(initializer.isUninitialized()).thenReturn(true);
+        final HibernateProxy proxy = mock(HibernateProxy.class);
+        when(proxy.getHibernateLazyInitializer()).thenReturn(initializer);
+
+        dao.initialize(proxy);
+
+        verify(initializer).initialize();
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/HibernateBundleTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/HibernateBundleTest.java
new file mode 100644
index 0000000..91fd0b3
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/HibernateBundleTest.java
@@ -0,0 +1,103 @@
+package io.dropwizard.hibernate;
+
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.hibernate4.Hibernate4Module;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.jersey.setup.JerseyEnvironment;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import org.hibernate.SessionFactory;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class HibernateBundleTest {
+    private final DataSourceFactory dbConfig = new DataSourceFactory();
+    private final ImmutableList<Class<?>> entities = ImmutableList.<Class<?>>of(Person.class);
+    private final SessionFactoryFactory factory = mock(SessionFactoryFactory.class);
+    private final SessionFactory sessionFactory = mock(SessionFactory.class);
+    private final Configuration configuration = mock(Configuration.class);
+    private final HealthCheckRegistry healthChecks = mock(HealthCheckRegistry.class);
+    private final JerseyEnvironment jerseyEnvironment = mock(JerseyEnvironment.class);
+    private final Environment environment = mock(Environment.class);
+    private final HibernateBundle<Configuration> bundle = new HibernateBundle<Configuration>(entities, factory) {
+        @Override
+        public DataSourceFactory getDataSourceFactory(Configuration configuration) {
+            return dbConfig;
+        }
+    };
+
+    @Before
+    @SuppressWarnings("unchecked")
+    public void setUp() throws Exception {
+        when(environment.healthChecks()).thenReturn(healthChecks);
+        when(environment.jersey()).thenReturn(jerseyEnvironment);
+
+        when(factory.build(eq(bundle),
+                           any(Environment.class),
+                           any(DataSourceFactory.class),
+                           anyList())).thenReturn(sessionFactory);
+    }
+
+    @Test
+    public void addsHibernateSupportToJackson() throws Exception {
+        final ObjectMapper objectMapperFactory = mock(ObjectMapper.class);
+
+        final Bootstrap<?> bootstrap = mock(Bootstrap.class);
+        when(bootstrap.getObjectMapper()).thenReturn(objectMapperFactory);
+
+        bundle.initialize(bootstrap);
+
+        final ArgumentCaptor<Module> captor = ArgumentCaptor.forClass(Module.class);
+        verify(objectMapperFactory).registerModule(captor.capture());
+
+        assertThat(captor.getValue()).isInstanceOf(Hibernate4Module.class);
+    }
+
+    @Test
+    public void buildsASessionFactory() throws Exception {
+        bundle.run(configuration, environment);
+
+        verify(factory).build(bundle, environment, dbConfig, entities);
+    }
+
+    @Test
+    public void registersATransactionalAdapter() throws Exception {
+        bundle.run(configuration, environment);
+
+        final ArgumentCaptor<UnitOfWorkResourceMethodDispatchAdapter> captor =
+                ArgumentCaptor.forClass(UnitOfWorkResourceMethodDispatchAdapter.class);
+        verify(jerseyEnvironment).register(captor.capture());
+
+        assertThat(captor.getValue().getSessionFactory()).isEqualTo(sessionFactory);
+    }
+
+    @Test
+    public void registersASessionFactoryHealthCheck() throws Exception {
+        dbConfig.setValidationQuery("SELECT something");
+
+        bundle.run(configuration, environment);
+
+        final ArgumentCaptor<SessionFactoryHealthCheck> captor =
+                ArgumentCaptor.forClass(SessionFactoryHealthCheck.class);
+        verify(healthChecks).register(eq("hibernate"), captor.capture());
+
+        assertThat(captor.getValue().getSessionFactory()).isEqualTo(sessionFactory);
+
+        assertThat(captor.getValue().getValidationQuery()).isEqualTo("SELECT something");
+    }
+
+    @Test
+    public void hasASessionFactory() throws Exception {
+        bundle.run(configuration, environment);
+
+        assertThat(bundle.getSessionFactory()).isEqualTo(sessionFactory);
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/JerseyIntegrationTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/JerseyIntegrationTest.java
new file mode 100644
index 0000000..24d3b22
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/JerseyIntegrationTest.java
@@ -0,0 +1,194 @@
+package io.dropwizard.hibernate;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jersey.DropwizardResourceConfig;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.setup.Environment;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.After;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.TimeZone;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class JerseyIntegrationTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    public static class PersonDAO extends AbstractDAO<Person> {
+        public PersonDAO(SessionFactory sessionFactory) {
+            super(sessionFactory);
+        }
+
+        public Optional<Person> findByName(String name) {
+            return Optional.fromNullable(get(name));
+        }
+
+        @Override
+        public Person persist(Person entity) {
+            return super.persist(entity);
+        }
+    }
+
+    @Path("/people/{name}")
+    @Produces(MediaType.APPLICATION_JSON)
+    public static class PersonResource {
+        private final PersonDAO dao;
+
+        public PersonResource(PersonDAO dao) {
+            this.dao = dao;
+        }
+
+        @GET
+        @UnitOfWork(readOnly = true)
+        public Optional<Person> find(@PathParam("name") String name) {
+            return dao.findByName(name);
+        }
+
+        @PUT
+        @UnitOfWork
+        public void save(Person person) {
+            dao.persist(person);
+        }
+    }
+
+    private SessionFactory sessionFactory;
+    private TimeZone defaultTZ;
+
+    @Override
+    @After
+    public void tearDown() throws Exception {
+        TimeZone.setDefault(defaultTZ);
+        super.tearDown();
+
+        if (sessionFactory != null) {
+            sessionFactory.close();
+        }
+    }
+
+    @Override
+    public void setUp() throws Exception {
+        super.setUp();
+
+        this.defaultTZ = TimeZone.getDefault();
+        TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        final MetricRegistry metricRegistry = new MetricRegistry();
+        final SessionFactoryFactory factory = new SessionFactoryFactory();
+        final DataSourceFactory dbConfig = new DataSourceFactory();
+        final HibernateBundle<?> bundle = mock(HibernateBundle.class);
+        final Environment environment = mock(Environment.class);
+        final LifecycleEnvironment lifecycleEnvironment = mock(LifecycleEnvironment.class);
+        when(environment.lifecycle()).thenReturn(lifecycleEnvironment);
+        when(environment.metrics()).thenReturn(metricRegistry);
+
+        dbConfig.setUrl("jdbc:hsqldb:mem:DbTest-" + System.nanoTime());
+        dbConfig.setUser("sa");
+        dbConfig.setDriverClass("org.hsqldb.jdbcDriver");
+        dbConfig.setValidationQuery("SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS");
+
+        try {
+            this.sessionFactory = factory.build(bundle,
+                                                environment,
+                                                dbConfig,
+                                                ImmutableList.<Class<?>>of(Person.class));
+        } catch (ClassNotFoundException e) {
+            throw new RuntimeException(e);
+        }
+        final Session session = sessionFactory.openSession();
+        try {
+            session.createSQLQuery("DROP TABLE people IF EXISTS").executeUpdate();
+            session.createSQLQuery(
+                    "CREATE TABLE people (name varchar(100) primary key, email varchar(100), birthday timestamp with time zone)")
+                   .executeUpdate();
+            session.createSQLQuery(
+                    "INSERT INTO people VALUES ('Coda', 'coda at example.com', '1979-01-02 00:22:00+0:00')")
+                   .executeUpdate();
+        } finally {
+            session.close();
+        }
+
+        final DropwizardResourceConfig config = DropwizardResourceConfig.forTesting(new MetricRegistry());
+        config.getSingletons().add(new UnitOfWorkResourceMethodDispatchAdapter(sessionFactory));
+        config.getSingletons().add(new PersonResource(new PersonDAO(sessionFactory)));
+        config.getSingletons().add(new JacksonMessageBodyProvider(Jackson.newObjectMapper(),
+                                                                  Validation.buildDefaultValidatorFactory().getValidator()));
+        return new LowLevelAppDescriptor.Builder(config).build();
+    }
+
+    @Test
+    public void findsExistingData() throws Exception {
+        final Person coda = client().resource("/people/Coda")
+                .accept(MediaType.APPLICATION_JSON)
+                .get(Person.class);
+
+        assertThat(coda.getName())
+                .isEqualTo("Coda");
+
+        assertThat(coda.getEmail())
+                .isEqualTo("coda at example.com");
+
+        assertThat(coda.getBirthday())
+                .isEqualTo(new DateTime(1979, 1, 2, 0, 22, DateTimeZone.UTC));
+    }
+
+    @Test
+    public void doesNotFindMissingData() throws Exception {
+        try {
+            client().resource("/people/Poof")
+                    .accept(MediaType.APPLICATION_JSON)
+                    .get(Person.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(404);
+        }
+    }
+
+    @Test
+    public void createsNewData() throws Exception {
+        final Person person = new Person();
+        person.setName("Hank");
+        person.setEmail("hank at example.com");
+        person.setBirthday(new DateTime(1971, 3, 14, 19, 12, DateTimeZone.UTC));
+
+        client().resource("/people/Hank").type(MediaType.APPLICATION_JSON).put(person);
+
+        final Person hank = client().resource("/people/Hank")
+                .accept(MediaType.APPLICATION_JSON)
+                .get(Person.class);
+
+        assertThat(hank.getName())
+                .isEqualTo("Hank");
+
+        assertThat(hank.getEmail())
+                .isEqualTo("hank at example.com");
+
+        assertThat(hank.getBirthday())
+                .isEqualTo(person.getBirthday());
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/Person.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/Person.java
new file mode 100644
index 0000000..478462b
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/Person.java
@@ -0,0 +1,52 @@
+package io.dropwizard.hibernate;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.joda.time.DateTime;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+
+ at Entity
+ at Table(name = "people")
+public class Person {
+    @Id
+    private String name;
+
+    @Column
+    private String email;
+
+    @Column
+    private DateTime birthday;
+
+    @JsonProperty
+    public String getName() {
+        return name;
+    }
+
+    @JsonProperty
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @JsonProperty
+    public String getEmail() {
+        return email;
+    }
+
+    @JsonProperty
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    @JsonProperty
+    public DateTime getBirthday() {
+        return birthday;
+    }
+
+    @JsonProperty
+    public void setBirthday(DateTime birthday) {
+        this.birthday = birthday;
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/ScanningHibernateBundleTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/ScanningHibernateBundleTest.java
new file mode 100644
index 0000000..8020ef0
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/ScanningHibernateBundleTest.java
@@ -0,0 +1,23 @@
+package io.dropwizard.hibernate;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class ScanningHibernateBundleTest {
+
+    @Test
+    public void testFindEntityClassesFromDirectory() {
+        //given
+        String packageWithEntities = "io.dropwizard.hibernate.fake.entities.pckg";
+        //when
+        ImmutableList<Class<?>> findEntityClassesFromDirectory = ScanningHibernateBundle.findEntityClassesFromDirectory(packageWithEntities);
+
+        //then
+        assertFalse(findEntityClassesFromDirectory.isEmpty());
+        assertEquals(4, findEntityClassesFromDirectory.size());
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryFactoryTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryFactoryTest.java
new file mode 100644
index 0000000..6d17335
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryFactoryTest.java
@@ -0,0 +1,99 @@
+package io.dropwizard.hibernate;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.setup.Environment;
+import org.hibernate.Session;
+import org.hibernate.SessionFactory;
+import org.hibernate.cfg.Configuration;
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class SessionFactoryFactoryTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    private final SessionFactoryFactory factory = new SessionFactoryFactory();
+
+    private final HibernateBundle<?> bundle = mock(HibernateBundle.class);
+    private final LifecycleEnvironment lifecycleEnvironment = mock(LifecycleEnvironment.class);
+    private final Environment environment = mock(Environment.class);
+    private final DataSourceFactory config = new DataSourceFactory();
+    private final MetricRegistry metricRegistry = new MetricRegistry();
+
+    private SessionFactory sessionFactory;
+
+    @Before
+    public void setUp() throws Exception {
+        when(environment.metrics()).thenReturn(metricRegistry);
+        when(environment.lifecycle()).thenReturn(lifecycleEnvironment);
+
+        config.setUrl("jdbc:hsqldb:mem:DbTest-" + System.currentTimeMillis());
+        config.setUser("sa");
+        config.setDriverClass("org.hsqldb.jdbcDriver");
+        config.setValidationQuery("SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS");
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (sessionFactory != null) {
+            sessionFactory.close();
+        }
+    }
+
+    @Test
+    public void managesTheSessionFactory() throws Exception {
+        build();
+
+        verify(lifecycleEnvironment).manage(any(SessionFactoryManager.class));
+    }
+
+    @Test
+    public void callsBundleToConfigure() throws Exception {
+      build();
+
+      verify(bundle).configure(any(Configuration.class));
+    }
+
+    @Test
+    public void buildsAWorkingSessionFactory() throws Exception {
+        build();
+
+        final Session session = sessionFactory.openSession();
+        try {
+            session.createSQLQuery("DROP TABLE people IF EXISTS").executeUpdate();
+            session.createSQLQuery("CREATE TABLE people (name varchar(100) primary key, email varchar(100), birthday timestamp)").executeUpdate();
+            session.createSQLQuery("INSERT INTO people VALUES ('Coda', 'coda at example.com', '1979-01-02 00:22:00')").executeUpdate();
+
+            final Person entity = (Person) session.get(Person.class, "Coda");
+
+            assertThat(entity.getName())
+                    .isEqualTo("Coda");
+
+            assertThat(entity.getEmail())
+                    .isEqualTo("coda at example.com");
+
+            assertThat(entity.getBirthday().toDateTime(DateTimeZone.UTC))
+                    .isEqualTo(new DateTime(1979, 1, 2, 0, 22, DateTimeZone.UTC));
+        } finally {
+            session.close();
+        }
+    }
+
+    private void build() throws ClassNotFoundException {
+        this.sessionFactory = factory.build(bundle,
+                                            environment,
+                                            config,
+                                            ImmutableList.<Class<?>>of(Person.class));
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryHealthCheckTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryHealthCheckTest.java
new file mode 100644
index 0000000..07b64c8
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryHealthCheckTest.java
@@ -0,0 +1,79 @@
+package io.dropwizard.hibernate;
+
+import com.codahale.metrics.health.HealthCheck;
+import org.hibernate.*;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.*;
+
+ at SuppressWarnings("HibernateResourceOpenedButNotSafelyClosed")
+public class SessionFactoryHealthCheckTest {
+    private final SessionFactory factory = mock(SessionFactory.class);
+    private final SessionFactoryHealthCheck healthCheck = new SessionFactoryHealthCheck(factory,
+                                                                                        "SELECT 1");
+
+    @Test
+    public void hasASessionFactory() throws Exception {
+        assertThat(healthCheck.getSessionFactory())
+                .isEqualTo(factory);
+    }
+
+    @Test
+    public void hasAValidationQuery() throws Exception {
+        assertThat(healthCheck.getValidationQuery())
+                .isEqualTo("SELECT 1");
+    }
+
+    @Test
+    public void isHealthyIfNoExceptionIsThrown() throws Exception {
+        final Session session = mock(Session.class);
+        when(factory.openSession()).thenReturn(session);
+
+        final Transaction transaction = mock(Transaction.class);
+        when(session.beginTransaction()).thenReturn(transaction);
+
+        final SQLQuery query = mock(SQLQuery.class);
+        when(session.createSQLQuery(anyString())).thenReturn(query);
+
+        assertThat(healthCheck.execute())
+                .isEqualTo(HealthCheck.Result.healthy());
+
+        final InOrder inOrder = inOrder(factory, session, transaction, query);
+        inOrder.verify(factory).openSession();
+        inOrder.verify(session).beginTransaction();
+        inOrder.verify(session).createSQLQuery("SELECT 1");
+        inOrder.verify(query).list();
+        inOrder.verify(transaction).commit();
+        inOrder.verify(session).close();
+    }
+
+    @Test
+    public void isUnhealthyIfAnExceptionIsThrown() throws Exception {
+        final Session session = mock(Session.class);
+        when(factory.openSession()).thenReturn(session);
+
+        final Transaction transaction = mock(Transaction.class);
+        when(session.beginTransaction()).thenReturn(transaction);
+        when(transaction.isActive()).thenReturn(true);
+
+        final SQLQuery query = mock(SQLQuery.class);
+        when(session.createSQLQuery(anyString())).thenReturn(query);
+        when(query.list()).thenThrow(new HibernateException("OH NOE"));
+
+        assertThat(healthCheck.execute().isHealthy())
+                .isFalse();
+
+        final InOrder inOrder = inOrder(factory, session, transaction, query);
+        inOrder.verify(factory).openSession();
+        inOrder.verify(session).beginTransaction();
+        inOrder.verify(session).createSQLQuery("SELECT 1");
+        inOrder.verify(query).list();
+        inOrder.verify(transaction).rollback();
+        inOrder.verify(session).close();
+
+        verify(transaction, never()).commit();
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryManagerTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryManagerTest.java
new file mode 100644
index 0000000..a339de8
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryManagerTest.java
@@ -0,0 +1,28 @@
+package io.dropwizard.hibernate;
+
+import io.dropwizard.db.ManagedDataSource;
+import org.hibernate.SessionFactory;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class SessionFactoryManagerTest {
+    private final SessionFactory factory = mock(SessionFactory.class);
+    private final ManagedDataSource dataSource = mock(ManagedDataSource.class);
+    private final SessionFactoryManager manager = new SessionFactoryManager(factory, dataSource);
+
+    @Test
+    public void closesTheFactoryOnStopping() throws Exception {
+        manager.stop();
+
+        verify(factory).close();
+    }
+
+    @Test
+    public void stopsTheDataSourceOnStopping() throws Exception {
+        manager.stop();
+
+        verify(dataSource).stop();
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkRequestDispatcherTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkRequestDispatcherTest.java
new file mode 100644
index 0000000..ddf232c
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkRequestDispatcherTest.java
@@ -0,0 +1,209 @@
+package io.dropwizard.hibernate;
+
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.spi.dispatch.RequestDispatcher;
+import org.hibernate.*;
+import org.hibernate.context.internal.ManagedSessionContext;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.mockito.Mockito.*;
+
+ at SuppressWarnings("HibernateResourceOpenedButNotSafelyClosed")
+public class UnitOfWorkRequestDispatcherTest {
+    private final UnitOfWork unitOfWork = mock(UnitOfWork.class);
+    private final RequestDispatcher underlying = mock(RequestDispatcher.class);
+    private final SessionFactory sessionFactory = mock(SessionFactory.class);
+    private final UnitOfWorkRequestDispatcher dispatcher =
+            new UnitOfWorkRequestDispatcher(unitOfWork, underlying, sessionFactory);
+
+    private final Object resource = mock(Object.class);
+    private final HttpContext context = mock(HttpContext.class);
+    private final Session session = mock(Session.class);
+    private final Transaction transaction = mock(Transaction.class);
+
+    @Before
+    public void setUp() throws Exception {
+        when(unitOfWork.readOnly()).thenReturn(false);
+        when(unitOfWork.cacheMode()).thenReturn(CacheMode.NORMAL);
+        when(unitOfWork.flushMode()).thenReturn(FlushMode.AUTO);
+        when(unitOfWork.transactional()).thenReturn(true);
+
+        when(sessionFactory.openSession()).thenReturn(session);
+        when(session.getSessionFactory()).thenReturn(sessionFactory);
+        when(session.beginTransaction()).thenReturn(transaction);
+        when(session.getTransaction()).thenReturn(transaction);
+
+        when(transaction.isActive()).thenReturn(true);
+    }
+
+    @Test
+    public void hasAUnitOfWork() throws Exception {
+        assertThat(dispatcher.getUnitOfWork())
+                .isEqualTo(unitOfWork);
+    }
+
+    @Test
+    public void hasADispatcher() throws Exception {
+        assertThat(dispatcher.getDispatcher())
+                .isEqualTo(underlying);
+    }
+
+    @Test
+    public void hasASessionFactory() throws Exception {
+        assertThat(dispatcher.getSessionFactory())
+                .isEqualTo(sessionFactory);
+    }
+
+    @Test
+    public void opensAndClosesASession() throws Exception {
+        dispatcher.dispatch(resource, context);
+
+        final InOrder inOrder = inOrder(sessionFactory, session, underlying);
+        inOrder.verify(sessionFactory).openSession();
+        inOrder.verify(underlying).dispatch(resource, context);
+        inOrder.verify(session).close();
+    }
+
+    @Test
+    public void bindsAndUnbindsTheSessionToTheManagedContext() throws Exception {
+        doAnswer(new Answer<Object>() {
+            @Override
+            public Object answer(InvocationOnMock invocation) throws Throwable {
+                assertThat(ManagedSessionContext.hasBind(sessionFactory))
+                        .isTrue();
+                return null;
+            }
+        }).when(underlying).dispatch(resource, context);
+
+        dispatcher.dispatch(resource, context);
+
+        assertThat(ManagedSessionContext.hasBind(sessionFactory))
+                .isFalse();
+    }
+
+    @Test
+    public void configuresTheSessionsReadOnlyDefault() throws Exception {
+        when(unitOfWork.readOnly()).thenReturn(true);
+
+        dispatcher.dispatch(resource, context);
+
+        verify(session).setDefaultReadOnly(true);
+    }
+
+    @Test
+    public void configuresTheSessionsCacheMode() throws Exception {
+        when(unitOfWork.cacheMode()).thenReturn(CacheMode.IGNORE);
+
+        dispatcher.dispatch(resource, context);
+
+        verify(session).setCacheMode(CacheMode.IGNORE);
+    }
+
+    @Test
+    public void configuresTheSessionsFlushMode() throws Exception {
+        when(unitOfWork.flushMode()).thenReturn(FlushMode.ALWAYS);
+
+        dispatcher.dispatch(resource, context);
+
+        verify(session).setFlushMode(FlushMode.ALWAYS);
+    }
+
+    @Test
+    public void doesNotBeginATransactionIfNotTransactional() throws Exception {
+        when(unitOfWork.transactional()).thenReturn(false);
+        when(session.getTransaction()).thenReturn(null);
+
+        dispatcher.dispatch(resource, context);
+
+        verify(session, never()).beginTransaction();
+        verifyZeroInteractions(transaction);
+    }
+
+    @Test
+    public void beginsAndCommitsATransactionIfTransactional() throws Exception {
+        dispatcher.dispatch(resource, context);
+
+        final InOrder inOrder = inOrder(session, transaction, underlying);
+        inOrder.verify(session).beginTransaction();
+        inOrder.verify(underlying).dispatch(resource, context);
+        inOrder.verify(transaction).commit();
+        inOrder.verify(session).close();
+    }
+
+    @Test
+    public void rollsBackTheTransactionOnException() throws Exception {
+        doThrow(new RuntimeException("OH NO")).when(underlying).dispatch(resource, context);
+
+        try {
+            dispatcher.dispatch(resource, context);
+            failBecauseExceptionWasNotThrown(RuntimeException.class);
+        } catch (RuntimeException e) {
+            assertThat(e.getMessage())
+                    .isEqualTo("OH NO");
+        }
+
+        final InOrder inOrder = inOrder(session, transaction, underlying);
+        inOrder.verify(session).beginTransaction();
+        inOrder.verify(underlying).dispatch(resource, context);
+        inOrder.verify(transaction).rollback();
+        inOrder.verify(session).close();
+    }
+
+    @Test
+    public void doesNotCommitAnInactiveTransaction() throws Exception {
+        when(transaction.isActive()).thenReturn(false);
+
+        dispatcher.dispatch(resource, context);
+
+        verify(transaction, never()).commit();
+    }
+
+    @Test
+    public void doesNotCommitANullTransaction() throws Exception {
+        when(session.getTransaction()).thenReturn(null);
+
+        dispatcher.dispatch(resource, context);
+
+        verify(transaction, never()).commit();
+    }
+
+    @Test
+    public void doesNotRollbackAnInactiveTransaction() throws Exception {
+        when(transaction.isActive()).thenReturn(false);
+
+        doThrow(new RuntimeException("OH NO")).when(underlying).dispatch(resource, context);
+
+        try {
+            dispatcher.dispatch(resource, context);
+            failBecauseExceptionWasNotThrown(RuntimeException.class);
+        } catch (RuntimeException e) {
+            assertThat(e.getMessage())
+                    .isEqualTo("OH NO");
+        }
+
+        verify(transaction, never()).rollback();
+    }
+
+    @Test
+    public void doesNotRollbackANullTransaction() throws Exception {
+        when(session.getTransaction()).thenReturn(null);
+
+        doThrow(new RuntimeException("OH NO")).when(underlying).dispatch(resource, context);
+
+        try {
+            dispatcher.dispatch(resource, context);
+            failBecauseExceptionWasNotThrown(RuntimeException.class);
+        } catch (RuntimeException e) {
+            assertThat(e.getMessage())
+                    .isEqualTo("OH NO");
+        }
+
+        verify(transaction, never()).rollback();
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchAdapterTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchAdapterTest.java
new file mode 100644
index 0000000..817f8f1
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchAdapterTest.java
@@ -0,0 +1,34 @@
+package io.dropwizard.hibernate;
+
+import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
+import org.hibernate.SessionFactory;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class UnitOfWorkResourceMethodDispatchAdapterTest {
+    private final SessionFactory sessionFactory = mock(SessionFactory.class);
+    private final UnitOfWorkResourceMethodDispatchAdapter adapter =
+            new UnitOfWorkResourceMethodDispatchAdapter(sessionFactory);
+
+    @Test
+    public void hasASessionFactory() throws Exception {
+        assertThat(adapter.getSessionFactory())
+                .isEqualTo(sessionFactory);
+    }
+
+    @Test
+    public void decoratesProviders() throws Exception {
+        final ResourceMethodDispatchProvider provider = mock(ResourceMethodDispatchProvider.class);
+
+        final UnitOfWorkResourceMethodDispatchProvider decorator =
+                (UnitOfWorkResourceMethodDispatchProvider) adapter.adapt(provider);
+
+        assertThat(decorator.getProvider())
+                .isEqualTo(provider);
+
+        assertThat(decorator.getSessionFactory())
+                .isEqualTo(sessionFactory);
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchProviderTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchProviderTest.java
new file mode 100644
index 0000000..5e01f56
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkResourceMethodDispatchProviderTest.java
@@ -0,0 +1,65 @@
+package io.dropwizard.hibernate;
+
+import com.sun.jersey.api.model.AbstractResourceMethod;
+import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
+import com.sun.jersey.spi.dispatch.RequestDispatcher;
+import org.hibernate.SessionFactory;
+import org.junit.Test;
+
+import java.lang.reflect.Method;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class UnitOfWorkResourceMethodDispatchProviderTest {
+    @SuppressWarnings("UnusedDeclaration")
+    private static class Example {
+        @UnitOfWork
+        public void annotated() {
+
+        }
+
+        public void nonAnnotated() {}
+
+    }
+
+    private final ResourceMethodDispatchProvider underlying =
+            mock(ResourceMethodDispatchProvider.class);
+    private final SessionFactory sessionFactory = mock(SessionFactory.class);
+    private final UnitOfWorkResourceMethodDispatchProvider provider =
+            new UnitOfWorkResourceMethodDispatchProvider(underlying, sessionFactory);
+
+    @Test
+    public void ignoresNonAnnotatedMethods() throws Exception {
+        final AbstractResourceMethod resourceMethod = mock(AbstractResourceMethod.class);
+        when(resourceMethod.getMethod()).thenReturn(Example.class.getDeclaredMethod("nonAnnotated"));
+
+        final RequestDispatcher dispatcher = mock(RequestDispatcher.class);
+        when(underlying.create(resourceMethod)).thenReturn(dispatcher);
+
+        assertThat(provider.create(resourceMethod))
+                .isEqualTo(dispatcher);
+    }
+
+    @Test
+    public void decoratesAnnotatedMethods() throws Exception {
+        final AbstractResourceMethod resourceMethod = mock(AbstractResourceMethod.class);
+        final Method method = Example.class.getDeclaredMethod("annotated");
+        when(resourceMethod.getMethod()).thenReturn(method);
+
+        final RequestDispatcher dispatcher = mock(RequestDispatcher.class);
+        when(underlying.create(resourceMethod)).thenReturn(dispatcher);
+
+        final UnitOfWorkRequestDispatcher decorator = (UnitOfWorkRequestDispatcher) provider.create(resourceMethod);
+
+        assertThat(decorator.getSessionFactory())
+                .isEqualTo(sessionFactory);
+
+        assertThat(decorator.getDispatcher())
+                .isEqualTo(dispatcher);
+
+        assertThat(decorator.getUnitOfWork())
+                .isEqualTo(method.getAnnotation(UnitOfWork.class));
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkTest.java
new file mode 100644
index 0000000..c26f9b6
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkTest.java
@@ -0,0 +1,49 @@
+package io.dropwizard.hibernate;
+
+import org.hibernate.CacheMode;
+import org.hibernate.FlushMode;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class UnitOfWorkTest {
+    private static class Example {
+        @UnitOfWork
+        public void example() {
+
+        }
+    }
+
+    private UnitOfWork unitOfWork;
+
+    @Before
+    public void setUp() throws Exception {
+        this.unitOfWork = Example.class.getDeclaredMethod("example")
+                                       .getAnnotation(UnitOfWork.class);
+    }
+
+    @Test
+    public void defaultsToReadWrite() throws Exception {
+        assertThat(unitOfWork.readOnly())
+                .isFalse();
+    }
+
+    @Test
+    public void defaultsToTransactional() throws Exception {
+        assertThat(unitOfWork.transactional())
+                .isTrue();
+    }
+
+    @Test
+    public void defaultsToNormalCaching() throws Exception {
+        assertThat(unitOfWork.cacheMode())
+                .isEqualTo(CacheMode.NORMAL);
+    }
+
+    @Test
+    public void defaultsToAutomaticFlushing() throws Exception {
+        assertThat(unitOfWork.flushMode())
+                .isEqualTo(FlushMode.AUTO);
+    }
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/FakeEntity1.java
new file mode 100644
index 0000000..37470c9
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/FakeEntity1.java
@@ -0,0 +1,8 @@
+package io.dropwizard.hibernate.fake.entities.pckg;
+
+import javax.persistence.Entity;
+
+ at Entity
+public class FakeEntity1 {
+
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/FakeEntity2.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/FakeEntity2.java
new file mode 100644
index 0000000..fbf8ed2
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/FakeEntity2.java
@@ -0,0 +1,8 @@
+package io.dropwizard.hibernate.fake.entities.pckg;
+
+import javax.persistence.Entity;
+
+ at Entity
+public class FakeEntity2 {
+
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/deep/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/deep/FakeEntity1.java
new file mode 100644
index 0000000..0e0c077
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/deep/FakeEntity1.java
@@ -0,0 +1,8 @@
+package io.dropwizard.hibernate.fake.entities.pckg.deep;
+
+import javax.persistence.Entity;
+
+ at Entity
+public class FakeEntity1 {
+
+}
diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/deep/deeper/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/deep/deeper/FakeEntity1.java
new file mode 100644
index 0000000..880a808
--- /dev/null
+++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/deep/deeper/FakeEntity1.java
@@ -0,0 +1,9 @@
+package io.dropwizard.hibernate.fake.entities.pckg.deep.deeper;
+
+import javax.persistence.Entity;
+
+ at Entity
+public class FakeEntity1 { 
+
+}
+ 
diff --git a/dropwizard-hibernate/src/test/resources/logback-test.xml b/dropwizard-hibernate/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-hibernate/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-jackson/pom.xml b/dropwizard-jackson/pom.xml
new file mode 100644
index 0000000..82dcb8c
--- /dev/null
+++ b/dropwizard-jackson/pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-jackson</artifactId>
+    <name>Dropwizard Jackson Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-util</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <version>${jackson.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-guava</artifactId>
+            <version>${jackson.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.guava</groupId>
+                    <artifactId>guava</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.module</groupId>
+            <artifactId>jackson-module-afterburner</artifactId>
+            <version>${jackson.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-joda</artifactId>
+            <version>${jackson.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>joda-time</groupId>
+                    <artifactId>joda-time</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategy.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategy.java
new file mode 100644
index 0000000..36a2ac4
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategy.java
@@ -0,0 +1,65 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.introspect.AnnotatedField;
+import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
+import com.fasterxml.jackson.databind.introspect.AnnotatedParameter;
+
+/**
+ * A {@link PropertyNamingStrategy} implementation which, if the declaring class of a property is
+ * annotated with {@link JsonSnakeCase}, uses a
+ * {@link com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy}, and uses
+ * the default {@link PropertyNamingStrategy} otherwise.
+ */
+public class AnnotationSensitivePropertyNamingStrategy extends PropertyNamingStrategy {
+    private static final long serialVersionUID = -1372862028366311230L;
+
+    private final LowerCaseWithUnderscoresStrategy snakeCase;
+
+    public AnnotationSensitivePropertyNamingStrategy() {
+        super();
+        this.snakeCase = new LowerCaseWithUnderscoresStrategy();
+    }
+
+    @Override
+    public String nameForConstructorParameter(MapperConfig<?> config,
+                                              AnnotatedParameter ctorParam,
+                                              String defaultName) {
+        if (ctorParam.getDeclaringClass().isAnnotationPresent(JsonSnakeCase.class)) {
+            return snakeCase.nameForConstructorParameter(config, ctorParam, defaultName);
+        }
+        return super.nameForConstructorParameter(config, ctorParam, defaultName);
+    }
+
+    @Override
+    public String nameForField(MapperConfig<?> config,
+                               AnnotatedField field,
+                               String defaultName) {
+        if (field.getDeclaringClass().isAnnotationPresent(JsonSnakeCase.class)) {
+            return snakeCase.nameForField(config, field, defaultName);
+        }
+
+        return super.nameForField(config, field, defaultName);
+    }
+
+    @Override
+    public String nameForGetterMethod(MapperConfig<?> config,
+                                      AnnotatedMethod method,
+                                      String defaultName) {
+        if (method.getDeclaringClass().isAnnotationPresent(JsonSnakeCase.class)) {
+            return snakeCase.nameForGetterMethod(config, method, defaultName);
+        }
+        return super.nameForGetterMethod(config, method, defaultName);
+    }
+
+    @Override
+    public String nameForSetterMethod(MapperConfig<?> config,
+                                      AnnotatedMethod method,
+                                      String defaultName) {
+        if (method.getDeclaringClass().isAnnotationPresent(JsonSnakeCase.class)) {
+            return snakeCase.nameForSetterMethod(config, method, defaultName);
+        }
+        return super.nameForSetterMethod(config, method, defaultName);
+    }
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Discoverable.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Discoverable.java
new file mode 100644
index 0000000..f4fd77f
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Discoverable.java
@@ -0,0 +1,8 @@
+package io.dropwizard.jackson;
+
+/**
+ * A tag interface which allows Dropwizard to load Jackson subtypes at runtime, which enables polymorphic
+ * configurations.
+ */
+public interface Discoverable {
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/DiscoverableSubtypeResolver.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/DiscoverableSubtypeResolver.java
new file mode 100644
index 0000000..0e73929
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/DiscoverableSubtypeResolver.java
@@ -0,0 +1,77 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.databind.jsontype.impl.StdSubtypeResolver;
+import com.google.common.base.Charsets;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.Enumeration;
+import java.util.List;
+
+/**
+ * A subtype resolver which discovers subtypes via
+ * {@code META-INF/services/io.dropwizard.jackson.Discoverable}.
+ */
+public class DiscoverableSubtypeResolver extends StdSubtypeResolver {
+    private static final Logger LOGGER = LoggerFactory.getLogger(DiscoverableSubtypeResolver.class);
+
+    private final ImmutableList<Class<?>> discoveredSubtypes;
+
+    public DiscoverableSubtypeResolver() {
+        this(Discoverable.class);
+    }
+
+    public DiscoverableSubtypeResolver(Class<?> rootKlass) {
+        final ImmutableList.Builder<Class<?>> subtypes = ImmutableList.builder();
+        for (Class<?> klass : discoverServices(rootKlass)) {
+            for (Class<?> subtype : discoverServices(klass)) {
+                subtypes.add(subtype);
+                registerSubtypes(subtype);
+            }
+        }
+        this.discoveredSubtypes = subtypes.build();
+    }
+
+    public ImmutableList<Class<?>> getDiscoveredSubtypes() {
+        return discoveredSubtypes;
+    }
+
+    protected ClassLoader getClassLoader() {
+        return this.getClass().getClassLoader();
+    }
+
+    protected List<Class<?>> discoverServices(Class<?> klass) {
+        final List<Class<?>> serviceClasses = Lists.newArrayList();
+        try {
+            // use classloader that loaded this class to find the service descriptors on the classpath
+            // better than ClassLoader.getSystemResources() which may not be the same classloader if ths app
+            // is running in a container (e.g. via maven exec:java)
+            final Enumeration<URL> resources = getClassLoader().getResources("META-INF/services/" + klass.getName());
+            while (resources.hasMoreElements()) {
+                final URL url = resources.nextElement();
+                try (InputStream input = url.openStream();
+                     InputStreamReader streamReader = new InputStreamReader(input, Charsets.UTF_8);
+                     BufferedReader reader = new BufferedReader(streamReader)) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        try {
+                            serviceClasses.add(getClassLoader().loadClass(line.trim()));
+                        } catch (ClassNotFoundException e) {
+                            LOGGER.info("Unable to load {}", line);
+                        }
+                    }
+                }
+            }
+        } catch (IOException e) {
+            LOGGER.warn("Unable to load META-INF/services/{}", klass.getName(), e);
+        }
+        return serviceClasses;
+    }
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/FuzzyEnumModule.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/FuzzyEnumModule.java
new file mode 100644
index 0000000..67fb155
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/FuzzyEnumModule.java
@@ -0,0 +1,80 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.Deserializers;
+import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer;
+import com.google.common.base.CharMatcher;
+import com.google.common.collect.Lists;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A module for deserializing enums that is more permissive than the default.
+ * <p/>
+ * This deserializer is more permissive in the following ways:
+ * <ul>
+ * <li>Whitespace is permitted but stripped from the input.</li>
+ * <li>Dashes in the value are converted to underscores.</li>
+ * <li>Matching against the enum values is case insensitive.</li>
+ * </ul>
+ */
+public class FuzzyEnumModule extends Module {
+    private static class PermissiveEnumDeserializer extends StdScalarDeserializer<Enum<?>> {
+        private final Enum<?>[] constants;
+        private final List<String> acceptedValues;
+
+        @SuppressWarnings("unchecked")
+        protected PermissiveEnumDeserializer(Class<Enum<?>> clazz) {
+            super(clazz);
+            this.constants = ((Class<Enum<?>>) handledType()).getEnumConstants();
+            this.acceptedValues = Lists.newArrayList();
+            for (Enum<?> constant : constants) {
+                acceptedValues.add(constant.name());
+            }
+        }
+
+        @Override
+
+        public Enum<?> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
+            final String text = CharMatcher.WHITESPACE
+                                           .removeFrom(jp.getText())
+                                           .replace('-', '_');
+            for (Enum<?> constant : constants) {
+                if (constant.name().equalsIgnoreCase(text)) {
+                    return constant;
+                }
+            }
+
+            throw ctxt.mappingException(text + " was not one of " + acceptedValues);
+        }
+    }
+
+    private static class PermissiveEnumDeserializers extends Deserializers.Base {
+        @Override
+        @SuppressWarnings("unchecked")
+        public JsonDeserializer<?> findEnumDeserializer(Class<?> type,
+                                                        DeserializationConfig config,
+                                                        BeanDescription desc) throws JsonMappingException {
+            return new PermissiveEnumDeserializer((Class<Enum<?>>) type);
+        }
+    }
+
+    @Override
+    public String getModuleName() {
+        return "permissive-enums";
+    }
+
+    @Override
+    public Version version() {
+        return Version.unknownVersion();
+    }
+
+    @Override
+    public void setupModule(final SetupContext context) {
+        context.addDeserializers(new PermissiveEnumDeserializers());
+    }
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/GuavaExtrasModule.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/GuavaExtrasModule.java
new file mode 100644
index 0000000..1ab5af8
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/GuavaExtrasModule.java
@@ -0,0 +1,64 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.Deserializers;
+import com.google.common.cache.CacheBuilderSpec;
+import com.google.common.net.HostAndPort;
+
+import java.io.IOException;
+
+public class GuavaExtrasModule extends Module {
+    private static class HostAndPortDeserializer extends JsonDeserializer<HostAndPort> {
+        @Override
+        public HostAndPort deserialize(JsonParser jp,
+                                       DeserializationContext ctxt) throws IOException {
+            return HostAndPort.fromString(jp.getText());
+        }
+    }
+
+    private static class CacheBuilderSpecDeserializer extends JsonDeserializer<CacheBuilderSpec> {
+        @Override
+        public CacheBuilderSpec deserialize(JsonParser jp,
+                                            DeserializationContext ctxt) throws IOException {
+            final String text = jp.getText();
+            if ("off".equalsIgnoreCase(text) || "disabled".equalsIgnoreCase(text)) {
+                return CacheBuilderSpec.disableCaching();
+            }
+            return CacheBuilderSpec.parse(text);
+        }
+    }
+
+    private static class GuavaExtrasDeserializers extends Deserializers.Base {
+        @Override
+        public JsonDeserializer<?> findBeanDeserializer(JavaType type,
+                                                        DeserializationConfig config,
+                                                        BeanDescription beanDesc) throws JsonMappingException {
+            if (CacheBuilderSpec.class.isAssignableFrom(type.getRawClass())) {
+                return new CacheBuilderSpecDeserializer();
+            }
+
+            if (HostAndPort.class.isAssignableFrom(type.getRawClass())) {
+                return new HostAndPortDeserializer();
+            }
+
+            return super.findBeanDeserializer(type, config, beanDesc);
+        }
+    }
+
+    @Override
+    public String getModuleName() {
+        return "guava-extras";
+    }
+
+    @Override
+    public Version version() {
+        return Version.unknownVersion();
+    }
+
+    @Override
+    public void setupModule(SetupContext context) {
+        context.addDeserializers(new GuavaExtrasDeserializers());
+    }
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java
new file mode 100644
index 0000000..bcfd56d
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java
@@ -0,0 +1,30 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.guava.GuavaModule;
+import com.fasterxml.jackson.datatype.joda.JodaModule;
+import com.fasterxml.jackson.module.afterburner.AfterburnerModule;
+
+/**
+ * A utility class for Jackson.
+ */
+public class Jackson {
+    private Jackson() { /* singleton */ }
+
+    /**
+     * Creates a new {@link ObjectMapper} with Guava, Logback, and Joda Time support, as well as
+     * support for {@link JsonSnakeCase}. Also includes all {@link Discoverable} interface implementations.
+     */
+    public static ObjectMapper newObjectMapper() {
+        final ObjectMapper mapper = new ObjectMapper();
+        mapper.registerModule(new GuavaModule());
+        mapper.registerModule(new LogbackModule());
+        mapper.registerModule(new GuavaExtrasModule());
+        mapper.registerModule(new JodaModule());
+        mapper.registerModule(new AfterburnerModule());
+        mapper.registerModule(new FuzzyEnumModule());
+        mapper.setPropertyNamingStrategy(new AnnotationSensitivePropertyNamingStrategy());
+        mapper.setSubtypeResolver(new DiscoverableSubtypeResolver());
+        return mapper;
+    }
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/JsonSnakeCase.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/JsonSnakeCase.java
new file mode 100644
index 0000000..cf5397d
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/JsonSnakeCase.java
@@ -0,0 +1,20 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.annotation.JacksonAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation which indicates that the annotated case class should be
+ * serialized and deserialized using {@code snake_case} JSON field names instead
+ * of {@code camelCase} field names.
+ */
+ at Target(ElementType.TYPE)
+ at Retention(RetentionPolicy.RUNTIME)
+ at JacksonAnnotation
+public @interface JsonSnakeCase {
+
+}
diff --git a/dropwizard-jackson/src/main/java/io/dropwizard/jackson/LogbackModule.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/LogbackModule.java
new file mode 100644
index 0000000..49c5bbb
--- /dev/null
+++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/LogbackModule.java
@@ -0,0 +1,79 @@
+package io.dropwizard.jackson;
+
+import ch.qos.logback.classic.Level;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.Version;
+import com.fasterxml.jackson.databind.*;
+import com.fasterxml.jackson.databind.deser.Deserializers;
+import com.fasterxml.jackson.databind.ser.Serializers;
+
+import java.io.IOException;
+
+public class LogbackModule extends Module {
+    private static class LevelDeserializer extends JsonDeserializer<Level> {
+        @Override
+        public Level deserialize(JsonParser jp,
+                                 DeserializationContext ctxt) throws IOException {
+
+            final String text = jp.getText();
+
+            // required because YAML maps "off" to a boolean false
+            if ("false".equalsIgnoreCase(text)) {
+                return Level.OFF;
+            }
+
+            // required because YAML maps "on" to a boolean true
+            if ("true".equalsIgnoreCase(text)) {
+                return Level.ALL;
+            }
+
+            return Level.toLevel(text, Level.INFO);
+        }
+    }
+
+    private static class LogbackDeserializers extends Deserializers.Base {
+        @Override
+        public JsonDeserializer<?> findBeanDeserializer(JavaType type,
+                                                        DeserializationConfig config,
+                                                        BeanDescription beanDesc) throws JsonMappingException {
+            if (Level.class.isAssignableFrom(type.getRawClass())) {
+                return new LevelDeserializer();
+            }
+            return super.findBeanDeserializer(type, config, beanDesc);
+        }
+    }
+
+    private static class LevelSerializer extends JsonSerializer<Level> {
+        @Override
+        public void serialize(Level value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
+            jgen.writeString(value.toString());
+        }
+    }
+
+    private static class LogbackSerializers extends Serializers.Base {
+        @Override
+        public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
+            if (Level.class.isAssignableFrom(type.getRawClass())) {
+                return new LevelSerializer();
+            }
+            return super.findSerializer(config, type, beanDesc);
+        }
+    }
+
+    @Override
+    public String getModuleName() {
+        return "LogbackModule";
+    }
+
+    @Override
+    public Version version() {
+        return Version.unknownVersion();
+    }
+
+    @Override
+    public void setupModule(SetupContext context) {
+        context.addSerializers(new LogbackSerializers());
+        context.addDeserializers(new LogbackDeserializers());
+    }
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategyTest.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategyTest.java
new file mode 100644
index 0000000..0b7f208
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategyTest.java
@@ -0,0 +1,68 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.PropertyNamingStrategy;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class AnnotationSensitivePropertyNamingStrategyTest {
+    public static class RegularExample {
+        @JsonProperty
+        String firstName;
+
+        @SuppressWarnings("UnusedDeclaration") // Jackson
+        private RegularExample() {}
+
+        public RegularExample(String firstName) {
+            this.firstName = firstName;
+        }
+    }
+
+    @JsonSnakeCase
+    public static class SnakeCaseExample {
+        @JsonProperty
+        String firstName;
+
+        @SuppressWarnings("UnusedDeclaration") // Jackson
+        private SnakeCaseExample() {}
+
+        public SnakeCaseExample(String firstName) {
+            this.firstName = firstName;
+        }
+    }
+
+    private final PropertyNamingStrategy strategy = new AnnotationSensitivePropertyNamingStrategy();
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Before
+    public void setUp() throws Exception {
+        mapper.setPropertyNamingStrategy(strategy);
+    }
+
+    @Test
+    public void serializesRegularProperties() throws Exception {
+        assertThat(mapper.writeValueAsString(new RegularExample("woo")))
+                .isEqualTo("{\"firstName\":\"woo\"}");
+    }
+
+    @Test
+    public void serializesSnakeCaseProperties() throws Exception {
+        assertThat(mapper.writeValueAsString(new SnakeCaseExample("woo")))
+                .isEqualTo("{\"first_name\":\"woo\"}");
+    }
+
+    @Test
+    public void deserializesRegularProperties() throws Exception {
+        assertThat(mapper.readValue("{\"firstName\":\"woo\"}", RegularExample.class).firstName)
+                .isEqualTo("woo");
+    }
+
+    @Test
+    public void deserializesSnakeCaseProperties() throws Exception {
+        assertThat(mapper.readValue("{\"first_name\":\"woo\"}", SnakeCaseExample.class).firstName)
+                .isEqualTo("woo");
+    }
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/DiscoverableSubtypeResolverTest.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/DiscoverableSubtypeResolverTest.java
new file mode 100644
index 0000000..0e1fdbc
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/DiscoverableSubtypeResolverTest.java
@@ -0,0 +1,26 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class DiscoverableSubtypeResolverTest {
+    private final ObjectMapper mapper = new ObjectMapper();
+    private final DiscoverableSubtypeResolver resolver = new DiscoverableSubtypeResolver(ExampleTag.class);
+
+    @Before
+    public void setUp() throws Exception {
+        mapper.setSubtypeResolver(resolver);
+    }
+
+    @Test
+    public void discoversSubtypes() throws Exception {
+        assertThat(mapper.readValue("{\"type\":\"a\"}", ExampleSPI.class))
+                .isInstanceOf(ImplA.class);
+
+        assertThat(mapper.readValue("{\"type\":\"b\"}", ExampleSPI.class))
+                .isInstanceOf(ImplB.class);
+    }
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ExampleSPI.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ExampleSPI.java
new file mode 100644
index 0000000..990d4f5
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ExampleSPI.java
@@ -0,0 +1,7 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+
+ at JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+public interface ExampleSPI extends ExampleTag {
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ExampleTag.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ExampleTag.java
new file mode 100644
index 0000000..3d76221
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ExampleTag.java
@@ -0,0 +1,4 @@
+package io.dropwizard.jackson;
+
+public interface ExampleTag {
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/FuzzyEnumModuleTest.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/FuzzyEnumModuleTest.java
new file mode 100644
index 0000000..6dddfe5
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/FuzzyEnumModuleTest.java
@@ -0,0 +1,77 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.ClientInfoStatus;
+import java.util.concurrent.TimeUnit;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class FuzzyEnumModuleTest {
+    private final ObjectMapper mapper = new ObjectMapper();
+    
+    private enum EnumWithLowercase {lower_case_enum, mixedCaseEnum};
+
+    @Before
+    public void setUp() throws Exception {
+        mapper.registerModule(new FuzzyEnumModule());
+    }
+
+    @Test
+    public void mapsUpperCaseEnums() throws Exception {
+        assertThat(mapper.readValue("\"SECONDS\"", TimeUnit.class))
+                .isEqualTo(TimeUnit.SECONDS);
+    }
+
+    @Test
+    public void mapsLowerCaseEnums() throws Exception {
+        assertThat(mapper.readValue("\"milliseconds\"", TimeUnit.class))
+                .isEqualTo(TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void mapsPaddedEnums() throws Exception {
+        assertThat(mapper.readValue("\"   MINUTES \"", TimeUnit.class))
+                .isEqualTo(TimeUnit.MINUTES);
+    }
+
+    @Test
+    public void mapsSpacedEnums() throws Exception {
+        assertThat(mapper.readValue("\"   MILLI SECONDS \"", TimeUnit.class))
+                .isEqualTo(TimeUnit.MILLISECONDS);
+    }
+
+    @Test
+    public void mapsDashedEnums() throws Exception {
+        assertThat(mapper.readValue("\"REASON-UNKNOWN\"", ClientInfoStatus.class))
+                .isEqualTo(ClientInfoStatus.REASON_UNKNOWN);
+    }
+
+    @Test
+    public void failsOnIncorrectValue() throws Exception {
+        try {
+            mapper.readValue("\"wrong\"", TimeUnit.class);
+            failBecauseExceptionWasNotThrown(JsonMappingException.class);
+        } catch (JsonMappingException e) {
+            assertThat(e.getOriginalMessage())
+                    .isEqualTo("wrong was not one of [NANOSECONDS, MICROSECONDS, MILLISECONDS, SECONDS, MINUTES, HOURS, DAYS]");
+        }
+    }
+ 
+    @Test
+    public void mapsToLowerCaseEnums() throws Exception {
+        assertThat(mapper.readValue("\"lower_case_enum\"", EnumWithLowercase.class))
+                .isEqualTo(EnumWithLowercase.lower_case_enum);
+    }
+    
+    @Test
+    public void mapsMixedCaseEnums() throws Exception {
+        assertThat(mapper.readValue("\"mixedCaseEnum\"", EnumWithLowercase.class))
+                .isEqualTo(EnumWithLowercase.mixedCaseEnum);
+    }
+   
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/GuavaExtrasModuleTest.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/GuavaExtrasModuleTest.java
new file mode 100644
index 0000000..3a4ae4d
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/GuavaExtrasModuleTest.java
@@ -0,0 +1,46 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.guava.GuavaModule;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheBuilderSpec;
+import com.google.common.net.HostAndPort;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class GuavaExtrasModuleTest {
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Before
+    public void setUp() throws Exception {
+        mapper.registerModule(new GuavaModule());
+        mapper.registerModule(new GuavaExtrasModule());
+    }
+
+    @Test
+    public void canDeserializeAHostAndPort() throws Exception {
+        assertThat(mapper.readValue("\"example.com:8080\"", HostAndPort.class))
+                .isEqualTo(HostAndPort.fromParts("example.com", 8080));
+    }
+
+    @Test
+    public void canDeserializeCacheBuilderSpecs() throws Exception {
+        assertThat(mapper.readValue("\"maximumSize=30\"", CacheBuilderSpec.class))
+                .isEqualTo(CacheBuilderSpec.parse("maximumSize=30"));
+    }
+
+    @Test
+    public void canDeserializeAbsentOptions() throws Exception {
+        assertThat(mapper.readValue("null", new TypeReference<Optional<String>>() {}))
+                .isEqualTo(Optional.absent());
+    }
+
+    @Test
+    public void canDeserializePresentOptions() throws Exception {
+        assertThat(mapper.readValue("\"woo\"", new TypeReference<Optional<String>>() {}))
+                .isEqualTo(Optional.of("woo"));
+    }
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ImplA.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ImplA.java
new file mode 100644
index 0000000..2889746
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ImplA.java
@@ -0,0 +1,7 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+ at JsonTypeName("a")
+public class ImplA implements ExampleSPI {
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ImplB.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ImplB.java
new file mode 100644
index 0000000..a2bb1ff
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/ImplB.java
@@ -0,0 +1,7 @@
+package io.dropwizard.jackson;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+ at JsonTypeName("b")
+public class ImplB implements ExampleSPI {
+}
diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/LogbackModuleTest.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/LogbackModuleTest.java
new file mode 100644
index 0000000..8455750
--- /dev/null
+++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/LogbackModuleTest.java
@@ -0,0 +1,35 @@
+package io.dropwizard.jackson;
+
+import ch.qos.logback.classic.Level;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class LogbackModuleTest {
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Before
+    public void setUp() throws Exception {
+        mapper.registerModule(new LogbackModule());
+    }
+
+    @Test
+    public void mapsStringsToLevels() throws Exception {
+        assertThat(mapper.readValue("\"info\"", Level.class))
+                .isEqualTo(Level.INFO);
+    }
+
+    @Test
+    public void mapsFalseToOff() throws Exception {
+        assertThat(mapper.readValue("\"false\"", Level.class))
+                .isEqualTo(Level.OFF);
+    }
+
+    @Test
+    public void mapsTrueToAll() throws Exception {
+        assertThat(mapper.readValue("\"true\"", Level.class))
+                .isEqualTo(Level.ALL);
+    }
+}
diff --git a/dropwizard-jackson/src/test/resources/META-INF/services/io.dropwizard.jackson.ExampleSPI b/dropwizard-jackson/src/test/resources/META-INF/services/io.dropwizard.jackson.ExampleSPI
new file mode 100644
index 0000000..2d775f5
--- /dev/null
+++ b/dropwizard-jackson/src/test/resources/META-INF/services/io.dropwizard.jackson.ExampleSPI
@@ -0,0 +1,2 @@
+io.dropwizard.jackson.ImplA
+io.dropwizard.jackson.ImplB
diff --git a/dropwizard-jackson/src/test/resources/META-INF/services/io.dropwizard.jackson.ExampleTag b/dropwizard-jackson/src/test/resources/META-INF/services/io.dropwizard.jackson.ExampleTag
new file mode 100644
index 0000000..60e80f8
--- /dev/null
+++ b/dropwizard-jackson/src/test/resources/META-INF/services/io.dropwizard.jackson.ExampleTag
@@ -0,0 +1 @@
+io.dropwizard.jackson.ExampleSPI
diff --git a/dropwizard-jdbi/pom.xml b/dropwizard-jdbi/pom.xml
new file mode 100755
index 0000000..05e933d
--- /dev/null
+++ b/dropwizard-jdbi/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-jdbi</artifactId>
+    <name>Dropwizard JDBI Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-db</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jdbi</groupId>
+            <artifactId>jdbi</artifactId>
+            <version>2.55</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-jdbi</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.jdbi</groupId>
+                    <artifactId>jdbi</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <version>${h2.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIFactory.java
new file mode 100755
index 0000000..eadb071
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIFactory.java
@@ -0,0 +1,72 @@
+package io.dropwizard.jdbi;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import com.codahale.metrics.jdbi.InstrumentedTimingCollector;
+import com.codahale.metrics.jdbi.strategies.DelegatingStatementNameStrategy;
+import com.codahale.metrics.jdbi.strategies.NameStrategies;
+import com.codahale.metrics.jdbi.strategies.StatementNameStrategy;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.db.ManagedDataSource;
+import io.dropwizard.jdbi.args.JodaDateTimeArgumentFactory;
+import io.dropwizard.jdbi.args.JodaDateTimeMapper;
+import io.dropwizard.jdbi.args.OptionalArgumentFactory;
+import io.dropwizard.jdbi.logging.LogbackLog;
+import io.dropwizard.setup.Environment;
+import org.skife.jdbi.v2.ColonPrefixNamedParamStatementRewriter;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.StatementContext;
+import org.slf4j.LoggerFactory;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+public class DBIFactory {
+    private static final Logger LOGGER = (Logger) LoggerFactory.getLogger(DBI.class);
+    private static final String RAW_SQL = name(DBI.class, "raw-sql");
+
+    private static class SanerNamingStrategy extends DelegatingStatementNameStrategy {
+        private SanerNamingStrategy() {
+            super(NameStrategies.CHECK_EMPTY,
+                  NameStrategies.CONTEXT_CLASS,
+                  NameStrategies.CONTEXT_NAME,
+                  NameStrategies.SQL_OBJECT,
+                  new StatementNameStrategy() {
+                      @Override
+                      public String getStatementName(StatementContext statementContext) {
+                          return RAW_SQL;
+                      }
+                  });
+        }
+    }
+
+    public DBI build(Environment environment,
+                     DataSourceFactory configuration,
+                     String name) throws ClassNotFoundException {
+        final ManagedDataSource dataSource = configuration.build(environment.metrics(), name);
+        return build(environment, configuration, dataSource, name);
+    }
+
+    public DBI build(Environment environment,
+                     DataSourceFactory configuration,
+                     ManagedDataSource dataSource,
+                     String name) {
+        final String validationQuery = configuration.getValidationQuery();
+        final DBI dbi = new DBI(dataSource);
+        environment.lifecycle().manage(dataSource);
+        environment.healthChecks().register(name, new DBIHealthCheck(dbi, validationQuery));
+        dbi.setSQLLog(new LogbackLog(LOGGER, Level.TRACE));
+        dbi.setTimingCollector(new InstrumentedTimingCollector(environment.metrics(),
+                                                               new SanerNamingStrategy()));
+        if (configuration.isAutoCommentsEnabled()) {
+            dbi.setStatementRewriter(new NamePrependingStatementRewriter(new ColonPrefixNamedParamStatementRewriter()));
+        }
+        dbi.registerArgumentFactory(new OptionalArgumentFactory(configuration.getDriverClass()));
+        dbi.registerContainerFactory(new ImmutableListContainerFactory());
+        dbi.registerContainerFactory(new ImmutableSetContainerFactory());
+        dbi.registerContainerFactory(new OptionalContainerFactory());
+        dbi.registerArgumentFactory(new JodaDateTimeArgumentFactory());
+        dbi.registerMapper(new JodaDateTimeMapper());
+
+        return dbi;
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIHealthCheck.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIHealthCheck.java
new file mode 100644
index 0000000..0510c15
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIHealthCheck.java
@@ -0,0 +1,23 @@
+package io.dropwizard.jdbi;
+
+import com.codahale.metrics.health.HealthCheck;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+
+public class DBIHealthCheck extends HealthCheck {
+    private final DBI dbi;
+    private final String validationQuery;
+
+    public DBIHealthCheck(DBI dbi, String validationQuery) {
+        this.dbi = dbi;
+        this.validationQuery = validationQuery;
+    }
+
+    @Override
+    protected Result check() throws Exception {
+        try (Handle handle = dbi.open()) {
+            handle.execute(validationQuery);
+        }
+        return Result.healthy();
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableListContainerFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableListContainerFactory.java
new file mode 100644
index 0000000..84796d3
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableListContainerFactory.java
@@ -0,0 +1,32 @@
+package io.dropwizard.jdbi;
+
+import com.google.common.collect.ImmutableList;
+import org.skife.jdbi.v2.ContainerBuilder;
+import org.skife.jdbi.v2.tweak.ContainerFactory;
+
+public class ImmutableListContainerFactory implements ContainerFactory<ImmutableList<?>> {
+    @Override
+    public boolean accepts(Class<?> type) {
+        return ImmutableList.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public ContainerBuilder<ImmutableList<?>> newContainerBuilderFor(Class<?> type) {
+        return new ImmutableListContainerBuilder();
+    }
+
+    private static class ImmutableListContainerBuilder implements ContainerBuilder<ImmutableList<?>> {
+        private final ImmutableList.Builder<Object> builder = ImmutableList.builder();
+
+        @Override
+        public ContainerBuilder<ImmutableList<?>> add(Object it) {
+            builder.add(it);
+            return this;
+        }
+
+        @Override
+        public ImmutableList<?> build() {
+            return builder.build();
+        }
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableSetContainerFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableSetContainerFactory.java
new file mode 100644
index 0000000..1a857af
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableSetContainerFactory.java
@@ -0,0 +1,33 @@
+package io.dropwizard.jdbi;
+
+import com.google.common.collect.ImmutableSet;
+import org.skife.jdbi.v2.ContainerBuilder;
+import org.skife.jdbi.v2.tweak.ContainerFactory;
+
+public class ImmutableSetContainerFactory implements ContainerFactory<ImmutableSet<?>> {
+    @Override
+    public boolean accepts(Class<?> type) {
+        return ImmutableSet.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public ContainerBuilder<ImmutableSet<?>> newContainerBuilderFor(Class<?> type) {
+        return new ImmutableSetContainerBuilder();
+    }
+
+
+    private static class ImmutableSetContainerBuilder implements ContainerBuilder<ImmutableSet<?>> {
+        private final ImmutableSet.Builder<Object> builder = ImmutableSet.builder();
+
+        @Override
+        public ContainerBuilder<ImmutableSet<?>> add(Object it) {
+            builder.add(it);
+            return this;
+        }
+
+        @Override
+        public ImmutableSet<?> build() {
+            return builder.build();
+        }
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/NamePrependingStatementRewriter.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/NamePrependingStatementRewriter.java
new file mode 100644
index 0000000..8d70a10
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/NamePrependingStatementRewriter.java
@@ -0,0 +1,31 @@
+package io.dropwizard.jdbi;
+
+import org.skife.jdbi.v2.Binding;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.RewrittenStatement;
+import org.skife.jdbi.v2.tweak.StatementRewriter;
+
+public class NamePrependingStatementRewriter implements StatementRewriter {
+    private final StatementRewriter rewriter;
+
+    NamePrependingStatementRewriter(StatementRewriter rewriter) {
+        this.rewriter = rewriter;
+    }
+
+    @Override
+    public RewrittenStatement rewrite(String sql, Binding params, StatementContext ctx) {
+        if ((ctx.getSqlObjectType() != null) && (ctx.getSqlObjectMethod() != null)) {
+            final StringBuilder query = new StringBuilder(sql.length() + 100);
+            query.append("/* ");
+            final String className = ctx.getSqlObjectType().getSimpleName();
+            if (!className.isEmpty()) {
+                query.append(className).append('.');
+            }
+            query.append(ctx.getSqlObjectMethod().getName());
+            query.append(" */ ");
+            query.append(sql);
+            return rewriter.rewrite(query.toString(), params, ctx);
+        }
+        return rewriter.rewrite(sql, params, ctx);
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/OptionalContainerFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/OptionalContainerFactory.java
new file mode 100755
index 0000000..c3a9bb8
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/OptionalContainerFactory.java
@@ -0,0 +1,34 @@
+package io.dropwizard.jdbi;
+
+import com.google.common.base.Optional;
+import org.skife.jdbi.v2.ContainerBuilder;
+import org.skife.jdbi.v2.tweak.ContainerFactory;
+
+public class OptionalContainerFactory implements ContainerFactory<Optional<?>> {
+
+    @Override
+    public boolean accepts(Class<?> type) {
+        return Optional.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public ContainerBuilder<Optional<?>> newContainerBuilderFor(Class<?> type) {
+        return new OptionalContainerBuilder();
+    }
+
+    private static class OptionalContainerBuilder implements ContainerBuilder<Optional<?>> {
+
+        Optional<?> optional = Optional.absent();
+
+        @Override
+        public ContainerBuilder<Optional<?>> add(Object it) {
+            optional = Optional.fromNullable(it);
+            return this;
+        }
+
+        @Override
+        public Optional<?> build() {
+            return optional;
+        }
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgument.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgument.java
new file mode 100644
index 0000000..4f85094
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgument.java
@@ -0,0 +1,33 @@
+package io.dropwizard.jdbi.args;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.sql.Types;
+
+/**
+ * An {@link Argument} for Joda {@link DateTime} objects.
+ */
+public class JodaDateTimeArgument implements Argument {
+
+    private final DateTime value;
+
+    JodaDateTimeArgument(final DateTime value) {
+        this.value = value;
+    }
+
+    @Override
+    public void apply(final int position,
+                      final PreparedStatement statement,
+                      final StatementContext ctx) throws SQLException {
+        if (value != null) {
+            statement.setTimestamp(position, new Timestamp(value.getMillis()));
+        } else {
+            statement.setNull(position, Types.TIMESTAMP);
+        }
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentFactory.java
new file mode 100644
index 0000000..6173c40
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentFactory.java
@@ -0,0 +1,26 @@
+package io.dropwizard.jdbi.args;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+/**
+ * An {@link ArgumentFactory} for Joda {@link DateTime} arguments.
+ */
+public class JodaDateTimeArgumentFactory implements ArgumentFactory<DateTime> {
+
+    @Override
+    public boolean accepts(final Class<?> expectedType,
+                           final Object value,
+                           final StatementContext ctx) {
+        return value instanceof DateTime;
+    }
+
+    @Override
+    public Argument build(final Class<?> expectedType,
+                          final DateTime value,
+                          final StatementContext ctx) {
+        return new JodaDateTimeArgument(value);
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeMapper.java
new file mode 100644
index 0000000..bf9d355
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeMapper.java
@@ -0,0 +1,32 @@
+package io.dropwizard.jdbi.args;
+
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.util.TypedMapper;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+
+/**
+ * A {@link TypedMapper} to map Joda {@link DateTime} objects.
+ */
+public class JodaDateTimeMapper extends TypedMapper<DateTime> {
+
+    @Override
+    protected DateTime extractByName(final ResultSet r, final String name) throws SQLException {
+        final Timestamp timestamp = r.getTimestamp(name);
+        if (timestamp == null) {
+            return null;
+        }
+        return new DateTime(timestamp.getTime());
+    }
+
+    @Override
+    protected DateTime extractByIndex(final ResultSet r, final int index) throws SQLException {
+        final Timestamp timestamp = r.getTimestamp(index);
+        if (timestamp == null) {
+            return null;
+        }
+        return new DateTime(timestamp.getTime());
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalArgumentFactory.java
new file mode 100644
index 0000000..b7b9705
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalArgumentFactory.java
@@ -0,0 +1,65 @@
+package io.dropwizard.jdbi.args;
+
+import com.google.common.base.Optional;
+import org.skife.jdbi.v2.StatementContext;
+import org.skife.jdbi.v2.tweak.Argument;
+import org.skife.jdbi.v2.tweak.ArgumentFactory;
+
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
+import java.sql.Types;
+
+public class OptionalArgumentFactory implements ArgumentFactory<Optional<Object>> {
+    private static class DefaultOptionalArgument implements Argument {
+        private final Optional<?> value;
+
+        private DefaultOptionalArgument(Optional<?> value) {
+            this.value = value;
+        }
+
+        @Override
+        public void apply(int position,
+                          PreparedStatement statement,
+                          StatementContext ctx) throws SQLException {
+            if (value.isPresent()) {
+                statement.setObject(position, value.get());
+            } else {
+                statement.setNull(position, Types.OTHER);
+            }
+        }
+    }
+
+    private static class MsSqlOptionalArgument implements Argument {
+        private final Optional<?> value;
+
+        private MsSqlOptionalArgument(Optional<?> value) {
+            this.value = value;
+        }
+
+        @Override
+        public void apply(int position,
+                          PreparedStatement statement,
+                          StatementContext ctx) throws SQLException {
+            statement.setObject(position, value.orNull());
+        }
+    }
+
+    private final String jdbcDriver;
+
+    public OptionalArgumentFactory(String jdbcDriver) {
+        this.jdbcDriver = jdbcDriver;
+    }
+
+    @Override
+    public boolean accepts(Class<?> expectedType, Object value, StatementContext ctx) {
+        return value instanceof Optional;
+    }
+
+    @Override
+    public Argument build(Class<?> expectedType, Optional<Object> value, StatementContext ctx) {
+        if ("com.microsoft.sqlserver.jdbc.SQLServerDriver".equals(jdbcDriver)) {
+            return new MsSqlOptionalArgument(value);
+        }
+        return new DefaultOptionalArgument(value);
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/bundles/DBIExceptionsBundle.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/bundles/DBIExceptionsBundle.java
new file mode 100644
index 0000000..dc9798a
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/bundles/DBIExceptionsBundle.java
@@ -0,0 +1,23 @@
+package io.dropwizard.jdbi.bundles;
+
+import io.dropwizard.Bundle;
+import io.dropwizard.jdbi.jersey.LoggingDBIExceptionMapper;
+import io.dropwizard.jdbi.jersey.LoggingSQLExceptionMapper;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+/**
+ * A bundle for logging SQLExceptions and DBIExceptions so that their actual causes aren't overlooked.
+ */
+public class DBIExceptionsBundle implements Bundle {
+    @Override
+    public void initialize(Bootstrap<?> bootstrap) {
+        // nothing doing
+    }
+
+    @Override
+    public void run(Environment environment) {
+        environment.jersey().register(new LoggingSQLExceptionMapper());
+        environment.jersey().register(new LoggingDBIExceptionMapper());
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapper.java
new file mode 100644
index 0000000..d026c8d
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapper.java
@@ -0,0 +1,29 @@
+package io.dropwizard.jdbi.jersey;
+
+import io.dropwizard.jersey.errors.LoggingExceptionMapper;
+import org.skife.jdbi.v2.exceptions.DBIException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.ext.Provider;
+import java.sql.SQLException;
+
+/**
+ * Iterates through a DBIException's cause if it's a SQLException otherwise log as normal.
+ */
+ at Provider
+public class LoggingDBIExceptionMapper extends LoggingExceptionMapper<DBIException> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingDBIExceptionMapper.class);
+
+    @Override
+    protected void logException(long id, DBIException exception) {
+        final Throwable cause = exception.getCause();
+        if (cause instanceof SQLException) {
+            for (Throwable throwable : (SQLException)cause) {
+                LOGGER.error(formatLogMessage(id, throwable), throwable);
+            }
+        } else {
+            LOGGER.error(formatLogMessage(id, exception), exception);
+        }
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapper.java
new file mode 100644
index 0000000..a329263
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapper.java
@@ -0,0 +1,24 @@
+package io.dropwizard.jdbi.jersey;
+
+import io.dropwizard.jersey.errors.LoggingExceptionMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.ext.Provider;
+import java.sql.SQLException;
+
+/**
+ * Iterates through SQLExceptions to log all causes
+ */
+ at Provider
+public class LoggingSQLExceptionMapper extends LoggingExceptionMapper<SQLException> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingSQLExceptionMapper.class);
+
+    @Override
+    protected void logException(long id, SQLException exception) {
+        final String message = formatLogMessage(id, exception);
+        for (Throwable throwable : exception) {
+            LOGGER.error(message, throwable);
+        }
+    }
+}
diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/logging/LogbackLog.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/logging/LogbackLog.java
new file mode 100644
index 0000000..ca1011b
--- /dev/null
+++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/logging/LogbackLog.java
@@ -0,0 +1,55 @@
+package io.dropwizard.jdbi.logging;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.logging.FormattedLog;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Logs SQL via Logback
+ */
+public class LogbackLog extends FormattedLog {
+    private final Logger log;
+    private final Level level;
+    private final String fqcn;
+
+    /**
+     * Logs to org.skife.jdbi.v2 logger at the debug level
+     */
+    public LogbackLog()
+    {
+        this((Logger) LoggerFactory.getLogger(DBI.class.getPackage().getName()));
+    }
+
+    /**
+     * Use an arbitrary logger to log to at the debug level
+     */
+    public LogbackLog(Logger log)
+    {
+        this(log, Level.DEBUG);
+    }
+
+    /**
+     * Specify both the logger and the level to log at
+     * @param log The logger to log to
+     * @param level the priority to log at
+     */
+    public LogbackLog(Logger log, Level level) {
+        this.log = log;
+        this.level = level;
+        this.fqcn = LogbackLog.class.getName();
+    }
+
+    @Override
+    protected final boolean isEnabled()
+    {
+        return log.isEnabledFor(level);
+    }
+
+    @Override
+    protected final void log(String msg)
+    {
+        log.log(null, fqcn, Level.toLocationAwareLoggerInteger(level), msg, null, null);
+    }
+}
diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/JDBITest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/JDBITest.java
new file mode 100755
index 0000000..604eeb2
--- /dev/null
+++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/JDBITest.java
@@ -0,0 +1,181 @@
+package io.dropwizard.jdbi;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.health.HealthCheckRegistry;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.db.ManagedDataSource;
+import io.dropwizard.lifecycle.Managed;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.setup.Environment;
+import org.joda.time.DateTime;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.skife.jdbi.v2.DBI;
+import org.skife.jdbi.v2.Handle;
+import org.skife.jdbi.v2.Query;
+import org.skife.jdbi.v2.util.StringMapper;
+
+import java.sql.Timestamp;
+import java.sql.Types;
+import java.util.List;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+public class JDBITest {
+    private final DataSourceFactory hsqlConfig = new DataSourceFactory();
+
+    {
+        LoggingFactory.bootstrap();
+        hsqlConfig.setUrl("jdbc:h2:mem:DbTest-" + System.currentTimeMillis());
+        hsqlConfig.setUser("sa");
+        hsqlConfig.setDriverClass("org.h2.Driver");
+        hsqlConfig.setValidationQuery("SELECT 1");
+    }
+
+    private final HealthCheckRegistry healthChecks = mock(HealthCheckRegistry.class);
+    private final LifecycleEnvironment lifecycleEnvironment = mock(LifecycleEnvironment.class);
+    private final Environment environment = mock(Environment.class);
+    private final DBIFactory factory = new DBIFactory();
+    private final List<Managed> managed = Lists.newArrayList();
+    private final MetricRegistry metricRegistry = new MetricRegistry();
+    private DBI dbi;
+
+    @Before
+    public void setUp() throws Exception {
+        when(environment.healthChecks()).thenReturn(healthChecks);
+        when(environment.lifecycle()).thenReturn(lifecycleEnvironment);
+        when(environment.metrics()).thenReturn(metricRegistry);
+
+        this.dbi = factory.build(environment, hsqlConfig, "hsql");
+        final ArgumentCaptor<Managed> managedCaptor = ArgumentCaptor.forClass(Managed.class);
+        verify(lifecycleEnvironment).manage(managedCaptor.capture());
+        managed.addAll(managedCaptor.getAllValues());
+        for (Managed obj : managed) {
+            obj.start();
+        }
+
+        try (Handle handle = dbi.open()) {
+            handle.createCall("DROP TABLE people IF EXISTS").invoke();
+            handle.createCall(
+                    "CREATE TABLE people (name varchar(100) primary key, email varchar(100), age int, created_at timestamp)")
+                  .invoke();
+            handle.createStatement("INSERT INTO people VALUES (?, ?, ?, ?)")
+                  .bind(0, "Coda Hale")
+                  .bind(1, "chale at yammer-inc.com")
+                  .bind(2, 30)
+                  .bind(3, new Timestamp(1365465078000L))
+                  .execute();
+            handle.createStatement("INSERT INTO people VALUES (?, ?, ?, ?)")
+                  .bind(0, "Kris Gale")
+                  .bind(1, "kgale at yammer-inc.com")
+                  .bind(2, 32)
+                  .bind(3, new Timestamp(1365465078000L))
+                  .execute();
+            handle.createStatement("INSERT INTO people VALUES (?, ?, ?, ?)")
+                  .bind(0, "Old Guy")
+                  .bindNull(1, Types.VARCHAR)
+                  .bind(2, 99)
+                  .bind(3, new Timestamp(1365465078000L))
+                  .execute();
+            handle.createStatement("INSERT INTO people VALUES (?, ?, ?, ?)")
+                  .bind(0, "Alice Example")
+                  .bind(1, "alice at example.org")
+                  .bind(2, 99)
+                  .bindNull(3, Types.TIMESTAMP)
+                  .execute();
+        }
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        for (Managed obj : managed) {
+            obj.stop();
+        }
+        this.dbi = null;
+    }
+
+    @Test
+    public void createsAValidDBI() throws Exception {
+        final Handle handle = dbi.open();
+
+        final Query<String> names = handle.createQuery("SELECT name FROM people WHERE age < ?")
+                                          .bind(0, 50)
+                                          .map(StringMapper.FIRST);
+        assertThat(ImmutableList.copyOf(names))
+                .containsOnly("Coda Hale", "Kris Gale");
+    }
+
+    @Test
+    public void managesTheDatabaseWithTheEnvironment() throws Exception {
+        verify(lifecycleEnvironment).manage(any(ManagedDataSource.class));
+    }
+
+    @Test
+    public void sqlObjectsCanAcceptOptionalParams() throws Exception {
+        final PersonDAO dao = dbi.open(PersonDAO.class);
+
+        assertThat(dao.findByName(Optional.of("Coda Hale")))
+                .isEqualTo("Coda Hale");
+    }
+
+    @Test
+    public void sqlObjectsCanReturnImmutableLists() throws Exception {
+        final PersonDAO dao = dbi.open(PersonDAO.class);
+
+        assertThat(dao.findAllNames())
+                .containsOnly("Coda Hale", "Kris Gale", "Old Guy", "Alice Example");
+    }
+
+    @Test
+    public void sqlObjectsCanReturnImmutableSets() throws Exception {
+        final PersonDAO dao = dbi.open(PersonDAO.class);
+
+        assertThat(dao.findAllUniqueNames())
+                .containsOnly("Coda Hale", "Kris Gale", "Old Guy", "Alice Example");
+    }
+
+    @Test
+    public void sqlObjectsCanReturnOptional() throws Exception {
+        final PersonDAO dao = dbi.open(PersonDAO.class);
+
+        final Optional<String> found = dao.findByEmail("chale at yammer-inc.com");
+        assertThat(found).isNotNull();
+        assertThat(found.isPresent()).isTrue();
+        assertThat(found.get()).isEqualTo("Coda Hale");
+
+
+        final Optional<String> missing = dao.findByEmail("cemalettin.koc at gmail.com");
+        assertThat(missing).isNotNull();
+        assertThat(missing.isPresent()).isFalse();
+        assertThat(missing.orNull()).isNull();
+    }
+
+    @Test
+    public void sqlObjectsCanReturnJodaDateTime() throws Exception {
+        final PersonDAO dao = dbi.open(PersonDAO.class);
+
+        final DateTime found = dao.getLatestCreatedAt(new DateTime(1365465077000L));
+        assertThat(found).isNotNull();
+        assertThat(found.getMillis()).isEqualTo(1365465078000L);
+        assertThat(found).isEqualTo(new DateTime(1365465078000L));
+
+        final DateTime notFound = dao.getCreatedAtByEmail("alice at example.org");
+        assertThat(notFound).isNull();
+
+        final Optional<DateTime> absentDateTime = dao.getCreatedAtByName("Alice Example");
+        assertThat(absentDateTime).isNotNull();
+        assertThat(absentDateTime.isPresent()).isFalse();
+
+        final Optional<DateTime> presentDateTime = dao.getCreatedAtByName("Coda Hale");
+        assertThat(presentDateTime).isNotNull();
+        assertThat(presentDateTime.isPresent()).isTrue();
+    }
+}
diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/PersonDAO.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/PersonDAO.java
new file mode 100755
index 0000000..a14fb30
--- /dev/null
+++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/PersonDAO.java
@@ -0,0 +1,34 @@
+package io.dropwizard.jdbi;
+
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import org.joda.time.DateTime;
+import org.skife.jdbi.v2.sqlobject.Bind;
+import org.skife.jdbi.v2.sqlobject.SqlQuery;
+import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult;
+
+public interface PersonDAO {
+    @SqlQuery("SELECT name FROM people WHERE name = :name")
+    public String findByName(@Bind("name") Optional<String> name);
+
+    @SqlQuery("SELECT name FROM people ORDER BY name ASC")
+    public ImmutableList<String> findAllNames();
+
+    @SqlQuery("SELECT DISTINCT name FROM people")
+    public ImmutableSet<String> findAllUniqueNames();
+
+    @SqlQuery("SELECT name FROM people WHERE email = :email ")
+    @SingleValueResult(String.class)
+    public Optional<String> findByEmail(@Bind("email")String email);
+
+    @SqlQuery("SELECT created_at FROM people WHERE created_at > :from ORDER BY created_at DESC LIMIT 1")
+    public DateTime getLatestCreatedAt(@Bind("from") DateTime from);
+
+    @SqlQuery("SELECT created_at FROM people WHERE name = :name")
+    @SingleValueResult(DateTime.class)
+    public Optional<DateTime> getCreatedAtByName(@Bind("name") String name);
+
+    @SqlQuery("SELECT created_at FROM people WHERE email = :email")
+    public DateTime getCreatedAtByEmail(@Bind("email") String email);
+}
diff --git a/dropwizard-jdbi/src/test/resources/logback-test.xml b/dropwizard-jdbi/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-jdbi/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-jersey/pom.xml b/dropwizard-jersey/pom.xml
new file mode 100644
index 0000000..bcaac1d
--- /dev/null
+++ b/dropwizard-jersey/pom.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-jersey</artifactId>
+    <name>Dropwizard Jersey Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jackson</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-validation</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-logging</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.orbit</groupId>
+            <artifactId>javax.servlet</artifactId>
+            <version>${servlet.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-core</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-server</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey</groupId>
+            <artifactId>jersey-servlet</artifactId>
+            <version>${jersey.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-jersey</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.sun.jersey</groupId>
+                    <artifactId>jersey-server</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.jaxrs</groupId>
+            <artifactId>jackson-jaxrs-json-provider</artifactId>
+            <version>${jackson.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.core</groupId>
+                    <artifactId>jackson-databind</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-grizzly2</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/DropwizardResourceConfig.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/DropwizardResourceConfig.java
new file mode 100644
index 0000000..98bb062
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/DropwizardResourceConfig.java
@@ -0,0 +1,205 @@
+package io.dropwizard.jersey;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jersey.InstrumentedResourceMethodDispatchAdapter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Ordering;
+import com.sun.jersey.api.core.ResourceConfig;
+import com.sun.jersey.api.core.ScanningResourceConfig;
+import com.sun.jersey.api.model.AbstractResource;
+import com.sun.jersey.api.model.AbstractResourceMethod;
+import com.sun.jersey.api.model.AbstractSubResourceLocator;
+import com.sun.jersey.api.model.AbstractSubResourceMethod;
+import com.sun.jersey.server.impl.modelapi.annotation.IntrospectionModeller;
+import io.dropwizard.jersey.caching.CacheControlledResourceMethodDispatchAdapter;
+import io.dropwizard.jersey.errors.LoggingExceptionMapper;
+import io.dropwizard.jersey.guava.OptionalQueryParamInjectableProvider;
+import io.dropwizard.jersey.guava.OptionalResourceMethodDispatchAdapter;
+import io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper;
+import io.dropwizard.jersey.validation.ConstraintViolationExceptionMapper;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Path;
+import javax.ws.rs.ext.Provider;
+import java.util.List;
+import java.util.Map;
+
+public class DropwizardResourceConfig extends ScanningResourceConfig {
+    private static final String NEWLINE = String.format("%n");
+    private static final Logger LOGGER = LoggerFactory.getLogger(DropwizardResourceConfig.class);
+    private String urlPattern;
+
+    public static DropwizardResourceConfig forTesting(MetricRegistry metricRegistry) {
+        return new DropwizardResourceConfig(true, metricRegistry);
+    }
+
+    public DropwizardResourceConfig(MetricRegistry metricRegistry) {
+        this(false, metricRegistry);
+    }
+
+    private DropwizardResourceConfig(boolean testOnly, MetricRegistry metricRegistry) {
+        super();
+        urlPattern = "/*";
+        getFeatures().put(FEATURE_DISABLE_WADL, Boolean.TRUE);
+        if (!testOnly) {
+            // create a subclass to pin it to Throwable
+            getSingletons().add(new LoggingExceptionMapper<Throwable>() {});
+            getSingletons().add(new ConstraintViolationExceptionMapper());
+            getSingletons().add(new JsonProcessingExceptionMapper());
+        }
+        getSingletons().add(new InstrumentedResourceMethodDispatchAdapter(metricRegistry));
+        getClasses().add(CacheControlledResourceMethodDispatchAdapter.class);
+        getClasses().add(OptionalResourceMethodDispatchAdapter.class);
+        getClasses().add(OptionalQueryParamInjectableProvider.class);
+    }
+
+    @Override
+    public void validate() {
+        super.validate();
+
+        logResources();
+        logProviders();
+        logEndpoints();
+    }
+
+    public String getUrlPattern() {
+        return urlPattern;
+    }
+
+    public void setUrlPattern(String urlPattern) {
+        this.urlPattern = urlPattern;
+    }
+
+    private void logResources() {
+        final ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+
+        for (Class<?> klass : getClasses()) {
+            if (ResourceConfig.isRootResourceClass(klass)) {
+                builder.add(klass.getCanonicalName());
+            }
+        }
+
+        for (Object o : getSingletons()) {
+            if (ResourceConfig.isRootResourceClass(o.getClass())) {
+                builder.add(o.getClass().getCanonicalName());
+            }
+        }
+
+        for (Object o : getExplicitRootResources().values()) {
+            if (o instanceof Class) {
+                builder.add(((Class<?>)o).getCanonicalName());
+            } else {
+                builder.add(o.getClass().getCanonicalName());
+            }
+        }
+
+        LOGGER.debug("resources = {}", builder.build());
+    }
+
+    private void logProviders() {
+        final ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+
+        for (Class<?> klass : getClasses()) {
+            if (ResourceConfig.isProviderClass(klass)) {
+                builder.add(klass.getCanonicalName());
+            }
+        }
+
+        for (Object o : getSingletons()) {
+            if (ResourceConfig.isProviderClass(o.getClass())) {
+                builder.add(o.getClass().getCanonicalName());
+            }
+        }
+
+        LOGGER.debug("providers = {}", builder.build());
+    }
+
+    private void logEndpoints() {
+        final StringBuilder msg = new StringBuilder(1024);
+        msg.append("The following paths were found for the configured resources:");
+        msg.append(NEWLINE).append(NEWLINE);
+
+        final ImmutableList.Builder<Class<?>> builder = ImmutableList.builder();
+        for (Object o : getSingletons()) {
+            if (ResourceConfig.isRootResourceClass(o.getClass())) {
+                builder.add(o.getClass());
+            }
+        }
+        for (Class<?> klass : getClasses()) {
+            if (ResourceConfig.isRootResourceClass(klass)) {
+                builder.add(klass);
+            }
+        }
+
+        String rootPath = urlPattern;
+        if (rootPath.endsWith("/*")) {
+            rootPath = rootPath.substring(0, rootPath.length() - 1);
+        }
+
+        for (Class<?> klass : builder.build()) {
+            final List<String> endpoints = Lists.newArrayList();
+            populateEndpoints(endpoints, rootPath, klass, false);
+
+            for (String line : Ordering.natural().sortedCopy(endpoints)) {
+                msg.append(line).append(NEWLINE);
+            }
+        }
+        for (Map.Entry<String, Object> entry : getExplicitRootResources().entrySet()) {
+            final Class<?> klass  = entry.getValue() instanceof Class ?
+                    (Class<?>) entry.getValue() :
+                    entry.getValue().getClass();
+            final AbstractResource resource =
+                    new AbstractResource(entry.getKey(),
+                                         IntrospectionModeller.createResource(klass));
+
+            final List<String> endpoints = Lists.newArrayList();
+            populateEndpoints(endpoints, rootPath, klass, false, resource);
+
+            for (String line : Ordering.natural().sortedCopy(endpoints)) {
+                msg.append(line).append(NEWLINE);
+            }
+        }
+
+        LOGGER.info(msg.toString());
+    }
+
+    private void populateEndpoints(List<String> endpoints, String basePath, Class<?> klass,
+                                   boolean isLocator) {
+        populateEndpoints(endpoints, basePath, klass, isLocator, IntrospectionModeller.createResource(klass));
+    }
+
+    private void populateEndpoints(List<String> endpoints, String basePath, Class<?> klass,
+                                   boolean isLocator, AbstractResource resource) {
+        if (!isLocator) {
+            basePath = normalizePath(basePath, resource.getPath().getValue());
+        }
+
+        for (AbstractResourceMethod method : resource.getResourceMethods()) {
+            endpoints.add(formatEndpoint(method.getHttpMethod(), basePath, klass));
+        }
+
+        for (AbstractSubResourceMethod method : resource.getSubResourceMethods()) {
+            final String path = normalizePath(basePath, method.getPath().getValue());
+            endpoints.add(formatEndpoint(method.getHttpMethod(), path, klass));
+        }
+
+        for (AbstractSubResourceLocator locator : resource.getSubResourceLocators()) {
+            final String path = normalizePath(basePath, locator.getPath().getValue());
+            populateEndpoints(endpoints, path, locator.getMethod().getReturnType(), true);
+        }
+    }
+
+    private String formatEndpoint(String method, String path, Class<?> klass) {
+        return String.format("    %-7s %s (%s)", method, path, klass.getCanonicalName());
+    }
+
+    private String normalizePath(String basePath, String path) {
+        if (basePath.endsWith("/")) {
+            return path.startsWith("/") ? basePath + path.substring(1) : basePath + path;
+        }
+        return path.startsWith("/") ? basePath + path : basePath + "/" + path;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/PATCH.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/PATCH.java
new file mode 100644
index 0000000..1fe5d8a
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/PATCH.java
@@ -0,0 +1,12 @@
+package io.dropwizard.jersey;
+
+import javax.ws.rs.HttpMethod;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+ at Target({ElementType.METHOD})
+ at Retention(RetentionPolicy.RUNTIME)
+ at HttpMethod("PATCH")
+public @interface PATCH { }
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControl.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControl.java
new file mode 100644
index 0000000..a8a4232
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControl.java
@@ -0,0 +1,192 @@
+package io.dropwizard.jersey.caching;
+
+import java.lang.annotation.*;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * An annotation which adds a constant {@code Cache-Control} header to the response produced by
+ * the annotated method.
+ */
+ at Documented
+ at Target(ElementType.METHOD)
+ at Retention(RetentionPolicy.RUNTIME)
+public @interface CacheControl {
+    /**
+     * If set, adds a {@code Cache-Control} header to the response which indicates the response is
+     * immutable and should be kept in cache for as long as possible. (Technically, this corresponds
+     * to a {@code max-age} of one year.
+     *
+     * @see #maxAge()
+     * @return {@code true} if the response should be considered immutable and cached indefinitely
+     */
+    boolean immutable() default false;
+
+    /**
+     * Controls the {@code private} setting of the {@code Cache-Control} header.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The private response directive indicates that the response message is intended for a
+     *     single user and MUST NOT be stored by a shared cache.  A private cache MAY store the
+     *     response.
+     *
+     *     If the private response directive specifies one or more field-names, this requirement is
+     *     limited to the field-values associated with the listed response header fields.  That is,
+     *     a shared cache MUST NOT store the specified field-names(s), whereas it MAY store the
+     *     remainder of the response message.
+     *
+     *     Note: This usage of the word "private" only controls where the response can be stored; it
+     *     cannot ensure the privacy of the message content.  Also, private response directives with
+     *     field-names are often handled by implementations as if an unqualified private directive
+     *     was received; i.e., the special handling for the qualified form is not widely
+     *     implemented.
+     * </blockquote>
+     *
+     * @return {@code true} if the response must not be stored by a shared cache
+     */
+    boolean isPrivate() default false;
+
+    /**
+     * Controls the {@code no-cache} setting of the {@code Cache-Control} header.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The no-cache response directive indicates that the response MUST NOT be used to satisfy a
+     *     subsequent request without successful validation on the origin server.  This allows an
+     *     origin server to prevent a cache from using it to satisfy a request without contacting
+     *     it, even by caches that have been configured to return stale responses.
+     *
+     *     If the no-cache response directive specifies one or more field-names, then a cache MAY
+     *     use the response to satisfy a subsequent request, subject to any other restrictions on
+     *     caching.  However, any header fields in the response that have the field-name(s) listed
+     *     MUST NOT be sent in the response to a subsequent request without successful revalidation
+     *     with the origin server.  This allows an origin server to prevent the re-use of certain
+     *     header fields in a response, while still allowing caching of the rest of the response.
+     *
+     *     Note: Most HTTP/1.0 caches will not recognize or obey this directive.  Also, no-cache
+     *     response directives with field-names are often handled by implementations as if an
+     *     unqualified no-cache directive was received; i.e., the special handling for the qualified
+     *     form is not widely implemented.
+     * </blockquote>
+     *
+     * @return {@code true} if the response must not be cached
+     */
+    boolean noCache() default false;
+
+    /**
+     * Controls the {@code no-store} setting of the {@code Cache-Control} header.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The no-store response directive indicates that a cache MUST NOT store any part of either
+     *     the immediate request or response.  This directive applies to both private and shared
+     *     caches.  "MUST NOT store" in this context means that the cache MUST NOT intentionally
+     *     store the information in non-volatile storage, and MUST make a best-effort attempt to
+     *     remove the information from volatile storage as promptly as possible after forwarding it.
+     *
+     *     This directive is NOT a reliable or sufficient mechanism for ensuring privacy.  In
+     *     particular, malicious or compromised caches might not recognize or obey this directive,
+     *     and communications networks might be vulnerable to eavesdropping.
+     * </blockquote>
+     *
+     * @return {@code true} if the response must not be stored
+     */
+    boolean noStore() default false;
+
+    /**
+     * Controls the {@code no-transform} setting of the {@code Cache-Control} header.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The no-transform response directive indicates that an intermediary (regardless of whether
+     *     it implements a cache) MUST NOT change the Content-Encoding, Content-Range or
+     *     Content-Type response header fields, nor the response representation.
+     * </blockquote>
+     *
+     * @return {@code true} if the response must not be transformed by intermediaries
+     */
+    boolean noTransform() default true;
+
+    /**
+     * Controls the {@code must-revalidate} setting of the {@code Cache-Control} header.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The must-revalidate response directive indicates that once it has become stale, a cache
+     *     MUST NOT use the response to satisfy subsequent requests without successful validation on
+     *     the origin server.
+     *
+     *     The must-revalidate directive is necessary to support reliable operation for certain
+     *     protocol features.  In all circumstances a cache MUST obey the must-revalidate directive;
+     *     in particular, if a cache cannot reach the origin server for any reason, it MUST generate
+     *     a 504 (Gateway Timeout) response.
+     *
+     *     The must-revalidate directive ought to be used by servers if and only if failure to
+     *     validate a request on the representation could result in incorrect operation, such as a
+     *     silently unexecuted financial transaction.
+     * </blockquote>
+     *
+     * @return {@code true} if caches must revalidate the content when it becomes stale
+     */
+    boolean mustRevalidate() default false;
+
+    /**
+     * Controls the {@code proxy-revalidate} setting of the {@code Cache-Control} header.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The proxy-revalidate response directive has the same meaning as the must-revalidate
+     *     response directive, except that it does not apply to private caches.
+     * </blockquote>
+     *
+     * @return {@code true} if only proxies must revalidate the content when it becomes stale
+     */
+    boolean proxyRevalidate() default false;
+
+    /**
+     * Controls the {@code max-age} setting of the {@code Cache-Control} header. The unit of this
+     * amount is determined by {@link #maxAgeUnit()}.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The max-age response directive indicates that the response is to be considered stale
+     *     after its age is greater than the specified number of seconds.
+     * </blockquote>
+     * 
+     * @see #maxAgeUnit()
+     * @return the number of {@link #maxAgeUnit()}s for which the response should be considered
+     *         fresh
+     */
+    int maxAge() default -1;
+
+    /**
+     * The time unit of {@link #maxAge()}.
+     *
+     * @return the time unit of {@link #maxAge()}
+     */
+    TimeUnit maxAgeUnit() default TimeUnit.SECONDS;
+
+    /**
+     * Controls the {@code s-max-age} setting of the {@code Cache-Control} header. The unit of this
+     * amount is controlled by {@link #sharedMaxAgeUnit()}.
+     *
+     * <p>From the HTTPbis spec:</p>
+     * <blockquote>
+     *     The s-maxage response directive indicates that, in shared caches, the maximum age
+     *     specified by this directive overrides the maximum age specified by either the max-age
+     *     directive or the Expires header field.  The s-maxage directive also implies the semantics
+     *     of the proxy-revalidate response directive.
+     * </blockquote>
+     *
+     * @return the number of {@link #sharedMaxAgeUnit()}s for which the response should be
+     *         considered fresh
+     */
+    int sharedMaxAge() default -1;
+
+    /**
+     * The time unit of {@link #sharedMaxAge()}.
+     *
+     * @return the time unit of {@link #sharedMaxAge()}
+     */
+    TimeUnit sharedMaxAgeUnit() default TimeUnit.SECONDS;
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControlledResourceMethodDispatchAdapter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControlledResourceMethodDispatchAdapter.java
new file mode 100644
index 0000000..ea9b123
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControlledResourceMethodDispatchAdapter.java
@@ -0,0 +1,70 @@
+package io.dropwizard.jersey.caching;
+
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.api.model.AbstractResourceMethod;
+import com.sun.jersey.spi.container.ResourceMethodDispatchAdapter;
+import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
+import com.sun.jersey.spi.dispatch.RequestDispatcher;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.ext.Provider;
+import java.util.concurrent.TimeUnit;
+
+ at Provider
+public class CacheControlledResourceMethodDispatchAdapter implements ResourceMethodDispatchAdapter {
+    private static class CacheControlledResourceMethodDispatchProvider implements ResourceMethodDispatchProvider {
+        private static final int ONE_YEAR_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(365);
+
+        private final ResourceMethodDispatchProvider provider;
+
+        private CacheControlledResourceMethodDispatchProvider(ResourceMethodDispatchProvider provider) {
+            this.provider = provider;
+        }
+
+        @Override
+        public RequestDispatcher create(AbstractResourceMethod abstractResourceMethod) {
+            final RequestDispatcher dispatcher = provider.create(abstractResourceMethod);
+            final CacheControl control = abstractResourceMethod.getAnnotation(CacheControl.class);
+            if (control != null) {
+                final javax.ws.rs.core.CacheControl cacheControl = new javax.ws.rs.core.CacheControl();
+                cacheControl.setPrivate(control.isPrivate());
+                cacheControl.setNoCache(control.noCache());
+                cacheControl.setNoStore(control.noStore());
+                cacheControl.setNoTransform(control.noTransform());
+                cacheControl.setMustRevalidate(control.mustRevalidate());
+                cacheControl.setProxyRevalidate(control.proxyRevalidate());
+                cacheControl.setMaxAge((int) control.maxAgeUnit().toSeconds(control.maxAge()));
+                cacheControl.setSMaxAge((int) control.sharedMaxAgeUnit()
+                                                     .toSeconds(control.sharedMaxAge()));
+                if (control.immutable()) {
+                    cacheControl.setMaxAge(ONE_YEAR_IN_SECONDS);
+                }
+                return new CacheControlledRequestDispatcher(dispatcher, cacheControl);
+            }
+            return dispatcher;
+        }
+    }
+
+    private static class CacheControlledRequestDispatcher implements RequestDispatcher {
+        private final RequestDispatcher dispatcher;
+        private final String cacheControl;
+
+        private CacheControlledRequestDispatcher(RequestDispatcher dispatcher, javax.ws.rs.core.CacheControl cacheControl) {
+            this.dispatcher = dispatcher;
+            this.cacheControl = cacheControl.toString();
+        }
+
+        @Override
+        public void dispatch(Object resource, HttpContext context) {
+            dispatcher.dispatch(resource, context);
+            if (!cacheControl.isEmpty()) {
+                context.getResponse().getHttpHeaders().add(HttpHeaders.CACHE_CONTROL, cacheControl);
+            }
+        }
+    }
+
+    @Override
+    public ResourceMethodDispatchProvider adapt(ResourceMethodDispatchProvider provider) {
+        return new CacheControlledResourceMethodDispatchProvider(provider);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/ErrorMessage.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/ErrorMessage.java
new file mode 100644
index 0000000..5670dfb
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/ErrorMessage.java
@@ -0,0 +1,13 @@
+package io.dropwizard.jersey.errors;
+
+public class ErrorMessage {
+    private final String message;
+
+    public ErrorMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/LoggingExceptionMapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/LoggingExceptionMapper.java
new file mode 100644
index 0000000..e980223
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/LoggingExceptionMapper.java
@@ -0,0 +1,40 @@
+package io.dropwizard.jersey.errors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import java.util.concurrent.ThreadLocalRandom;
+
+public abstract class LoggingExceptionMapper<E extends Throwable> implements ExceptionMapper<E> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(LoggingExceptionMapper.class);
+
+    @Override
+    public Response toResponse(E exception) {
+        if (exception instanceof WebApplicationException) {
+            return ((WebApplicationException) exception).getResponse();
+        }
+
+        final long id = ThreadLocalRandom.current().nextLong();
+        logException(id, exception);
+        return Response.serverError()
+                       .entity(new ErrorMessage(formatErrorMessage(id, exception)))
+                       .build();
+    }
+
+    @SuppressWarnings("UnusedParameters")
+    protected String formatErrorMessage(long id, E exception) {
+        return String.format("There was an error processing your request. It has been logged (ID %016x).", id);
+    }
+
+    protected void logException(long id, E exception) {
+        LOGGER.error(formatLogMessage(id, exception), exception);
+    }
+
+    @SuppressWarnings("UnusedParameters")
+    protected String formatLogMessage(long id, Throwable exception) {
+        return String.format("Error handling a request: %016x", id);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/filter/AllowedMethodsFilter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/filter/AllowedMethodsFilter.java
new file mode 100644
index 0000000..06223ea
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/filter/AllowedMethodsFilter.java
@@ -0,0 +1,56 @@
+package io.dropwizard.jersey.filter;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Set;
+
+public class AllowedMethodsFilter implements Filter {
+
+    public static final String ALLOWED_METHODS_PARAM = "allowedMethods";
+    public static final Set<String> DEFAULT_ALLOWED_METHODS = ImmutableSet.of(
+            "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"
+    );
+
+    private static final Logger LOG = LoggerFactory.getLogger(AllowedMethodsFilter.class);
+
+    private Set<String> allowedMethods = Sets.newHashSet();
+
+    @Override
+    public void init(FilterConfig config) {
+        final String allowedMethodsConfig = config.getInitParameter(ALLOWED_METHODS_PARAM);
+        if (allowedMethodsConfig == null) {
+            allowedMethods.addAll(DEFAULT_ALLOWED_METHODS);
+        }
+        else {
+            allowedMethods.addAll(Arrays.asList(allowedMethodsConfig.split(",")));
+        }
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
+        handle((HttpServletRequest)request, (HttpServletResponse)response, chain);
+    }
+
+    private void handle(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
+        if (allowedMethods.contains(request.getMethod())) {
+            chain.doFilter(request, response);
+        }
+        else {
+            LOG.debug("Request with disallowed method {} blocked", request.getMethod());
+            response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+        }
+    }
+
+    @Override
+    public void destroy() {
+        allowedMethods.clear();
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalQueryParamInjectableProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalQueryParamInjectableProvider.java
new file mode 100644
index 0000000..79f63f7
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalQueryParamInjectableProvider.java
@@ -0,0 +1,123 @@
+package io.dropwizard.jersey.guava;
+
+import com.google.common.base.Optional;
+import com.sun.jersey.api.ParamException;
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.api.model.Parameter;
+import com.sun.jersey.core.spi.component.ComponentContext;
+import com.sun.jersey.core.spi.component.ComponentScope;
+import com.sun.jersey.core.spi.component.ProviderServices;
+import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable;
+import com.sun.jersey.server.impl.model.parameter.multivalued.ExtractorContainerException;
+import com.sun.jersey.server.impl.model.parameter.multivalued.MultivaluedParameterExtractor;
+import com.sun.jersey.server.impl.model.parameter.multivalued.MultivaluedParameterExtractorFactory;
+import com.sun.jersey.server.impl.model.parameter.multivalued.StringReaderFactory;
+import com.sun.jersey.spi.inject.Injectable;
+import com.sun.jersey.spi.inject.InjectableProvider;
+
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.ext.Provider;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+ at Provider
+public class OptionalQueryParamInjectableProvider implements InjectableProvider<QueryParam, Parameter> {
+    private static class QueryParamInjectable extends AbstractHttpContextInjectable<Object> {
+        private final MultivaluedParameterExtractor extractor;
+        private final boolean decode;
+
+        private QueryParamInjectable(MultivaluedParameterExtractor extractor,
+                                     boolean decode) {
+            this.extractor = extractor;
+            this.decode = decode;
+        }
+
+        @Override
+        public Object getValue(HttpContext c) {
+            try {
+                return extractor.extract(c.getUriInfo().getQueryParameters(decode));
+            } catch (ExtractorContainerException e) {
+                throw new ParamException.QueryParamException(e.getCause(),
+                                                             extractor.getName(),
+                                                             extractor.getDefaultStringValue());
+            }
+        }
+    }
+
+    private static class OptionalExtractor implements MultivaluedParameterExtractor {
+        private final MultivaluedParameterExtractor extractor;
+
+        private OptionalExtractor(MultivaluedParameterExtractor extractor) {
+            this.extractor = extractor;
+        }
+
+        @Override
+        public String getName() {
+            return extractor.getName();
+        }
+
+        @Override
+        public String getDefaultStringValue() {
+            return extractor.getDefaultStringValue();
+        }
+
+        @Override
+        public Object extract(MultivaluedMap<String, String> parameters) {
+            return Optional.fromNullable(extractor.extract(parameters));
+        }
+    }
+
+    private final ProviderServices services;
+    private MultivaluedParameterExtractorFactory factory;
+
+    public OptionalQueryParamInjectableProvider(@Context ProviderServices services) {
+        this.services = services;
+    }
+
+    @Override
+    public ComponentScope getScope() {
+        return ComponentScope.PerRequest;
+    }
+
+    @Override
+    public Injectable<?> getInjectable(ComponentContext ic,
+                                       QueryParam a,
+                                       Parameter c) {
+        if (isExtractable(c)) {
+            final OptionalExtractor extractor = new OptionalExtractor(getFactory().get(unpack(c)));
+            return new QueryParamInjectable(extractor, !c.isEncoded());
+        }
+        return null;
+    }
+
+    private boolean isExtractable(Parameter param) {
+        return (param.getSourceName() != null) && !param.getSourceName().isEmpty() &&
+                param.getParameterClass().isAssignableFrom(Optional.class) &&
+                (param.getParameterType() instanceof ParameterizedType);
+    }
+
+    private Parameter unpack(Parameter param) {
+        final Type typeParameter = ((ParameterizedType) param.getParameterType()).getActualTypeArguments()[0];
+        return new Parameter(param.getAnnotations(),
+                             param.getAnnotation(),
+                             param.getSource(),
+                             param.getSourceName(),
+                             typeParameter,
+                             (Class<?>) typeParameter,
+                             param.isEncoded(),
+                             param.getDefaultValue());
+    }
+
+    private MultivaluedParameterExtractorFactory getFactory() {
+        if (factory == null) {
+            final StringReaderFactory stringReaderFactory = new StringReaderFactory();
+            stringReaderFactory.init(services);
+
+            this.factory = new MultivaluedParameterExtractorFactory(stringReaderFactory);
+        }
+
+        return factory;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalResourceMethodDispatchAdapter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalResourceMethodDispatchAdapter.java
new file mode 100644
index 0000000..8ed0725
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalResourceMethodDispatchAdapter.java
@@ -0,0 +1,54 @@
+package io.dropwizard.jersey.guava;
+
+import com.google.common.base.Optional;
+import com.sun.jersey.api.NotFoundException;
+import com.sun.jersey.api.core.HttpContext;
+import com.sun.jersey.api.model.AbstractResourceMethod;
+import com.sun.jersey.spi.container.ResourceMethodDispatchAdapter;
+import com.sun.jersey.spi.container.ResourceMethodDispatchProvider;
+import com.sun.jersey.spi.dispatch.RequestDispatcher;
+
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class OptionalResourceMethodDispatchAdapter implements ResourceMethodDispatchAdapter {
+    private static class OptionalResourceMethodDispatchProvider implements ResourceMethodDispatchProvider {
+        private final ResourceMethodDispatchProvider provider;
+
+        private OptionalResourceMethodDispatchProvider(ResourceMethodDispatchProvider provider) {
+            this.provider = provider;
+        }
+
+        @Override
+        public RequestDispatcher create(AbstractResourceMethod abstractResourceMethod) {
+            return new OptionalRequestDispatcher(provider.create(abstractResourceMethod));
+        }
+    }
+
+    private static class OptionalRequestDispatcher implements RequestDispatcher {
+        private final RequestDispatcher dispatcher;
+
+        private OptionalRequestDispatcher(RequestDispatcher dispatcher) {
+            this.dispatcher = dispatcher;
+        }
+
+        @Override
+        public void dispatch(Object resource, HttpContext context) {
+            dispatcher.dispatch(resource, context);
+            final Object entity = context.getResponse().getEntity();
+            if (entity instanceof Optional) {
+                final Optional<?> optional = (Optional<?>) entity;
+                if (optional.isPresent()) {
+                    context.getResponse().setEntity(optional.get());
+                } else {
+                    throw new NotFoundException();
+                }
+            }
+        }
+    }
+
+    @Override
+    public ResourceMethodDispatchProvider adapt(ResourceMethodDispatchProvider provider) {
+        return new OptionalResourceMethodDispatchProvider(provider);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProvider.java
new file mode 100755
index 0000000..fc33acc
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProvider.java
@@ -0,0 +1,139 @@
+package io.dropwizard.jersey.jackson;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider;
+import io.dropwizard.validation.ConstraintViolations;
+import io.dropwizard.validation.Validated;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.ConstraintViolationException;
+import javax.validation.Valid;
+import javax.validation.Validator;
+import javax.validation.groups.Default;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.*;
+
+/**
+ * A Jersey provider which enables using Jackson to parse request entities into objects and generate
+ * response entities from objects. Any request entity method parameters annotated with
+ * {@code @Valid} are validated, and an informative 422 Unprocessable Entity response is returned
+ * should the entity be invalid.
+ * <p/>
+ * (Essentially, extends {@link JacksonJaxbJsonProvider} with validation and support for
+ * {@link JsonIgnoreType}.)
+ */
+public class JacksonMessageBodyProvider extends JacksonJaxbJsonProvider {
+    /**
+     * The default group array used in case any of the validate methods is called without a group.
+     */
+    private static final Class<?>[] DEFAULT_GROUP_ARRAY = new Class<?>[]{ Default.class };
+    private final ObjectMapper mapper;
+    private final Validator validator;
+
+    public JacksonMessageBodyProvider(ObjectMapper mapper, Validator validator) {
+        this.validator = validator;
+        this.mapper = mapper;
+        setMapper(mapper);
+    }
+
+    @Override
+    public boolean isReadable(Class<?> type,
+                              Type genericType,
+                              Annotation[] annotations,
+                              MediaType mediaType) {
+        return isProvidable(type) && super.isReadable(type, genericType, annotations, mediaType);
+    }
+
+    @Override
+    public Object readFrom(Class<Object> type,
+                           Type genericType,
+                           Annotation[] annotations,
+                           MediaType mediaType,
+                           MultivaluedMap<String, String> httpHeaders,
+                           InputStream entityStream) throws IOException {
+        return validate(annotations, super.readFrom(type,
+                                                    genericType,
+                                                    annotations,
+                                                    mediaType,
+                                                    httpHeaders,
+                                                    entityStream));
+    }
+
+    private Object validate(Annotation[] annotations, Object value) {
+        if(null == value) {
+            throw new ConstraintViolationException("The request entity was empty",
+                    Collections.<ConstraintViolation<Object>>emptySet());
+        }
+
+        final Class<?>[] classes = findValidationGroups(annotations);
+
+        if (classes != null) {
+            Set<ConstraintViolation<Object>> violations = null;
+
+            if(value instanceof Map) {
+                violations = validate(((Map)value).values(), classes);
+            } else if(value instanceof Iterable) {
+                violations = validate((Iterable)value, classes);
+            } else if(value.getClass().isArray()) {
+                violations = new HashSet<>();
+
+                Object[] values = (Object[]) value;
+                for(Object item : values) {
+                    violations.addAll(validator.validate(item, classes));
+                }
+            } else {
+                violations = validator.validate(value, classes);
+            }
+
+            if (violations != null && !violations.isEmpty()) {
+                throw new ConstraintViolationException("The request entity had the following errors:",
+                        ConstraintViolations.copyOf(violations));
+            }
+        }
+
+        return value;
+    }
+
+    private Set<ConstraintViolation<Object>> validate(Iterable values, Class<?>[] classes) {
+        Set<ConstraintViolation<Object>> violations = new HashSet<>();
+        for(Object value : values) {
+            violations.addAll(validator.validate(value, classes));
+        }
+
+        return violations;
+    }
+
+    private Class<?>[] findValidationGroups(Annotation[] annotations) {
+        for (Annotation annotation : annotations) {
+            if (annotation.annotationType() == Valid.class) {
+                return DEFAULT_GROUP_ARRAY;
+            } else if (annotation.annotationType() == Validated.class) {
+                return  ((Validated) annotation).value();
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type,
+                               Type genericType,
+                               Annotation[] annotations,
+                               MediaType mediaType) {
+        return isProvidable(type) && super.isWriteable(type, genericType, annotations, mediaType);
+    }
+
+    private boolean isProvidable(Class<?> type) {
+        final JsonIgnoreType ignore = type.getAnnotation(JsonIgnoreType.class);
+        return (ignore == null) || !ignore.value();
+    }
+
+    public ObjectMapper getObjectMapper() {
+        return mapper;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapper.java
new file mode 100644
index 0000000..e209aca
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapper.java
@@ -0,0 +1,46 @@
+package io.dropwizard.jersey.jackson;
+
+import com.fasterxml.jackson.core.JsonGenerationException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import io.dropwizard.jersey.errors.ErrorMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {
+    private static final Logger LOGGER = LoggerFactory.getLogger(JsonProcessingExceptionMapper.class);
+
+    @Override
+    public Response toResponse(JsonProcessingException exception) {
+        /*
+         * If the error is in the JSON generation, it's a server error.
+         */
+        if (exception instanceof JsonGenerationException) {
+            LOGGER.warn("Error generating JSON", exception);
+            return Response.serverError().build();
+        }
+
+        final String message = exception.getOriginalMessage();
+
+        /*
+         * If we can't deserialize the JSON because someone forgot a no-arg constructor, it's a
+         * server error and we should inform the developer.
+         */
+        if (message.startsWith("No suitable constructor found")) {
+            LOGGER.error("Unable to deserialize the specific type", exception);
+            return Response.serverError().build();
+        }
+
+        /*
+         * Otherwise, it's those pesky users.
+         */
+        LOGGER.debug("Unable to process JSON", exception);
+        return Response.status(Response.Status.BAD_REQUEST)
+                       .entity(new ErrorMessage(message))
+                       .build();
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/AbstractParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/AbstractParam.java
new file mode 100644
index 0000000..dfaea12
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/AbstractParam.java
@@ -0,0 +1,115 @@
+package io.dropwizard.jersey.params;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+/**
+ * An abstract base class from which to build Jersey parameter classes.
+ *
+ * @param <T> the type of value wrapped by the parameter
+ */
+public abstract class AbstractParam<T> {
+    private final T value;
+
+    /**
+     * Given an input value from a client, creates a parameter wrapping its parsed value.
+     *
+     * @param input an input value from a client request
+     */
+    @SuppressWarnings({"AbstractMethodCallInConstructor", "OverriddenMethodCallDuringObjectConstruction"})
+    protected AbstractParam(String input) {
+        try {
+            this.value = parse(input);
+        } catch (Exception e) {
+            throw new WebApplicationException(error(input, e));
+        }
+    }
+
+    /**
+     * Given a string representation which was unable to be parsed and the exception thrown, produce
+     * a {@link Response} to be sent to the client.
+     *
+     * By default, generates a {@code 400 Bad Request} with a plain text entity generated by
+     * {@link #errorMessage(String, Exception)}.
+     *
+     * @param input the raw input value
+     * @param e the exception thrown while parsing {@code input}
+     * @return the {@link Response} to be sent to the client
+     */
+    protected Response error(String input, Exception e) {
+        return Response.status(getErrorStatus())
+                       .entity(errorMessage(input, e))
+                       .type(mediaType())
+                       .build();
+    }
+
+    /**
+     * Returns the media type of the error message entity.
+     *
+     * @return the media type of the error message entity
+     */
+    protected MediaType mediaType() {
+        return MediaType.TEXT_PLAIN_TYPE;
+    }
+
+    /**
+     * Given a string representation which was unable to be parsed and the exception thrown, produce
+     * an entity to be sent to the client.
+     *
+     * @param input the raw input value
+     * @param e the exception thrown while parsing {@code input}
+     * @return the error message to be sent the client
+     */
+    protected String errorMessage(String input, Exception e) {
+        return String.format("Invalid parameter: %s (%s)", input, e.getMessage());
+    }
+
+    /**
+     * Given a string representation which was unable to be parsed, produce a {@link Status} for the
+     * {@link Response} to be sent to the client.
+     *
+     * @return the HTTP {@link Status} of the error message
+     */
+    @SuppressWarnings("MethodMayBeStatic")
+    protected Status getErrorStatus() {
+        return Status.BAD_REQUEST;
+    }
+
+   /**
+    * Given a string representation, parse it and return an instance of the parameter type.
+    *
+    * @param input the raw input
+    * @return {@code input}, parsed as an instance of {@code T}
+    * @throws Exception if there is an error parsing the input
+    */
+    protected abstract T parse(String input) throws Exception;
+
+    /**
+     * Returns the underlying value.
+     *
+     * @return the underlying value
+     */
+    public T get() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) { return true; }
+        if ((obj == null) || (getClass() != obj.getClass())) { return false; }
+        final AbstractParam<?> that = (AbstractParam<?>) obj;
+        return value.equals(that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return value.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return value.toString();
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/BooleanParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/BooleanParam.java
new file mode 100644
index 0000000..da61bf0
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/BooleanParam.java
@@ -0,0 +1,29 @@
+package io.dropwizard.jersey.params;
+
+/**
+ * A parameter encapsulating boolean values. If the query parameter value is {@code "true"},
+ * regardless of case, the returned value is {@link Boolean#TRUE}. If the query parameter value is
+ * {@code "false"}, regardless of case, the returned value is {@link Boolean#FALSE}. All other
+ * values will return a {@code 400 Bad Request} response.
+ */
+public class BooleanParam extends AbstractParam<Boolean> {
+    public BooleanParam(String input) {
+        super(input);
+    }
+
+    @Override
+    protected String errorMessage(String input, Exception e) {
+        return '"' + input + "\" must be \"true\" or \"false\".";
+    }
+
+    @Override
+    protected Boolean parse(String input) throws Exception {
+        if ("true".equalsIgnoreCase(input)) {
+            return Boolean.TRUE;
+        }
+        if ("false".equalsIgnoreCase(input)) {
+            return Boolean.FALSE;
+        }
+        throw new Exception();
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/DateTimeParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/DateTimeParam.java
new file mode 100644
index 0000000..12ba3fc
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/DateTimeParam.java
@@ -0,0 +1,19 @@
+package io.dropwizard.jersey.params;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+
+/**
+ * A parameter encapsulating date/time values. All non-parsable values will return a {@code 400 Bad
+ * Request} response. All values returned are in UTC.
+ */
+public class DateTimeParam extends AbstractParam<DateTime> {
+    public DateTimeParam(String input) {
+        super(input);
+    }
+
+    @Override
+    protected DateTime parse(String input) throws Exception {
+        return new DateTime(input, DateTimeZone.UTC);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/IntParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/IntParam.java
new file mode 100644
index 0000000..559c9b7
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/IntParam.java
@@ -0,0 +1,21 @@
+package io.dropwizard.jersey.params;
+
+/**
+ * A parameter encapsulating integer values. All non-decimal values will return a
+ * {@code 400 Bad Request} response.
+ */
+public class IntParam extends AbstractParam<Integer> {
+    public IntParam(String input) {
+        super(input);
+    }
+
+    @Override
+    protected String errorMessage(String input, Exception e) {
+        return '"' + input + "\" is not a number.";
+    }
+
+    @Override
+    protected Integer parse(String input) {
+        return Integer.valueOf(input);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LongParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LongParam.java
new file mode 100644
index 0000000..6fcdea7
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LongParam.java
@@ -0,0 +1,21 @@
+package io.dropwizard.jersey.params;
+
+/**
+ * A parameter encapsulating long values. All non-decimal values will return a {@code 400 Bad
+ * Request} response.
+ */
+public class LongParam extends AbstractParam<Long> {
+    public LongParam(String input) {
+        super(input);
+    }
+
+    @Override
+    protected String errorMessage(String input, Exception e) {
+        return '"' + input + "\" is not a number.";
+    }
+
+    @Override
+    protected Long parse(String input) {
+        return Long.valueOf(input);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/UUIDParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/UUIDParam.java
new file mode 100644
index 0000000..070c3b0
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/UUIDParam.java
@@ -0,0 +1,25 @@
+package io.dropwizard.jersey.params;
+
+import java.util.UUID;
+
+/**
+ * A parameter encapsulating UUID values. All non-parsable values will return a {@code 400 Bad
+ * Request} response.
+ */
+public class UUIDParam extends AbstractParam<UUID> {
+
+    public UUIDParam(String input) {
+        super(input);
+    }
+
+    @Override
+    protected String errorMessage(String input, Exception e) {
+        return '"' + input + "\" is not a UUID.";
+    }
+
+    @Override
+    protected UUID parse(String input) throws Exception {
+        return UUID.fromString(input);
+    }
+
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Flash.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Flash.java
new file mode 100644
index 0000000..7e1f8f6
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Flash.java
@@ -0,0 +1,28 @@
+package io.dropwizard.jersey.sessions;
+
+import com.google.common.base.Optional;
+
+import javax.servlet.http.HttpSession;
+
+public class Flash<T> {
+    private static final String ATTRIBUTE = "flash";
+    private final HttpSession session;
+    private final T value;
+
+    @SuppressWarnings("unchecked")
+    Flash(HttpSession session) {
+        this.session = session;
+        this.value = (T) session.getAttribute(ATTRIBUTE);
+        if (this.value != null) {
+            session.removeAttribute(ATTRIBUTE);
+        }
+    }
+
+    public Optional<T> get() {
+        return Optional.fromNullable(value);
+    }
+
+    public void set(T value) {
+        session.setAttribute(ATTRIBUTE, value);
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/FlashProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/FlashProvider.java
new file mode 100644
index 0000000..b63947b
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/FlashProvider.java
@@ -0,0 +1,47 @@
+package io.dropwizard.jersey.sessions;
+
+import com.sun.jersey.api.model.Parameter;
+import com.sun.jersey.core.spi.component.ComponentContext;
+import com.sun.jersey.core.spi.component.ComponentScope;
+import com.sun.jersey.spi.inject.Injectable;
+import com.sun.jersey.spi.inject.InjectableProvider;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class FlashProvider implements InjectableProvider<Session, Parameter> {
+    private final ThreadLocal<HttpServletRequest> request;
+
+    public FlashProvider(@Context ThreadLocal<HttpServletRequest> request) {
+        this.request = request;
+    }
+
+    @Override
+    public ComponentScope getScope() {
+        return ComponentScope.PerRequest;
+    }
+
+    @Override
+    public Injectable<?> getInjectable(ComponentContext ic, final Session annotation, Parameter parameter) {
+        if (parameter.getParameterClass().isAssignableFrom(Flash.class)) {
+            return new Injectable<Flash<?>>() {
+                @Override
+                public Flash<?> getValue() {
+                    final HttpServletRequest req = request.get();
+                    if (req != null) {
+                        final HttpSession session = req.getSession(!annotation.doNotCreate());
+                        if (session != null) {
+                            return new Flash<>(session);
+                        }
+                        return null;
+                    }
+                    return null;
+                }
+            };
+        }
+        return null;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/HttpSessionProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/HttpSessionProvider.java
new file mode 100644
index 0000000..171969c
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/HttpSessionProvider.java
@@ -0,0 +1,43 @@
+package io.dropwizard.jersey.sessions;
+
+import com.sun.jersey.api.model.Parameter;
+import com.sun.jersey.core.spi.component.ComponentContext;
+import com.sun.jersey.core.spi.component.ComponentScope;
+import com.sun.jersey.spi.inject.Injectable;
+import com.sun.jersey.spi.inject.InjectableProvider;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class HttpSessionProvider implements InjectableProvider<Session, Parameter> {
+    private final ThreadLocal<HttpServletRequest> request;
+
+    public HttpSessionProvider(@Context ThreadLocal<HttpServletRequest> request) {
+        this.request = request;
+    }
+
+    @Override
+    public ComponentScope getScope() {
+        return ComponentScope.PerRequest;
+    }
+
+    @Override
+    public Injectable<?> getInjectable(ComponentContext ic, final Session session, Parameter parameter) {
+        if (parameter.getParameterClass().isAssignableFrom(HttpSession.class)) {
+            return new Injectable<HttpSession>() {
+                @Override
+                public HttpSession getValue() {
+                    final HttpServletRequest req = request.get();
+                    if (req != null) {
+                        return req.getSession(!session.doNotCreate());
+                    }
+                    return null;
+                }
+            };
+        }
+        return null;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Session.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Session.java
new file mode 100644
index 0000000..1e21cd7
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Session.java
@@ -0,0 +1,10 @@
+package io.dropwizard.jersey.sessions;
+
+import java.lang.annotation.*;
+
+ at Documented
+ at Retention(RetentionPolicy.RUNTIME)
+ at Target({ElementType.PARAMETER, ElementType.FIELD})
+public @interface Session {
+    boolean doNotCreate() default false;
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyContainerHolder.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyContainerHolder.java
new file mode 100644
index 0000000..bc57d0d
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyContainerHolder.java
@@ -0,0 +1,19 @@
+package io.dropwizard.jersey.setup;
+
+import com.sun.jersey.spi.container.servlet.ServletContainer;
+
+public class JerseyContainerHolder {
+    private ServletContainer container;
+
+    public JerseyContainerHolder(ServletContainer container) {
+        this.container = container;
+    }
+
+    public ServletContainer getContainer() {
+        return container;
+    }
+
+    public void setContainer(ServletContainer container) {
+        this.container = container;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyEnvironment.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyEnvironment.java
new file mode 100644
index 0000000..6125fec
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyEnvironment.java
@@ -0,0 +1,113 @@
+package io.dropwizard.jersey.setup;
+
+import com.google.common.base.Function;
+import com.sun.jersey.api.core.ResourceConfig;
+import com.sun.jersey.core.spi.scanning.PackageNamesScanner;
+import com.sun.jersey.spi.container.servlet.ServletContainer;
+import io.dropwizard.jersey.DropwizardResourceConfig;
+
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class JerseyEnvironment {
+    private final JerseyContainerHolder holder;
+    private final DropwizardResourceConfig config;
+
+    public JerseyEnvironment(JerseyContainerHolder holder,
+                             DropwizardResourceConfig config) {
+        this.holder = holder;
+        this.config = config;
+    }
+
+    public void disable() {
+        holder.setContainer(null);
+    }
+
+    public void replace(Function<ResourceConfig, ServletContainer> replace) {
+        holder.setContainer(replace.apply(config));
+    }
+
+    /**
+     * Adds the given object as a Jersey singleton component.
+     *
+     * @param component a Jersey singleton component
+     */
+    public void register(Object component) {
+        config.getSingletons().add(checkNotNull(component));
+    }
+
+    /**
+     * Adds the given class as a Jersey component. <p/><b>N.B.:</b> This class must either have a
+     * no-args constructor or use Jersey's built-in dependency injection.
+     *
+     * @param componentClass a Jersey component class
+     */
+    public void register(Class<?> componentClass) {
+        config.getClasses().add(checkNotNull(componentClass));
+    }
+
+    /**
+     * Adds array of package names which will be used to scan for components. Packages will be
+     * scanned recursively, including all nested packages.
+     *
+     * @param packages array of package names
+     */
+    public void packages(String... packages) {
+        config.init(new PackageNamesScanner(checkNotNull(packages)));
+    }
+
+    /**
+     * Enables the Jersey feature with the given name.
+     *
+     * @param featureName the name of the feature to be enabled
+     * @see com.sun.jersey.api.core.ResourceConfig
+     */
+    public void enable(String featureName) {
+        config.getFeatures().put(checkNotNull(featureName), Boolean.TRUE);
+    }
+
+    /**
+     * Disables the Jersey feature with the given name.
+     *
+     * @param featureName the name of the feature to be disabled
+     * @see com.sun.jersey.api.core.ResourceConfig
+     */
+    public void disable(String featureName) {
+        config.getFeatures().put(checkNotNull(featureName), Boolean.FALSE);
+    }
+
+    /**
+     * Sets the given Jersey property.
+     *
+     * @param name  the name of the Jersey property
+     * @param value the value of the Jersey property
+     * @see com.sun.jersey.api.core.ResourceConfig
+     */
+    public void property(String name, @Nullable Object value) {
+        config.getProperties().put(checkNotNull(name), value);
+    }
+
+    /**
+     * Gets the given Jersey property.
+     *
+     * @param name the name of the Jersey property
+     * @see com.sun.jersey.api.core.ResourceConfig
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T getProperty(String name) {
+        return (T) config.getProperties().get(name);
+    }
+
+    public String getUrlPattern() {
+        return config.getUrlPattern();
+    }
+
+    public void setUrlPattern(String urlPattern) {
+        config.setUrlPattern(urlPattern);
+    }
+
+    public ResourceConfig getResourceConfig() {
+        return config;
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapper.java
new file mode 100644
index 0000000..ce782f6
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapper.java
@@ -0,0 +1,20 @@
+package io.dropwizard.jersey.validation;
+
+import javax.validation.ConstraintViolationException;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.ext.ExceptionMapper;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class ConstraintViolationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
+    private static final int UNPROCESSABLE_ENTITY = 422;
+
+    @Override
+    public Response toResponse(ConstraintViolationException exception) {
+        final ValidationErrorMessage message = new ValidationErrorMessage(exception.getConstraintViolations());
+
+        return Response.status(UNPROCESSABLE_ENTITY)
+                       .entity(message)
+                       .build();
+    }
+}
diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ValidationErrorMessage.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ValidationErrorMessage.java
new file mode 100644
index 0000000..bdd1e2c
--- /dev/null
+++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ValidationErrorMessage.java
@@ -0,0 +1,21 @@
+package io.dropwizard.jersey.validation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.validation.ConstraintViolations;
+
+import javax.validation.ConstraintViolation;
+import java.util.Set;
+
+public class ValidationErrorMessage {
+    private final ImmutableList<String> errors;
+
+    public ValidationErrorMessage(Set<ConstraintViolation<?>> errors) {
+        this.errors = ConstraintViolations.formatUntyped(errors);
+    }
+
+    @JsonProperty
+    public ImmutableList<String> getErrors() {
+        return errors;
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/DropwizardResourceConfigTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/DropwizardResourceConfigTest.java
new file mode 100644
index 0000000..766d43c
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/DropwizardResourceConfigTest.java
@@ -0,0 +1,46 @@
+package io.dropwizard.jersey;
+
+import com.codahale.metrics.MetricRegistry;
+import com.sun.jersey.core.spi.scanning.PackageNamesScanner;
+import io.dropwizard.jersey.dummy.DummyResource;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+ at SuppressWarnings("unchecked")
+public class DropwizardResourceConfigTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Test
+    public void findsResourceClassInPackage() {
+        final DropwizardResourceConfig rc = DropwizardResourceConfig.forTesting(new MetricRegistry());
+        rc.init(new PackageNamesScanner(new String[] { DummyResource.class.getPackage().getName() }));
+
+        assertThat(rc.getRootResourceClasses())
+                .containsOnly(DummyResource.class);
+    }
+
+    @Test
+    public void findsResourceClassesInPackageAndSubpackage() {
+        final DropwizardResourceConfig rc = DropwizardResourceConfig.forTesting(new MetricRegistry());
+        rc.init(new PackageNamesScanner(new String[] { getClass().getPackage().getName() }));
+
+        assertThat(rc.getRootResourceClasses())
+                .contains
+                        (DummyResource.class, TestResource.class);
+    }
+
+    @Path("/dummy")
+    public static class TestResource {
+        @GET
+        public String foo() {
+            return "bar";
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CacheControlledResourceMethodDispatchAdapterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CacheControlledResourceMethodDispatchAdapterTest.java
new file mode 100644
index 0000000..cdffc50
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CacheControlledResourceMethodDispatchAdapterTest.java
@@ -0,0 +1,95 @@
+package io.dropwizard.jersey.caching;
+
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.core.HttpHeaders;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class CacheControlledResourceMethodDispatchAdapterTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.caching").build();
+    }
+
+    @Test
+    public void immutableResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/immutable").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-transform, max-age=31536000");
+    }
+
+    @Test
+    public void privateResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/private").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("private, no-transform");
+    }
+
+    @Test
+    public void maxAgeResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/max-age").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-transform, max-age=1123200");
+    }
+
+    @Test
+    public void noCacheResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/no-cache").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-cache, no-transform");
+    }
+
+    @Test
+    public void noStoreResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/no-store").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-store, no-transform");
+    }
+
+    @Test
+    public void noTransformResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/no-transform").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .isNull();
+    }
+
+    @Test
+    public void mustRevalidateResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/must-revalidate").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-transform, must-revalidate");
+    }
+
+    @Test
+    public void proxyRevalidateResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/proxy-revalidate").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-transform, proxy-revalidate");
+    }
+
+    @Test
+    public void sharedMaxAgeResponsesHaveCacheControlHeaders() throws Exception {
+        final ClientResponse response = resource().path("/caching/shared-max-age").get(ClientResponse.class);
+
+        assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL))
+                .containsOnly("no-transform, s-maxage=46800");
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CachingResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CachingResource.java
new file mode 100644
index 0000000..3b2fdb4
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CachingResource.java
@@ -0,0 +1,74 @@
+package io.dropwizard.jersey.caching;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.util.concurrent.TimeUnit;
+
+ at Path("/caching/")
+ at Produces(MediaType.TEXT_PLAIN)
+public class CachingResource {
+    @GET
+    @Path("/immutable")
+    @CacheControl(immutable = true)
+    public String showImmutable() {
+        return "immutable";
+    }
+
+    @GET
+    @Path("/private")
+    @CacheControl(isPrivate = true)
+    public String showPrivate() {
+        return "private";
+    }
+
+    @GET
+    @Path("/max-age")
+    @CacheControl(maxAge = 13, maxAgeUnit = TimeUnit.DAYS)
+    public String showMaxAge() {
+        return "max-age";
+    }
+
+    @GET
+    @Path("/no-cache")
+    @CacheControl(noCache = true)
+    public String showNoCache() {
+        return "no-cache";
+    }
+
+    @GET
+    @Path("/no-store")
+    @CacheControl(noStore = true)
+    public String showNoStore() {
+        return "no-store";
+    }
+
+    @GET
+    @Path("/no-transform")
+    @CacheControl(noTransform = false)
+    public String showNoTransform() {
+        return "no-transform";
+    }
+
+    @GET
+    @Path("/must-revalidate")
+    @CacheControl(mustRevalidate = true)
+    public String showMustRevalidate() {
+        return "must-revalidate";
+    }
+
+    @GET
+    @Path("/proxy-revalidate")
+    @CacheControl(proxyRevalidate = true)
+    public String showProxyRevalidate() {
+        return "proxy-revalidate";
+    }
+
+    @GET
+    @Path("/shared-max-age")
+    @CacheControl(sharedMaxAge = 13, sharedMaxAgeUnit = TimeUnit.HOURS)
+    public String showSharedMaxAge() {
+        return "shared-max-age";
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/dummy/DummyResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/dummy/DummyResource.java
new file mode 100644
index 0000000..01fe131
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/dummy/DummyResource.java
@@ -0,0 +1,12 @@
+package io.dropwizard.jersey.dummy;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+ at Path("/")
+public class DummyResource {
+    @GET
+    public String foo() {
+        return "bar";
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultJacksonMessageBodyProvider.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultJacksonMessageBodyProvider.java
new file mode 100644
index 0000000..559a4cd
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultJacksonMessageBodyProvider.java
@@ -0,0 +1,14 @@
+package io.dropwizard.jersey.errors;
+
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+
+import javax.validation.Validation;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class DefaultJacksonMessageBodyProvider extends JacksonMessageBodyProvider {
+    public DefaultJacksonMessageBodyProvider() {
+        super(Jackson.newObjectMapper(), Validation.buildDefaultValidatorFactory().getValidator());
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultLoggingExceptionMapper.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultLoggingExceptionMapper.java
new file mode 100644
index 0000000..3ef7106
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultLoggingExceptionMapper.java
@@ -0,0 +1,7 @@
+package io.dropwizard.jersey.errors;
+
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class DefaultLoggingExceptionMapper extends LoggingExceptionMapper<Throwable> {
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/ExceptionResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/ExceptionResource.java
new file mode 100644
index 0000000..5435486
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/ExceptionResource.java
@@ -0,0 +1,16 @@
+package io.dropwizard.jersey.errors;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+
+ at Path("/exception/")
+ at Produces(MediaType.APPLICATION_JSON)
+public class ExceptionResource {
+    @GET
+    public String show() throws IOException {
+        throw new IOException("WHAT");
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/LoggingExceptionMapperTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/LoggingExceptionMapperTest.java
new file mode 100644
index 0000000..c8e4ab7
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/LoggingExceptionMapperTest.java
@@ -0,0 +1,38 @@
+package io.dropwizard.jersey.errors;
+
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class LoggingExceptionMapperTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.errors").build();
+    }
+
+    @Test
+    public void returnsAnErrorMessage() throws Exception {
+        try {
+            resource().path("/exception/").type(MediaType.APPLICATION_JSON).get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(500);
+
+            assertThat(e.getResponse().getEntity(String.class))
+                    .startsWith("{\"message\":\"There was an error processing your request. It has been logged (ID ");
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/AllowedMethodsFilterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/AllowedMethodsFilterTest.java
new file mode 100644
index 0000000..3209782
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/AllowedMethodsFilterTest.java
@@ -0,0 +1,106 @@
+package io.dropwizard.jersey.filter;
+
+import com.google.common.collect.ImmutableMap;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.*;
+
+public class AllowedMethodsFilterTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    private static final int DISALLOWED_STATUS_CODE = ClientResponse.Status.METHOD_NOT_ALLOWED.getStatusCode();
+    private static final int OK_STATUS_CODE = ClientResponse.Status.OK.getStatusCode();
+
+    private final HttpServletRequest request = mock(HttpServletRequest.class);
+    private final HttpServletResponse response = mock(HttpServletResponse.class);
+    private final FilterChain chain = mock(FilterChain.class);
+    private final FilterConfig config = mock(FilterConfig.class);
+    private final AllowedMethodsFilter filter = new AllowedMethodsFilter();
+
+    @Before
+    public void setUp() {
+        filter.init(config);
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.filter")
+                .addFilter(AllowedMethodsFilter.class, "allowedMethods", ImmutableMap.of(AllowedMethodsFilter.ALLOWED_METHODS_PARAM, "GET,POST"))
+                .build();
+    }
+
+    private int getResponseStatusForRequestMethod(String method) {
+        final ClientResponse response = resource().path("/ping").method(method, ClientResponse.class);
+
+        try {
+            return response.getStatus();
+        }
+        finally {
+            response.close();
+        }
+    }
+
+    @Test
+    public void testGetRequestAllowed() {
+        assertEquals(OK_STATUS_CODE, getResponseStatusForRequestMethod("GET"));
+    }
+
+    @Test
+    public void testPostRequestAllowed() {
+        assertEquals(OK_STATUS_CODE, getResponseStatusForRequestMethod("POST"));
+    }
+
+    @Test
+    public void testPutRequestBlocked() {
+        assertEquals(DISALLOWED_STATUS_CODE, getResponseStatusForRequestMethod("PUT"));
+    }
+
+    @Test
+    public void testDeleteRequestBlocked() {
+        assertEquals(DISALLOWED_STATUS_CODE, getResponseStatusForRequestMethod("DELETE"));
+    }
+
+    @Test
+    public void testTraceRequestBlocked() {
+        assertEquals(DISALLOWED_STATUS_CODE, getResponseStatusForRequestMethod("TRACE"));
+    }
+
+    @Test
+    public void allowsAllowedMethod() throws Exception {
+        when(request.getMethod()).thenReturn("GET");
+        filter.doFilter(request, response, chain);
+
+        verify(chain).doFilter(request, response);
+    }
+
+    @Test
+    public void blocksDisallowedMethod() throws Exception {
+        when(request.getMethod()).thenReturn("TRACE");
+        filter.doFilter(request, response, chain);
+
+        verify(chain, never()).doFilter(request, response);
+    }
+
+    @Test
+    public void disallowedMethodCausesMethodNotAllowedResponse() throws IOException, ServletException {
+        when(request.getMethod()).thenReturn("TRACE");
+        filter.doFilter(request, response, chain);
+        verify(response).sendError(eq(DISALLOWED_STATUS_CODE));
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/DummyResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/DummyResource.java
new file mode 100644
index 0000000..a5f2cfc
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/DummyResource.java
@@ -0,0 +1,34 @@
+package io.dropwizard.jersey.filter;
+
+import io.dropwizard.jersey.PATCH;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Response;
+
+ at Path("/ping")
+public class DummyResource {
+    @GET
+    public Response get() {
+        return Response.ok().build();
+    }
+
+    @POST
+    public Response post() {
+        return Response.ok().build();
+    }
+
+    @PATCH
+    public Response patch() {
+        return Response.ok().build();
+    }
+
+    @PUT
+    public Response put() {
+        return Response.ok().build();
+    }
+
+    @DELETE
+    public Response delete() {
+        return Response.ok().build();
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalParamResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalParamResource.java
new file mode 100644
index 0000000..68fe065
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalParamResource.java
@@ -0,0 +1,18 @@
+package io.dropwizard.jersey.guava;
+
+import com.google.common.base.Optional;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+ at Path("/optional-param/")
+ at Produces(MediaType.TEXT_PLAIN)
+public class OptionalParamResource {
+    @GET
+    public String show(@QueryParam("id") Optional<Integer> id) {
+        return id.or(-1).toString();
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalQueryParamInjectableProviderTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalQueryParamInjectableProviderTest.java
new file mode 100644
index 0000000..1103f1a
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalQueryParamInjectableProviderTest.java
@@ -0,0 +1,35 @@
+package io.dropwizard.jersey.guava;
+
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class OptionalQueryParamInjectableProviderTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.guava").build();
+    }
+
+    @Test
+    public void injectsAnAbsentOptionalInsteadOfNull() throws Exception {
+        assertThat(resource().path("/optional-param/")
+                             .get(String.class))
+                .isEqualTo("-1");
+    }
+
+    @Test
+    public void injectsAPresentOptionalInsteadOfValue() throws Exception {
+        assertThat(resource().path("/optional-param/")
+                             .queryParam("id", "200")
+                             .get(String.class))
+                .isEqualTo("200");
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalResourceMethodDispatchAdapterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalResourceMethodDispatchAdapterTest.java
new file mode 100644
index 0000000..6f733b9
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalResourceMethodDispatchAdapterTest.java
@@ -0,0 +1,41 @@
+package io.dropwizard.jersey.guava;
+
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class OptionalResourceMethodDispatchAdapterTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.guava").build();
+    }
+
+    @Test
+    public void presentOptionalsReturnTheirValue() throws Exception {
+        assertThat(resource().path("/optional-return/")
+                             .queryParam("id", "woo")
+                             .get(String.class))
+                .isEqualTo("woo");
+    }
+
+    @Test
+    public void absentOptionalsThrowANotFound() throws Exception {
+        try {
+            resource().path("/optional-return/").get(String.class);
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(404);
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalReturnResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalReturnResource.java
new file mode 100644
index 0000000..ca37e55
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalReturnResource.java
@@ -0,0 +1,18 @@
+package io.dropwizard.jersey.guava;
+
+import com.google.common.base.Optional;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+ at Path("/optional-return/")
+ at Produces(MediaType.TEXT_PLAIN)
+public class OptionalReturnResource {
+    @GET
+    public Optional<String> show(@QueryParam("id") String id) {
+        return Optional.fromNullable(id);
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/BrokenRepresentation.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/BrokenRepresentation.java
new file mode 100644
index 0000000..3701090
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/BrokenRepresentation.java
@@ -0,0 +1,23 @@
+package io.dropwizard.jersey.jackson;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class BrokenRepresentation {
+    private List<String> messages;
+
+    public BrokenRepresentation(List<String> messages) {
+        this.messages = messages;
+    }
+
+    @JsonProperty
+    public List<String> getMessages() {
+        return messages;
+    }
+
+    @JsonProperty
+    public void setMessages(List<String> messages) {
+        this.messages = messages;
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/DefaultJacksonMessageBodyProvider.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/DefaultJacksonMessageBodyProvider.java
new file mode 100644
index 0000000..181ebdf
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/DefaultJacksonMessageBodyProvider.java
@@ -0,0 +1,13 @@
+package io.dropwizard.jersey.jackson;
+
+import io.dropwizard.jackson.Jackson;
+
+import javax.validation.Validation;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class DefaultJacksonMessageBodyProvider extends JacksonMessageBodyProvider {
+    public DefaultJacksonMessageBodyProvider() {
+        super(Jackson.newObjectMapper(), Validation.buildDefaultValidatorFactory().getValidator());
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProviderTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProviderTest.java
new file mode 100755
index 0000000..6b2b7da
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProviderTest.java
@@ -0,0 +1,576 @@
+package io.dropwizard.jersey.jackson;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreType;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Objects;
+import com.google.common.reflect.TypeToken;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
+import com.sun.jersey.core.util.StringKeyObjectValueIgnoreCaseMultivaluedMap;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.validation.ConstraintViolations;
+import io.dropwizard.validation.Validated;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.ConstraintViolationException;
+import javax.validation.Valid;
+import javax.validation.Validation;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.MediaType;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.*;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assume.assumeThat;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+
+// TODO: 4/24/13 <coda> -- move JacksonMessageBodyProviderTest to JerseyTest
+
+ at SuppressWarnings("unchecked")
+public class JacksonMessageBodyProviderTest {
+    private static final Annotation[] NONE = new Annotation[0];
+
+    public static class Example {
+        @Min(0)
+        @JsonProperty
+        int id;
+
+        @Override
+        public int hashCode() {
+            return id;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            return Objects.equal(this.id, obj);
+        }
+    }
+
+    public static class ListExample {
+        @NotEmpty
+        @Valid
+        @JsonProperty
+        List<Example> examples;
+    }
+
+    public interface Partial1{}
+    public interface Partial2{}
+
+    public static class PartialExample {
+        @Min(value = 0, groups = Partial1.class)
+        @JsonProperty
+        int id;
+
+        @NotNull(groups = Partial2.class)
+        @JsonProperty
+        String text;
+    }
+
+    @JsonIgnoreType
+    public static interface Ignorable {
+
+    }
+
+    @JsonIgnoreType(false)
+    public static interface NonIgnorable extends Ignorable {
+
+    }
+
+    private final ObjectMapper mapper = spy(Jackson.newObjectMapper());
+    private final JacksonMessageBodyProvider provider =
+            new JacksonMessageBodyProvider(mapper,
+                                           Validation.buildDefaultValidatorFactory().getValidator());
+
+    @Before
+    public void setUp() throws Exception {
+        assumeThat(Locale.getDefault().getLanguage(), is("en"));
+    }
+
+    @Test
+    public void readsDeserializableTypes() throws Exception {
+        assertThat(provider.isReadable(Example.class, null, null, null))
+                .isTrue();
+    }
+
+    @Test
+    public void writesSerializableTypes() throws Exception {
+        assertThat(provider.isWriteable(Example.class, null, null, null))
+                .isTrue();
+    }
+
+    @Test
+    public void doesNotWriteIgnoredTypes() throws Exception {
+        assertThat(provider.isWriteable(Ignorable.class, null, null, null))
+                .isFalse();
+    }
+
+    @Test
+    public void writesUnIgnoredTypes() throws Exception {
+        assertThat(provider.isWriteable(NonIgnorable.class, null, null, null))
+                .isTrue();
+    }
+
+    @Test
+    public void doesNotReadIgnoredTypes() throws Exception {
+        assertThat(provider.isReadable(Ignorable.class, null, null, null))
+                .isFalse();
+    }
+
+    @Test
+    public void readsUnIgnoredTypes() throws Exception {
+        assertThat(provider.isReadable(NonIgnorable.class, null, null, null))
+                .isTrue();
+    }
+
+    @Test
+    public void isChunked() throws Exception {
+        assertThat(provider.getSize(null, null, null, null, null))
+                .isEqualTo(-1);
+    }
+
+    @Test
+    public void deserializesRequestEntities() throws Exception {
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":1}".getBytes());
+        final Class<?> klass = Example.class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+                                             Example.class,
+                                             NONE,
+                                             MediaType.APPLICATION_JSON_TYPE,
+                                             new MultivaluedMapImpl(),
+                                             entity);
+
+        assertThat(obj)
+                .isInstanceOf(Example.class);
+
+        assertThat(((Example) obj).id)
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void returnsPartialValidatedRequestEntities() throws Exception {
+        final Validated valid = mock(Validated.class);
+        doReturn(Validated.class).when(valid).annotationType();
+        when(valid.value()).thenReturn(new Class<?>[]{Partial1.class, Partial2.class});
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":1,\"text\":\"hello Cemo\"}".getBytes());
+        final Class<?> klass = PartialExample.class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+            PartialExample.class,
+            new Annotation[]{valid},
+            MediaType.APPLICATION_JSON_TYPE,
+            new MultivaluedMapImpl(),
+            entity);
+
+        assertThat(obj)
+            .isInstanceOf(PartialExample.class);
+
+        assertThat(((PartialExample) obj).id)
+            .isEqualTo(1);
+    }
+
+    @Test
+    public void returnsPartialValidatedByGroupRequestEntities() throws Exception {
+        final Validated valid = mock(Validated.class);
+        doReturn(Validated.class).when(valid).annotationType();
+        when(valid.value()).thenReturn(new Class<?>[]{Partial1.class});
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":1}".getBytes());
+        final Class<?> klass = PartialExample.class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+            PartialExample.class,
+            new Annotation[]{valid},
+            MediaType.APPLICATION_JSON_TYPE,
+            new MultivaluedMapImpl(),
+            entity);
+
+        assertThat(obj)
+            .isInstanceOf(PartialExample.class);
+
+        assertThat(((PartialExample) obj).id)
+            .isEqualTo(1);
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForPartialValidatedRequestEntities() throws Exception {
+        final Validated valid = mock(Validated.class);
+        doReturn(Validated.class).when(valid).annotationType();
+        when(valid.value()).thenReturn(new Class<?>[]{Partial1.class, Partial2.class});
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":1}".getBytes());
+
+        try {
+            final Class<?> klass = PartialExample.class;
+            provider.readFrom((Class<Object>) klass,
+                              PartialExample.class,
+                              new Annotation[]{ valid },
+                              MediaType.APPLICATION_JSON_TYPE,
+                              new MultivaluedMapImpl(),
+                              entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch(ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                .containsOnly("text may not be null (was null)");
+        }
+    }
+
+    @Test
+    public void returnsValidatedRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":1}".getBytes());
+        final Class<?> klass = Example.class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+                                             Example.class,
+                                             new Annotation[]{ valid },
+                                             MediaType.APPLICATION_JSON_TYPE,
+                                             new MultivaluedMapImpl(),
+                                             entity);
+
+        assertThat(obj)
+                .isInstanceOf(Example.class);
+
+        assertThat(((Example) obj).id)
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForInvalidRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":-1}".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                              Example.class,
+                              new Annotation[]{ valid },
+                              MediaType.APPLICATION_JSON_TYPE,
+                              new MultivaluedMapImpl(),
+                              entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .containsOnly("id must be greater than or equal to 0 (was -1)");
+        }
+    }
+
+    @Test
+    public void throwsAJsonProcessingExceptionForMalformedRequestEntities() throws Exception {
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":-1d".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                              Example.class,
+                              NONE,
+                              MediaType.APPLICATION_JSON_TYPE,
+                              new MultivaluedMapImpl(),
+                              entity);
+            failBecauseExceptionWasNotThrown(WebApplicationException.class);
+        } catch (JsonProcessingException e) {
+            assertThat(e.getMessage())
+                    .startsWith("Unexpected character ('d' (code 100)): " +
+                                        "was expecting comma to separate OBJECT entries\n");
+        }
+    }
+
+    @Test
+    public void serializesResponseEntities() throws Exception {
+        final ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+        final Example example = new Example();
+        example.id = 500;
+
+        provider.writeTo(example,
+                         Example.class,
+                         Example.class,
+                         NONE,
+                         MediaType.APPLICATION_JSON_TYPE,
+                         new StringKeyObjectValueIgnoreCaseMultivaluedMap(),
+                         output);
+
+        assertThat(output.toString())
+                .isEqualTo("{\"id\":500}");
+    }
+
+    @Test(expected = ConstraintViolationException.class)
+    public void throwsAConstraintViolationExceptionForEmptyRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final Class<?> klass = Example.class;
+        provider.readFrom((Class<Object>) klass,
+                Example.class,
+                new Annotation[]{valid},
+                MediaType.APPLICATION_JSON_TYPE,
+                new MultivaluedMapImpl(),
+                null);
+    }
+
+    @Test
+    public void returnsValidatedArrayRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("[{\"id\":1}, {\"id\":2}]".getBytes());
+        final Class<?> klass = Example[].class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+                Example[].class,
+                new Annotation[]{ valid },
+                MediaType.APPLICATION_JSON_TYPE,
+                new MultivaluedMapImpl(),
+                entity);
+
+        assertThat(obj)
+                .isInstanceOf(Example[].class);
+
+        assertThat(((Example[]) obj)[0].id)
+                .isEqualTo(1);
+        assertThat(((Example[]) obj)[1].id)
+                .isEqualTo(2);
+    }
+
+    @Test
+    public void returnsValidatedCollectionRequestEntities() throws Exception {
+        testValidatedCollectionType(Collection.class,
+                new TypeToken<Collection<Example>>() {}.getType());
+    }
+
+    @Test
+    public void returnsValidatedSetRequestEntities() throws Exception {
+        testValidatedCollectionType(Set.class,
+                new TypeToken<Set<Example>>() {}.getType());
+    }
+
+    @Test
+    public void returnsValidatedListRequestEntities() throws Exception {
+        testValidatedCollectionType(List.class,
+                new TypeToken<List<Example>>() {}.getType());
+    }
+
+    @Test
+    public void returnsValidatedMapRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"one\": {\"id\":1}, \"two\": {\"id\":2}}".getBytes());
+        final Class<?> klass = Map.class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+                new TypeToken<Map<Object, Example>>() {}.getType(),
+                new Annotation[]{ valid },
+                MediaType.APPLICATION_JSON_TYPE,
+                new MultivaluedMapImpl(),
+                entity);
+
+        assertThat(obj)
+                .isInstanceOf(Map.class);
+
+        Map<Object, Example> map = (Map<Object, Example>) obj;
+        assertThat(map.get("one").id).isEqualTo(1);
+        assertThat(map.get("two").id).isEqualTo(2);
+    }
+
+    private void testValidatedCollectionType(Class<?> klass, Type type) throws IOException {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("[{\"id\":1}, {\"id\":2}]".getBytes());
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+                type,
+                new Annotation[]{ valid },
+                MediaType.APPLICATION_JSON_TYPE,
+                new MultivaluedMapImpl(),
+                entity);
+
+        assertThat(obj)
+                .isInstanceOf(klass);
+
+        Iterator<Example> iterator = ((Iterable<Example>)obj).iterator();
+        assertThat(iterator.next().id).isEqualTo(1);
+        assertThat(iterator.next().id).isEqualTo(2);
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForInvalidCollectionRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("[{\"id\":-1}, {\"id\":-2}]".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                    new TypeToken<Collection<Example>>() {}.getType(),
+                    new Annotation[]{ valid },
+                    MediaType.APPLICATION_JSON_TYPE,
+                    new MultivaluedMapImpl(),
+                    entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .contains("id must be greater than or equal to 0 (was -1)",
+                            "id must be greater than or equal to 0 (was -2)");
+        }
+    }
+
+    @Test
+    public void throwsASingleInvalidEntityExceptionForInvalidCollectionRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("[{\"id\":1}, {\"id\":-2}]".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                    new TypeToken<Collection<Example>>() {}.getType(),
+                    new Annotation[]{ valid },
+                    MediaType.APPLICATION_JSON_TYPE,
+                    new MultivaluedMapImpl(),
+                    entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .contains("id must be greater than or equal to 0 (was -2)");
+        }
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForInvalidSetRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("[{\"id\":-1}, {\"id\":-2}]".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                    new TypeToken<Set<Example>>() {}.getType(),
+                    new Annotation[]{ valid },
+                    MediaType.APPLICATION_JSON_TYPE,
+                    new MultivaluedMapImpl(),
+                    entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .contains("id must be greater than or equal to 0 (was -1)",
+                            "id must be greater than or equal to 0 (was -2)");
+        }
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForInvalidListRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("[{\"id\":-1}, {\"id\":-2}]".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                    new TypeToken<List<Example>>() {}.getType(),
+                    new Annotation[]{ valid },
+                    MediaType.APPLICATION_JSON_TYPE,
+                    new MultivaluedMapImpl(),
+                    entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .containsOnly("id must be greater than or equal to 0 (was -1)",
+                            "id must be greater than or equal to 0 (was -2)");
+        }
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForInvalidMapRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity = new ByteArrayInputStream("{\"one\": {\"id\":-1}, \"two\": {\"id\":-2}}".getBytes());
+
+        try {
+            final Class<?> klass = Example.class;
+            provider.readFrom((Class<Object>) klass,
+                    new TypeToken<Map<Object, Example>>() {}.getType(),
+                    new Annotation[]{ valid },
+                    MediaType.APPLICATION_JSON_TYPE,
+                    new MultivaluedMapImpl(),
+                    entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .contains("id must be greater than or equal to 0 (was -1)",
+                            "id must be greater than or equal to 0 (was -2)");
+        }
+    }
+
+    @Test
+    public void returnsValidatedEmbeddedListRequestEntities() throws IOException {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity =
+                new ByteArrayInputStream("[ {\"examples\": [ {\"id\":1 } ] } ]".getBytes());
+        Class<?> klass = List.class;
+
+        final Object obj = provider.readFrom((Class<Object>) klass,
+                new TypeToken<List<ListExample>>() {}.getType(),
+                new Annotation[]{ valid },
+                MediaType.APPLICATION_JSON_TYPE,
+                new MultivaluedMapImpl(),
+                entity);
+
+        assertThat(obj)
+                .isInstanceOf(klass);
+
+        Iterator<ListExample> iterator = ((Iterable<ListExample>)obj).iterator();
+        assertThat(iterator.next().examples.get(0).id).isEqualTo(1);
+    }
+
+    @Test
+    public void throwsAnInvalidEntityExceptionForInvalidEmbeddedListRequestEntities() throws Exception {
+        final Annotation valid = mock(Annotation.class);
+        doReturn(Valid.class).when(valid).annotationType();
+
+        final ByteArrayInputStream entity =
+                new ByteArrayInputStream("[ {\"examples\": [ {\"id\":1 } ] }, { } ]".getBytes());
+
+        try {
+            final Class<?> klass = List.class;
+            provider.readFrom((Class<Object>) klass,
+                    new TypeToken<List<ListExample>>() {}.getType(),
+                    new Annotation[]{ valid },
+                    MediaType.APPLICATION_JSON_TYPE,
+                    new MultivaluedMapImpl(),
+                    entity);
+            failBecauseExceptionWasNotThrown(ConstraintViolationException.class);
+        } catch (ConstraintViolationException e) {
+            assertThat(ConstraintViolations.formatUntyped(e.getConstraintViolations()))
+                    .containsOnly("examples may not be empty (was null)");
+        }
+    }
+
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapperTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapperTest.java
new file mode 100644
index 0000000..ee1bec2
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapperTest.java
@@ -0,0 +1,51 @@
+package io.dropwizard.jersey.jackson;
+
+import com.google.common.collect.ImmutableList;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Fail.failBecauseExceptionWasNotThrown;
+
+public class JsonProcessingExceptionMapperTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.jackson").build();
+    }
+
+    @Test
+    public void returnsA500ForNonDeserializableRepresentationClasses() throws Exception {
+        try {
+            resource().path("/json/broken")
+                      .type(MediaType.APPLICATION_JSON)
+                      .post(new BrokenRepresentation(ImmutableList.of("whee")));
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(500);
+        }
+    }
+
+    @Test
+    public void returnsA400ForNonDeserializableRequestEntities() throws Exception {
+        try {
+            resource().path("/json/ok")
+                      .type(MediaType.APPLICATION_JSON)
+                      .post("{\"bork\":100}");
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(400);
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonResource.java
new file mode 100644
index 0000000..d5aee88
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonResource.java
@@ -0,0 +1,28 @@
+package io.dropwizard.jersey.jackson;
+
+import com.google.common.collect.ImmutableList;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+import java.util.List;
+
+ at Path("/json/")
+ at Produces(MediaType.APPLICATION_JSON)
+ at Consumes(MediaType.APPLICATION_JSON)
+public class JsonResource {
+    @POST
+    @Path("/broken")
+    public void broken(BrokenRepresentation rep) throws IOException {
+        System.out.println(rep);
+    }
+
+    @POST
+    @Path("/ok")
+    public List<String> ok(OkRepresentation rep) throws IOException {
+        return ImmutableList.of(rep.getMessage());
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/OkRepresentation.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/OkRepresentation.java
new file mode 100644
index 0000000..2c083da
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/OkRepresentation.java
@@ -0,0 +1,17 @@
+package io.dropwizard.jersey.jackson;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class OkRepresentation {
+    private String message;
+
+    @JsonProperty
+    public String getMessage() {
+        return message;
+    }
+
+    @JsonProperty
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/BooleanParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/BooleanParamTest.java
new file mode 100644
index 0000000..ad12911
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/BooleanParamTest.java
@@ -0,0 +1,77 @@
+package io.dropwizard.jersey.params;
+
+import org.junit.Test;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class BooleanParamTest {
+    @Test
+    public void trueReturnsTrue() throws Exception {
+        final BooleanParam param = new BooleanParam("true");
+
+        assertThat(param.get())
+                .isTrue();
+    }
+
+    @Test
+    public void uppercaseTrueReturnsTrue() throws Exception {
+        final BooleanParam param = new BooleanParam("TRUE");
+
+        assertThat(param.get())
+                .isTrue();
+    }
+
+    @Test
+    public void falseReturnsFalse() throws Exception {
+        final BooleanParam param = new BooleanParam("false");
+
+        assertThat(param.get())
+                .isFalse();
+    }
+
+    @Test
+    public void uppercaseFalseReturnsFalse() throws Exception {
+        final BooleanParam param = new BooleanParam("FALSE");
+
+        assertThat(param.get())
+                .isFalse();
+    }
+
+    @Test
+    @SuppressWarnings("ResultOfObjectAllocationIgnored")
+    public void nullThrowsAnException() throws Exception {
+        try {
+            new BooleanParam(null);
+            failBecauseExceptionWasNotThrown(WebApplicationException.class);
+        } catch (WebApplicationException e) {
+            final Response response = e.getResponse();
+
+            assertThat(response.getStatus())
+                    .isEqualTo(400);
+
+            assertThat((String) response.getEntity())
+                    .isEqualTo("\"null\" must be \"true\" or \"false\".");
+        }
+    }
+
+    @Test
+    @SuppressWarnings("ResultOfObjectAllocationIgnored")
+    public void nonBooleanValuesThrowAnException() throws Exception {
+        try {
+            new BooleanParam("foo");
+            failBecauseExceptionWasNotThrown(WebApplicationException.class);
+        } catch (WebApplicationException e) {
+            final Response response = e.getResponse();
+
+            assertThat(response.getStatus())
+                    .isEqualTo(400);
+
+            assertThat(response.getEntity())
+                    .isEqualTo("\"foo\" must be \"true\" or \"false\".");
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/DateTimeParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/DateTimeParamTest.java
new file mode 100644
index 0000000..0783d24
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/DateTimeParamTest.java
@@ -0,0 +1,17 @@
+package io.dropwizard.jersey.params;
+
+import org.joda.time.DateTime;
+import org.joda.time.DateTimeZone;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class DateTimeParamTest {
+    @Test
+    public void parsesDateTimes() throws Exception {
+        final DateTimeParam param = new DateTimeParam("2012-11-19");
+
+        assertThat(param.get())
+                .isEqualTo(new DateTime(2012, 11, 19, 0, 0, DateTimeZone.UTC));
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/IntParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/IntParamTest.java
new file mode 100644
index 0000000..c60288a
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/IntParamTest.java
@@ -0,0 +1,36 @@
+package io.dropwizard.jersey.params;
+
+import org.junit.Test;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class IntParamTest {
+    @Test
+    public void anIntegerReturnsAnInteger() throws Exception {
+        final IntParam param = new IntParam("200");
+
+        assertThat(param.get())
+                .isEqualTo(200);
+    }
+
+    @Test
+    @SuppressWarnings("ResultOfObjectAllocationIgnored")
+    public void aNonIntegerThrowsAnException() throws Exception {
+        try {
+            new IntParam("foo");
+            failBecauseExceptionWasNotThrown(WebApplicationException.class);
+        } catch (WebApplicationException e) {
+            final Response response = e.getResponse();
+
+            assertThat(response.getStatus())
+                    .isEqualTo(400);
+
+            assertThat(response.getEntity())
+                    .isEqualTo("\"foo\" is not a number.");
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LongParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LongParamTest.java
new file mode 100644
index 0000000..cb5953e
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LongParamTest.java
@@ -0,0 +1,36 @@
+package io.dropwizard.jersey.params;
+
+import org.junit.Test;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class LongParamTest {
+    @Test
+    public void aLongReturnsALong() throws Exception {
+        final LongParam param = new LongParam("200");
+
+        assertThat(param.get())
+                .isEqualTo(200L);
+    }
+
+    @Test
+    @SuppressWarnings("ResultOfObjectAllocationIgnored")
+    public void aNonIntegerThrowsAnException() throws Exception {
+        try {
+            new LongParam("foo");
+            failBecauseExceptionWasNotThrown(WebApplicationException.class);
+        } catch (WebApplicationException e) {
+            final Response response = e.getResponse();
+
+            assertThat(response.getStatus())
+                    .isEqualTo(400);
+
+            assertThat((String) response.getEntity())
+                    .isEqualTo("\"foo\" is not a number.");
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/UUIDParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/UUIDParamTest.java
new file mode 100644
index 0000000..7acaa05
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/UUIDParamTest.java
@@ -0,0 +1,40 @@
+package io.dropwizard.jersey.params;
+
+import org.junit.Test;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response;
+import java.util.UUID;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+
+public class UUIDParamTest {
+
+    @Test
+    public void aUUIDStringReturnsAUUIDObject() throws Exception {
+        final String uuidString = "067e6162-3b6f-4ae2-a171-2470b63dff00";
+        final UUID uuid = UUID.fromString(uuidString);
+
+        final UUIDParam param = new UUIDParam(uuidString);
+        assertThat(param.get())
+                .isEqualTo(uuid);
+    }
+
+    @Test
+    @SuppressWarnings("ResultOfObjectAllocationIgnored")
+    public void aNonUUIDThrowsAnException() throws Exception {
+        try {
+            new UUIDParam("foo");
+            failBecauseExceptionWasNotThrown(WebApplicationException.class);
+        } catch (WebApplicationException e) {
+            final Response response = e.getResponse();
+
+            assertThat(response.getStatus())
+                    .isEqualTo(400);
+
+            assertThat((String) response.getEntity())
+                    .isEqualTo("\"foo\" is not a UUID.");
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashProviderTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashProviderTest.java
new file mode 100644
index 0000000..68a971d
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashProviderTest.java
@@ -0,0 +1,59 @@
+package io.dropwizard.jersey.sessions;
+
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.NewCookie;
+import java.util.List;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class FlashProviderTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.sessions").build();
+    }
+
+    @Test
+    public void passesInHttpSessions() throws Exception {
+        final ClientResponse firstResponse = resource().path("/flash/")
+                                                       .type(MediaType.TEXT_PLAIN)
+                                                       .post(ClientResponse.class, "Mr. Peeps");
+
+        final List<NewCookie> cookies = firstResponse.getCookies();
+        firstResponse.close();
+
+        final WebResource.Builder builder =
+                resource().path("/flash/").accept(MediaType.TEXT_PLAIN);
+
+        for (NewCookie cookie : cookies) {
+            builder.cookie(cookie);
+        }
+
+        final ClientResponse secondResponse = builder.get(ClientResponse.class);
+        assertThat(secondResponse.getEntity(String.class))
+                .isEqualTo("Mr. Peeps");
+
+        final WebResource.Builder anotherBuilder =
+                resource().path("/flash/").accept(MediaType.TEXT_PLAIN);
+
+        for (NewCookie cookie : cookies) {
+            anotherBuilder.cookie(cookie);
+        }
+
+        final String thirdResponse = anotherBuilder.get(String.class);
+        assertThat(thirdResponse)
+                .isEqualTo("null");
+    }
+}
+
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashResource.java
new file mode 100644
index 0000000..fbac9c3
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashResource.java
@@ -0,0 +1,25 @@
+package io.dropwizard.jersey.sessions;
+
+import javax.ws.rs.*;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.UriInfo;
+import java.util.Objects;
+
+ at Path("/flash/")
+ at Consumes(MediaType.TEXT_PLAIN)
+ at Produces(MediaType.TEXT_PLAIN)
+public class FlashResource {
+    @Context UriInfo uriInfo;
+
+    @POST
+    public void setName(@Session Flash<String> flash,
+                        String name) {
+        flash.set(name);
+    }
+
+    @GET
+    public String getName(@Session Flash<String> flash) {
+        return Objects.toString(flash.get().orNull());
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/HttpSessionProviderTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/HttpSessionProviderTest.java
new file mode 100644
index 0000000..fcec858
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/HttpSessionProviderTest.java
@@ -0,0 +1,46 @@
+package io.dropwizard.jersey.sessions;
+
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.NewCookie;
+import java.util.List;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class HttpSessionProviderTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.sessions").build();
+    }
+
+    @Test
+    public void passesInHttpSessions() throws Exception {
+        final ClientResponse firstResponse = resource().path("/session/")
+                                                       .type(MediaType.TEXT_PLAIN)
+                                                       .post(ClientResponse.class, "Mr. Peeps");
+        final List<NewCookie> cookies = firstResponse.getCookies();
+        firstResponse.close();
+
+        final WebResource.Builder builder = resource().path("/session/")
+                                                      .accept(MediaType.TEXT_PLAIN);
+
+        for (NewCookie cookie : cookies) {
+            builder.cookie(cookie);
+        }
+
+        final String secondResponse = builder.get(String.class);
+        assertThat(secondResponse)
+                .isEqualTo("Mr. Peeps");
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/SessionResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/SessionResource.java
new file mode 100644
index 0000000..14be5a7
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/SessionResource.java
@@ -0,0 +1,22 @@
+package io.dropwizard.jersey.sessions;
+
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.*;
+import javax.ws.rs.core.MediaType;
+import java.util.Objects;
+
+ at Path("/session/")
+ at Consumes(MediaType.TEXT_PLAIN)
+ at Produces(MediaType.TEXT_PLAIN)
+public class SessionResource {
+    @GET
+    public String getName(@Session HttpSession session) {
+        return Objects.toString(session.getAttribute("name"));
+    }
+
+    @POST
+    public void setName(@Session HttpSession session,
+                        String name) {
+        session.setAttribute("name", name);
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapperTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapperTest.java
new file mode 100644
index 0000000..bb9da42
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapperTest.java
@@ -0,0 +1,43 @@
+package io.dropwizard.jersey.validation;
+
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.WebAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.Test;
+
+import javax.ws.rs.core.MediaType;
+import java.util.Locale;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.failBecauseExceptionWasNotThrown;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assume.assumeThat;
+
+public class ConstraintViolationExceptionMapperTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        return new WebAppDescriptor.Builder("io.dropwizard.jersey.validation").build();
+    }
+
+    @Test
+    public void returnsAnErrorMessage() throws Exception {
+        assumeThat(Locale.getDefault().getLanguage(), is("en"));
+
+        try {
+            resource().path("/valid/").type(MediaType.APPLICATION_JSON).post("{}");
+            failBecauseExceptionWasNotThrown(UniformInterfaceException.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(422);
+
+            assertThat(e.getResponse().getEntity(String.class))
+                    .isEqualTo("{\"errors\":[\"name may not be empty (was null)\"]}");
+        }
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/DefaultJacksonMessageBodyProvider.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/DefaultJacksonMessageBodyProvider.java
new file mode 100644
index 0000000..ed13d90
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/DefaultJacksonMessageBodyProvider.java
@@ -0,0 +1,15 @@
+package io.dropwizard.jersey.validation;
+
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+
+import javax.validation.Validation;
+import javax.ws.rs.ext.Provider;
+
+ at Provider
+public class DefaultJacksonMessageBodyProvider extends JacksonMessageBodyProvider {
+    public DefaultJacksonMessageBodyProvider() {
+        super(Jackson.newObjectMapper(), Validation.buildDefaultValidatorFactory().getValidator());
+    }
+}
+
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidRepresentation.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidRepresentation.java
new file mode 100644
index 0000000..2628c20
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidRepresentation.java
@@ -0,0 +1,19 @@
+package io.dropwizard.jersey.validation;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.hibernate.validator.constraints.NotEmpty;
+
+public class ValidRepresentation {
+    @NotEmpty
+    private String name;
+
+    @JsonProperty
+    public String getName() {
+        return name;
+    }
+
+    @JsonProperty
+    public void setName(String name) {
+        this.name = name;
+    }
+}
diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidatingResource.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidatingResource.java
new file mode 100644
index 0000000..0b55f77
--- /dev/null
+++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidatingResource.java
@@ -0,0 +1,20 @@
+package io.dropwizard.jersey.validation;
+
+import io.dropwizard.validation.Validated;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import java.io.IOException;
+
+ at Path("/valid/")
+ at Produces(MediaType.APPLICATION_JSON)
+ at Consumes(MediaType.APPLICATION_JSON)
+public class ValidatingResource {
+    @POST
+    public String blah(@Validated ValidRepresentation representation) throws IOException {
+        return representation.getName();
+    }
+}
diff --git a/dropwizard-jersey/src/test/resources/logback-test.xml b/dropwizard-jersey/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-jersey/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-jetty/pom.xml b/dropwizard-jetty/pom.xml
new file mode 100644
index 0000000..515c0b5
--- /dev/null
+++ b/dropwizard-jetty/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-jetty</artifactId>
+    <name>Dropwizard Jetty Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-logging</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-jetty9</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>jetty-server</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlets</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-http</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-configuration</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/BiDiGzipFilter.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/BiDiGzipFilter.java
new file mode 100644
index 0000000..7632551
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/BiDiGzipFilter.java
@@ -0,0 +1,238 @@
+package io.dropwizard.jetty;
+
+import com.google.common.base.Charsets;
+import org.eclipse.jetty.http.HttpHeader;
+import org.eclipse.jetty.servlets.IncludableGzipFilter;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.Charset;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.zip.Deflater;
+import java.util.zip.DeflaterInputStream;
+import java.util.zip.GZIPInputStream;
+
+/**
+ * An extension of {@link IncludableGzipFilter} which decompresses gzip- and deflate-encoded request
+ * entities.
+ */
+public class BiDiGzipFilter extends IncludableGzipFilter {
+    private final ThreadLocal<Deflater> localDeflater = new ThreadLocal<>();
+
+    public Set<String> getMimeTypes() {
+        return _mimeTypes;
+    }
+
+    public int getBufferSize() {
+        return _bufferSize;
+    }
+
+    public int getMinGzipSize() {
+        return _minGzipSize;
+    }
+
+    public int getDeflateCompressionLevel() {
+        return _deflateCompressionLevel;
+    }
+
+    public boolean isDeflateNoWrap() {
+        return _deflateNoWrap;
+    }
+
+    public Set<String> getMethods() {
+        return _methods;
+    }
+
+    public Set<String> getExcludedAgents() {
+        return _excludedAgents;
+    }
+
+    public Set<Pattern> getExcludedAgentPatterns() {
+        return _excludedAgentPatterns;
+    }
+
+    public Set<String> getExcludedPaths() {
+        return _excludedPaths;
+    }
+
+    public Set<Pattern> getExcludedPathPatterns() {
+        return _excludedPathPatterns;
+    }
+
+    public String getVary() {
+        return _vary;
+    }
+
+    public void setMimeTypes(Set<String> mimeTypes) {
+        _mimeTypes.clear();
+        _mimeTypes.addAll(mimeTypes);
+    }
+
+    public void setBufferSize(int bufferSize) {
+        this._bufferSize = bufferSize;
+    }
+
+    public void setMinGzipSize(int minGzipSize) {
+        this._minGzipSize = minGzipSize;
+    }
+
+    public void setDeflateCompressionLevel(int level) {
+        this._deflateCompressionLevel = level;
+    }
+
+    public void setDeflateNoWrap(boolean noWrap) {
+        this._deflateNoWrap = noWrap;
+    }
+
+    public void setMethods(Set<String> methods) {
+        this._methods.clear();
+        this._methods.addAll(methods);
+    }
+
+    public void setExcludedAgents(Set<String> userAgents) {
+        this._excludedAgents = userAgents;
+    }
+
+    public void setExcludedAgentPatterns(Set<Pattern> userAgentPatterns) {
+        this._excludedAgentPatterns = userAgentPatterns;
+    }
+
+    public void setExcludedPaths(Set<String> paths) {
+        this._excludedPaths = paths;
+    }
+
+    public void setExcludedPathPatterns(Set<Pattern> patterns) {
+        this._excludedPathPatterns = patterns;
+    }
+
+    public void setVary(String vary) {
+        this._vary = vary;
+    }
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
+        final HttpServletRequest request = (HttpServletRequest) req;
+        final String encoding = request.getHeader(HttpHeader.CONTENT_ENCODING.asString());
+        if (GZIP.equalsIgnoreCase(encoding)) {
+            super.doFilter(wrapGzippedRequest(request), res, chain);
+        } else if (DEFLATE.equalsIgnoreCase(encoding)) {
+            super.doFilter(wrapDeflatedRequest(request), res, chain);
+        } else {
+            super.doFilter(req, res, chain);
+        }
+    }
+
+    private Deflater buildDeflater() {
+        final Deflater deflater = localDeflater.get();
+        if (deflater != null) {
+            return deflater;
+        }
+        return new Deflater(_deflateCompressionLevel, _deflateNoWrap);
+    }
+
+    private ServletRequest wrapDeflatedRequest(HttpServletRequest request) throws IOException {
+        final Deflater deflater = buildDeflater();
+        final DeflaterInputStream input = new DeflaterInputStream(request.getInputStream(), deflater, _bufferSize) {
+            @Override
+            public void close() throws IOException {
+                deflater.reset();
+                localDeflater.set(deflater);
+                super.close();
+            }
+        };
+        return new WrappedServletRequest(request, input);
+    }
+
+    private ServletRequest wrapGzippedRequest(HttpServletRequest request) throws IOException {
+        return new WrappedServletRequest(request, new GZIPInputStream(request.getInputStream(), _bufferSize));
+    }
+
+    private static class WrappedServletRequest extends HttpServletRequestWrapper {
+        private final ServletInputStream input;
+        private final BufferedReader reader;
+
+        private WrappedServletRequest(HttpServletRequest request,
+                                      InputStream inputStream) throws IOException {
+            super(request);
+            this.input = new WrappedServletInputStream(inputStream);
+            this.reader = new BufferedReader(new InputStreamReader(input, getCharset()));
+        }
+
+        private Charset getCharset() {
+            final String encoding = getCharacterEncoding();
+            if (encoding == null || !Charset.isSupported(encoding)) {
+                return Charsets.ISO_8859_1;
+            }
+            return Charset.forName(encoding);
+        }
+
+        @Override
+        public ServletInputStream getInputStream() throws IOException {
+            return input;
+        }
+
+        @Override
+        public BufferedReader getReader() throws IOException {
+            return reader;
+        }
+    }
+
+    private static class WrappedServletInputStream extends ServletInputStream {
+        private final InputStream input;
+
+        private WrappedServletInputStream(InputStream input) {
+            this.input = input;
+        }
+
+        @Override
+        public void close() throws IOException {
+            input.close();
+        }
+
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            return input.read(b, off, len);
+        }
+
+        @Override
+        public int available() throws IOException {
+            return input.available();
+        }
+
+        @Override
+        public void mark(int readlimit) {
+            input.mark(readlimit);
+        }
+
+        @Override
+        public boolean markSupported() {
+            return input.markSupported();
+        }
+
+        @Override
+        public int read() throws IOException {
+            return input.read();
+        }
+
+        @Override
+        public void reset() throws IOException {
+            input.reset();
+        }
+
+        @Override
+        public long skip(long n) throws IOException {
+            return input.skip(n);
+        }
+
+        @Override
+        public int read(byte[] b) throws IOException {
+            return input.read(b);
+        }
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ConnectorFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ConnectorFactory.java
new file mode 100644
index 0000000..27c9e93
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ConnectorFactory.java
@@ -0,0 +1,28 @@
+package io.dropwizard.jetty;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.dropwizard.jackson.Discoverable;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+/**
+ * A factory for creating Jetty {@link Connector}s.
+ */
+ at JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+public interface ConnectorFactory extends Discoverable {
+    /**
+     * Create a new connector.
+     *
+     * @param server     the application's {@link Server} instance
+     * @param metrics    the application's metrics
+     * @param name       the application's name
+     * @param threadPool the application's thread pool
+     * @return a {@link Connector}
+     */
+    Connector build(Server server,
+                    MetricRegistry metrics,
+                    String name,
+                    ThreadPool threadPool);
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ContextRoutingHandler.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ContextRoutingHandler.java
new file mode 100644
index 0000000..19f3dee
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ContextRoutingHandler.java
@@ -0,0 +1,41 @@
+package io.dropwizard.jetty;
+
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.util.ArrayTernaryTrie;
+import org.eclipse.jetty.util.Trie;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ * A Jetty router which routes requests based on context path.
+ */
+public class ContextRoutingHandler extends AbstractHandler {
+    private final Trie<Handler> handlers;
+
+    public ContextRoutingHandler(Map<String, ? extends Handler> handlers) {
+        this.handlers = new ArrayTernaryTrie<>(false);
+        for (Map.Entry<String, ? extends Handler> entry : handlers.entrySet()) {
+            if (!this.handlers.put(entry.getKey(), entry.getValue())) {
+                throw new IllegalStateException("Too many handlers");
+            }
+            addBean(entry.getValue());
+        }
+    }
+
+    @Override
+    public void handle(String target,
+                       Request baseRequest,
+                       HttpServletRequest request,
+                       HttpServletResponse response) throws IOException, ServletException {
+        final Handler handler = handlers.getBest(baseRequest.getRequestURI());
+        if (handler != null) {
+            handler.handle(target, baseRequest, request, response);
+        }
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/GzipFilterFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/GzipFilterFactory.java
new file mode 100644
index 0000000..1e4dc0a
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/GzipFilterFactory.java
@@ -0,0 +1,168 @@
+package io.dropwizard.jetty;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Sets;
+import io.dropwizard.util.Size;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.zip.Deflater;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class GzipFilterFactory {
+    private boolean enabled = true;
+
+    @NotNull
+    private Size minimumEntitySize = Size.bytes(256);
+
+    @NotNull
+    private Size bufferSize = Size.kilobytes(8);
+
+    private Set<String> excludedUserAgents = Sets.newHashSet();
+    private Set<Pattern> excludedUserAgentPatterns = Sets.newHashSet();
+    private Set<String> compressedMimeTypes = Sets.newHashSet();
+    private Set<String> includedMethods = Sets.newHashSet();
+    private boolean gzipCompatibleDeflation = true;
+    private String vary = "Accept-Encoding";
+
+    @Min(Deflater.DEFAULT_COMPRESSION)
+    @Max(Deflater.BEST_COMPRESSION)
+    private int deflateCompressionLevel = Deflater.DEFAULT_COMPRESSION;
+
+    @JsonProperty
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    @JsonProperty
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    @JsonProperty
+    public Size getMinimumEntitySize() {
+        return minimumEntitySize;
+    }
+
+    @JsonProperty
+    public void setMinimumEntitySize(Size size) {
+        this.minimumEntitySize = checkNotNull(size);
+    }
+
+    @JsonProperty
+    public Size getBufferSize() {
+        return bufferSize;
+    }
+
+    @JsonProperty
+    public void setBufferSize(Size size) {
+        this.bufferSize = checkNotNull(size);
+    }
+
+    @JsonProperty
+    public Set<String> getExcludedUserAgents() {
+        return excludedUserAgents;
+    }
+
+    @JsonProperty
+    public void setExcludedUserAgents(Set<String> userAgents) {
+        this.excludedUserAgents = userAgents;
+    }
+
+    @JsonProperty
+    public Set<String> getCompressedMimeTypes() {
+        return compressedMimeTypes;
+    }
+
+    @JsonProperty
+    public void setCompressedMimeTypes(Set<String> mimeTypes) {
+        this.compressedMimeTypes = mimeTypes;
+    }
+
+    @JsonProperty
+    public int getDeflateCompressionLevel() {
+        return deflateCompressionLevel;
+    }
+
+    @JsonProperty
+    public void setDeflateCompressionLevel(int level) {
+        this.deflateCompressionLevel = level;
+    }
+
+    @JsonProperty
+    public boolean isGzipCompatibleDeflation() {
+        return gzipCompatibleDeflation;
+    }
+
+    @JsonProperty
+    public void setGzipCompatibleDeflation(boolean compatible) {
+        this.gzipCompatibleDeflation = compatible;
+    }
+
+    @JsonProperty
+    public Set<Pattern> getExcludedUserAgentPatterns() {
+        return excludedUserAgentPatterns;
+    }
+
+    @JsonProperty
+    public void setExcludedUserAgentPatterns(Set<Pattern> patterns) {
+        this.excludedUserAgentPatterns = patterns;
+    }
+
+    @JsonProperty
+    public Set<String> getIncludedMethods() {
+        return includedMethods;
+    }
+
+    @JsonProperty
+    public void setIncludedMethods(Set<String> methods) {
+        this.includedMethods = methods;
+    }
+
+    @JsonProperty
+    public String getVary() {
+        return vary;
+    }
+
+    @JsonProperty
+    public void setVary(String vary) {
+        this.vary = vary;
+    }
+
+    public BiDiGzipFilter build() {
+        final BiDiGzipFilter filter = new BiDiGzipFilter();
+        filter.setMinGzipSize((int) minimumEntitySize.toBytes());
+
+        filter.setBufferSize((int) bufferSize.toBytes());
+
+        filter.setDeflateCompressionLevel(deflateCompressionLevel);
+
+        if (excludedUserAgents != null) {
+            filter.setExcludedAgents(excludedUserAgents);
+        }
+
+        if (compressedMimeTypes != null) {
+            filter.setMimeTypes(compressedMimeTypes);
+        }
+
+        if (includedMethods != null) {
+            filter.setMethods(includedMethods);
+        }
+
+        if (excludedUserAgentPatterns != null) {
+            filter.setExcludedAgentPatterns(excludedUserAgentPatterns);
+        }
+
+        if (vary != null) {
+            filter.setVary(vary);
+        }
+
+        filter.setDeflateNoWrap(gzipCompatibleDeflation);
+
+        return filter;
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpConnectorFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpConnectorFactory.java
new file mode 100644
index 0000000..a691836
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpConnectorFactory.java
@@ -0,0 +1,500 @@
+package io.dropwizard.jetty;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jetty9.InstrumentedConnectionFactory;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.util.Duration;
+import io.dropwizard.util.Size;
+import io.dropwizard.util.SizeUnit;
+import io.dropwizard.validation.MinDuration;
+import io.dropwizard.validation.MinSize;
+import io.dropwizard.validation.PortRange;
+import org.eclipse.jetty.io.ArrayByteBufferPool;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.*;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.util.concurrent.TimeUnit;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+/**
+ * Builds HTTP connectors.
+ *
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code port}</td>
+ *         <td>8080</td>
+ *         <td>The TCP/IP port on which to listen for incoming connections.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code bindHost}</td>
+ *         <td>(none)</td>
+ *         <td>The hostname to bind to.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code headerCacheSize}</td>
+ *         <td>512 bytes</td>
+ *         <td>The size of the header field cache.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code outputBufferSize}</td>
+ *         <td>32KiB</td>
+ *         <td>
+ *             The size of the buffer into which response content is aggregated before being sent to
+ *             the client.  A larger buffer can improve performance by allowing a content producer
+ *             to run without blocking, however larger buffers consume more memory and may induce
+ *             some latency before a client starts processing the content.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxRequestHeaderSize}</td>
+ *         <td>8KiB</td>
+ *         <td>
+ *             The maximum size of a request header. Larger headers will allow for more and/or
+ *             larger cookies plus larger form content encoded  in a URL. However, larger headers
+ *             consume more memory and can make a server more vulnerable to denial of service
+ *             attacks.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxResponseHeaderSize}</td>
+ *         <td>8KiB</td>
+ *         <td>
+ *             The maximum size of a response header. Larger headers will allow for more and/or
+ *             larger cookies and longer HTTP headers (eg for redirection).  However, larger headers
+ *             will also consume more memory.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code inputBufferSize}</td>
+ *         <td>8KiB</td>
+ *         <td>The size of the per-connection input buffer.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code idleTimeout}</td>
+ *         <td>30 seconds</td>
+ *         <td>
+ *             The maximum idle time for a connection, which roughly translates to the
+ *             {@link java.net.Socket#setSoTimeout(int)} call, although with NIO implementations
+ *             other mechanisms may be used to implement the timeout.
+ *             <p/>
+ *             The max idle time is applied:
+ *             <ul>
+ *                 <li>When waiting for a new message to be received on a connection</li>
+ *                 <li>When waiting for a new message to be sent on a connection</li>
+ *             </ul>
+ *             <p/>
+ *             This value is interpreted as the maximum time between some progress being made on the
+ *             connection. So if a single byte is read or written, then the timeout is reset.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code minBufferPoolSize}</td>
+ *         <td>64 bytes</td>
+ *         <td>The minimum size of the buffer pool.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code bufferPoolIncrement}</td>
+ *         <td>1KiB</td>
+ *         <td>The increment by which the buffer pool should be increased.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxBufferPoolSize}</td>
+ *         <td>64KiB</td>
+ *         <td>The maximum size of the buffer pool.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code acceptorThreads}</td>
+ *         <td>half the # of CPUs</td>
+ *         <td>The number of worker threads dedicated to accepting connections.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code selectorThreads}</td>
+ *         <td>the # of CPUs</td>
+ *         <td>The number of worker threads dedicated to sending and receiving data.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code acceptQueueSize}</td>
+ *         <td>(OS default)</td>
+ *         <td>The size of the TCP/IP accept queue for the listening socket.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code reuseAddress}</td>
+ *         <td>true</td>
+ *         <td>Whether or not {@code SO_REUSEADDR} is enabled on the listening socket.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code soLingerTime}</td>
+ *         <td>(disabled)</td>
+ *         <td>Enable/disable {@code SO_LINGER} with the specified linger time.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code useServerHeader}</td>
+ *         <td>false</td>
+ *         <td>Whether or not to add the {@code Server} header to each response.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code useDateHeader}</td>
+ *         <td>true</td>
+ *         <td>Whether or not to add the {@code Date} header to each response.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code useForwardedHeaders}</td>
+ *         <td>true</td>
+ *         <td>
+ *             Whether or not to look at {@code X-Forwarded-*} headers added by proxies. See
+ *             {@link ForwardedRequestCustomizer} for details.
+ *         </td>
+ *     </tr>
+ * </table>
+ */
+ at JsonTypeName("http")
+public class HttpConnectorFactory implements ConnectorFactory {
+    public static ConnectorFactory application() {
+        final HttpConnectorFactory factory = new HttpConnectorFactory();
+        factory.port = 8080;
+        return factory;
+    }
+
+    public static ConnectorFactory admin() {
+        final HttpConnectorFactory factory = new HttpConnectorFactory();
+        factory.port = 8081;
+        return factory;
+    }
+
+    @PortRange
+    private int port = 8080;
+
+    private String bindHost = null;
+
+    @NotNull
+    @MinSize(128)
+    private Size headerCacheSize = Size.bytes(512);
+
+    @NotNull
+    @MinSize(value = 8, unit = SizeUnit.KILOBYTES)
+    private Size outputBufferSize = Size.kilobytes(32);
+
+    @NotNull
+    @MinSize(value = 1, unit = SizeUnit.KILOBYTES)
+    private Size maxRequestHeaderSize = Size.kilobytes(8);
+
+    @NotNull
+    @MinSize(value = 1, unit = SizeUnit.KILOBYTES)
+    private Size maxResponseHeaderSize = Size.kilobytes(8);
+
+    @NotNull
+    @MinSize(value = 1, unit = SizeUnit.KILOBYTES)
+    private Size inputBufferSize = Size.kilobytes(8);
+
+    @NotNull
+    @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS)
+    private Duration idleTimeout = Duration.seconds(30);
+
+    @NotNull
+    @MinSize(value = 1, unit = SizeUnit.BYTES)
+    private Size minBufferPoolSize = Size.bytes(64);
+
+    @NotNull
+    @MinSize(value = 1, unit = SizeUnit.BYTES)
+    private Size bufferPoolIncrement = Size.bytes(1024);
+
+    @NotNull
+    @MinSize(value = 1, unit = SizeUnit.BYTES)
+    private Size maxBufferPoolSize = Size.kilobytes(64);
+
+    @Min(1)
+    private int acceptorThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2);
+
+    @Min(1)
+    private int selectorThreads = Runtime.getRuntime().availableProcessors();
+
+    @Min(0)
+    private Integer acceptQueueSize;
+
+    private boolean reuseAddress = true;
+    private Duration soLingerTime = null;
+    private boolean useServerHeader = false;
+    private boolean useDateHeader = true;
+    private boolean useForwardedHeaders = true;
+
+    @JsonProperty
+    public int getPort() {
+        return port;
+    }
+
+    @JsonProperty
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    @JsonProperty
+    public String getBindHost() {
+        return bindHost;
+    }
+
+    @JsonProperty
+    public void setBindHost(String bindHost) {
+        this.bindHost = bindHost;
+    }
+
+    @JsonProperty
+    public Size getHeaderCacheSize() {
+        return headerCacheSize;
+    }
+
+    @JsonProperty
+    public void setHeaderCacheSize(Size headerCacheSize) {
+        this.headerCacheSize = headerCacheSize;
+    }
+
+    @JsonProperty
+    public Size getOutputBufferSize() {
+        return outputBufferSize;
+    }
+
+    @JsonProperty
+    public void setOutputBufferSize(Size outputBufferSize) {
+        this.outputBufferSize = outputBufferSize;
+    }
+
+    @JsonProperty
+    public Size getMaxRequestHeaderSize() {
+        return maxRequestHeaderSize;
+    }
+
+    @JsonProperty
+    public void setMaxRequestHeaderSize(Size maxRequestHeaderSize) {
+        this.maxRequestHeaderSize = maxRequestHeaderSize;
+    }
+
+    @JsonProperty
+    public Size getMaxResponseHeaderSize() {
+        return maxResponseHeaderSize;
+    }
+
+    @JsonProperty
+    public void setMaxResponseHeaderSize(Size maxResponseHeaderSize) {
+        this.maxResponseHeaderSize = maxResponseHeaderSize;
+    }
+
+    @JsonProperty
+    public Size getInputBufferSize() {
+        return inputBufferSize;
+    }
+
+    @JsonProperty
+    public void setInputBufferSize(Size inputBufferSize) {
+        this.inputBufferSize = inputBufferSize;
+    }
+
+    @JsonProperty
+    public Duration getIdleTimeout() {
+        return idleTimeout;
+    }
+
+    @JsonProperty
+    public void setIdleTimeout(Duration idleTimeout) {
+        this.idleTimeout = idleTimeout;
+    }
+
+    @JsonProperty
+    public Size getMinBufferPoolSize() {
+        return minBufferPoolSize;
+    }
+
+    @JsonProperty
+    public void setMinBufferPoolSize(Size minBufferPoolSize) {
+        this.minBufferPoolSize = minBufferPoolSize;
+    }
+
+    @JsonProperty
+    public Size getBufferPoolIncrement() {
+        return bufferPoolIncrement;
+    }
+
+    @JsonProperty
+    public void setBufferPoolIncrement(Size bufferPoolIncrement) {
+        this.bufferPoolIncrement = bufferPoolIncrement;
+    }
+
+    @JsonProperty
+    public Size getMaxBufferPoolSize() {
+        return maxBufferPoolSize;
+    }
+
+    @JsonProperty
+    public void setMaxBufferPoolSize(Size maxBufferPoolSize) {
+        this.maxBufferPoolSize = maxBufferPoolSize;
+    }
+
+    @JsonProperty
+    public int getAcceptorThreads() {
+        return acceptorThreads;
+    }
+
+    @JsonProperty
+    public void setAcceptorThreads(int acceptorThreads) {
+        this.acceptorThreads = acceptorThreads;
+    }
+
+    @JsonProperty
+    public int getSelectorThreads() {
+        return selectorThreads;
+    }
+
+    @JsonProperty
+    public void setSelectorThreads(int selectorThreads) {
+        this.selectorThreads = selectorThreads;
+    }
+
+    @JsonProperty
+    public Integer getAcceptQueueSize() {
+        return acceptQueueSize;
+    }
+
+    @JsonProperty
+    public void setAcceptQueueSize(Integer acceptQueueSize) {
+        this.acceptQueueSize = acceptQueueSize;
+    }
+
+    @JsonProperty
+    public boolean isReuseAddress() {
+        return reuseAddress;
+    }
+
+    @JsonProperty
+    public void setReuseAddress(boolean reuseAddress) {
+        this.reuseAddress = reuseAddress;
+    }
+
+    @JsonProperty
+    public Duration getSoLingerTime() {
+        return soLingerTime;
+    }
+
+    @JsonProperty
+    public void setSoLingerTime(Duration soLingerTime) {
+        this.soLingerTime = soLingerTime;
+    }
+
+    @JsonProperty
+    public boolean isUseServerHeader() {
+        return useServerHeader;
+    }
+
+    @JsonProperty
+    public void setUseServerHeader(boolean useServerHeader) {
+        this.useServerHeader = useServerHeader;
+    }
+
+    @JsonProperty
+    public boolean isUseDateHeader() {
+        return useDateHeader;
+    }
+
+    @JsonProperty
+    public void setUseDateHeader(boolean useDateHeader) {
+        this.useDateHeader = useDateHeader;
+    }
+
+    @JsonProperty
+    public boolean isUseForwardedHeaders() {
+        return useForwardedHeaders;
+    }
+
+    @JsonProperty
+    public void setUseForwardedHeaders(boolean useForwardedHeaders) {
+        this.useForwardedHeaders = useForwardedHeaders;
+    }
+
+    @Override
+    public Connector build(Server server,
+                           MetricRegistry metrics,
+                           String name,
+                           ThreadPool threadPool) {
+        final HttpConfiguration httpConfig = buildHttpConfiguration();
+
+        final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig);
+
+        final Scheduler scheduler = new ScheduledExecutorScheduler();
+
+        final ByteBufferPool bufferPool = buildBufferPool();
+
+        final String timerName = name(HttpConnectionFactory.class,
+                                      bindHost,
+                                      Integer.toString(port),
+                                      "connections");
+        return buildConnector(server, scheduler, bufferPool, name, threadPool,
+                              new InstrumentedConnectionFactory(httpConnectionFactory,
+                                                                metrics.timer(timerName)));
+    }
+
+    protected ServerConnector buildConnector(Server server,
+                                             Scheduler scheduler,
+                                             ByteBufferPool bufferPool,
+                                             String name,
+                                             ThreadPool threadPool,
+                                             ConnectionFactory... factories) {
+        final ServerConnector connector = new ServerConnector(server,
+                                                              threadPool,
+                                                              scheduler,
+                                                              bufferPool,
+                                                              acceptorThreads,
+                                                              selectorThreads,
+                                                              factories);
+        connector.setPort(port);
+        connector.setHost(bindHost);
+        if (acceptQueueSize != null) {
+            connector.setAcceptQueueSize(acceptQueueSize);
+        }
+        connector.setReuseAddress(reuseAddress);
+        if (soLingerTime != null) {
+            connector.setSoLingerTime((int) soLingerTime.toSeconds());
+        }
+        connector.setIdleTimeout(idleTimeout.toMilliseconds());
+        connector.setName(name);
+
+        return connector;
+    }
+
+    protected HttpConnectionFactory buildHttpConnectionFactory(HttpConfiguration httpConfig) {
+        final HttpConnectionFactory httpConnectionFactory = new HttpConnectionFactory(httpConfig);
+        httpConnectionFactory.setInputBufferSize((int) inputBufferSize.toBytes());
+        return httpConnectionFactory;
+    }
+
+    protected HttpConfiguration buildHttpConfiguration() {
+        final HttpConfiguration httpConfig = new HttpConfiguration();
+        httpConfig.setHeaderCacheSize((int) headerCacheSize.toBytes());
+        httpConfig.setOutputBufferSize((int) outputBufferSize.toBytes());
+        httpConfig.setRequestHeaderSize((int) maxRequestHeaderSize.toBytes());
+        httpConfig.setResponseHeaderSize((int) maxResponseHeaderSize.toBytes());
+        httpConfig.setSendDateHeader(useDateHeader);
+        httpConfig.setSendServerVersion(useServerHeader);
+
+        if (useForwardedHeaders) {
+            httpConfig.addCustomizer(new ForwardedRequestCustomizer());
+        }
+        return httpConfig;
+    }
+
+    protected ByteBufferPool buildBufferPool() {
+        return new ArrayByteBufferPool((int) minBufferPoolSize.toBytes(),
+                                       (int) bufferPoolIncrement.toBytes(),
+                                       (int) maxBufferPoolSize.toBytes());
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpsConnectorFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpsConnectorFactory.java
new file mode 100644
index 0000000..e1a3224
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpsConnectorFactory.java
@@ -0,0 +1,675 @@
+package io.dropwizard.jetty;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jetty9.InstrumentedConnectionFactory;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+
+import org.eclipse.jetty.http.HttpVersion;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.*;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.ThreadPool;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import io.dropwizard.validation.ValidationMethod;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+
+import java.io.File;
+import java.net.URI;
+import java.security.KeyStore;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+/**
+ * Builds HTTPS connectors (HTTP over TLS/SSL).
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code keyStorePath}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>
+ *             The path to the Java key store which contains the host certificate and private key.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code keyStorePassword}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>
+ *             The password used to access the key store.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code keyStoreType}</td>
+ *         <td>{@code JKS}</td>
+ *         <td>
+ *             The type of key store (usually {@code JKS}, {@code PKCS12}, {@code JCEKS},
+ *             {@code Windows-MY}, or {@code Windows-ROOT}).
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code keyStoreProvider}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The JCE provider to use to access the key store.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code trustStorePath}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The path to the Java key store which contains the CA certificates used to establish
+ *             trust.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code trustStorePassword}</td>
+ *         <td>(none)</td>
+ *         <td>The password used to access the trust store.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code trustStoreType}</td>
+ *         <td>{@code JKS}</td>
+ *         <td>
+ *             The type of trust store (usually {@code JKS}, {@code PKCS12}, {@code JCEKS},
+ *             {@code Windows-MY}, or {@code Windows-ROOT}).
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code trustStoreProvider}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The JCE provider to use to access the trust store.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code keyManagerPassword}</td>
+ *         <td>(none)</td>
+ *         <td>The password, if any, for the key manager.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code needClientAuth}</td>
+ *         <td>(none)</td>
+ *         <td>Whether or not client authentication is required.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code wantClientAuth}</td>
+ *         <td>(none)</td>
+ *         <td>Whether or not client authentication is requested.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code certAlias}</td>
+ *         <td>(none)</td>
+ *         <td>The alias of the certificate to use.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code crlPath}</td>
+ *         <td>(none)</td>
+ *         <td>The path to the file which contains the Certificate Revocation List.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code enableCRLDP}</td>
+ *         <td>false</td>
+ *         <td>Whether or not CRL Distribution Points (CRLDP) support is enabled.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code enableOCSP}</td>
+ *         <td>false</td>
+ *         <td>Whether or not On-Line Certificate Status Protocol (OCSP) support is enabled.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxCertPathLength}</td>
+ *         <td>(unlimited)</td>
+ *         <td>The maximum certification path length.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code ocspResponderUrl}</td>
+ *         <td>(none)</td>
+ *         <td>The location of the OCSP responder.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code jceProvider}</td>
+ *         <td>(none)</td>
+ *         <td>The name of the JCE provider to use for cryptographic support.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code validateCerts}</td>
+ *         <td>true</td>
+ *         <td>
+ *             Whether or not to validate TLS certificates before starting. If enabled, Dropwizard
+ *             will refuse to start with expired or otherwise invalid certificates.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code validatePeers}</td>
+ *         <td>true</td>
+ *         <td>Whether or not to validate TLS peer certificates.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code supportedProtocols}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             A list of protocols (e.g., {@code SSLv3}, {@code TLSv1}) which are supported. All
+ *             other protocols will be refused.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code supportedCipherSuites}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             A list of cipher suites (e.g., {@code TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}) which
+ *             are supported. All other cipher suites will be refused
+ *         </td>
+ *     </tr>
+ *    <tr>
+ *         <td>{@code excludedCipherSuites}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             A list of cipher suites (e.g., {@code TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256}) which
+ *             are excluded. These cipher suites will be refused.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code allowRenegotiation}</td>
+ *         <td>true</td>
+ *         <td>Whether or not TLS renegotiation is allowed.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code endpointIdentificationAlgorithm}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             Which endpoint identification algorithm, if any, to use during the TLS handshake.
+ *         </td>
+ *     </tr>
+ * </table>
+ * <p/>
+ * For more configuration parameters, see {@link HttpConnectorFactory}.
+ *
+ * @see HttpConnectorFactory
+ */
+ at JsonTypeName("https")
+public class HttpsConnectorFactory extends HttpConnectorFactory {
+    private static final Logger LOGGER = LoggerFactory.getLogger(HttpsConnectorFactory.class);
+    private static final AtomicBoolean LOGGED = new AtomicBoolean(false);
+
+    private String keyStorePath;
+
+    private String keyStorePassword;
+
+    @NotEmpty
+    private String keyStoreType = "JKS";
+
+    private String keyStoreProvider;
+
+    private String trustStorePath;
+
+    private String trustStorePassword;
+
+    @NotEmpty
+    private String trustStoreType = "JKS";
+
+    private String trustStoreProvider;
+
+    private String keyManagerPassword;
+
+    private Boolean needClientAuth;
+    private Boolean wantClientAuth;
+    private String certAlias;
+    private File crlPath;
+    private Boolean enableCRLDP;
+    private Boolean enableOCSP;
+    private Integer maxCertPathLength;
+    private URI ocspResponderUrl;
+    private String jceProvider;
+    private boolean validateCerts = true;
+    private boolean validatePeers = true;
+    private List<String> supportedProtocols;
+    private List<String> supportedCipherSuites;
+    private List<String> excludedCipherSuites;
+    private boolean allowRenegotiation = true;
+    private String endpointIdentificationAlgorithm;
+
+    @JsonProperty
+    public boolean getAllowRenegotiation() {
+        return allowRenegotiation;
+    }
+
+    @JsonProperty
+    public void setAllowRenegotiation(boolean allowRenegotiation) {
+        this.allowRenegotiation = allowRenegotiation;
+    }
+
+    @JsonProperty
+    public String getEndpointIdentificationAlgorithm() {
+        return endpointIdentificationAlgorithm;
+    }
+
+    @JsonProperty
+    public void setEndpointIdentificationAlgorithm(String endpointIdentificationAlgorithm) {
+        this.endpointIdentificationAlgorithm = endpointIdentificationAlgorithm;
+    }
+
+    @JsonProperty
+    public String getKeyStorePath() {
+        return keyStorePath;
+    }
+
+    @JsonProperty
+    public void setKeyStorePath(String keyStorePath) {
+        this.keyStorePath = keyStorePath;
+    }
+
+    @JsonProperty
+    public String getKeyStorePassword() {
+        return keyStorePassword;
+    }
+
+    @JsonProperty
+    public void setKeyStorePassword(String keyStorePassword) {
+        this.keyStorePassword = keyStorePassword;
+    }
+
+    @JsonProperty
+    public String getKeyStoreType() {
+        return keyStoreType;
+    }
+
+    @JsonProperty
+    public void setKeyStoreType(String keyStoreType) {
+        this.keyStoreType = keyStoreType;
+    }
+
+    @JsonProperty
+    public String getKeyStoreProvider() {
+        return keyStoreProvider;
+    }
+
+    @JsonProperty
+    public void setKeyStoreProvider(String keyStoreProvider) {
+        this.keyStoreProvider = keyStoreProvider;
+    }
+
+    @JsonProperty
+    public String getTrustStoreType() {
+        return trustStoreType;
+    }
+
+    @JsonProperty
+    public void setTrustStoreType(String trustStoreType) {
+        this.trustStoreType = trustStoreType;
+    }
+
+    @JsonProperty
+    public String getTrustStoreProvider() {
+        return trustStoreProvider;
+    }
+
+    @JsonProperty
+    public void setTrustStoreProvider(String trustStoreProvider) {
+        this.trustStoreProvider = trustStoreProvider;
+    }
+
+    @JsonProperty
+    public String getKeyManagerPassword() {
+        return keyManagerPassword;
+    }
+
+    @JsonProperty
+    public void setKeyManagerPassword(String keyManagerPassword) {
+        this.keyManagerPassword = keyManagerPassword;
+    }
+
+    @JsonProperty
+    public String getTrustStorePath() {
+        return trustStorePath;
+    }
+
+    @JsonProperty
+    public void setTrustStorePath(String trustStorePath) {
+        this.trustStorePath = trustStorePath;
+    }
+
+    @JsonProperty
+    public String getTrustStorePassword() {
+        return trustStorePassword;
+    }
+
+    @JsonProperty
+    public void setTrustStorePassword(String trustStorePassword) {
+        this.trustStorePassword = trustStorePassword;
+    }
+
+    @JsonProperty
+    public Boolean getNeedClientAuth() {
+        return needClientAuth;
+    }
+
+    @JsonProperty
+    public void setNeedClientAuth(Boolean needClientAuth) {
+        this.needClientAuth = needClientAuth;
+    }
+
+    @JsonProperty
+    public Boolean getWantClientAuth() {
+        return wantClientAuth;
+    }
+
+    @JsonProperty
+    public void setWantClientAuth(Boolean wantClientAuth) {
+        this.wantClientAuth = wantClientAuth;
+    }
+
+    @JsonProperty
+    public String getCertAlias() {
+        return certAlias;
+    }
+
+    @JsonProperty
+    public void setCertAlias(String certAlias) {
+        this.certAlias = certAlias;
+    }
+
+    @JsonProperty
+    public File getCrlPath() {
+        return crlPath;
+    }
+
+    @JsonProperty
+    public void setCrlPath(File crlPath) {
+        this.crlPath = crlPath;
+    }
+
+    @JsonProperty
+    public Boolean getEnableCRLDP() {
+        return enableCRLDP;
+    }
+
+    @JsonProperty
+    public void setEnableCRLDP(Boolean enableCRLDP) {
+        this.enableCRLDP = enableCRLDP;
+    }
+
+    @JsonProperty
+    public Boolean getEnableOCSP() {
+        return enableOCSP;
+    }
+
+    @JsonProperty
+    public void setEnableOCSP(Boolean enableOCSP) {
+        this.enableOCSP = enableOCSP;
+    }
+
+    @JsonProperty
+    public Integer getMaxCertPathLength() {
+        return maxCertPathLength;
+    }
+
+    @JsonProperty
+    public void setMaxCertPathLength(Integer maxCertPathLength) {
+        this.maxCertPathLength = maxCertPathLength;
+    }
+
+    @JsonProperty
+    public URI getOcspResponderUrl() {
+        return ocspResponderUrl;
+    }
+
+    @JsonProperty
+    public void setOcspResponderUrl(URI ocspResponderUrl) {
+        this.ocspResponderUrl = ocspResponderUrl;
+    }
+
+    @JsonProperty
+    public String getJceProvider() {
+        return jceProvider;
+    }
+
+    @JsonProperty
+    public void setJceProvider(String jceProvider) {
+        this.jceProvider = jceProvider;
+    }
+
+    @JsonProperty
+    public boolean getValidatePeers() {
+        return validatePeers;
+    }
+
+    @JsonProperty
+    public void setValidatePeers(boolean validatePeers) {
+        this.validatePeers = validatePeers;
+    }
+
+    @JsonProperty
+    public List<String> getSupportedProtocols() {
+        return supportedProtocols;
+    }
+
+    @JsonProperty
+    public void setSupportedProtocols(List<String> supportedProtocols) {
+        this.supportedProtocols = supportedProtocols;
+    }
+
+    @JsonProperty
+    public List<String> getSupportedCipherSuites() {
+        return supportedCipherSuites;
+    }
+    
+    @JsonProperty
+    public List<String> getExcludedCipherSuites() {
+        return excludedCipherSuites;
+    }
+
+    @JsonProperty
+    public void setExcludedCipherSuites(List<String> excludedCipherSuites) {
+        this.excludedCipherSuites = excludedCipherSuites;
+    }
+
+    @JsonProperty
+    public void setSupportedCipherSuites(List<String> supportedCipherSuites) {
+        this.supportedCipherSuites = supportedCipherSuites;
+    }
+
+    @JsonProperty
+    public boolean isValidateCerts() {
+        return validateCerts;
+    }
+
+    @JsonProperty
+    public void setValidateCerts(boolean validateCerts) {
+        this.validateCerts = validateCerts;
+    }
+
+    @ValidationMethod(message="keyStorePath should not be null")
+    public boolean isValidKeyStorePath() {
+        return keyStoreType.startsWith("Windows-") || keyStorePath != null;
+    }
+
+    @ValidationMethod(message="keyStorePassword should not be null or empty")
+    public boolean isValidKeyStorePassword() {
+        return keyStoreType.startsWith("Windows-") ||
+                !Strings.isNullOrEmpty(keyStorePassword);
+    }
+
+    @Override
+    public Connector build(Server server, MetricRegistry metrics, String name, ThreadPool threadPool) {
+        logSupportedParameters();
+
+        final HttpConfiguration httpConfig = buildHttpConfiguration();
+
+        final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig);
+
+        final SslContextFactory sslContextFactory = buildSslContextFactory();
+        server.addBean(sslContextFactory);
+
+        final SslConnectionFactory sslConnectionFactory =
+                new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.toString());
+
+        final Scheduler scheduler = new ScheduledExecutorScheduler();
+
+        final ByteBufferPool bufferPool = buildBufferPool();
+
+        final String timerName = name(HttpConnectionFactory.class,
+                                      getBindHost(),
+                                      Integer.toString(getPort()),
+                                      "connections");
+
+        return buildConnector(server, scheduler, bufferPool, name, threadPool,
+                              new InstrumentedConnectionFactory(sslConnectionFactory,
+                                                                metrics.timer(timerName)),
+                              httpConnectionFactory);
+    }
+
+    @Override
+    protected HttpConfiguration buildHttpConfiguration() {
+        final HttpConfiguration config = super.buildHttpConfiguration();
+        config.setSecureScheme("https");
+        config.setSecurePort(getPort());
+        config.addCustomizer(new SecureRequestCustomizer());
+        return config;
+    }
+
+    protected void logSupportedParameters() {
+        if (LOGGED.compareAndSet(false, true)) {
+            try {
+                final SSLContext context = SSLContext.getDefault();
+                final String[] protocols = context.getSupportedSSLParameters().getProtocols();
+                final SSLSocketFactory factory = context.getSocketFactory();
+                final String[] cipherSuites = factory.getSupportedCipherSuites();
+                LOGGER.info("Supported protocols: {}", Arrays.toString(protocols));
+                LOGGER.info("Supported cipher suites: {}", Arrays.toString(cipherSuites));
+            } catch (NoSuchAlgorithmException ignored) {
+
+            }
+        }
+    }
+
+    protected SslContextFactory buildSslContextFactory() {
+        final SslContextFactory factory = new SslContextFactory(keyStorePath);
+        final String keyStoreType = getKeyStoreType();
+        if (keyStoreType.startsWith("Windows-")) {
+            try {
+                final KeyStore keyStore = KeyStore.getInstance(keyStoreType);
+
+                keyStore.load(null, null);
+                factory.setKeyStore(keyStore);
+            } catch (Exception e) {
+                throw new IllegalStateException("Windows key store not supported", e);
+            }
+        } else {
+            factory.setKeyStoreType(keyStoreType);
+            factory.setKeyStorePassword(keyStorePassword);
+        }
+
+        if (keyStoreProvider != null) {
+            factory.setKeyStoreProvider(keyStoreProvider);
+        }
+
+        final String trustStoreType = getTrustStoreType();
+        if (trustStoreType.startsWith("Windows-")) {
+          try {
+            final KeyStore keyStore = KeyStore.getInstance(trustStoreType);
+
+            keyStore.load(null, null);
+            factory.setTrustStore(keyStore);
+          } catch (Exception e) {
+            throw new IllegalStateException("Windows key store not supported", e);
+          }
+        } else {
+            if (trustStorePath != null) {
+                factory.setTrustStorePath(trustStorePath);
+            }
+            if (trustStorePassword != null) {
+                factory.setTrustStorePassword(trustStorePassword);
+            }
+            factory.setTrustStoreType(trustStoreType);
+        }
+
+        if (trustStoreProvider != null) {
+            factory.setTrustStoreProvider(trustStoreProvider);
+        }
+
+        if (keyManagerPassword != null) {
+            factory.setKeyManagerPassword(keyManagerPassword);
+        }
+
+        if (needClientAuth != null) {
+            factory.setNeedClientAuth(needClientAuth);
+        }
+
+        if (wantClientAuth != null) {
+            factory.setWantClientAuth(wantClientAuth);
+        }
+
+        if (certAlias != null) {
+            factory.setCertAlias(certAlias);
+        }
+
+        if (crlPath != null) {
+            factory.setCrlPath(crlPath.getAbsolutePath());
+        }
+
+        if (enableCRLDP != null) {
+            factory.setEnableCRLDP(enableCRLDP);
+        }
+
+        if (enableOCSP != null) {
+            factory.setEnableOCSP(enableOCSP);
+        }
+
+        if (maxCertPathLength != null) {
+            factory.setMaxCertPathLength(maxCertPathLength);
+        }
+
+        if (ocspResponderUrl != null) {
+            factory.setOcspResponderURL(ocspResponderUrl.toASCIIString());
+        }
+
+        if (jceProvider != null) {
+            factory.setProvider(jceProvider);
+        }
+
+        factory.setRenegotiationAllowed(allowRenegotiation);
+        factory.setEndpointIdentificationAlgorithm(endpointIdentificationAlgorithm);
+
+        // TODO: 6/20/13 <coda> -- figure out SSL session caching
+        // This doesn't seem to be hooked up to anything yet in Jetty.
+        // factory.setSessionCachingEnabled(false);
+        // factory.setSslSessionCacheSize(10);
+        // factory.setSslSessionTimeout(10);
+
+        factory.setValidateCerts(validateCerts);
+        factory.setValidatePeerCerts(validatePeers);
+
+        if (supportedProtocols != null) {
+            factory.setIncludeProtocols(Iterables.toArray(supportedProtocols, String.class));
+        }
+
+        if (supportedCipherSuites != null) {
+            factory.setIncludeCipherSuites(Iterables.toArray(supportedCipherSuites, String.class));
+        }
+        
+        if (excludedCipherSuites != null) {
+            factory.setExcludeCipherSuites(Iterables.toArray(excludedCipherSuites, String.class));
+        }
+
+        return factory;
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/MutableServletContextHandler.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/MutableServletContextHandler.java
new file mode 100644
index 0000000..1e23b70
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/MutableServletContextHandler.java
@@ -0,0 +1,29 @@
+package io.dropwizard.jetty;
+
+import org.eclipse.jetty.servlet.ServletContextHandler;
+
+public class MutableServletContextHandler extends ServletContextHandler {
+    public boolean isSecurityEnabled() {
+        return (this._options & SECURITY) != 0;
+    }
+
+    public void setSecurityEnabled(boolean enabled) {
+        if (enabled) {
+            this._options |= SECURITY;
+        } else {
+            this._options &= ~SECURITY;
+        }
+    }
+
+    public boolean isSessionsEnabled() {
+        return (this._options & SESSIONS) != 0;
+    }
+
+    public void setSessionsEnabled(boolean enabled) {
+        if (enabled) {
+            this._options |= SESSIONS;
+        } else {
+            this._options &= ~SESSIONS;
+        }
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/NonblockingServletHolder.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/NonblockingServletHolder.java
new file mode 100644
index 0000000..04b01d7
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/NonblockingServletHolder.java
@@ -0,0 +1,54 @@
+package io.dropwizard.jetty;
+
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import java.io.IOException;
+
+/**
+ * A {@link ServletHolder} subclass which removes the synchronization around servlet initialization
+ * by requiring a pre-initialized servlet holder.
+ */
+public class NonblockingServletHolder extends ServletHolder {
+    private final Servlet servlet;
+
+    public NonblockingServletHolder(Servlet servlet) {
+        super(servlet);
+        setInitOrder(1);
+        this.servlet = servlet;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return super.equals(o);
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override
+    public Servlet getServlet() throws ServletException {
+        return servlet;
+    }
+
+    @Override
+    public void handle(Request baseRequest,
+                       ServletRequest request,
+                       ServletResponse response) throws ServletException, IOException {
+        final boolean asyncSupported = baseRequest.isAsyncSupported();
+        if (!isAsyncSupported()) {
+            baseRequest.setAsyncSupported(false);
+        }
+        try {
+            servlet.service(request, response);
+        } finally {
+            baseRequest.setAsyncSupported(asyncSupported);
+        }
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/RequestLogFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/RequestLogFactory.java
new file mode 100644
index 0000000..0aa8ec4
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/RequestLogFactory.java
@@ -0,0 +1,103 @@
+package io.dropwizard.jetty;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.CoreConstants;
+import ch.qos.logback.core.LayoutBase;
+import ch.qos.logback.core.spi.AppenderAttachableImpl;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.logging.AppenderFactory;
+import io.dropwizard.logging.ConsoleAppenderFactory;
+import org.eclipse.jetty.server.RequestLog;
+import org.slf4j.LoggerFactory;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.TimeZone;
+
+/**
+ * A factory for creating {@link RequestLog} instances.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code timeZone}</td>
+ *         <td>UTC</td>
+ *         <td>The time zone to which request timestamps will be converted.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code appenders}</td>
+ *         <td>a default {@link ConsoleAppenderFactory console} appender</td>
+ *         <td>
+ *             The set of {@link AppenderFactory appenders} to which requests will be logged.
+ *         </td>
+ *     </tr>
+ * </table>
+ */
+public class RequestLogFactory {
+    private static class RequestLogLayout extends LayoutBase<ILoggingEvent> {
+        @Override
+        public String doLayout(ILoggingEvent event) {
+            return event.getFormattedMessage() + CoreConstants.LINE_SEPARATOR;
+        }
+    }
+
+    @NotNull
+    private TimeZone timeZone = TimeZone.getTimeZone("UTC");
+
+    @Valid
+    @NotNull
+    private ImmutableList<AppenderFactory> appenders = ImmutableList.<AppenderFactory>of(
+            new ConsoleAppenderFactory()
+    );
+
+    @JsonProperty
+    public ImmutableList<AppenderFactory> getAppenders() {
+        return appenders;
+    }
+
+    @JsonProperty
+    public void setAppenders(ImmutableList<AppenderFactory> appenders) {
+        this.appenders = appenders;
+    }
+
+    @JsonProperty
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    @JsonProperty
+    public void setTimeZone(TimeZone timeZone) {
+        this.timeZone = timeZone;
+    }
+
+    @JsonIgnore
+    public boolean isEnabled() {
+        return !appenders.isEmpty();
+    }
+
+    public RequestLog build(String name) {
+        final Logger logger = (Logger) LoggerFactory.getLogger("http.request");
+        logger.setAdditive(false);
+
+        final LoggerContext context = logger.getLoggerContext();
+
+        final RequestLogLayout layout = new RequestLogLayout();
+        layout.start();
+
+        final AppenderAttachableImpl<ILoggingEvent> attachable = new AppenderAttachableImpl<>();
+        for (AppenderFactory output : this.appenders) {
+            attachable.addAppender(output.build(context, name, layout));
+        }
+
+        return new Slf4jRequestLog(attachable, timeZone);
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/RoutingHandler.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/RoutingHandler.java
new file mode 100644
index 0000000..c1d5797
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/RoutingHandler.java
@@ -0,0 +1,57 @@
+package io.dropwizard.jetty;
+
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+
+public class RoutingHandler extends AbstractHandler {
+    /**
+     * We use an array of entries instead of a map here for performance reasons. We're only ever
+     * comparing connectors by reference, not by equality, so avoiding the overhead of a map is
+     * a lot faster. See RoutingHandlerBenchmark for details, but tested against an
+     * ImmutableMap-backed implementation it was ~54us vs. ~4500us for 1,000,000 iterations.
+     */
+    private static class Entry {
+        final Connector connector;
+        final Handler handler;
+
+        private Entry(Connector connector, Handler handler) {
+            this.connector = connector;
+            this.handler = handler;
+        }
+    }
+
+    private final Entry[] entries;
+
+    public RoutingHandler(Map<Connector, Handler> handlers) {
+        this.entries = new Entry[handlers.size()];
+        int i = 0;
+        for (Map.Entry<Connector, Handler> entry : handlers.entrySet()) {
+            this.entries[i++] = new Entry(entry.getKey(), entry.getValue());
+            addBean(entry.getValue());
+        }
+    }
+
+    @Override
+    public void handle(String target,
+                       Request baseRequest,
+                       HttpServletRequest request,
+                       HttpServletResponse response) throws IOException, ServletException {
+        final Connector connector = baseRequest.getHttpChannel().getConnector();
+        for (Entry entry : entries) {
+            // reference equality works fine — none of the connectors implement #equals(Object)
+            if (entry.connector == connector) {
+                entry.handler.handle(target, baseRequest, request, response);
+                return;
+            }
+        }
+    }
+}
+
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/Slf4jRequestLog.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/Slf4jRequestLog.java
new file mode 100644
index 0000000..32a441d
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/Slf4jRequestLog.java
@@ -0,0 +1,66 @@
+package io.dropwizard.jetty;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.classic.spi.LoggingEvent;
+import ch.qos.logback.core.spi.AppenderAttachableImpl;
+import org.eclipse.jetty.server.AbstractNCSARequestLog;
+import org.eclipse.jetty.server.RequestLog;
+
+import java.io.IOException;
+import java.util.TimeZone;
+
+/**
+ * A SLF4J-backed {@link RequestLog} implementation of {@link AbstractNCSARequestLog}.
+ */
+public class Slf4jRequestLog extends AbstractNCSARequestLog {
+    private final AppenderAttachableImpl<ILoggingEvent> appenders;
+
+    /**
+     * Creates a new request log.
+     *
+     * @param appenders     the appenders to which requests will be logged
+     * @param timeZone      the timezone to which timestamps will be converted
+     */
+    public Slf4jRequestLog(AppenderAttachableImpl<ILoggingEvent> appenders, TimeZone timeZone) {
+        this.appenders = appenders;
+
+        setLogLatency(true);
+        setLogTimeZone(timeZone);
+        setExtended(true);
+        setPreferProxiedForAddress(true);
+
+        // the appenders already started
+        try {
+            start();
+        } catch (Exception e) {
+            throw new IllegalStateException("Should have succeeded doing a noop start", e);
+        }
+    }
+
+    @Override
+    protected boolean isEnabled() {
+        return true;
+    }
+
+    @Override
+    public void write(String entry) throws IOException {
+        final LoggingEvent event = new LoggingEvent();
+        event.setLevel(Level.INFO);
+        event.setLoggerName("http.request");
+        event.setMessage(entry);
+        event.setTimeStamp(System.currentTimeMillis());
+
+        appenders.appendLoopOnAppenders(event);
+    }
+
+    public void setLogTimeZone(TimeZone tz) {
+        setLogTimeZone(tz.getID());
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        appenders.detachAndStopAllAppenders();
+        super.doStop();
+    }
+}
diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/setup/ServletEnvironment.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/setup/ServletEnvironment.java
new file mode 100644
index 0000000..1e2f1e8
--- /dev/null
+++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/setup/ServletEnvironment.java
@@ -0,0 +1,151 @@
+package io.dropwizard.jetty.setup;
+
+import io.dropwizard.jetty.MutableServletContextHandler;
+import io.dropwizard.jetty.NonblockingServletHolder;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterRegistration;
+import javax.servlet.Servlet;
+import javax.servlet.ServletRegistration;
+import java.util.*;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class ServletEnvironment {
+    private static final Logger LOGGER = LoggerFactory.getLogger(ServletEnvironment.class);
+
+    private final MutableServletContextHandler handler;
+
+    private final Set<String> servlets = new HashSet<>();
+    private final Set<String> filters = new HashSet<>();
+
+    public ServletEnvironment(MutableServletContextHandler handler) {
+        this.handler = handler;
+    }
+
+    /**
+     * Add a servlet instance.
+     *
+     * @param name    the servlet's name
+     * @param servlet the servlet instance
+     * @return a {@link javax.servlet.ServletRegistration.Dynamic} instance allowing for further
+     *         configuration
+     */
+    public ServletRegistration.Dynamic addServlet(String name, Servlet servlet) {
+        final ServletHolder holder = new NonblockingServletHolder(checkNotNull(servlet));
+        holder.setName(name);
+        handler.getServletHandler().addServlet(holder);
+
+        ServletRegistration.Dynamic registration = holder.getRegistration();
+        checkDuplicateRegistration(name, servlets, "servlet");
+
+        return registration;
+    }
+
+    /**
+     * Add a servlet class.
+     *
+     * @param name  the servlet's name
+     * @param klass the servlet class
+     * @return a {@link javax.servlet.ServletRegistration.Dynamic} instance allowing for further configuration
+     */
+    public ServletRegistration.Dynamic addServlet(String name, Class<? extends Servlet> klass) {
+        final ServletHolder holder = new ServletHolder(checkNotNull(klass));
+        holder.setName(name);
+        handler.getServletHandler().addServlet(holder);
+
+        ServletRegistration.Dynamic registration = holder.getRegistration();
+        checkDuplicateRegistration(name, servlets, "servlet");
+
+        return registration;
+    }
+
+    /**
+     * Add a filter instance.
+     *
+     * @param name   the filter's name
+     * @param filter the filter instance
+     * @return a {@link javax.servlet.FilterRegistration.Dynamic} instance allowing for further
+     *         configuration
+     */
+    public FilterRegistration.Dynamic addFilter(String name, Filter filter) {
+        final FilterHolder holder = new FilterHolder(checkNotNull(filter));
+        holder.setName(name);
+        handler.getServletHandler().addFilter(holder);
+
+        FilterRegistration.Dynamic registration = holder.getRegistration();
+        checkDuplicateRegistration(name, filters, "filter");
+
+        return registration;
+    }
+
+    /**
+     * Add a filter class.
+     *
+     * @param name  the filter's name
+     * @param klass the filter class
+     * @return a {@link javax.servlet.FilterRegistration.Dynamic} instance allowing for further configuration
+     */
+    public FilterRegistration.Dynamic addFilter(String name, Class<? extends Filter> klass) {
+        final FilterHolder holder = new FilterHolder(checkNotNull(klass));
+        holder.setName(name);
+        handler.getServletHandler().addFilter(holder);
+
+        FilterRegistration.Dynamic registration = holder.getRegistration();
+        checkDuplicateRegistration(name, filters, "filter");
+
+        return registration;
+    }
+
+    /**
+     * Add one or more servlet event listeners.
+     *
+     * @param listeners one or more listener instances that implement {@link
+     *                  javax.servlet.ServletContextListener}, {@link javax.servlet.ServletContextAttributeListener},
+     *                  {@link javax.servlet.ServletRequestListener} or {@link
+     *                  javax.servlet.ServletRequestAttributeListener}
+     */
+    public void addServletListeners(EventListener... listeners) {
+        for (EventListener listener : listeners) {
+            handler.addEventListener(listener);
+        }
+    }
+
+    public void setProtectedTargets(String... targets) {
+        handler.setProtectedTargets(Arrays.copyOf(targets, targets.length));
+    }
+
+    public void setResourceBase(String resourceBase) {
+        handler.setResourceBase(resourceBase);
+    }
+
+    public void setInitParameter(String name, String value) {
+        handler.setInitParameter(name, value);
+    }
+
+    public void setSessionHandler(SessionHandler sessionHandler) {
+        handler.setSessionsEnabled(sessionHandler != null);
+        handler.setSessionHandler(sessionHandler);
+    }
+
+    public void setSecurityHandler(SecurityHandler securityHandler) {
+        handler.setSecurityEnabled(securityHandler != null);
+        handler.setSecurityHandler(securityHandler);
+    }
+
+    public void addMimeMapping(String extension, String type) {
+        handler.getMimeTypes().addMimeMapping(extension, type);
+    }
+
+    private void checkDuplicateRegistration(String name, Set<String> items, String type) {
+        if(!items.add(name)) {
+            LOGGER.warn("Overriding the existing {} registered with the name: {}", type, name);
+        }
+    }
+}
diff --git a/dropwizard-jetty/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/dropwizard-jetty/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
new file mode 100644
index 0000000..97e7b11
--- /dev/null
+++ b/dropwizard-jetty/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
@@ -0,0 +1 @@
+io.dropwizard.jetty.ConnectorFactory
diff --git a/dropwizard-jetty/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory b/dropwizard-jetty/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory
new file mode 100644
index 0000000..78e0dba
--- /dev/null
+++ b/dropwizard-jetty/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory
@@ -0,0 +1,2 @@
+io.dropwizard.jetty.HttpConnectorFactory
+io.dropwizard.jetty.HttpsConnectorFactory
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/ContextRoutingHandlerTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/ContextRoutingHandlerTest.java
new file mode 100644
index 0000000..3dc2971
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/ContextRoutingHandlerTest.java
@@ -0,0 +1,72 @@
+package io.dropwizard.jetty;
+
+import com.google.common.collect.ImmutableMap;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.mockito.Mockito.*;
+
+public class ContextRoutingHandlerTest {
+    private final Request baseRequest = mock(Request.class);
+    private final HttpServletRequest request = mock(HttpServletRequest.class);
+    private final HttpServletResponse response = mock(HttpServletResponse.class);
+
+    private final Handler handler1 = mock(Handler.class);
+    private final Handler handler2 = mock(Handler.class);
+
+    private ContextRoutingHandler handler;
+
+    @Before
+    public void setUp() throws Exception {
+        this.handler = new ContextRoutingHandler(ImmutableMap.of(
+                "/", handler1,
+                "/admin", handler2
+        ));
+    }
+
+    @Test
+    public void routesToTheBestPrefixMatch() throws Exception {
+        when(baseRequest.getRequestURI()).thenReturn("/hello-world");
+
+        handler.handle("/hello-world", baseRequest, request, response);
+
+        verify(handler1).handle("/hello-world", baseRequest, request, response);
+        verify(handler2, never()).handle("/hello-world", baseRequest, request, response);
+    }
+
+    @Test
+    public void routesToTheLongestPrefixMatch() throws Exception {
+        when(baseRequest.getRequestURI()).thenReturn("/admin/woo");
+
+        handler.handle("/admin/woo", baseRequest, request, response);
+
+        verify(handler1, never()).handle("/admin/woo", baseRequest, request, response);
+        verify(handler2).handle("/admin/woo", baseRequest, request, response);
+    }
+
+    @Test
+    public void passesHandlingNonMatchingRequests() throws Exception {
+        when(baseRequest.getRequestURI()).thenReturn("WAT");
+
+        handler.handle("WAT", baseRequest, request, response);
+
+        verify(handler1, never()).handle("WAT", baseRequest, request, response);
+        verify(handler2, never()).handle("WAT", baseRequest, request, response);
+    }
+
+    @Test
+    public void startsAndStopsAllHandlers() throws Exception {
+        handler.start();
+        handler.stop();
+
+        final InOrder inOrder = inOrder(handler1, handler2);
+        inOrder.verify(handler1).start();
+        inOrder.verify(handler2).start();
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/GzipFilterFactoryTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/GzipFilterFactoryTest.java
new file mode 100644
index 0000000..ba50273
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/GzipFilterFactoryTest.java
@@ -0,0 +1,63 @@
+package io.dropwizard.jetty;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.io.Resources;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.util.Size;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import java.io.File;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class GzipFilterFactoryTest {
+    private GzipFilterFactory gzip;
+
+    @Before
+    public void setUp() throws Exception {
+        this.gzip = new ConfigurationFactory<>(GzipFilterFactory.class,
+                                               Validation.buildDefaultValidatorFactory()
+                                                                 .getValidator(),
+                                               Jackson.newObjectMapper(), "dw")
+                .build(new File(Resources.getResource("yaml/gzip.yml").toURI()));
+    }
+
+    @Test
+    public void canBeEnabled() throws Exception {
+        assertThat(gzip.isEnabled())
+                .isFalse();
+    }
+
+    @Test
+    public void hasAMinimumEntitySize() throws Exception {
+        assertThat(gzip.getMinimumEntitySize())
+                .isEqualTo(Size.kilobytes(12));
+    }
+
+    @Test
+    public void hasABufferSize() throws Exception {
+        assertThat(gzip.getBufferSize())
+                .isEqualTo(Size.kilobytes(32));
+    }
+
+    @Test
+    public void hasExcludedUserAgents() throws Exception {
+        assertThat(gzip.getExcludedUserAgents())
+                .isEqualTo(ImmutableSet.of("IE"));
+    }
+
+    @Test
+    public void hasCompressedMimeTypes() throws Exception {
+        assertThat(gzip.getCompressedMimeTypes())
+                .isEqualTo(ImmutableSet.of("text/plain"));
+    }
+
+    @Test
+    public void varyIsOnlyForAcceptEncoding() throws Exception {
+        assertThat(gzip.getVary())
+                .isEqualTo("Accept-Encoding");
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpConnectorFactoryTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpConnectorFactoryTest.java
new file mode 100644
index 0000000..44d2166
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpConnectorFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.jetty;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class HttpConnectorFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(HttpConnectorFactory.class);
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpsConnectorFactoryTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpsConnectorFactoryTest.java
new file mode 100644
index 0000000..508b0b0
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpsConnectorFactoryTest.java
@@ -0,0 +1,87 @@
+package io.dropwizard.jetty;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.util.Collection;
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+import org.apache.commons.lang.SystemUtils;
+import org.junit.Test;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class HttpsConnectorFactoryTest {
+    private static final String WINDOWS_MY_KEYSTORE_NAME = "Windows-MY";
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(HttpsConnectorFactory.class);
+    }
+
+    @Test
+    public void nonWindowsKeyStoreValidation() throws Exception {
+        HttpsConnectorFactory factory = new HttpsConnectorFactory();
+        Collection<String> properties = getViolationProperties(validator.validate(factory));
+        assertThat(properties.contains("validKeyStorePassword")).isEqualTo(true);
+        assertThat(properties.contains("validKeyStorePath")).isEqualTo(true);
+    }
+
+    @Test
+    public void windowsKeyStoreValidation() throws Exception {
+        HttpsConnectorFactory factory = new HttpsConnectorFactory();
+        factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME);
+        Collection<String> properties = getViolationProperties(validator.validate(factory));
+        assertThat(properties.contains("validKeyStorePassword")).isEqualTo(false);
+        assertThat(properties.contains("validKeyStorePath")).isEqualTo(false);
+    }
+
+    @Test
+    public void windowsKeyStore() throws Exception {
+        HttpsConnectorFactory factory = new HttpsConnectorFactory();
+        factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME);
+        if (canAccessWindowsKeyStore()) {
+            factory.buildSslContextFactory();
+            return;
+        } else {
+            try {
+                factory.buildSslContextFactory();
+                fail("Windows key store should not be supported here");
+            } catch (IllegalStateException ex) {
+                assertThat(ex.getMessage()).containsIgnoringCase("not supported");
+            }
+        }
+    }
+
+    private boolean canAccessWindowsKeyStore() {
+        if (SystemUtils.IS_OS_WINDOWS) {
+            try {
+                KeyStore.getInstance(WINDOWS_MY_KEYSTORE_NAME);
+                return true;
+            } catch (KeyStoreException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private <T> Collection<String> getViolationProperties(Set<ConstraintViolation<T>> violations) {
+        return Collections2.transform(violations, new Function<ConstraintViolation<T>, String>() {
+            @Override
+            public String apply(ConstraintViolation<T> input) {
+                return input.getPropertyPath().toString();
+            }
+        });
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/MutableServletContextHandlerTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/MutableServletContextHandlerTest.java
new file mode 100644
index 0000000..3707e46
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/MutableServletContextHandlerTest.java
@@ -0,0 +1,67 @@
+package io.dropwizard.jetty;
+
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class MutableServletContextHandlerTest {
+    private final MutableServletContextHandler handler = new MutableServletContextHandler();
+
+    @Test
+    public void defaultsToSessionsBeingDisabled() throws Exception {
+        assertThat(handler.isSessionsEnabled())
+                .isFalse();
+    }
+
+    @Test
+    public void defaultsToSecurityBeingDisabled() throws Exception {
+        assertThat(handler.isSecurityEnabled())
+                .isFalse();
+    }
+
+    @Test
+    public void canEnableSessionManagement() throws Exception {
+        handler.setSessionsEnabled(true);
+
+        assertThat(handler.isSessionsEnabled())
+                .isTrue();
+
+        assertThat(handler.isSecurityEnabled())
+                .isFalse();
+    }
+
+    @Test
+    public void canDisableSessionManagement() throws Exception {
+        handler.setSessionsEnabled(true);
+        handler.setSessionsEnabled(false);
+
+        assertThat(handler.isSessionsEnabled())
+                .isFalse();
+
+        assertThat(handler.isSecurityEnabled())
+                .isFalse();
+    }
+
+    @Test
+    public void canEnableSecurity() throws Exception {
+        handler.setSecurityEnabled(true);
+
+        assertThat(handler.isSessionsEnabled())
+                .isFalse();
+
+        assertThat(handler.isSecurityEnabled())
+                .isTrue();
+    }
+
+    @Test
+    public void canDisableSecurity() throws Exception {
+        handler.setSecurityEnabled(true);
+        handler.setSecurityEnabled(false);
+
+        assertThat(handler.isSessionsEnabled())
+                .isFalse();
+
+        assertThat(handler.isSecurityEnabled())
+                .isFalse();
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/NonblockingServletHolderTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/NonblockingServletHolderTest.java
new file mode 100644
index 0000000..069b942
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/NonblockingServletHolderTest.java
@@ -0,0 +1,51 @@
+package io.dropwizard.jetty;
+
+import org.eclipse.jetty.server.Request;
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import javax.servlet.Servlet;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class NonblockingServletHolderTest {
+    private final Servlet servlet = mock(Servlet.class);
+    private final NonblockingServletHolder holder = new NonblockingServletHolder(servlet);
+    private final Request baseRequest = mock(Request.class);
+    private final ServletRequest request = mock(ServletRequest.class);
+    private final ServletResponse response = mock(ServletResponse.class);
+
+    @Test
+    public void hasAServlet() throws Exception {
+        assertThat(holder.getServlet())
+                .isEqualTo(servlet);
+    }
+
+    @Test
+    public void servicesRequests() throws Exception {
+        holder.handle(baseRequest, request, response);
+
+        verify(servlet).service(request, response);
+    }
+
+    @Test
+    public void temporarilyDisablesAsyncRequestsIfDisabled() throws Exception {
+        holder.setAsyncSupported(false);
+
+        holder.handle(baseRequest, request, response);
+
+        final InOrder inOrder = inOrder(baseRequest, servlet);
+
+        inOrder.verify(baseRequest).setAsyncSupported(false);
+        inOrder.verify(servlet).service(request, response);
+    }
+
+    @Test
+    public void isEagerlyInitialized() throws Exception {
+        assertThat(holder.getInitOrder())
+                .isEqualTo(1);
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RequestLogFactoryTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RequestLogFactoryTest.java
new file mode 100644
index 0000000..960d984
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RequestLogFactoryTest.java
@@ -0,0 +1,40 @@
+package io.dropwizard.jetty;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.Resources;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.logging.ConsoleAppenderFactory;
+import io.dropwizard.logging.FileAppenderFactory;
+import io.dropwizard.logging.SyslogAppenderFactory;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import java.io.File;
+import java.util.TimeZone;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class RequestLogFactoryTest {
+    private RequestLogFactory requestLog;
+
+    @Before
+    public void setUp() throws Exception {
+        final ObjectMapper objectMapper = Jackson.newObjectMapper();
+        objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class,
+                                                           FileAppenderFactory.class,
+                                                           SyslogAppenderFactory.class);
+        this.requestLog = new ConfigurationFactory<>(RequestLogFactory.class,
+                                                     Validation.buildDefaultValidatorFactory()
+                                                                       .getValidator(),
+                                                     objectMapper, "dw")
+                .build(new File(Resources.getResource("yaml/requestLog.yml").toURI()));
+    }
+
+    @Test
+    public void defaultTimeZoneIsUTC() {
+        assertThat(requestLog.getTimeZone())
+            .isEqualTo(TimeZone.getTimeZone("UTC"));
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RoutingHandlerTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RoutingHandlerTest.java
new file mode 100644
index 0000000..cce6444
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RoutingHandlerTest.java
@@ -0,0 +1,62 @@
+package io.dropwizard.jetty;
+
+import com.google.common.collect.ImmutableMap;
+import org.eclipse.jetty.server.Connector;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.HttpChannel;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class RoutingHandlerTest {
+    private final Connector connector1 = mock(Connector.class);
+    private final Connector connector2 = mock(Connector.class);
+    private final Handler handler1 = spy(new ContextHandler());
+    private final Handler handler2 = spy(new ContextHandler());
+
+    private final RoutingHandler handler = new RoutingHandler(ImmutableMap.of(connector1,
+                                                                              handler1,
+                                                                              connector2,
+                                                                              handler2));
+
+    @Test
+    public void startsAndStopsAllHandlers() throws Exception {
+        handler.start();
+        try {
+            assertThat(handler1.isStarted())
+                    .isTrue();
+            assertThat(handler2.isStarted())
+                    .isTrue();
+        } finally {
+            handler.stop();
+        }
+
+        assertThat(handler1.isStopped())
+                .isTrue();
+        assertThat(handler2.isStopped())
+                .isTrue();
+    }
+
+    @Test
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public void routesRequestsToTheConnectorSpecificHandler() throws Exception {
+        final HttpChannel channel = mock(HttpChannel.class);
+        when(channel.getConnector()).thenReturn(connector1);
+
+        final Request baseRequest = mock(Request.class);
+        when(baseRequest.getHttpChannel()).thenReturn(channel);
+
+        final HttpServletRequest request = mock(HttpServletRequest.class);
+        final HttpServletResponse response = mock(HttpServletResponse.class);
+
+        handler.handle("target", baseRequest, request, response);
+
+        verify(handler1).handle("target", baseRequest, request, response);
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/Slf4jRequestLogTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/Slf4jRequestLogTest.java
new file mode 100644
index 0000000..bd333b6
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/Slf4jRequestLogTest.java
@@ -0,0 +1,78 @@
+package io.dropwizard.jetty;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.spi.AppenderAttachableImpl;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.server.HttpChannelState;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class Slf4jRequestLogTest {
+    @SuppressWarnings("unchecked")
+    private final Appender<ILoggingEvent> appender = mock(Appender.class);
+    private final AppenderAttachableImpl<ILoggingEvent> appenders = new AppenderAttachableImpl<>();
+    private final Slf4jRequestLog slf4jRequestLog = new Slf4jRequestLog(appenders, TimeZone.getTimeZone("UTC"));
+
+    private final Request request = mock(Request.class);
+    private final Response response = mock(Response.class);
+    private final HttpChannelState channelState = mock(HttpChannelState.class);
+
+    @Before
+    public void setUp() throws Exception {
+        when(channelState.isInitial()).thenReturn(true);
+
+        when(request.getRemoteAddr()).thenReturn("10.0.0.1");
+        when(request.getTimeStamp()).thenReturn(TimeUnit.SECONDS.toMillis(1353042047));
+        when(request.getMethod()).thenReturn("GET");
+        when(request.getUri()).thenReturn(new HttpURI("/test/things?yay"));
+        when(request.getProtocol()).thenReturn("HTTP/1.1");
+        when(request.getHttpChannelState()).thenReturn(channelState);
+        when(request.getTimeStamp()).thenReturn(TimeUnit.SECONDS.toMillis(1353042048));
+
+        when(response.getStatus()).thenReturn(200);
+        when(response.getContentCount()).thenReturn(8290L);
+
+        appenders.addAppender(appender);
+
+        slf4jRequestLog.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        slf4jRequestLog.stop();
+    }
+
+    @Test
+    public void logsRequestsToTheAppenders() throws Exception {
+        final ILoggingEvent event = logAndCapture();
+
+        // It would be lovely if the clock could be injected so we could test this reliably, but
+        // I suppose we should just trust the Jetty folks.
+        assertThat(event.getFormattedMessage())
+                .startsWith("10.0.0.1");
+
+        assertThat(event.getLevel())
+                .isEqualTo(Level.INFO);
+    }
+
+    private ILoggingEvent logAndCapture() {
+        slf4jRequestLog.log(request, response);
+
+        final ArgumentCaptor<ILoggingEvent> captor = ArgumentCaptor.forClass(ILoggingEvent.class);
+        verify(appender, timeout(1000)).doAppend(captor.capture());
+
+        return captor.getValue();
+    }
+}
diff --git a/dropwizard-jetty/src/test/java/io/dropwizard/jetty/setup/ServletEnvironmentTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/setup/ServletEnvironmentTest.java
new file mode 100644
index 0000000..42e2fdb
--- /dev/null
+++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/setup/ServletEnvironmentTest.java
@@ -0,0 +1,159 @@
+package io.dropwizard.jetty.setup;
+
+import io.dropwizard.jetty.MutableServletContextHandler;
+import org.eclipse.jetty.continuation.ContinuationFilter;
+import org.eclipse.jetty.http.MimeTypes;
+import org.eclipse.jetty.security.SecurityHandler;
+import org.eclipse.jetty.server.session.SessionHandler;
+import org.eclipse.jetty.servlet.FilterHolder;
+import org.eclipse.jetty.servlet.ServletHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import javax.servlet.*;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+public class ServletEnvironmentTest {
+    private final ServletHandler servletHandler = mock(ServletHandler.class);
+    private final MutableServletContextHandler handler = mock(MutableServletContextHandler.class);
+    private final ServletEnvironment environment = new ServletEnvironment(handler);
+
+    @Before
+    public void setUp() throws Exception {
+        when(handler.getServletHandler()).thenReturn(servletHandler);
+    }
+
+    @Test
+    public void addsServletInstances() throws Exception {
+        final Servlet servlet = mock(Servlet.class);
+
+        final ServletRegistration.Dynamic builder = environment.addServlet("servlet", servlet);
+        assertThat(builder)
+                .isNotNull();
+
+        final ArgumentCaptor<ServletHolder> holder = ArgumentCaptor.forClass(ServletHolder.class);
+        verify(servletHandler).addServlet(holder.capture());
+
+        assertThat(holder.getValue().getName())
+                .isEqualTo("servlet");
+
+        assertThat(holder.getValue().getServlet())
+                .isEqualTo(servlet);
+    }
+
+    @Test
+    public void addsServletClasses() throws Exception {
+        final ServletRegistration.Dynamic builder = environment.addServlet("servlet", GenericServlet.class);
+        assertThat(builder)
+                .isNotNull();
+
+        final ArgumentCaptor<ServletHolder> holder = ArgumentCaptor.forClass(ServletHolder.class);
+        verify(servletHandler).addServlet(holder.capture());
+
+        assertThat(holder.getValue().getName())
+                .isEqualTo("servlet");
+
+        // this is ugly, but comparing classes sucks with these type bounds
+        assertThat(holder.getValue().getHeldClass().equals(GenericServlet.class))
+                .isTrue();
+    }
+
+    @Test
+    public void addsFilterInstances() throws Exception {
+        final Filter filter = mock(Filter.class);
+
+        final FilterRegistration.Dynamic builder = environment.addFilter("filter", filter);
+        assertThat(builder)
+                .isNotNull();
+
+        final ArgumentCaptor<FilterHolder> holder = ArgumentCaptor.forClass(FilterHolder.class);
+        verify(servletHandler).addFilter(holder.capture());
+
+        assertThat(holder.getValue().getName())
+                .isEqualTo("filter");
+
+        assertThat(holder.getValue().getFilter())
+                .isEqualTo(filter);
+    }
+
+    @Test
+    public void addsFilterClasses() throws Exception {
+        final FilterRegistration.Dynamic builder = environment.addFilter("filter", ContinuationFilter.class);
+        assertThat(builder)
+                .isNotNull();
+
+        final ArgumentCaptor<FilterHolder> holder = ArgumentCaptor.forClass(FilterHolder.class);
+        verify(servletHandler).addFilter(holder.capture());
+
+        assertThat(holder.getValue().getName())
+                .isEqualTo("filter");
+
+        // this is ugly, but comparing classes sucks with these type bounds
+        assertThat(holder.getValue().getHeldClass().equals(ContinuationFilter.class))
+                .isTrue();
+    }
+
+    @Test
+    public void addsServletListeners() throws Exception {
+        final ServletContextListener listener = mock(ServletContextListener.class);
+        environment.addServletListeners(listener);
+
+        verify(handler).addEventListener(listener);
+    }
+
+    @Test
+    public void addsProtectedTargets() throws Exception {
+        environment.setProtectedTargets("/woo");
+
+        verify(handler).setProtectedTargets(new String[]{"/woo"});
+    }
+
+    @Test
+    public void setsResourceBase() throws Exception {
+        environment.setResourceBase("/woo");
+
+        verify(handler).setResourceBase("/woo");
+    }
+
+    @Test
+    public void setsInitParams() throws Exception {
+        environment.setInitParameter("a", "b");
+
+        verify(handler).setInitParameter("a", "b");
+    }
+
+    @Test
+    public void setsSessionHandlers() throws Exception {
+        final SessionHandler sessionHandler = mock(SessionHandler.class);
+
+        environment.setSessionHandler(sessionHandler);
+
+        verify(handler).setSessionHandler(sessionHandler);
+        verify(handler).setSessionsEnabled(true);
+    }
+
+
+    @Test
+    public void setsSecurityHandlers() throws Exception {
+        final SecurityHandler securityHandler = mock(SecurityHandler.class);
+
+        environment.setSecurityHandler(securityHandler);
+
+        verify(handler).setSecurityHandler(securityHandler);
+        verify(handler).setSecurityEnabled(true);
+    }
+
+    @Test
+    public void addsMimeMapping() {
+        final MimeTypes mimeTypes = mock(MimeTypes.class);
+        when(handler.getMimeTypes()).thenReturn(mimeTypes);
+
+        environment.addMimeMapping("example/foo", "foo");
+
+        verify(mimeTypes).addMimeMapping("example/foo", "foo");
+    }
+}
diff --git a/dropwizard-jetty/src/test/resources/logback-test.xml b/dropwizard-jetty/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-jetty/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-jetty/src/test/resources/yaml/gzip.yml b/dropwizard-jetty/src/test/resources/yaml/gzip.yml
new file mode 100644
index 0000000..4ec438a
--- /dev/null
+++ b/dropwizard-jetty/src/test/resources/yaml/gzip.yml
@@ -0,0 +1,5 @@
+enabled: false
+minimumEntitySize: 12KB
+bufferSize: 32KB
+excludedUserAgents: ["IE"]
+compressedMimeTypes: ["text/plain"]
diff --git a/dropwizard-jetty/src/test/resources/yaml/requestLog.yml b/dropwizard-jetty/src/test/resources/yaml/requestLog.yml
new file mode 100644
index 0000000..e476e25
--- /dev/null
+++ b/dropwizard-jetty/src/test/resources/yaml/requestLog.yml
@@ -0,0 +1,5 @@
+appenders:
+  - type: file
+    currentLogFilename: "/var/log/dingo/dingo.log"
+    archivedLogFilenamePattern: "/var/log/dingo/dingo-%d.log.zip"
+    archivedFileCount: 5
diff --git a/dropwizard-lifecycle/pom.xml b/dropwizard-lifecycle/pom.xml
new file mode 100644
index 0000000..f581901
--- /dev/null
+++ b/dropwizard-lifecycle/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-lifecycle</artifactId>
+    <name>Dropwizard Lifecycle Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-util</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-logging</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ExecutorServiceManager.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ExecutorServiceManager.java
new file mode 100644
index 0000000..46b1f23
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ExecutorServiceManager.java
@@ -0,0 +1,34 @@
+package io.dropwizard.lifecycle;
+
+import io.dropwizard.util.Duration;
+
+import java.util.concurrent.ExecutorService;
+
+public class ExecutorServiceManager implements Managed {
+    private final ExecutorService executor;
+    private final Duration shutdownPeriod;
+    private final String poolName;
+
+    public ExecutorServiceManager(ExecutorService executor, Duration shutdownPeriod, String poolName) {
+        this.executor = executor;
+        this.shutdownPeriod = shutdownPeriod;
+        this.poolName = poolName;
+    }
+
+    @Override
+    public void start() throws Exception {
+        // OK BOSS
+    }
+
+    @Override
+    public void stop() throws Exception {
+        executor.shutdown();
+        executor.awaitTermination(shutdownPeriod.getQuantity(), shutdownPeriod.getUnit());
+    }
+
+    @Override
+    public String toString() {
+        return super.toString() + '(' + poolName + ')';
+    }
+
+}
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/JettyManaged.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/JettyManaged.java
new file mode 100644
index 0000000..920800b
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/JettyManaged.java
@@ -0,0 +1,39 @@
+package io.dropwizard.lifecycle;
+
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+
+/**
+ * A wrapper for {@link Managed} instances which ties them to a Jetty {@link
+ * org.eclipse.jetty.util.component.LifeCycle}.
+ */
+public class JettyManaged extends AbstractLifeCycle implements Managed {
+    private final Managed managed;
+
+    /**
+     * Creates a new JettyManaged wrapping {@code managed}.
+     *
+     * @param managed a {@link Managed} instance to be wrapped
+     */
+    public JettyManaged(Managed managed) {
+        this.managed = managed;
+    }
+
+    public Managed getManaged() {
+        return managed;
+    }
+
+    @Override
+    protected void doStart() throws Exception {
+        managed.start();
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        managed.stop();
+    }
+
+    @Override
+    public String toString() {
+        return managed.toString();
+    }
+}
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/Managed.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/Managed.java
new file mode 100644
index 0000000..9faf25a
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/Managed.java
@@ -0,0 +1,21 @@
+package io.dropwizard.lifecycle;
+
+/**
+ * An interface for objects which need to be started and stopped as the application is started or
+ * stopped.
+ */
+public interface Managed {
+    /**
+     * Starts the object. Called <i>before</i> the application becomes available.
+     *
+     * @throws Exception if something goes wrong; this will halt the application startup.
+     */
+    public void start() throws Exception;
+
+    /**
+     * Stops the object. Called <i>after</i> the application is no longer accepting requests.
+     *
+     * @throws Exception if something goes wrong.
+     */
+    public void stop() throws Exception;
+}
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ServerLifecycleListener.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ServerLifecycleListener.java
new file mode 100644
index 0000000..e0ea0d8
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ServerLifecycleListener.java
@@ -0,0 +1,9 @@
+package io.dropwizard.lifecycle;
+
+import org.eclipse.jetty.server.Server;
+
+import java.util.EventListener;
+
+public interface ServerLifecycleListener extends EventListener {
+    void serverStarted(Server server);
+}
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilder.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilder.java
new file mode 100644
index 0000000..fe5fbf1
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilder.java
@@ -0,0 +1,78 @@
+package io.dropwizard.lifecycle.setup;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.dropwizard.lifecycle.ExecutorServiceManager;
+import io.dropwizard.util.Duration;
+
+import java.util.concurrent.*;
+
+public class ExecutorServiceBuilder {
+    private final LifecycleEnvironment environment;
+    private final String nameFormat;
+    private int corePoolSize;
+    private int maximumPoolSize;
+    private Duration keepAliveTime;
+    private Duration shutdownTime;
+    private BlockingQueue<Runnable> workQueue;
+    private ThreadFactory threadFactory;
+    private RejectedExecutionHandler handler;
+
+    public ExecutorServiceBuilder(LifecycleEnvironment environment, String nameFormat) {
+        this.environment = environment;
+        this.nameFormat = nameFormat;
+        this.corePoolSize = 0;
+        this.maximumPoolSize = Integer.MAX_VALUE;
+        this.keepAliveTime = Duration.seconds(60);
+        this.shutdownTime = Duration.seconds(5);
+        this.workQueue = new LinkedBlockingQueue<>();
+        this.threadFactory = new ThreadFactoryBuilder().setNameFormat(nameFormat).build();
+        this.handler = new ThreadPoolExecutor.AbortPolicy();
+    }
+
+    public ExecutorServiceBuilder minThreads(int threads) {
+        this.corePoolSize = threads;
+        return this;
+    }
+
+    public ExecutorServiceBuilder maxThreads(int threads) {
+        this.maximumPoolSize = threads;
+        return this;
+    }
+
+    public ExecutorServiceBuilder keepAliveTime(Duration time) {
+        this.keepAliveTime = time;
+        return this;
+    }
+
+    public ExecutorServiceBuilder shutdownTime(Duration time) {
+        this.shutdownTime = time;
+        return this;
+    }
+
+    public ExecutorServiceBuilder workQueue(BlockingQueue<Runnable> workQueue) {
+        this.workQueue = workQueue;
+        return this;
+    }
+
+    public ExecutorServiceBuilder rejectedExecutionHandler(RejectedExecutionHandler handler) {
+        this.handler = handler;
+        return this;
+    }
+
+    public ExecutorServiceBuilder threadFactory(ThreadFactory threadFactory) {
+        this.threadFactory = threadFactory;
+        return this;
+    }
+
+    public ExecutorService build() {
+        final ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,
+                                                                   maximumPoolSize,
+                                                                   keepAliveTime.getQuantity(),
+                                                                   keepAliveTime.getUnit(),
+                                                                   workQueue,
+                                                                   threadFactory,
+                                                                   handler);
+        environment.manage(new ExecutorServiceManager(executor, shutdownTime, nameFormat));
+        return executor;
+    }
+}
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/LifecycleEnvironment.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/LifecycleEnvironment.java
new file mode 100644
index 0000000..e126a93
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/LifecycleEnvironment.java
@@ -0,0 +1,94 @@
+package io.dropwizard.lifecycle.setup;
+
+import com.google.common.collect.Lists;
+import io.dropwizard.lifecycle.JettyManaged;
+import io.dropwizard.lifecycle.Managed;
+import io.dropwizard.lifecycle.ServerLifecycleListener;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class LifecycleEnvironment {
+    private static final Logger LOGGER = LoggerFactory.getLogger(LifecycleEnvironment.class);
+
+    private final List<LifeCycle> managedObjects;
+    private final List<LifeCycle.Listener> lifecycleListeners;
+
+    public LifecycleEnvironment() {
+        this.managedObjects = Lists.newArrayList();
+        this.lifecycleListeners = Lists.newArrayList();
+    }
+
+    /**
+     * Adds the given {@link Managed} instance to the set of objects managed by the server's
+     * lifecycle. When the server starts, {@code managed} will be started. When the server stops,
+     * {@code managed} will be stopped.
+     *
+     * @param managed a managed object
+     */
+    public void manage(Managed managed) {
+        managedObjects.add(new JettyManaged(checkNotNull(managed)));
+    }
+
+    /**
+     * Adds the given Jetty {@link LifeCycle} instances to the server's lifecycle.
+     *
+     * @param managed a Jetty-managed object
+     */
+    public void manage(LifeCycle managed) {
+        managedObjects.add(checkNotNull(managed));
+    }
+
+    public ExecutorServiceBuilder executorService(String nameFormat) {
+        return new ExecutorServiceBuilder(this, nameFormat);
+    }
+
+    public ScheduledExecutorServiceBuilder scheduledExecutorService(String nameFormat) {
+        return new ScheduledExecutorServiceBuilder(this, nameFormat);
+    }
+
+    public void addServerLifecycleListener(ServerLifecycleListener listener) {
+        lifecycleListeners.add(new ServerListener(listener));
+    }
+
+    public void addLifeCycleListener(LifeCycle.Listener listener) {
+        lifecycleListeners.add(listener);
+    }
+
+    public void attach(ContainerLifeCycle container) {
+        for (LifeCycle object : managedObjects) {
+            container.addBean(object);
+        }
+        container.addLifeCycleListener(new AbstractLifeCycle.AbstractLifeCycleListener() {
+            @Override
+            public void lifeCycleStarting(LifeCycle event) {
+                LOGGER.debug("managed objects = {}", managedObjects);
+            }
+        });
+        for (LifeCycle.Listener listener : lifecycleListeners) {
+            container.addLifeCycleListener(listener);
+        }
+    }
+
+    private static class ServerListener extends AbstractLifeCycle.AbstractLifeCycleListener {
+        private final ServerLifecycleListener listener;
+
+        private ServerListener(ServerLifecycleListener listener) {
+            this.listener = listener;
+        }
+
+        @Override
+        public void lifeCycleStarted(LifeCycle event) {
+            if (event instanceof Server) {
+                listener.serverStarted((Server) event);
+            }
+        }
+    }
+}
diff --git a/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ScheduledExecutorServiceBuilder.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ScheduledExecutorServiceBuilder.java
new file mode 100644
index 0000000..5dd8a19
--- /dev/null
+++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ScheduledExecutorServiceBuilder.java
@@ -0,0 +1,51 @@
+package io.dropwizard.lifecycle.setup;
+
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import io.dropwizard.lifecycle.ExecutorServiceManager;
+import io.dropwizard.util.Duration;
+
+import java.util.concurrent.*;
+
+public class ScheduledExecutorServiceBuilder {
+    private final LifecycleEnvironment environment;
+    private final String nameFormat;
+    private int poolSize;
+    private ThreadFactory threadFactory;
+    private Duration shutdownTime;
+    private RejectedExecutionHandler handler;
+
+    public ScheduledExecutorServiceBuilder(LifecycleEnvironment environment, String nameFormat) {
+        this.environment = environment;
+        this.nameFormat = nameFormat;
+        this.poolSize = 1;
+        this.threadFactory = new ThreadFactoryBuilder().setNameFormat(nameFormat).build();
+        this.shutdownTime = Duration.seconds(5);
+        this.handler = new ThreadPoolExecutor.AbortPolicy();
+    }
+
+    public ScheduledExecutorServiceBuilder threads(int threads) {
+        this.poolSize = threads;
+        return this;
+    }
+
+    public ScheduledExecutorServiceBuilder shutdownTime(Duration time) {
+        this.shutdownTime = time;
+        return this;
+    }
+
+    public ScheduledExecutorServiceBuilder rejectedExecutionHandler(RejectedExecutionHandler handler) {
+        this.handler = handler;
+        return this;
+    }
+
+    public ScheduledExecutorServiceBuilder threadFactory(ThreadFactory threadFactory) {
+        this.threadFactory = threadFactory;
+        return this;
+    }
+
+    public ScheduledExecutorService build() {
+        final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(poolSize, threadFactory, handler);
+        environment.manage(new ExecutorServiceManager(executor, shutdownTime, nameFormat));
+        return executor;
+    }
+}
diff --git a/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/JettyManagedTest.java b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/JettyManagedTest.java
new file mode 100644
index 0000000..1ffee9a
--- /dev/null
+++ b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/JettyManagedTest.java
@@ -0,0 +1,22 @@
+package io.dropwizard.lifecycle;
+
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.mock;
+
+public class JettyManagedTest {
+    private final Managed managed = mock(Managed.class);
+    private final JettyManaged jettyManaged = new JettyManaged(managed);
+
+    @Test
+    public void startsAndStops() throws Exception {
+        jettyManaged.start();
+        jettyManaged.stop();
+
+        final InOrder inOrder = inOrder(managed);
+        inOrder.verify(managed).start();
+        inOrder.verify(managed).stop();
+    }
+}
diff --git a/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/LifecycleEnvironmentTest.java b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/LifecycleEnvironmentTest.java
new file mode 100644
index 0000000..e20766d
--- /dev/null
+++ b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/LifecycleEnvironmentTest.java
@@ -0,0 +1,45 @@
+package io.dropwizard.lifecycle.setup;
+
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.lifecycle.JettyManaged;
+import io.dropwizard.lifecycle.Managed;
+import org.eclipse.jetty.util.component.ContainerLifeCycle;
+import org.eclipse.jetty.util.component.LifeCycle;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class LifecycleEnvironmentTest {
+    private final LifecycleEnvironment environment = new LifecycleEnvironment();
+
+    @Test
+    public void managesLifeCycleObjects() throws Exception {
+        final LifeCycle lifeCycle = mock(LifeCycle.class);
+        environment.manage(lifeCycle);
+
+        final ContainerLifeCycle container = new ContainerLifeCycle();
+        environment.attach(container);
+
+        assertThat(container.getBeans())
+                .contains(lifeCycle);
+    }
+
+    @Test
+    public void managesManagedObjects() throws Exception {
+        final Managed managed = mock(Managed.class);
+        environment.manage(managed);
+
+        final ContainerLifeCycle container = new ContainerLifeCycle();
+        environment.attach(container);
+
+        final Object bean = ImmutableList.copyOf(container.getBeans()).get(0);
+        assertThat(bean)
+                .isInstanceOf(JettyManaged.class);
+
+        final JettyManaged jettyManaged = (JettyManaged) bean;
+
+        assertThat(jettyManaged.getManaged())
+                .isEqualTo(managed);
+    }
+}
diff --git a/dropwizard-lifecycle/src/test/resources/logback-test.xml b/dropwizard-lifecycle/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-lifecycle/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-logging/pom.xml b/dropwizard-logging/pom.xml
new file mode 100644
index 0000000..e4edab1
--- /dev/null
+++ b/dropwizard-logging/pom.xml
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-logging</artifactId>
+    <name>Dropwizard Logging Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jackson</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-validation</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-logback</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>ch.qos.logback</groupId>
+                    <artifactId>logback-classic</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jul-to-slf4j</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>jcl-over-slf4j</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-util</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-configuration</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/AbstractAppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/AbstractAppenderFactory.java
new file mode 100644
index 0000000..b5ce3d5
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/AbstractAppenderFactory.java
@@ -0,0 +1,148 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.filter.ThresholdFilter;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.spi.FilterAttachable;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Strings;
+import io.dropwizard.util.Duration;
+import io.dropwizard.validation.MinDuration;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A base implementation of {@link AppenderFactory}.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code threshold}</td>
+ *         <td>ALL</td>
+ *         <td>The minimum event level the appender will handle.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code logFormat}</td>
+ *         <td>(none)</td>
+ *         <td>An appender-specific log format.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code bounded}</td>
+ *         <td>true</td>
+ *         <td>Whether or not the appender should block when its queue is full.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code batchSize}</td>
+ *         <td>128</td>
+ *         <td>
+ *             The maximum number of events to write in a single batch.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code batchDuration}</td>
+ *         <td>100ms</td>
+ *         <td>
+ *             The maximum amount of time to wait for a full batch before writing a partial batch.
+ *         </td>
+ *     </tr>
+ * </table>
+ */
+public abstract class AbstractAppenderFactory implements AppenderFactory {
+    private boolean bounded;
+
+    @NotNull
+    protected Level threshold = Level.ALL;
+
+    protected String logFormat;
+
+    @Min(1)
+    @Max(Integer.MAX_VALUE)
+    private int batchSize = 128;
+
+    @NotNull
+    @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS)
+    private Duration batchDuration = Duration.milliseconds(100);
+
+    @JsonProperty
+    public boolean isBounded() {
+        return bounded;
+    }
+
+    @JsonProperty
+    public void setBounded(boolean bounded) {
+        this.bounded = bounded;
+    }
+
+    @JsonProperty
+    public int getBatchSize() {
+        return batchSize;
+    }
+
+    @JsonProperty
+    public void setBatchSize(int batchSize) {
+        this.batchSize = batchSize;
+    }
+
+    @JsonProperty
+    public Duration getBatchDuration() {
+        return batchDuration;
+    }
+
+    @JsonProperty
+    public void setBatchDuration(Duration batchDuration) {
+        this.batchDuration = batchDuration;
+    }
+
+    @JsonProperty
+    public Level getThreshold() {
+        return threshold;
+    }
+
+    @JsonProperty
+    public void setThreshold(Level threshold) {
+        this.threshold = threshold;
+    }
+
+    @JsonProperty
+    public String getLogFormat() {
+        return logFormat;
+    }
+
+    @JsonProperty
+    public void setLogFormat(String logFormat) {
+        this.logFormat = logFormat;
+    }
+
+    protected Appender<ILoggingEvent> wrapAsync(Appender<ILoggingEvent> appender) {
+        final AsyncAppender asyncAppender = new AsyncAppender(appender, batchSize, batchDuration, bounded);
+        asyncAppender.start();
+        return asyncAppender;
+    }
+
+    protected void addThresholdFilter(FilterAttachable<ILoggingEvent> appender, Level threshold) {
+        final ThresholdFilter filter = new ThresholdFilter();
+        filter.setLevel(threshold.toString());
+        filter.start();
+        appender.addFilter(filter);
+    }
+
+    protected DropwizardLayout buildLayout(LoggerContext context, TimeZone timeZone) {
+        final DropwizardLayout formatter = new DropwizardLayout(context, timeZone);
+        if (!Strings.isNullOrEmpty(logFormat)) {
+            formatter.setPattern(logFormat);
+        }
+        formatter.start();
+        return formatter;
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/AppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/AppenderFactory.java
new file mode 100644
index 0000000..dfb8b5f
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/AppenderFactory.java
@@ -0,0 +1,38 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.Layout;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.dropwizard.jackson.Discoverable;
+
+/**
+ * A service provider interface for creating Logback {@link Appender} instances.
+ * <p/>
+ * To create your own, just:
+ * <ol>
+ * <li>Create a class which implements {@link AppenderFactory}.</li>
+ * <li>Annotate it with {@code @JsonTypeName} and give it a unique type name.</li>
+ * <li>add a {@code META-INF/services/io.dropwizard.logging.AppenderFactory} file with your
+ * implementation's full class name to the class path.</li>
+ * </ol>
+ *
+ * @see ConsoleAppenderFactory
+ * @see FileAppenderFactory
+ * @see SyslogAppenderFactory
+ */
+ at JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+public interface AppenderFactory extends Discoverable {
+    /**
+     * Given a Logback context, an application name, and a layout, build a new appender.
+     *
+     * @param context         the Logback context
+     * @param applicationName the application name
+     * @param layout          the layout for logging
+     * @return a new, started {@link Appender}
+     */
+    Appender<ILoggingEvent> build(LoggerContext context,
+                                  String applicationName,
+                                  Layout<ILoggingEvent> layout);
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/AsyncAppender.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/AsyncAppender.java
new file mode 100644
index 0000000..5391813
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/AsyncAppender.java
@@ -0,0 +1,117 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.AppenderBase;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Queues;
+import io.dropwizard.util.Duration;
+import org.eclipse.jetty.util.ConcurrentArrayBlockingQueue;
+
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An asynchronous appender. Log entries are added to an in-memory queue and an offline thread
+ * handles the responsibility sending batches of events to the delegate. The worker thread will
+ * wait for either a specific number of events or a specific amount of time to have passed before
+ * processing events.
+ */
+public class AsyncAppender extends AppenderBase<ILoggingEvent> {
+    private static final AtomicInteger THREAD_COUNTER = new AtomicInteger();
+
+    private class Worker extends Thread {
+        private final int batchSize;
+        private final Duration batchDuration;
+        private volatile boolean running = true;
+        private final List<ILoggingEvent> events;
+
+        private Worker(int batchSize, Duration batchDuration) {
+            this.batchSize = batchSize;
+            this.batchDuration = batchDuration;
+            this.events = Lists.newArrayListWithCapacity(batchSize);
+        }
+
+        @Override
+        public void run() {
+            while (running) {
+                try {
+                    // drain until we have a full batch or the duration is up
+                    Queues.drain(queue,
+                                 events,
+                                 batchSize,
+                                 batchDuration.getQuantity(),
+                                 batchDuration.getUnit());
+                } catch (InterruptedException e) {
+                    Thread.currentThread().interrupt();
+                }
+
+                for (ILoggingEvent event : events) {
+                    delegate.doAppend(event);
+                }
+
+                events.clear();
+            }
+
+            // flush any remaining events to the delegate and stop it
+            for (ILoggingEvent event : queue) {
+                delegate.doAppend(event);
+            }
+            delegate.stop();
+        }
+
+        public void shutdown() {
+            this.running = false;
+            this.interrupt();
+        }
+    }
+
+    private final BlockingQueue<ILoggingEvent> queue;
+    private final Worker worker;
+    private final Appender<ILoggingEvent> delegate;
+
+    public AsyncAppender(Appender<ILoggingEvent> delegate,
+                         int batchSize,
+                         Duration batchDuration,
+                         boolean bounded) {
+        this.queue = buildQueue(batchSize, bounded);
+        this.worker = new Worker(batchSize, batchDuration);
+        this.delegate = delegate;
+        setName("async-" + delegate.getName());
+    }
+
+    public Appender<ILoggingEvent> getDelegate() {
+        return delegate;
+    }
+
+    private ConcurrentArrayBlockingQueue<ILoggingEvent> buildQueue(int batchSize, boolean bounded) {
+        if (bounded) {
+            return new ConcurrentArrayBlockingQueue.Bounded<>(batchSize * 2);
+        }
+        return new ConcurrentArrayBlockingQueue.Unbounded<>();
+    }
+
+    @Override
+    public void start() {
+        super.start();
+        worker.setName(getName() + "-" + THREAD_COUNTER.incrementAndGet());
+        worker.start();
+    }
+
+    @Override
+    public void stop() {
+        super.stop();
+        worker.shutdown();
+    }
+
+    @Override
+    protected void append(ILoggingEvent event) {
+        event.prepareForDeferredProcessing();
+        try {
+            queue.put(event);
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/ConsoleAppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/ConsoleAppenderFactory.java
new file mode 100644
index 0000000..920eec2
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/ConsoleAppenderFactory.java
@@ -0,0 +1,116 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.Layout;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import javax.validation.constraints.NotNull;
+import java.util.TimeZone;
+
+/**
+ * An {@link AppenderFactory} implementation which provides an appender that writes events to the console.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code type}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>The appender type. Must be {@code console}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code threshold}</td>
+ *         <td>{@code ALL}</td>
+ *         <td>The lowest level of events to print to the console.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code timeZone}</td>
+ *         <td>{@code UTC}</td>
+ *         <td>The time zone to which event timestamps will be converted.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code target}</td>
+ *         <td>{@code stdout}</td>
+ *         <td>
+ *             The name of the standard stream to which events will be written.
+ *             Can be {@code stdout} or {@code stderr}.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code logFormat}</td>
+ *         <td>the default format</td>
+ *         <td>
+ *             The Logback pattern with which events will be formatted. See
+ *             <a href="http://logback.qos.ch/manual/layouts.html#conversionWord">the Logback documentation</a>
+ *             for details.
+ *         </td>
+ *     </tr>
+ * </table>
+ *
+ * @see AbstractAppenderFactory
+ */
+ at JsonTypeName("console")
+public class ConsoleAppenderFactory extends AbstractAppenderFactory {
+    @SuppressWarnings("UnusedDeclaration")
+    public enum ConsoleStream {
+        STDOUT("System.out"),
+        STDERR("System.err");
+
+        private final String value;
+
+        ConsoleStream(String value) {
+            this.value = value;
+        }
+
+        public String get() {
+            return value;
+        }
+    }
+
+    @NotNull
+    private TimeZone timeZone = TimeZone.getTimeZone("UTC");
+
+    @NotNull
+    private ConsoleStream target = ConsoleStream.STDOUT;
+
+    @JsonProperty
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    @JsonProperty
+    public void setTimeZone(TimeZone timeZone) {
+        this.timeZone = timeZone;
+    }
+
+    @JsonProperty
+    public ConsoleStream getTarget() {
+        return target;
+    }
+
+    @JsonProperty
+    public void setTarget(ConsoleStream target) {
+        this.target = target;
+    }
+
+    @Override
+    public Appender<ILoggingEvent> build(LoggerContext context, String applicationName, Layout<ILoggingEvent> layout) {
+        final ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
+        appender.setName("console-appender");
+        appender.setContext(context);
+        appender.setTarget(target.get());
+        appender.setLayout(layout == null ? buildLayout(context, timeZone) : layout);
+        addThresholdFilter(appender, threshold);
+        appender.start();
+
+        return wrapAsync(appender);
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/DropwizardLayout.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/DropwizardLayout.java
new file mode 100644
index 0000000..24da69e
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/DropwizardLayout.java
@@ -0,0 +1,26 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.PatternLayout;
+
+import java.util.TimeZone;
+
+/**
+ * A base layout for Dropwizard.
+ * <ul>
+ *     <li>Disables pattern headers.</li>
+ *     <li>Prefixes logged exceptions with {@code !}.</li>
+ *     <li>Sets the pattern to the given timezone.</li>
+ * </ul>
+ */
+public class DropwizardLayout extends PatternLayout {
+    public DropwizardLayout(LoggerContext context, TimeZone timeZone) {
+        super();
+        setOutputPatternAsHeader(false);
+        getDefaultConverterMap().put("ex", PrefixedThrowableProxyConverter.class.getName());
+        getDefaultConverterMap().put("xEx", PrefixedExtendedThrowableProxyConverter.class.getName());
+        getDefaultConverterMap().put("rEx", PrefixedRootCauseFirstThrowableProxyConverter.class.getName());
+        setPattern("%-5p [%d{ISO8601," + timeZone.getID() + "}] %c: %m%n%rEx");
+        setContext(context);
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/FileAppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/FileAppenderFactory.java
new file mode 100644
index 0000000..3a6d668
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/FileAppenderFactory.java
@@ -0,0 +1,197 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.Layout;
+import ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy;
+import ch.qos.logback.core.rolling.RollingFileAppender;
+import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.validation.ValidationMethod;
+
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.util.TimeZone;
+
+/**
+ * An {@link AppenderFactory} implementation which provides an appender that writes events to a file, archiving older
+ * files as it goes.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code type}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>The appender type. Must be {@code file}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code threshold}</td>
+ *         <td>{@code ALL}</td>
+ *         <td>The lowest level of events to write to the file.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code currentLogFilename}</td>
+ *         <td><b>REQUIRED</b></td>
+ *         <td>The filename where current events are logged.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code archive}</td>
+ *         <td>{@code true}</td>
+ *         <td>Whether or not to archive old events in separate files.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code archivedLogFilenamePattern}</td>
+ *         <td><b>REQUIRED</b> if {@code archive} is {@code true}.</td>
+ *         <td>
+ *             The filename pattern for archived files. {@code %d} is replaced with the date in {@code yyyy-MM-dd} form,
+ *             and the fact that it ends with {@code .gz} indicates the file will be gzipped as it's archived. Likewise,
+ *             filename patterns which end in {@code .zip} will be filled as they are archived.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code archivedFileCount}</td>
+ *         <td>{@code 5}</td>
+ *         <td>
+ *             The number of archived files to keep. Must be greater than {@code 0}.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code timeZone}</td>
+ *         <td>{@code UTC}</td>
+ *         <td>The time zone to which event timestamps will be converted.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code logFormat}</td>
+ *         <td>the default format</td>
+ *         <td>
+ *             The Logback pattern with which events will be formatted. See
+ *             <a href="http://logback.qos.ch/manual/layouts.html#conversionWord">the Logback documentation</a>
+ *             for details.
+ *         </td>
+ *     </tr>
+ * </table>
+ *
+ * @see AbstractAppenderFactory
+ */
+ at JsonTypeName("file")
+public class FileAppenderFactory extends AbstractAppenderFactory {
+    @NotNull
+    private String currentLogFilename;
+
+    private boolean archive = true;
+
+    private String archivedLogFilenamePattern;
+
+    @Min(1)
+    private int archivedFileCount = 5;
+
+    @NotNull
+    private TimeZone timeZone = TimeZone.getTimeZone("UTC");
+
+    @JsonProperty
+    public String getCurrentLogFilename() {
+        return currentLogFilename;
+    }
+
+    @JsonProperty
+    public void setCurrentLogFilename(String currentLogFilename) {
+        this.currentLogFilename = currentLogFilename;
+    }
+
+    @JsonProperty
+    public boolean isArchive() {
+        return archive;
+    }
+
+    @JsonProperty
+    public void setArchive(boolean archive) {
+        this.archive = archive;
+    }
+
+    @JsonProperty
+    public String getArchivedLogFilenamePattern() {
+        return archivedLogFilenamePattern;
+    }
+
+    @JsonProperty
+    public void setArchivedLogFilenamePattern(String archivedLogFilenamePattern) {
+        this.archivedLogFilenamePattern = archivedLogFilenamePattern;
+    }
+
+    @JsonProperty
+    public int getArchivedFileCount() {
+        return archivedFileCount;
+    }
+
+    @JsonProperty
+    public void setArchivedFileCount(int archivedFileCount) {
+        this.archivedFileCount = archivedFileCount;
+    }
+
+    @JsonProperty
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    @JsonProperty
+    public void setTimeZone(TimeZone timeZone) {
+        this.timeZone = timeZone;
+    }
+
+    @JsonIgnore
+    @ValidationMethod(message = "must have archivedLogFilenamePattern if archive is true")
+    public boolean isValidArchiveConfiguration() {
+        return !archive || (archivedLogFilenamePattern != null);
+    }
+
+    @Override
+    public Appender<ILoggingEvent> build(LoggerContext context, String applicationName, Layout<ILoggingEvent> layout) {
+        final FileAppender<ILoggingEvent> appender = buildAppender(context);
+        appender.setName("file-appender");
+
+        appender.setAppend(true);
+        appender.setContext(context);
+        appender.setLayout(layout == null ? buildLayout(context, timeZone) : layout);
+        appender.setFile(currentLogFilename);
+        appender.setPrudent(false);
+        addThresholdFilter(appender, threshold);
+        appender.stop();
+        appender.start();
+
+        return wrapAsync(appender);
+    }
+
+    protected FileAppender<ILoggingEvent> buildAppender(LoggerContext context) {
+        if (archive) {
+            final RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
+            final DefaultTimeBasedFileNamingAndTriggeringPolicy<ILoggingEvent> triggeringPolicy =
+                    new DefaultTimeBasedFileNamingAndTriggeringPolicy<>();
+            triggeringPolicy.setContext(context);
+
+            final TimeBasedRollingPolicy<ILoggingEvent> rollingPolicy = new TimeBasedRollingPolicy<>();
+            rollingPolicy.setContext(context);
+            rollingPolicy.setFileNamePattern(archivedLogFilenamePattern);
+            rollingPolicy.setTimeBasedFileNamingAndTriggeringPolicy(
+                    triggeringPolicy);
+            triggeringPolicy.setTimeBasedRollingPolicy(rollingPolicy);
+            rollingPolicy.setMaxHistory(archivedFileCount);
+
+            appender.setRollingPolicy(rollingPolicy);
+            appender.setTriggeringPolicy(triggeringPolicy);
+
+            rollingPolicy.setParent(appender);
+            rollingPolicy.start();
+            return appender;
+        }
+        return new FileAppender<>();
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingFactory.java
new file mode 100644
index 0000000..68b6d12
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingFactory.java
@@ -0,0 +1,163 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.filter.ThresholdFilter;
+import ch.qos.logback.classic.jmx.JMXConfigurator;
+import ch.qos.logback.classic.jul.LevelChangePropagator;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.ConsoleAppender;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.logback.InstrumentedAppender;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.slf4j.ILoggerFactory;
+import org.slf4j.LoggerFactory;
+import org.slf4j.bridge.SLF4JBridgeHandler;
+
+import javax.management.*;
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.lang.management.ManagementFactory;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class LoggingFactory {
+    // initially configure for WARN+ console logging
+    public static void bootstrap() {
+        bootstrap(Level.WARN);
+    }
+
+    public static void bootstrap(Level level) {
+        hijackJDKLogging();
+
+        final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+        root.detachAndStopAllAppenders();
+
+        final DropwizardLayout formatter = new DropwizardLayout(root.getLoggerContext(),
+                                                        TimeZone.getDefault());
+        formatter.start();
+
+        final ThresholdFilter filter = new ThresholdFilter();
+        filter.setLevel(level.toString());
+        filter.start();
+
+        final ConsoleAppender<ILoggingEvent> appender = new ConsoleAppender<>();
+        appender.addFilter(filter);
+        appender.setContext(root.getLoggerContext());
+        appender.setLayout(formatter);
+        appender.start();
+
+        root.addAppender(appender);
+    }
+
+    private static void hijackJDKLogging() {
+        SLF4JBridgeHandler.removeHandlersForRootLogger();
+        SLF4JBridgeHandler.install();
+    }
+
+    @NotNull
+    private Level level = Level.INFO;
+
+    @NotNull
+    private ImmutableMap<String, Level> loggers = ImmutableMap.of();
+
+    @Valid
+    @NotNull
+    private ImmutableList<AppenderFactory> appenders = ImmutableList.<AppenderFactory>of(
+            new ConsoleAppenderFactory()
+    );
+
+    @JsonProperty
+    public Level getLevel() {
+        return level;
+    }
+
+    @JsonProperty
+    public void setLevel(Level level) {
+        this.level = level;
+    }
+
+    @JsonProperty
+    public ImmutableMap<String, Level> getLoggers() {
+        return loggers;
+    }
+
+    @JsonProperty
+    public void setLoggers(Map<String, Level> loggers) {
+        this.loggers = ImmutableMap.copyOf(loggers);
+    }
+
+    @JsonProperty
+    public ImmutableList<AppenderFactory> getAppenders() {
+        return appenders;
+    }
+
+    @JsonProperty
+    public void setAppenders(List<AppenderFactory> appenders) {
+        this.appenders = ImmutableList.copyOf(appenders);
+    }
+
+    public void configure(MetricRegistry metricRegistry, String name) {
+        hijackJDKLogging();
+
+        final Logger root = configureLevels();
+
+        for (AppenderFactory output : appenders) {
+            root.addAppender(output.build(root.getLoggerContext(), name, null));
+        }
+
+        final MBeanServer server = ManagementFactory.getPlatformMBeanServer();
+        try {
+            final ObjectName objectName = new ObjectName("io.dropwizard:type=Logging");
+            if (!server.isRegistered(objectName)) {
+                server.registerMBean(new JMXConfigurator(root.getLoggerContext(),
+                                                         server,
+                                                         objectName),
+                                     objectName);
+            }
+        } catch (MalformedObjectNameException | InstanceAlreadyExistsException |
+                NotCompliantMBeanException | MBeanRegistrationException e) {
+            throw new RuntimeException(e);
+        }
+
+        configureInstrumentation(root, metricRegistry);
+    }
+
+    public void stop() {
+        ILoggerFactory loggerFactory = LoggerFactory.getILoggerFactory();
+        if (loggerFactory instanceof LoggerContext) {
+            LoggerContext context = (LoggerContext) loggerFactory;
+            context.stop();
+        }
+    }
+
+    private void configureInstrumentation(Logger root, MetricRegistry metricRegistry) {
+        final InstrumentedAppender appender = new InstrumentedAppender(metricRegistry);
+        appender.setContext(root.getLoggerContext());
+        appender.start();
+        root.addAppender(appender);
+    }
+
+    private Logger configureLevels() {
+        final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME);
+        root.getLoggerContext().reset();
+
+        final LevelChangePropagator propagator = new LevelChangePropagator();
+        propagator.setContext(root.getLoggerContext());
+        propagator.setResetJUL(true);
+
+        root.getLoggerContext().addListener(propagator);
+
+        root.setLevel(level);
+
+        for (Map.Entry<String, Level> entry : loggers.entrySet()) {
+            ((Logger) LoggerFactory.getLogger(entry.getKey())).setLevel(entry.getValue());
+        }
+
+        return root;
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedExtendedThrowableProxyConverter.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedExtendedThrowableProxyConverter.java
new file mode 100644
index 0000000..dc1f009
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedExtendedThrowableProxyConverter.java
@@ -0,0 +1,17 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.pattern.ExtendedThrowableProxyConverter;
+import ch.qos.logback.classic.spi.StackTraceElementProxy;
+import ch.qos.logback.classic.spi.ThrowableProxyUtil;
+
+/**
+ * An {@link ExtendedThrowableProxyConverter} which prefixes stack traces with {@code !}.
+ */
+public class PrefixedExtendedThrowableProxyConverter extends PrefixedThrowableProxyConverter {
+    @Override
+    protected void extraData(StringBuilder builder, StackTraceElementProxy step) {
+        if (step != null) {
+            ThrowableProxyUtil.subjoinPackagingData(builder, step);
+        }
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverter.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverter.java
new file mode 100644
index 0000000..2600abb
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverter.java
@@ -0,0 +1,24 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.pattern.RootCauseFirstThrowableProxyConverter;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+
+import java.util.regex.Pattern;
+
+import static io.dropwizard.logging.PrefixedThrowableProxyConverter.*;
+
+/**
+ * A {@link RootCauseFirstThrowableProxyConverter} that prefixes stack traces with {@code !}.
+ */
+public class PrefixedRootCauseFirstThrowableProxyConverter
+        extends RootCauseFirstThrowableProxyConverter {
+
+    private static final String CAUSING = PREFIX + "Causing:";
+    private static final Pattern CAUSING_PATTERN = Pattern.compile("^" + Pattern.quote(PREFIX) + "Wrapped by:", Pattern.MULTILINE);
+
+    @Override
+    protected String throwableProxyToString(IThrowableProxy tp) {
+        final String prefixed = PATTERN.matcher(super.throwableProxyToString(tp)).replaceAll(PREFIX);
+        return CAUSING_PATTERN.matcher(prefixed).replaceAll(CAUSING);
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedThrowableProxyConverter.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedThrowableProxyConverter.java
new file mode 100644
index 0000000..f13bfe1
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedThrowableProxyConverter.java
@@ -0,0 +1,20 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.pattern.ThrowableProxyConverter;
+import ch.qos.logback.classic.spi.IThrowableProxy;
+
+import java.util.regex.Pattern;
+
+/**
+ * A {@link ThrowableProxyConverter} which prefixes stack traces with {@code !}.
+ */
+public class PrefixedThrowableProxyConverter extends ThrowableProxyConverter {
+
+    static final Pattern PATTERN = Pattern.compile("^\\t?", Pattern.MULTILINE);
+    static final String PREFIX = "! ";
+
+    @Override
+    protected String throwableProxyToString(IThrowableProxy tp) {
+        return PATTERN.matcher(super.throwableProxyToString(tp)).replaceAll(PREFIX);
+    }
+}
diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/SyslogAppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/SyslogAppenderFactory.java
new file mode 100644
index 0000000..0f0021b
--- /dev/null
+++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/SyslogAppenderFactory.java
@@ -0,0 +1,217 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.net.SyslogAppender;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.Layout;
+import ch.qos.logback.core.net.SyslogConstants;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+import java.lang.management.ManagementFactory;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * An {@link AppenderFactory} implementation which provides an appender that sends events to a
+ * syslog server.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code host}</td>
+ *         <td>{@code localhost}</td>
+ *         <td>The hostname of the syslog server.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code port}</td>
+ *         <td>{@code 514}</td>
+ *         <td>The port on which the syslog server is listening.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code facility}</td>
+ *         <td>{@code local0}</td>
+ *         <td>
+ *             The syslog facility to use. Can be either {@code auth}, {@code authpriv},
+ *             {@code daemon}, {@code cron}, {@code ftp}, {@code lpr}, {@code kern}, {@code mail},
+ *             {@code news}, {@code syslog}, {@code user}, {@code uucp}, {@code local0},
+ *             {@code local1}, {@code local2}, {@code local3}, {@code local4}, {@code local5},
+ *             {@code local6}, or {@code local7}.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code threshold}</td>
+ *         <td>{@code ALL}</td>
+ *         <td>The lowest level of events to write to the file.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code logFormat}</td>
+ *         <td>the default format</td>
+ *         <td>
+ *             The Logback pattern with which events will be formatted. See
+ *             <a href="http://logback.qos.ch/manual/layouts.html#conversionWord">the Logback documentation</a>
+ *             for details.
+ *         </td>
+ *     </tr>
+ * </table>
+ *
+ * @see AbstractAppenderFactory
+ */
+ at JsonTypeName("syslog")
+public class SyslogAppenderFactory extends AbstractAppenderFactory {
+    public enum Facility {
+        AUTH,
+        AUTHPRIV,
+        DAEMON,
+        CRON,
+        FTP,
+        LPR,
+        KERN,
+        MAIL,
+        NEWS,
+        SYSLOG,
+        USER,
+        UUCP,
+        LOCAL0,
+        LOCAL1,
+        LOCAL2,
+        LOCAL3,
+        LOCAL4,
+        LOCAL5,
+        LOCAL6,
+        LOCAL7
+    }
+
+    private static final String LOG_TOKEN_NAME = "%app";
+    private static final String LOG_TOKEN_PID = "%pid";
+
+    private static final Pattern PID_PATTERN = Pattern.compile("(\\d+)@");
+    private static String PID = "";
+
+    // make an attempt to get the PID of the process
+    // this will only work on UNIX platforms; for others, the PID will be "unknown"
+    static {
+        final Matcher matcher = PID_PATTERN.matcher(ManagementFactory.getRuntimeMXBean().getName());
+        if (matcher.find()) {
+            PID = "[" + matcher.group(1) + "]";
+        }
+    }
+
+    @NotNull
+    private String host = "localhost";
+
+    @Min(1)
+    @Max(65535)
+    private int port = SyslogConstants.SYSLOG_PORT;
+
+    @NotNull
+    private Facility facility = Facility.LOCAL0;
+
+    // PrefixedThrowableProxyConverter does not apply to syslog appenders, as stack traces are sent separately from
+    // the main message. This means that the standard prefix of `!` is not used for syslog
+    @NotNull
+    private String stackTracePrefix = SyslogAppender.DEFAULT_STACKTRACE_PATTERN;
+
+    // prefix the logFormat with the application name and PID (if available)
+    private String logFormat = LOG_TOKEN_NAME + LOG_TOKEN_PID + ": " +
+            SyslogAppender.DEFAULT_SUFFIX_PATTERN;
+
+    private boolean includeStackTrace = true;
+
+    /**
+     * Returns the Logback pattern with which events will be formatted.
+     */
+    @Override
+    @JsonProperty
+    public String getLogFormat() {
+        return logFormat;
+    }
+
+    /**
+     * Sets the Logback pattern with which events will be formatted.
+     */
+    @Override
+    @JsonProperty
+    public void setLogFormat(String logFormat) {
+        this.logFormat = logFormat;
+    }
+
+    /**
+     * Returns the hostname of the syslog server.
+     */
+    @JsonProperty
+    public String getHost() {
+        return host;
+    }
+
+    @JsonProperty
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    @JsonProperty
+    public Facility getFacility() {
+        return facility;
+    }
+
+    @JsonProperty
+    public void setFacility(Facility facility) {
+        this.facility = facility;
+    }
+
+    @JsonProperty
+    public int getPort() {
+        return port;
+    }
+
+    @JsonProperty
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    @JsonProperty
+    public boolean getIncludeStackTrace() {
+        return includeStackTrace;
+    }
+
+    @JsonProperty
+    public void setIncludeStackTrace(boolean includeStackTrace) {
+        this.includeStackTrace = includeStackTrace;
+    }
+
+    @JsonProperty
+    public String getStackTracePrefix() {
+        return stackTracePrefix;
+    }
+
+    @JsonProperty
+    public void setStackTracePrefix(String stackTracePrefix) {
+        this.stackTracePrefix = stackTracePrefix;
+    }
+
+    @Override
+    public Appender<ILoggingEvent> build(LoggerContext context, String applicationName, Layout<ILoggingEvent> layout) {
+        final SyslogAppender appender = new SyslogAppender();
+        appender.setName("syslog-appender");
+        appender.setContext(context);
+        appender.setSuffixPattern(logFormat.replaceAll(LOG_TOKEN_PID, PID).replaceAll(LOG_TOKEN_NAME, Matcher.quoteReplacement(applicationName)));
+        appender.setSyslogHost(host);
+        appender.setPort(port);
+        appender.setFacility(facility.toString().toLowerCase(Locale.ENGLISH));
+        appender.setThrowableExcluded(!includeStackTrace);
+        appender.setStackTracePattern(stackTracePrefix);
+        addThresholdFilter(appender, threshold);
+        appender.start();
+        return wrapAsync(appender);
+    }
+}
diff --git a/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
new file mode 100644
index 0000000..f479de7
--- /dev/null
+++ b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
@@ -0,0 +1 @@
+io.dropwizard.logging.AppenderFactory
diff --git a/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory
new file mode 100644
index 0000000..7b88e1d
--- /dev/null
+++ b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.logging.AppenderFactory
@@ -0,0 +1,3 @@
+io.dropwizard.logging.ConsoleAppenderFactory
+io.dropwizard.logging.FileAppenderFactory
+io.dropwizard.logging.SyslogAppenderFactory
\ No newline at end of file
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/AsyncAppenderTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/AsyncAppenderTest.java
new file mode 100644
index 0000000..a7efd9a
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/AsyncAppenderTest.java
@@ -0,0 +1,34 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import io.dropwizard.util.Duration;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.*;
+
+public class AsyncAppenderTest {
+    @SuppressWarnings("unchecked")
+    private final Appender<ILoggingEvent> delegate = mock(Appender.class);
+    private final AsyncAppender appender = new AsyncAppender(delegate, 100, Duration.milliseconds(100), true);
+
+    @Before
+    public void setUp() throws Exception {
+        appender.start();
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        appender.stop();
+    }
+
+    @Test
+    public void delegatesAppending() throws Exception {
+        final ILoggingEvent event = mock(ILoggingEvent.class);
+        appender.append(event);
+
+        verify(delegate, timeout(200)).doAppend(event);
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/ConsoleAppenderFactoryTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/ConsoleAppenderFactoryTest.java
new file mode 100644
index 0000000..4b5933f
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/ConsoleAppenderFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.logging;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class ConsoleAppenderFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(ConsoleAppenderFactory.class);
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/DropwizardLayoutTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/DropwizardLayoutTest.java
new file mode 100644
index 0000000..b542b9e
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/DropwizardLayoutTest.java
@@ -0,0 +1,39 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import org.junit.Test;
+
+import java.util.TimeZone;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class DropwizardLayoutTest {
+    private final LoggerContext context = mock(LoggerContext.class);
+    private final TimeZone timeZone = TimeZone.getTimeZone("UTC");
+    private final DropwizardLayout layout = new DropwizardLayout(context, timeZone);
+
+    @Test
+    public void prefixesThrowables() throws Exception {
+        assertThat(layout.getDefaultConverterMap().get("ex"))
+                .isEqualTo(PrefixedThrowableProxyConverter.class.getName());
+    }
+
+    @Test
+    public void prefixesExtendedThrowables() throws Exception {
+        assertThat(layout.getDefaultConverterMap().get("xEx"))
+                .isEqualTo(PrefixedExtendedThrowableProxyConverter.class.getName());
+    }
+
+    @Test
+    public void hasAContext() throws Exception {
+        assertThat(layout.getContext())
+                .isEqualTo(context);
+    }
+
+    @Test
+    public void hasAPatternWithATimeZoneAndExtendedThrowables() throws Exception {
+        assertThat(layout.getPattern())
+                .isEqualTo("%-5p [%d{ISO8601,UTC}] %c: %m%n%rEx");
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/FileAppenderFactoryTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/FileAppenderFactoryTest.java
new file mode 100644
index 0000000..8cac8bd
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/FileAppenderFactoryTest.java
@@ -0,0 +1,34 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.rolling.RollingFileAppender;
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class FileAppenderFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(FileAppenderFactory.class);
+    }
+
+    @Test
+    public void isRolling() throws Exception {
+        // the method we want to test is protected, so we need to override it so we can see it
+        FileAppenderFactory fileAppenderFactory = new FileAppenderFactory() {
+            @Override
+            public FileAppender<ILoggingEvent> buildAppender(LoggerContext context) {
+                return super.buildAppender(context);
+            }
+        };
+
+        fileAppenderFactory.setCurrentLogFilename("logfile.log");
+        fileAppenderFactory.setArchive(true);
+        fileAppenderFactory.setArchivedLogFilenamePattern("example-%d.log.gz");
+        assertThat(fileAppenderFactory.buildAppender(new LoggerContext())).isInstanceOf(RollingFileAppender.class);
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/LoggingFactoryTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/LoggingFactoryTest.java
new file mode 100644
index 0000000..abca68b
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/LoggingFactoryTest.java
@@ -0,0 +1,45 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.Level;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.io.Resources;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.jackson.Jackson;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import java.io.File;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class LoggingFactoryTest {
+    private final ObjectMapper objectMapper = Jackson.newObjectMapper();
+    private final ConfigurationFactory<LoggingFactory> factory =
+            new ConfigurationFactory<>(LoggingFactory.class,
+                                       Validation.buildDefaultValidatorFactory().getValidator(),
+                                       objectMapper, "dw");
+    private LoggingFactory config;
+
+    @Before
+    public void setUp() throws Exception {
+        objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class,
+                                                           FileAppenderFactory.class,
+                                                           SyslogAppenderFactory.class);
+
+        this.config = factory.build(new File(Resources.getResource("yaml/logging.yml").toURI()));
+    }
+
+    @Test
+    public void hasADefaultLevel() throws Exception {
+        assertThat(config.getLevel())
+                .isEqualTo(Level.INFO);
+    }
+
+    @Test
+    public void hasASetOfOverriddenLevels() throws Exception {
+        assertThat(config.getLoggers())
+                .isEqualTo(ImmutableMap.of("com.example.app", Level.DEBUG));
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedExtendedThrowableProxyConverterTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedExtendedThrowableProxyConverterTest.java
new file mode 100644
index 0000000..6eecdfb
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedExtendedThrowableProxyConverterTest.java
@@ -0,0 +1,28 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import com.google.common.collect.Lists;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class PrefixedExtendedThrowableProxyConverterTest {
+    private final PrefixedExtendedThrowableProxyConverter converter = new PrefixedExtendedThrowableProxyConverter();
+    private final ThrowableProxy proxy = new ThrowableProxy(new IOException("noo"));
+
+    @Before
+    public void setup() {
+        converter.setOptionList(Lists.newArrayList("full"));
+        converter.start();
+    }
+
+    @Test
+    public void prefixesExceptionsWithExclamationMarks() throws Exception {
+        assertThat(converter.throwableProxyToString(proxy))
+                .startsWith(String.format("! java.io.IOException: noo%n" +
+                                                  "! at io.dropwizard.logging.PrefixedExtendedThrowableProxyConverterTest.<init>(PrefixedExtendedThrowableProxyConverterTest.java:14)%n"));
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverterTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverterTest.java
new file mode 100644
index 0000000..1eb1ba3
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverterTest.java
@@ -0,0 +1,86 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import com.google.common.collect.Lists;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.SocketTimeoutException;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+/**
+ * Tests {@link PrefixedRootCauseFirstThrowableProxyConverter}.
+ */
+public class PrefixedRootCauseFirstThrowableProxyConverterTest {
+
+    private final PrefixedRootCauseFirstThrowableProxyConverter converter
+            = new PrefixedRootCauseFirstThrowableProxyConverter();
+
+    private final ThrowableProxy proxy = new ThrowableProxy(getException());
+
+    private Exception getException() {
+        try {
+            throwOuterWrapper();
+        } catch (Exception e) {
+            return e;
+        }
+
+        return null; // unpossible, tell the type-system
+    }
+
+    private void throwRoot() throws SocketTimeoutException {
+        throw new SocketTimeoutException("Timed-out reading from socket");
+    }
+
+    private void throwInnerWrapper() throws IOException {
+        try {
+            throwRoot();
+        } catch (SocketTimeoutException ste) {
+            throw new IOException("Fairly general error doing some IO", ste);
+        }
+    }
+
+    private void throwOuterWrapper() {
+        try {
+            throwInnerWrapper();
+        } catch (IOException e) {
+            throw new RuntimeException("Very general error doing something", e);
+        }
+    }
+
+    @Before
+    public void setup() {
+        converter.setOptionList(Lists.newArrayList("full"));
+        converter.start();
+    }
+
+    @Test
+    public void prefixesExceptionsWithExclamationMarks() throws Exception {
+        assertThat(converter.throwableProxyToString(proxy))
+                .startsWith(String.format(
+                        "! java.net.SocketTimeoutException: Timed-out reading from socket%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.throwRoot(PrefixedRootCauseFirstThrowableProxyConverterTest.java:34)%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.throwInnerWrapper(PrefixedRootCauseFirstThrowableProxyConverterTest.java:39)%n" +
+                        "! ... 31 common frames omitted%n" +
+                        "! Causing: java.io.IOException: Fairly general error doing some IO%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.throwInnerWrapper(PrefixedRootCauseFirstThrowableProxyConverterTest.java:41)%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.throwOuterWrapper(PrefixedRootCauseFirstThrowableProxyConverterTest.java:47)%n" +
+                        "! ... 30 common frames omitted%n" +
+                        "! Causing: java.lang.RuntimeException: Very general error doing something%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.throwOuterWrapper(PrefixedRootCauseFirstThrowableProxyConverterTest.java:49)%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.getException(PrefixedRootCauseFirstThrowableProxyConverterTest.java:25)%n" +
+                        "! at io.dropwizard.logging.PrefixedRootCauseFirstThrowableProxyConverterTest.<init>(PrefixedRootCauseFirstThrowableProxyConverterTest.java:21)%n"));
+    }
+
+    /**
+     * This test uses a regular expression to ensure that the final frame in the printed stack trace
+     * is the "main" function.
+     */
+    @Test
+    public void finalFrameIsMain() throws Exception {
+        assertThat(converter.throwableProxyToString(proxy))
+                .matches(String.format("^[\\s\\S]+! at \\S+\\.([^.]+)\\.main\\(\\1\\.java:\\d+\\)\\s*$"));
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedThrowableProxyConverterTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedThrowableProxyConverterTest.java
new file mode 100644
index 0000000..0cbaaa5
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedThrowableProxyConverterTest.java
@@ -0,0 +1,28 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.spi.ThrowableProxy;
+import com.google.common.collect.Lists;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class PrefixedThrowableProxyConverterTest {
+    private final PrefixedThrowableProxyConverter converter = new PrefixedThrowableProxyConverter();
+    private final ThrowableProxy proxy = new ThrowableProxy(new IOException("noo"));
+
+    @Before
+    public void setup() {
+        converter.setOptionList(Lists.newArrayList("full"));
+        converter.start();
+    }
+
+    @Test
+    public void prefixesExceptionsWithExclamationMarks() throws Exception {
+        assertThat(converter.throwableProxyToString(proxy))
+                .startsWith(String.format("! java.io.IOException: noo%n" +
+                                                  "! at io.dropwizard.logging.PrefixedThrowableProxyConverterTest.<init>(PrefixedThrowableProxyConverterTest.java:14)%n"));
+    }
+}
diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/SyslogAppenderFactoryTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/SyslogAppenderFactoryTest.java
new file mode 100644
index 0000000..8147e80
--- /dev/null
+++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/SyslogAppenderFactoryTest.java
@@ -0,0 +1,60 @@
+package io.dropwizard.logging;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.net.SyslogAppender;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.AsyncAppenderBase;
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class SyslogAppenderFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(SyslogAppenderFactory.class);
+    }
+
+    @Test
+    public void defaultIncludesAppName() throws Exception {
+        assertThat(new SyslogAppenderFactory().getLogFormat())
+                .contains("%app");
+    }
+
+    @Test
+    public void defaultIncludesPid() throws Exception {
+        assertThat(new SyslogAppenderFactory().getLogFormat())
+                .contains("%pid");
+    }
+
+    @Test
+    public void patternIncludesAppNameAndPid() throws Exception {
+        Appender<ILoggingEvent> wrapper = new SyslogAppenderFactory()
+                .build(new LoggerContext(), "MyApplication", null);
+
+        // hack to get at the SyslogAppender beneath the AsyncAppender
+        // todo: find a nicer way to do all this
+        Field delegate = AsyncAppender.class.getDeclaredField("delegate");
+        delegate.setAccessible(true);
+        SyslogAppender appender = (SyslogAppender) delegate.get(wrapper);
+
+        assertThat(appender.getSuffixPattern())
+                .matches("^MyApplication\\[\\d+\\].+");
+    }
+
+    @Test
+    public void stackTracePatternCanBeSet() throws Exception {
+        SyslogAppenderFactory syslogAppenderFactory = new SyslogAppenderFactory();
+        syslogAppenderFactory.setStackTracePrefix("--->");
+        AsyncAppender wrapper = (AsyncAppender) syslogAppenderFactory.build(
+                new LoggerContext(), "MyApplication", null);
+        SyslogAppender delegate = (SyslogAppender) wrapper.getDelegate();
+
+        assertThat(delegate.getStackTracePattern())
+                .isEqualTo("--->");
+    }
+}
diff --git a/dropwizard-logging/src/test/resources/logback-test.xml b/dropwizard-logging/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-logging/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-logging/src/test/resources/yaml/logging.yml b/dropwizard-logging/src/test/resources/yaml/logging.yml
new file mode 100644
index 0000000..186d67f
--- /dev/null
+++ b/dropwizard-logging/src/test/resources/yaml/logging.yml
@@ -0,0 +1,14 @@
+level: INFO
+loggers:
+  com.example.app: DEBUG
+appenders:
+  - type: console
+    threshold: ALL
+  - type: file
+    threshold: ALL
+    currentLogFilename: ./logs/example.log
+    archivedLogFilenamePattern: ./logs/example-%d.log.gz
+    archivedFileCount: 5
+  - type: syslog
+    host: localhost
+    facility: local0
diff --git a/dropwizard-metrics-ganglia/pom.xml b/dropwizard-metrics-ganglia/pom.xml
new file mode 100644
index 0000000..34cfa12
--- /dev/null
+++ b/dropwizard-metrics-ganglia/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-metrics-ganglia</artifactId>
+    <name>Dropwizard Metrics Support for Ganglia</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-metrics</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-ganglia</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-metrics-ganglia/src/main/java/io/dropwizard/metrics/ganglia/GangliaReporterFactory.java b/dropwizard-metrics-ganglia/src/main/java/io/dropwizard/metrics/ganglia/GangliaReporterFactory.java
new file mode 100644
index 0000000..634cc64
--- /dev/null
+++ b/dropwizard-metrics-ganglia/src/main/java/io/dropwizard/metrics/ganglia/GangliaReporterFactory.java
@@ -0,0 +1,211 @@
+package io.dropwizard.metrics.ganglia;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.codahale.metrics.ganglia.GangliaReporter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import com.google.common.base.Optional;
+import info.ganglia.gmetric4j.gmetric.GMetric;
+import io.dropwizard.metrics.BaseReporterFactory;
+import io.dropwizard.util.Duration;
+import io.dropwizard.validation.MinDuration;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.hibernate.validator.constraints.Range;
+
+import javax.validation.constraints.NotNull;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * A factory for {@link GangliaReporter} instances.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>host</td>
+ *         <td>localhost</td>
+ *         <td>The hostname (or group) of the Ganglia server(s) to report to.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>port</td>
+ *         <td>8649</td>
+ *         <td>The port of the Ganglia server(s) to report to.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>mode</td>
+ *         <td>unicast</td>
+ *         <td>The UDP addressing mode to announce the metrics with. One of {@code unicast} or
+ *         {@code multicast}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>ttl</td>
+ *         <td>1</td>
+ *         <td>The time-to-live of the UDP packets for the announced metrics.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>uuid</td>
+ *         <td><i>None</i></td>
+ *         <td>The UUID to tag announced metrics with.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>spoof</td>
+ *         <td><i>None</i></td>
+ *         <td>The hostname and port to use instead of this nodes for the announced metrics. In the
+ *         format {@code hostname:port}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>tmax</td>
+ *         <td>60</td>
+ *         <td>The tmax value to annouce metrics with.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>dmax</td>
+ *         <td>0</td>
+ *         <td>The dmax value to announce metrics with.</td>
+ *     </tr>
+ * </table>
+ */
+ at JsonTypeName("ganglia")
+public class GangliaReporterFactory extends BaseReporterFactory {
+    @NotNull
+    @MinDuration(0)
+    private Duration tmax = Duration.seconds(1);
+
+    @NotNull
+    @MinDuration(0)
+    private Duration dmax = Duration.seconds(0);
+
+    @NotEmpty
+    private String host = "localhost";
+
+    @Range(min = 1, max = 49151)
+    private int port = 8649;
+
+    @NotNull
+    private GMetric.UDPAddressingMode mode = GMetric.UDPAddressingMode.UNICAST;
+
+    @Range(min = 0, max = 255)
+    private int ttl = 1;
+
+    private String prefix;
+    private UUID uuid;
+    private String spoof;
+
+    @JsonProperty
+    public Duration getTmax() {
+        return tmax;
+    }
+
+    @JsonProperty
+    public void setTmax(Duration tmax) {
+        this.tmax = tmax;
+    }
+
+    @JsonProperty
+    public Duration getDmax() {
+        return dmax;
+    }
+
+    @JsonProperty
+    public void setDmax(Duration dmax) {
+        this.dmax = dmax;
+    }
+
+    @JsonProperty
+    public String getHost() {
+        return host;
+    }
+
+    @JsonProperty
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    @JsonProperty
+    public int getPort() {
+        return port;
+    }
+
+    @JsonProperty
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    @JsonProperty
+    public GMetric.UDPAddressingMode getMode() {
+        return mode;
+    }
+
+    @JsonProperty
+    public void setMode(GMetric.UDPAddressingMode mode) {
+        this.mode = mode;
+    }
+
+    @JsonProperty
+    public int getTtl() {
+        return ttl;
+    }
+
+    @JsonProperty
+    public void setTtl(int ttl) {
+        this.ttl = ttl;
+    }
+
+    public String getPrefix() {
+        return prefix;
+    }
+
+    public void setPrefix(String prefix) {
+        this.prefix = prefix;
+    }
+
+    @JsonProperty
+    public Optional<UUID> getUuid() {
+        return Optional.fromNullable(uuid);
+    }
+
+    @JsonProperty
+    public void setUuid(UUID uuid) {
+        this.uuid = uuid;
+    }
+
+    @JsonProperty
+    public Optional<String> getSpoof() {
+        return Optional.fromNullable(spoof);
+    }
+
+    @JsonProperty
+    public void setSpoof(String spoof) {
+        this.spoof = spoof;
+    }
+
+    @Override
+    public ScheduledReporter build(MetricRegistry registry) {
+        try {
+            GMetric ganglia = new GMetric(host,
+                                          port,
+                                          mode,
+                                          ttl,
+                                          uuid != null || spoof != null,
+                                          uuid,
+                                          spoof);
+
+            return GangliaReporter.forRegistry(registry)
+                                  .convertDurationsTo(getDurationUnit())
+                                  .convertRatesTo(getRateUnit())
+                                  .filter(getFilter())
+                                  .prefixedWith(getPrefix())
+                                  .withDMax((int) dmax.toSeconds())
+                                  .withTMax((int) tmax.toSeconds())
+                                  .build(ganglia);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}
diff --git a/dropwizard-metrics-ganglia/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory b/dropwizard-metrics-ganglia/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory
new file mode 100644
index 0000000..c402436
--- /dev/null
+++ b/dropwizard-metrics-ganglia/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory
@@ -0,0 +1 @@
+io.dropwizard.metrics.ganglia.GangliaReporterFactory
diff --git a/dropwizard-metrics-ganglia/src/test/java/io/dropwizard/metrics/ganglia/GangliaReporterFactoryTest.java b/dropwizard-metrics-ganglia/src/test/java/io/dropwizard/metrics/ganglia/GangliaReporterFactoryTest.java
new file mode 100644
index 0000000..1dc6a25
--- /dev/null
+++ b/dropwizard-metrics-ganglia/src/test/java/io/dropwizard/metrics/ganglia/GangliaReporterFactoryTest.java
@@ -0,0 +1,13 @@
+package io.dropwizard.metrics.ganglia;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.fest.assertions.api.Assertions;
+import org.junit.Test;
+
+public class GangliaReporterFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        Assertions.assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(GangliaReporterFactory.class);
+    }
+}
diff --git a/dropwizard-metrics-graphite/pom.xml b/dropwizard-metrics-graphite/pom.xml
new file mode 100644
index 0000000..6d5be7e
--- /dev/null
+++ b/dropwizard-metrics-graphite/pom.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-metrics-graphite</artifactId>
+    <name>Dropwizard Metrics Support for Graphite</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-metrics</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-graphite</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-metrics-graphite/src/main/java/io/dropwizard/metrics/graphite/GraphiteReporterFactory.java b/dropwizard-metrics-graphite/src/main/java/io/dropwizard/metrics/graphite/GraphiteReporterFactory.java
new file mode 100644
index 0000000..60b55eb
--- /dev/null
+++ b/dropwizard-metrics-graphite/src/main/java/io/dropwizard/metrics/graphite/GraphiteReporterFactory.java
@@ -0,0 +1,94 @@
+package io.dropwizard.metrics.graphite;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.codahale.metrics.graphite.Graphite;
+import com.codahale.metrics.graphite.GraphiteReporter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.metrics.BaseReporterFactory;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.hibernate.validator.constraints.Range;
+
+import javax.validation.constraints.NotNull;
+import java.net.InetSocketAddress;
+
+/**
+ * A factory for {@link GraphiteReporter} instances.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>host</td>
+ *         <td>localhost</td>
+ *         <td>The hostname of the Graphite server to report to.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>port</td>
+ *         <td>8080</td>
+ *         <td>The port of the Graphite server to report to.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>prefix</td>
+ *         <td><i>None</i></td>
+ *         <td>The prefix for Metric key names to report to Graphite.</td>
+ *     </tr>
+ * </table>
+ */
+ at JsonTypeName("graphite")
+public class GraphiteReporterFactory extends BaseReporterFactory {
+    @NotEmpty
+    private String host = "localhost";
+
+    @Range(min = 0, max = 49151)
+    private int port = 8080;
+
+    @NotNull
+    private String prefix = "";
+
+    @JsonProperty
+    public String getHost() {
+        return host;
+    }
+
+    @JsonProperty
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    @JsonProperty
+    public int getPort() {
+        return port;
+    }
+
+    @JsonProperty
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    @JsonProperty
+    public String getPrefix() {
+        return prefix;
+    }
+
+    @JsonProperty
+    public void setPrefix(String prefix) {
+        this.prefix = prefix;
+    }
+
+    @Override
+    public ScheduledReporter build(MetricRegistry registry) {
+        final Graphite graphite = new Graphite(new InetSocketAddress(host, port));
+        return GraphiteReporter.forRegistry(registry)
+                               .convertDurationsTo(getDurationUnit())
+                               .convertRatesTo(getRateUnit())
+                               .filter(getFilter())
+                               .prefixedWith(getPrefix())
+                               .build(graphite);
+    }
+}
diff --git a/dropwizard-metrics-graphite/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory b/dropwizard-metrics-graphite/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory
new file mode 100644
index 0000000..22810cc
--- /dev/null
+++ b/dropwizard-metrics-graphite/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory
@@ -0,0 +1 @@
+io.dropwizard.metrics.graphite.GraphiteReporterFactory
diff --git a/dropwizard-metrics-graphite/src/test/java/io/dropwizard/metrics/graphite/GraphiteReporterFactoryTest.java b/dropwizard-metrics-graphite/src/test/java/io/dropwizard/metrics/graphite/GraphiteReporterFactoryTest.java
new file mode 100644
index 0000000..caf17df
--- /dev/null
+++ b/dropwizard-metrics-graphite/src/test/java/io/dropwizard/metrics/graphite/GraphiteReporterFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.metrics.graphite;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class GraphiteReporterFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(GraphiteReporterFactory.class);
+    }
+}
diff --git a/dropwizard-metrics/pom.xml b/dropwizard-metrics/pom.xml
new file mode 100644
index 0000000..ecd8331
--- /dev/null
+++ b/dropwizard-metrics/pom.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-metrics</artifactId>
+    <name>Dropwizard Metrics Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-lifecycle</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jackson</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-validation</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-logging</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-configuration</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseFormattedReporterFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseFormattedReporterFactory.java
new file mode 100644
index 0000000..ec600f1
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseFormattedReporterFactory.java
@@ -0,0 +1,43 @@
+package io.dropwizard.metrics;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.validation.constraints.NotNull;
+import java.util.Locale;
+
+/**
+ * A base {@link ReporterFactory} for configuring metric reporters with formatting options.
+ * <p/>
+ * Configures formatting options common to some {@link com.codahale.metrics.ScheduledReporter}s.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>locale</td>
+ *         <td>System default {@link Locale}.</td>
+ *         <td>The {@link Locale} for formatting numbers, dates and times.</td>
+ *     </tr>
+ *     <tr>
+ *         <td colspan="3">See {@link BaseReporterFactory} for more options.</td>
+ *     </tr>
+ * </table>
+ */
+public abstract class BaseFormattedReporterFactory extends BaseReporterFactory {
+    @NotNull
+    private Locale locale = Locale.getDefault();
+
+    @JsonProperty
+    public Locale getLocale() {
+        return locale;
+    }
+
+    @JsonProperty
+    public void setLocale(Locale locale) {
+        this.locale = locale;
+    }
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseReporterFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseReporterFactory.java
new file mode 100644
index 0000000..941521b
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseReporterFactory.java
@@ -0,0 +1,150 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.Metric;
+import com.codahale.metrics.MetricFilter;
+import com.codahale.metrics.ScheduledReporter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+import io.dropwizard.util.Duration;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A base {@link ReporterFactory} for configuring metric reporters.
+ * <p/>
+ * Configures options common to all {@link ScheduledReporter}s.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>durationUnit</td>
+ *         <td>milliseconds</td>
+ *         <td>The unit to report durations as. Overrides per-metric duration units.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>rateUnit</td>
+ *         <td>seconds</td>
+ *         <td>The unit to report rates as. Overrides per-metric rate units.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>excludes</td>
+ *         <td>No excluded metrics.</td>
+ *         <td>Metrics to exclude from reports, by name. When defined, matching metrics will not be
+ *         reported. See {@link #getFilter()}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>includes</td>
+ *         <td>All metrics included.</td>
+ *         <td>Metrics to include in reports, by name. When defined, only these metrics will be
+ *         reported. See {@link #getFilter()}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>frequency</td>
+ *         <td>1 second</td>
+ *         <td>The frequency to report metrics. Overrides the {@link
+ *         MetricsFactory#getFrequency() default}.</td>
+ *     </tr>
+ * </table>
+ */
+public abstract class BaseReporterFactory implements ReporterFactory {
+    @NotNull
+    private TimeUnit durationUnit = TimeUnit.MILLISECONDS;
+
+    @NotNull
+    private TimeUnit rateUnit = TimeUnit.SECONDS;
+
+    @NotNull
+    private ImmutableSet<String> excludes = ImmutableSet.of();
+
+    @NotNull
+    private ImmutableSet<String> includes = ImmutableSet.of();
+
+    @NotNull
+    @Valid
+    private Optional<Duration> frequency = Optional.absent();
+
+    public TimeUnit getDurationUnit() {
+        return durationUnit;
+    }
+
+    @JsonProperty
+    public void setDurationUnit(TimeUnit durationUnit) {
+        this.durationUnit = durationUnit;
+    }
+
+    @JsonProperty
+    public TimeUnit getRateUnit() {
+        return rateUnit;
+    }
+
+    @JsonProperty
+    public void setRateUnit(final TimeUnit rateUnit) {
+        this.rateUnit = rateUnit;
+    }
+
+    @JsonProperty
+    public ImmutableSet<String> getIncludes() {
+        return includes;
+    }
+
+    @JsonProperty
+    public void setIncludes(ImmutableSet<String> includes) {
+        this.includes = includes;
+    }
+
+    @JsonProperty
+    public ImmutableSet<String> getExcludes() {
+        return excludes;
+    }
+
+    @JsonProperty
+    public void setExcludes(ImmutableSet<String> excludes) {
+        this.excludes = excludes;
+    }
+
+    @JsonProperty
+    public Optional<Duration> getFrequency() {
+        return frequency;
+    }
+
+    @JsonProperty
+    public void setFrequency(Optional<Duration> frequency) {
+        this.frequency = frequency;
+    }
+
+    /**
+     * Gets a {@link MetricFilter} that specifically includes and excludes configured metrics.
+     * <p/>
+     * Filtering works in 3 ways:
+     * <dl>
+     *     <dt><i>excludes</i>-only</dt>
+     *     <dd>All metrics are reported, except those with a name listed in <i>excludes</i>.</dd>
+     *     <dt><i>includes</i>-only</dt>
+     *     <dd>No metrics are reported, except those with a name listed in <i>includes</i>.</dd>
+     *     <dt>mixed (both <i>includes</i> and <i>excludes</i></dt>
+     *     <dd>All metrics are reported, except those with a name listed in <i>excludes</i>, unless
+     *     they're also listed in <i>includes</i> (<i>includes</i> takes precedence).</dd>
+     * </dl>
+     *
+     * @return the filter for selecting metrics based on the configured excludes/includes.
+     * @see #getIncludes()
+     * @see #getExcludes()
+     */
+    public MetricFilter getFilter() {
+        return new MetricFilter() {
+            @Override
+            public boolean matches(final String name, final Metric metric) {
+                return (!getIncludes().isEmpty() && getIncludes().contains(name))
+                        || !getExcludes().contains(name);
+            }
+        };
+    }
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ConsoleReporterFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ConsoleReporterFactory.java
new file mode 100644
index 0000000..4c52d28
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ConsoleReporterFactory.java
@@ -0,0 +1,95 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.ConsoleReporter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import javax.validation.constraints.NotNull;
+import java.io.PrintStream;
+import java.util.TimeZone;
+
+/**
+ * A factory for configuring and building {@link ConsoleReporter} instances.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>timeZone</td>
+ *         <td>UTC</td>
+ *         <td>The timezone to display dates/times for.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>output</td>
+ *         <td>stdout</td>
+ *         <td>The stream to write to. One of {@code stdout} or {@code stderr}.</td>
+ *     </tr>
+ *     <tr>
+ *         <td colspan="3">See {@link BaseFormattedReporterFactory} for more options.</td>
+ *     </tr>
+ *     <tr>
+ *         <td colspan="3">See {@link BaseReporterFactory} for more options.</td>
+ *     </tr>
+ * </table>
+ */
+ at JsonTypeName("console")
+public class ConsoleReporterFactory extends BaseFormattedReporterFactory {
+    @SuppressWarnings("UnusedDeclaration")
+    public enum ConsoleStream {
+        STDOUT(System.out),
+        STDERR(System.err);
+
+        private final PrintStream printStream;
+
+        ConsoleStream(PrintStream printStream) {
+            this.printStream = printStream;
+        }
+
+        public PrintStream get() {
+            return printStream;
+        }
+    }
+
+    @NotNull
+    private TimeZone timeZone = TimeZone.getTimeZone("UTC");
+
+    @NotNull
+    private ConsoleStream output = ConsoleStream.STDOUT;
+
+    @JsonProperty
+    public TimeZone getTimeZone() {
+        return timeZone;
+    }
+
+    @JsonProperty
+    public void setTimeZone(TimeZone timeZone) {
+        this.timeZone = timeZone;
+    }
+
+    @JsonProperty
+    public ConsoleStream getOutput() {
+        return output;
+    }
+
+    @JsonProperty
+    public void setOutput(ConsoleStream stream) {
+        this.output = stream;
+    }
+
+    public ScheduledReporter build(MetricRegistry registry) {
+        return ConsoleReporter.forRegistry(registry)
+                              .convertDurationsTo(getDurationUnit())
+                              .convertRatesTo(getRateUnit())
+                              .filter(getFilter())
+                              .formattedFor(getLocale())
+                              .formattedFor(getTimeZone())
+                              .outputTo(getOutput().get())
+                              .build();
+    }
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/CsvReporterFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/CsvReporterFactory.java
new file mode 100644
index 0000000..d613f40
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/CsvReporterFactory.java
@@ -0,0 +1,70 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.CsvReporter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+
+import javax.validation.constraints.NotNull;
+import java.io.File;
+
+/**
+ * A factory for configuring and building {@link CsvReporter} instances.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>file</td>
+ *         <td>No default. You must define a directory.</td>
+ *         <td>The directory where the csv metrics will be written. If the
+ *         directory does not exist on startup, an attempt will be made to
+ *         create it and any parent directories as necessary. If this
+ *         operation fails dropwizard will fail on startup, but it may
+ *         have succeeded in creating some of the necessary parent
+ *         directories.</td>
+ *     </tr>
+ *     <tr>
+ *         <td colspan="3">See {@link BaseFormattedReporterFactory} for more options.</td>
+ *     </tr>
+ *     <tr>
+ *         <td colspan="3">See {@link BaseReporterFactory} for more options.</td>
+ *     </tr>
+ * </table>
+ */
+ at JsonTypeName("csv")
+public class CsvReporterFactory extends BaseFormattedReporterFactory {
+    @NotNull
+    private File file;
+
+    @JsonProperty
+    public File getFile() {
+        return file;
+    }
+
+    @JsonProperty
+    public void setFile(File file) {
+        this.file = file;
+    }
+
+    @Override
+    public ScheduledReporter build(MetricRegistry registry) {
+        boolean creation = file.mkdirs();
+        if (!creation && !file.exists()) {
+            String msg = "Failed to create" + file.getAbsolutePath();
+            throw new RuntimeException(msg);
+        }
+
+        return CsvReporter.forRegistry(registry)
+                          .convertDurationsTo(getDurationUnit())
+                          .convertRatesTo(getRateUnit())
+                          .filter(getFilter())
+                          .formatFor(getLocale())
+                          .build(getFile());
+    }
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/MetricsFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/MetricsFactory.java
new file mode 100644
index 0000000..f1d6690
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/MetricsFactory.java
@@ -0,0 +1,94 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import io.dropwizard.util.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+/**
+ * A factory for configuring the metrics sub-system for the environment.
+ * <p/>
+ * Configures an optional list of {@link com.codahale.metrics.ScheduledReporter reporters} with a
+ * default {@link #frequency}.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>frequency</td>
+ *         <td>1 second</td>
+ *         <td>The frequency to report metrics. Overridable per-reporter.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>reporters</td>
+ *         <td>No reporters.</td>
+ *         <td>A list of {@link ReporterFactory reporters} to report metrics.</td>
+ *     </tr>
+ * </table>
+ */
+public class MetricsFactory {
+    private static final Logger LOGGER = LoggerFactory.getLogger(MetricsFactory.class);
+
+    @Valid
+    @NotNull
+    private Duration frequency = Duration.minutes(1);
+
+    @Valid
+    @NotNull
+    private ImmutableList<ReporterFactory> reporters = ImmutableList.of();
+
+    @JsonProperty
+    public ImmutableList<ReporterFactory> getReporters() {
+        return reporters;
+    }
+
+    @JsonProperty
+    public void setReporters(ImmutableList<ReporterFactory> reporters) {
+        this.reporters = reporters;
+    }
+
+    @JsonProperty
+    public Duration getFrequency() {
+        return frequency;
+    }
+
+    @JsonProperty
+    public void setFrequency(Duration frequency) {
+        this.frequency = frequency;
+    }
+
+    /**
+     * Configures the given lifecycle with the {@link com.codahale.metrics.ScheduledReporter
+     * reporters} configured for the given registry.
+     * <p />
+     * The reporters are tied in to the given lifecycle, such that their {@link #getFrequency()
+     * frequency} for reporting metrics begins when the lifecycle {@link
+     * io.dropwizard.lifecycle.Managed#start() starts}, and stops when the lifecycle
+     * {@link io.dropwizard.lifecycle.Managed#stop() stops}.
+     *
+     * @param environment the lifecycle to manage the reporters.
+     * @param registry the metric registry to report metrics from.
+     */
+    public void configure(LifecycleEnvironment environment, MetricRegistry registry) {
+        for (ReporterFactory reporter : reporters) {
+            try {
+                final ScheduledReporterManager manager =
+                        new ScheduledReporterManager(reporter.build(registry),
+                                                     reporter.getFrequency().or(getFrequency()));
+                environment.manage(manager);
+            } catch (Exception e) {
+                LOGGER.warn("Failed to create reporter, metrics may not be properly reported.", e);
+            }
+        }
+    }
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ReporterFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ReporterFactory.java
new file mode 100644
index 0000000..fda9f88
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ReporterFactory.java
@@ -0,0 +1,42 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.google.common.base.Optional;
+import io.dropwizard.jackson.Discoverable;
+import io.dropwizard.util.Duration;
+
+/**
+ * A service provider interface for creating metrics {@link ScheduledReporter reporters}.
+ * <p/>
+ * To create your own, just:
+ * <ol>
+ *     <li>Create a class which implements {@link ReporterFactory}.</li>
+ *     <li>Annotate it with {@code @JsonTypeName} and give it a unique type name.</li>
+ *     <li>Add a {@code META-INF/services/io.dropwizard.metrics.ReporterFactory}
+ *     file with your implementation's full class name to the class path.</li>
+ * </ol>
+ *
+ * @see ConsoleReporterFactory
+ * @see CsvReporterFactory
+ * @see Slf4jReporterFactory
+ */
+ at JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+public interface ReporterFactory extends Discoverable {
+    /**
+     * Returns the frequency for reporting metrics.
+     *
+     * @return the frequency for reporting metrics.
+     */
+    Optional<Duration> getFrequency();
+
+    /**
+     * Configures and builds a {@link ScheduledReporter} instance for the given registry.
+     *
+     * @param registry the metrics registry to report metrics from.
+     *
+     * @return a reporter configured for the given metrics registry.
+     */
+    ScheduledReporter build(MetricRegistry registry);
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ScheduledReporterManager.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ScheduledReporterManager.java
new file mode 100644
index 0000000..42f5b85
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ScheduledReporterManager.java
@@ -0,0 +1,44 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.ScheduledReporter;
+import io.dropwizard.lifecycle.Managed;
+import io.dropwizard.util.Duration;
+
+/**
+ * Manages a {@link ScheduledReporter} lifecycle.
+ */
+public class ScheduledReporterManager implements Managed {
+    private final ScheduledReporter reporter;
+    private final Duration period;
+
+    /**
+     * Manages the given {@code reporter} by reporting with the given {@code period}.
+     *
+     * @param reporter the reporter to manage.
+     * @param period the frequency to report metrics at.
+     */
+    public ScheduledReporterManager(ScheduledReporter reporter, Duration period) {
+        this.reporter = reporter;
+        this.period = period;
+    }
+
+    /**
+     * Begins reporting metrics using the configured {@link ScheduledReporter}.
+     *
+     * @throws Exception
+     */
+    @Override
+    public void start() throws Exception {
+        reporter.start(period.getQuantity(), period.getUnit());
+    }
+
+    /**
+     * Stops the configured {@link ScheduledReporter} from reporting metrics.
+     *
+     * @throws Exception
+     */
+    @Override
+    public void stop() throws Exception {
+        reporter.stop();
+    }
+}
diff --git a/dropwizard-metrics/src/main/java/io/dropwizard/metrics/Slf4jReporterFactory.java b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/Slf4jReporterFactory.java
new file mode 100644
index 0000000..1603cba
--- /dev/null
+++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/Slf4jReporterFactory.java
@@ -0,0 +1,81 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.ScheduledReporter;
+import com.codahale.metrics.Slf4jReporter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MarkerFactory;
+
+/**
+ * A {@link ReporterFactory} for {@link Slf4jReporter} instances.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>logger</td>
+ *         <td>metrics</td>
+ *         <td>The name of the logger to write metrics to.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>markerName</td>
+ *         <td>(none)</td>
+ *         <td>The name of the marker to mark logged metrics with.</td>
+ *     </tr>
+ *     <tr>
+ *         <td colspan="3">See {@link BaseReporterFactory} for more options.</td>
+ *     </tr>
+ * </table>
+ */
+ at JsonTypeName("log")
+public class Slf4jReporterFactory extends BaseReporterFactory {
+    @NotEmpty
+    private String loggerName = "metrics";
+
+    private String markerName;
+
+    @JsonProperty("logger")
+    public String getLoggerName() {
+        return loggerName;
+    }
+
+    @JsonProperty("logger")
+    public void setLoggerName(String loggerName) {
+        this.loggerName = loggerName;
+    }
+
+    public Logger getLogger() {
+        return LoggerFactory.getLogger(getLoggerName());
+    }
+
+    @JsonProperty
+    public String getMarkerName() {
+        return markerName;
+    }
+
+    @JsonProperty
+    public void setMarkerName(String markerName) {
+        this.markerName = markerName;
+    }
+
+    public ScheduledReporter build(MetricRegistry registry) {
+        final Slf4jReporter.Builder builder = Slf4jReporter.forRegistry(registry)
+                                                           .convertDurationsTo(getDurationUnit())
+                                                           .convertRatesTo(getRateUnit())
+                                                           .filter(getFilter())
+                                                           .outputTo(getLogger());
+        if (markerName != null) {
+            builder.markWith(MarkerFactory.getMarker(markerName));
+        }
+
+        return builder.build();
+    }
+}
diff --git a/dropwizard-metrics/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/dropwizard-metrics/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
new file mode 100644
index 0000000..ded5391
--- /dev/null
+++ b/dropwizard-metrics/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
@@ -0,0 +1 @@
+io.dropwizard.metrics.ReporterFactory
diff --git a/dropwizard-metrics/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory b/dropwizard-metrics/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory
new file mode 100644
index 0000000..c7a251c
--- /dev/null
+++ b/dropwizard-metrics/src/main/resources/META-INF/services/io.dropwizard.metrics.ReporterFactory
@@ -0,0 +1,3 @@
+io.dropwizard.metrics.ConsoleReporterFactory
+io.dropwizard.metrics.CsvReporterFactory
+io.dropwizard.metrics.Slf4jReporterFactory
diff --git a/dropwizard-metrics/src/test/java/io/dropwizard/metrics/ConsoleReporterFactoryTest.java b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/ConsoleReporterFactoryTest.java
new file mode 100644
index 0000000..c2e5a9b
--- /dev/null
+++ b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/ConsoleReporterFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.metrics;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class ConsoleReporterFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(ConsoleReporterFactory.class);
+    }
+}
diff --git a/dropwizard-metrics/src/test/java/io/dropwizard/metrics/CsvReporterFactoryTest.java b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/CsvReporterFactoryTest.java
new file mode 100644
index 0000000..2b66836
--- /dev/null
+++ b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/CsvReporterFactoryTest.java
@@ -0,0 +1,47 @@
+package io.dropwizard.metrics;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.Resources;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import io.dropwizard.lifecycle.setup.LifecycleEnvironment;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import java.io.File;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class CsvReporterFactoryTest {
+    private final ObjectMapper objectMapper = Jackson.newObjectMapper();
+    private final ConfigurationFactory<MetricsFactory> factory =
+            new ConfigurationFactory<>(MetricsFactory.class,
+                                       Validation.buildDefaultValidatorFactory().getValidator(),
+                                       objectMapper, "dw");
+
+    @Before
+    public void setUp() throws Exception {
+        objectMapper.getSubtypeResolver().registerSubtypes(ConsoleReporterFactory.class,
+                                                           CsvReporterFactory.class,
+                                                           Slf4jReporterFactory.class);
+    }
+
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(CsvReporterFactory.class);
+    }
+
+    @Test
+    public void directoryCreatedOnStartup() throws Exception {
+        File dir = new File("metrics");
+        dir.delete();
+
+        MetricsFactory config = factory.build(new File(Resources.getResource("yaml/metrics.yml").toURI()));
+        config.configure(new LifecycleEnvironment(), new MetricRegistry());
+        assertThat(dir.exists()).isEqualTo(true);
+    }
+}
diff --git a/dropwizard-metrics/src/test/java/io/dropwizard/metrics/MetricsFactoryTest.java b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/MetricsFactoryTest.java
new file mode 100644
index 0000000..ca4cf03
--- /dev/null
+++ b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/MetricsFactoryTest.java
@@ -0,0 +1,50 @@
+package io.dropwizard.metrics;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.io.Resources;
+import io.dropwizard.configuration.ConfigurationFactory;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.util.Duration;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import java.io.File;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class MetricsFactoryTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    private final ObjectMapper objectMapper = Jackson.newObjectMapper();
+    private final ConfigurationFactory<MetricsFactory> factory =
+            new ConfigurationFactory<>(MetricsFactory.class,
+                                       Validation.buildDefaultValidatorFactory().getValidator(),
+                                       objectMapper, "dw");
+    private MetricsFactory config;
+
+    @Before
+    public void setUp() throws Exception {
+        objectMapper.getSubtypeResolver().registerSubtypes(ConsoleReporterFactory.class,
+                                                           CsvReporterFactory.class,
+                                                           Slf4jReporterFactory.class);
+
+        this.config = factory.build(new File(Resources.getResource("yaml/metrics.yml").toURI()));
+    }
+
+    @Test
+    public void hasADefaultFrequency() throws Exception {
+        assertThat(config.getFrequency())
+                .isEqualTo(Duration.seconds(10));
+    }
+
+    @Test
+    public void hasReporters() throws Exception {
+        CsvReporterFactory csvReporter = new CsvReporterFactory();
+        csvReporter.setFile(new File("metrics"));
+        assertThat(config.getReporters()).hasSize(3);
+    }
+}
diff --git a/dropwizard-metrics/src/test/java/io/dropwizard/metrics/Slf4jReporterFactoryTest.java b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/Slf4jReporterFactoryTest.java
new file mode 100644
index 0000000..1bdaf3c
--- /dev/null
+++ b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/Slf4jReporterFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.metrics;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class Slf4jReporterFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(Slf4jReporterFactory.class);
+    }
+}
diff --git a/dropwizard-metrics/src/test/resources/yaml/metrics.yml b/dropwizard-metrics/src/test/resources/yaml/metrics.yml
new file mode 100644
index 0000000..b66d4d4
--- /dev/null
+++ b/dropwizard-metrics/src/test/resources/yaml/metrics.yml
@@ -0,0 +1,11 @@
+frequency: 10 seconds
+reporters:
+  - type: console
+    output: stdout
+    timeZone: PST
+    durationUnit: milliseconds
+    rateUnit: seconds
+  - type: csv
+    file: metrics
+  - type: log
+    logger: metrics
diff --git a/dropwizard-migrations/pom.xml b/dropwizard-migrations/pom.xml
new file mode 100644
index 0000000..aee2e2a
--- /dev/null
+++ b/dropwizard-migrations/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-migrations</artifactId>
+    <name>Dropwizard Migrations</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-db</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.liquibase</groupId>
+            <artifactId>liquibase-core</artifactId>
+            <version>3.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.mattbertolini</groupId>
+            <artifactId>liquibase-slf4j</artifactId>
+            <version>1.2.1</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.liquibase</groupId>
+                    <artifactId>liquibase-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/AbstractLiquibaseCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/AbstractLiquibaseCommand.java
new file mode 100644
index 0000000..6e8ab32
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/AbstractLiquibaseCommand.java
@@ -0,0 +1,99 @@
+package io.dropwizard.migrations;
+
+import com.codahale.metrics.MetricRegistry;
+import io.dropwizard.Configuration;
+import io.dropwizard.cli.ConfiguredCommand;
+import io.dropwizard.db.DataSourceFactory;
+import io.dropwizard.db.DatabaseConfiguration;
+import io.dropwizard.db.ManagedDataSource;
+import io.dropwizard.setup.Bootstrap;
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.exception.LiquibaseException;
+import liquibase.exception.ValidationFailedException;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.sql.SQLException;
+
+public abstract class AbstractLiquibaseCommand<T extends Configuration> extends ConfiguredCommand<T> {
+    private final DatabaseConfiguration<T> strategy;
+    private final Class<T> configurationClass;
+
+    protected AbstractLiquibaseCommand(String name,
+                                       String description,
+                                       DatabaseConfiguration<T> strategy,
+                                       Class<T> configurationClass) {
+        super(name, description);
+        this.strategy = strategy;
+        this.configurationClass = configurationClass;
+    }
+
+    @Override
+    protected Class<T> getConfigurationClass() {
+        return configurationClass;
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("--migrations")
+                .dest("migrations-file")
+                .help("the file containing the Liquibase migrations for the application");
+
+        subparser.addArgument("--catalog")
+                .dest("catalog")
+                .help("Specify the database catalog (use database default if omitted)");
+
+        subparser.addArgument("--schema")
+                .dest("schema")
+                .help("Specify the database schema (use database default if omitted)");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    protected void run(Bootstrap<T> bootstrap, Namespace namespace, T configuration) throws Exception {
+        final DataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration);
+        dbConfig.setMaxSize(1);
+        dbConfig.setMinSize(1);
+        dbConfig.setInitialSize(1);
+
+        try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) {
+            run(namespace, liquibase);
+        } catch (ValidationFailedException e) {
+            e.printDescriptiveError(System.err);
+            throw e;
+        }
+    }
+
+    private CloseableLiquibase openLiquibase(final DataSourceFactory dataSourceFactory, final Namespace namespace)
+            throws ClassNotFoundException, SQLException, LiquibaseException {
+        final CloseableLiquibase liquibase;
+        final ManagedDataSource dataSource = dataSourceFactory.build(new MetricRegistry(), "liquibase");
+
+        final String migrationsFile = namespace.getString("migrations-file");
+        if (migrationsFile == null) {
+            liquibase = new CloseableLiquibase(dataSource);
+        } else {
+            liquibase = new CloseableLiquibase(dataSource, migrationsFile);
+        }
+
+        final Database database = liquibase.getDatabase();
+        final String catalogName = namespace.getString("catalog");
+        final String schemaName = namespace.getString("schema");
+
+        if(database.supportsCatalogs() && catalogName != null) {
+            database.setDefaultCatalogName(catalogName);
+            database.setOutputDefaultCatalog(true);
+        }
+        if(database.supportsSchemas() && schemaName != null) {
+            database.setDefaultSchemaName(schemaName);
+            database.setOutputDefaultSchema(true);
+        }
+
+        return liquibase;
+    }
+
+    protected abstract void run(Namespace namespace, Liquibase liquibase) throws Exception;
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibase.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibase.java
new file mode 100644
index 0000000..944ba55
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibase.java
@@ -0,0 +1,34 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.db.ManagedDataSource;
+import liquibase.Liquibase;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.LiquibaseException;
+import liquibase.resource.ClassLoaderResourceAccessor;
+import liquibase.resource.FileSystemResourceAccessor;
+
+import java.sql.SQLException;
+
+public class CloseableLiquibase extends Liquibase implements AutoCloseable {
+    private static final String DEFAULT_MIGRATIONS_FILE = "migrations.xml";
+    private final ManagedDataSource dataSource;
+
+    public CloseableLiquibase(ManagedDataSource dataSource) throws LiquibaseException, ClassNotFoundException, SQLException {
+        super(DEFAULT_MIGRATIONS_FILE,
+              new ClassLoaderResourceAccessor(),
+              new JdbcConnection(dataSource.getConnection()));
+        this.dataSource = dataSource;
+    }
+
+    public CloseableLiquibase(ManagedDataSource dataSource, String file) throws LiquibaseException, ClassNotFoundException, SQLException {
+        super(file,
+              new FileSystemResourceAccessor(),
+              new JdbcConnection(dataSource.getConnection()));
+        this.dataSource = dataSource;
+    }
+
+    @Override
+    public void close() throws Exception {
+        dataSource.stop();
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCalculateChecksumCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCalculateChecksumCommand.java
new file mode 100644
index 0000000..43e2d93
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCalculateChecksumCommand.java
@@ -0,0 +1,35 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import liquibase.change.CheckSum;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DbCalculateChecksumCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    private static final Logger LOGGER = LoggerFactory.getLogger("liquibase");
+
+    public DbCalculateChecksumCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("calculate-checksum", "Calculates and prints a checksum for a change set", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("id").nargs(1).help("change set id");
+        subparser.addArgument("author").nargs(1).help("author name");
+    }
+
+    @Override
+    public void run(Namespace namespace,
+                    Liquibase liquibase) throws Exception {
+        final CheckSum checkSum = liquibase.calculateCheckSum("migrations.xml",
+                                                              namespace.<String>getList("id").get(0),
+                                                              namespace.<String>getList("author").get(0));
+        LOGGER.info("checksum = {}", checkSum);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbClearChecksumsCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbClearChecksumsCommand.java
new file mode 100644
index 0000000..4c4e8e3
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbClearChecksumsCommand.java
@@ -0,0 +1,21 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.inf.Namespace;
+
+public class DbClearChecksumsCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbClearChecksumsCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("clear-checksums",
+              "Removes all saved checksums from the database log",
+              strategy,
+              configurationClass);
+    }
+
+    @Override
+    public void run(Namespace namespace,
+                    Liquibase liquibase) throws Exception {
+        liquibase.clearCheckSums();
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCommand.java
new file mode 100644
index 0000000..abcf50f
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCommand.java
@@ -0,0 +1,54 @@
+package io.dropwizard.migrations;
+
+import com.google.common.collect.Maps;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.util.SortedMap;
+
+public class DbCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    private static final String COMMAND_NAME_ATTR = "subcommand";
+    private final SortedMap<String, AbstractLiquibaseCommand<T>> subcommands;
+
+    public DbCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("db", "Run database migration tasks", strategy, configurationClass);
+        this.subcommands = Maps.newTreeMap();
+        addSubcommand(new DbCalculateChecksumCommand<>(strategy, configurationClass));
+        addSubcommand(new DbClearChecksumsCommand<>(strategy, configurationClass));
+        addSubcommand(new DbDropAllCommand<>(strategy, configurationClass));
+        addSubcommand(new DbDumpCommand<>(strategy, configurationClass));
+        addSubcommand(new DbFastForwardCommand<>(strategy, configurationClass));
+        addSubcommand(new DbGenerateDocsCommand<>(strategy, configurationClass));
+        addSubcommand(new DbLocksCommand<>(strategy, configurationClass));
+        addSubcommand(new DbMigrateCommand<>(strategy, configurationClass));
+        addSubcommand(new DbPrepareRollbackCommand<>(strategy, configurationClass));
+        addSubcommand(new DbRollbackCommand<>(strategy, configurationClass));
+        addSubcommand(new DbStatusCommand<>(strategy, configurationClass));
+        addSubcommand(new DbTagCommand<>(strategy, configurationClass));
+        addSubcommand(new DbTestCommand<>(strategy, configurationClass));
+    }
+
+    private void addSubcommand(AbstractLiquibaseCommand<T> subcommand) {
+        subcommands.put(subcommand.getName(), subcommand);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        for (AbstractLiquibaseCommand<T> subcommand : subcommands.values()) {
+            final Subparser cmdParser = subparser.addSubparsers()
+                                                 .addParser(subcommand.getName())
+                                                 .setDefault(COMMAND_NAME_ATTR, subcommand.getName())
+                                                 .description(subcommand.getDescription());
+            subcommand.configure(cmdParser);
+        }
+    }
+
+    @Override
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        final AbstractLiquibaseCommand<T> subcommand = subcommands.get(namespace.getString(COMMAND_NAME_ATTR));
+        subcommand.run(namespace, liquibase);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDropAllCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDropAllCommand.java
new file mode 100644
index 0000000..e7c7ba1
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDropAllCommand.java
@@ -0,0 +1,28 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+public class DbDropAllCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbDropAllCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("drop-all", "Delete all user-owned objects from the database.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+        subparser.addArgument("--confirm-delete-everything")
+                 .action(Arguments.storeTrue())
+                 .required(true)
+                 .help("indicate you understand this deletes everything in your database");
+    }
+
+    @Override
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        liquibase.dropAll();
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDumpCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDumpCommand.java
new file mode 100644
index 0000000..01707b2
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDumpCommand.java
@@ -0,0 +1,215 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.CatalogAndSchema;
+import liquibase.Liquibase;
+import liquibase.database.Database;
+import liquibase.diff.DiffGeneratorFactory;
+import liquibase.diff.DiffResult;
+import liquibase.diff.compare.CompareControl;
+import liquibase.diff.output.DiffOutputControl;
+import liquibase.diff.output.changelog.DiffToChangeLog;
+import liquibase.exception.DatabaseException;
+import liquibase.exception.UnexpectedLiquibaseException;
+import liquibase.snapshot.DatabaseSnapshot;
+import liquibase.snapshot.InvalidExampleException;
+import liquibase.snapshot.SnapshotControl;
+import liquibase.snapshot.SnapshotGeneratorFactory;
+import liquibase.structure.DatabaseObject;
+import liquibase.structure.core.Column;
+import liquibase.structure.core.Data;
+import liquibase.structure.core.ForeignKey;
+import liquibase.structure.core.Index;
+import liquibase.structure.core.PrimaryKey;
+import liquibase.structure.core.Sequence;
+import liquibase.structure.core.Table;
+import liquibase.structure.core.UniqueConstraint;
+import liquibase.structure.core.View;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.ArgumentGroup;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import javax.xml.parsers.ParserConfigurationException;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.util.HashSet;
+import java.util.Set;
+
+public class DbDumpCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbDumpCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("dump",
+              "Generate a dump of the existing database state.",
+              strategy,
+              configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-o", "--output")
+                 .dest("output")
+                 .help("Write output to <file> instead of stdout");
+
+        final ArgumentGroup tables = subparser.addArgumentGroup("Tables");
+        tables.addArgument("--tables")
+              .action(Arguments.storeTrue())
+              .dest("tables")
+              .help("Check for added or removed tables (default)");
+        tables.addArgument("--ignore-tables")
+              .action(Arguments.storeFalse())
+              .dest("tables")
+              .help("Ignore tables");
+
+        final ArgumentGroup columns = subparser.addArgumentGroup("Columns");
+        columns.addArgument("--columns")
+               .action(Arguments.storeTrue())
+               .dest("columns")
+               .help("Check for added, removed, or modified tables (default)");
+        columns.addArgument("--ignore-columns")
+               .action(Arguments.storeFalse())
+               .dest("columns")
+               .help("Ignore columns");
+
+        final ArgumentGroup views = subparser.addArgumentGroup("Views");
+        views.addArgument("--views")
+             .action(Arguments.storeTrue())
+             .dest("views")
+             .help("Check for added, removed, or modified views (default)");
+        views.addArgument("--ignore-views")
+             .action(Arguments.storeFalse())
+             .dest("views")
+             .help("Ignore views");
+
+        final ArgumentGroup primaryKeys = subparser.addArgumentGroup("Primary Keys");
+        primaryKeys.addArgument("--primary-keys")
+                   .action(Arguments.storeTrue())
+                   .dest("primary-keys")
+                   .help("Check for changed primary keys (default)");
+        primaryKeys.addArgument("--ignore-primary-keys")
+                   .action(Arguments.storeFalse())
+                   .dest("primary-keys")
+                   .help("Ignore primary keys");
+
+        final ArgumentGroup uniqueConstraints = subparser.addArgumentGroup("Unique Constraints");
+        uniqueConstraints.addArgument("--unique-constraints")
+                         .action(Arguments.storeTrue())
+                         .dest("unique-constraints")
+                         .help("Check for changed unique constraints (default)");
+        uniqueConstraints.addArgument("--ignore-unique-constraints")
+                         .action(Arguments.storeFalse())
+                         .dest("unique-constraints")
+                         .help("Ignore unique constraints");
+
+        final ArgumentGroup indexes = subparser.addArgumentGroup("Indexes");
+        indexes.addArgument("--indexes")
+               .action(Arguments.storeTrue())
+               .dest("indexes")
+               .help("Check for changed indexes (default)");
+        indexes.addArgument("--ignore-indexes")
+               .action(Arguments.storeFalse())
+               .dest("indexes")
+               .help("Ignore indexes");
+
+        final ArgumentGroup foreignKeys = subparser.addArgumentGroup("Foreign Keys");
+        foreignKeys.addArgument("--foreign-keys")
+                   .action(Arguments.storeTrue())
+                   .dest("foreign-keys")
+                   .help("Check for changed foreign keys (default)");
+        foreignKeys.addArgument("--ignore-foreign-keys")
+                   .action(Arguments.storeFalse())
+                   .dest("foreign-keys")
+                   .help("Ignore foreign keys");
+
+        final ArgumentGroup sequences = subparser.addArgumentGroup("Sequences");
+        sequences.addArgument("--sequences")
+                 .action(Arguments.storeTrue())
+                 .dest("sequences")
+                 .help("Check for changed sequences (default)");
+        sequences.addArgument("--ignore-sequences")
+                 .action(Arguments.storeFalse())
+                 .dest("sequences")
+                 .help("Ignore foreign keys");
+
+        final ArgumentGroup data = subparser.addArgumentGroup("Data");
+        data.addArgument("--data")
+            .action(Arguments.storeTrue())
+            .dest("data")
+            .help("Check for changed data")
+            .setDefault(Boolean.FALSE);
+        data.addArgument("--ignore-data")
+            .action(Arguments.storeFalse())
+            .dest("data")
+            .help("Ignore data (default)")
+            .setDefault(Boolean.FALSE);
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        final Set<Class<? extends DatabaseObject>> compareTypes = new HashSet<>();
+
+        if (namespace.getBoolean("columns")) {
+            compareTypes.add(Column.class);
+        }
+        if (namespace.getBoolean("data")) {
+            compareTypes.add(Data.class);
+        }
+        if (namespace.getBoolean("foreign-keys")) {
+            compareTypes.add(ForeignKey.class);
+        }
+        if (namespace.getBoolean("indexes")) {
+            compareTypes.add(Index.class);
+        }
+        if (namespace.getBoolean("primary-keys")) {
+            compareTypes.add(PrimaryKey.class);
+        }
+        if (namespace.getBoolean("sequences")) {
+            compareTypes.add(Sequence.class);
+        }
+        if (namespace.getBoolean("tables")) {
+            compareTypes.add(Table.class);
+        }
+        if (namespace.getBoolean("unique-constraints")) {
+            compareTypes.add(UniqueConstraint.class);
+        }
+        if (namespace.getBoolean("views")) {
+            compareTypes.add(View.class);
+        }
+
+        final DiffToChangeLog diffToChangeLog = new DiffToChangeLog(new DiffOutputControl());
+        final Database database = liquibase.getDatabase();
+
+        final String filename = namespace.getString("output");
+        if (filename != null) {
+            try (PrintStream file = new PrintStream(filename, StandardCharsets.UTF_8.name())) {
+                generateChangeLog(database, database.getDefaultSchema(), diffToChangeLog, file, compareTypes);
+            }
+        } else {
+            generateChangeLog(database, database.getDefaultSchema(), diffToChangeLog, System.out, compareTypes);
+        }
+    }
+
+    private void generateChangeLog(final Database database, final CatalogAndSchema catalogAndSchema,
+                                   final DiffToChangeLog changeLogWriter, PrintStream outputStream,
+                                   final Set<Class<? extends DatabaseObject>> compareTypes) throws DatabaseException, IOException, ParserConfigurationException {
+        @SuppressWarnings("unchecked")
+        final SnapshotControl snapshotControl = new SnapshotControl(database, compareTypes.toArray(new Class[compareTypes.size()]));
+        final CompareControl compareControl = new CompareControl(new CompareControl.SchemaComparison[]{new CompareControl.SchemaComparison(catalogAndSchema, catalogAndSchema)}, compareTypes);
+        final CatalogAndSchema[] compareControlSchemas = compareControl.getSchemas(CompareControl.DatabaseRole.REFERENCE);
+
+        try {
+            final DatabaseSnapshot referenceSnapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(compareControlSchemas, database, snapshotControl);
+            final DatabaseSnapshot comparisonSnapshot = SnapshotGeneratorFactory.getInstance().createSnapshot(compareControlSchemas, null, snapshotControl);
+            final DiffResult diffResult = DiffGeneratorFactory.getInstance().compare(referenceSnapshot, comparisonSnapshot, compareControl);
+
+            changeLogWriter.setDiffResult(diffResult);
+            changeLogWriter.print(outputStream);
+        } catch (InvalidExampleException e) {
+            throw new UnexpectedLiquibaseException(e);
+        }
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbFastForwardCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbFastForwardCommand.java
new file mode 100644
index 0000000..e5ae706
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbFastForwardCommand.java
@@ -0,0 +1,72 @@
+package io.dropwizard.migrations;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+public class DbFastForwardCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    protected DbFastForwardCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("fast-forward",
+              "Mark the next pending change set as applied without running it",
+              strategy,
+              configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-n", "--dry-run")
+                 .action(Arguments.storeTrue())
+                 .dest("dry-run")
+                 .setDefault(Boolean.FALSE)
+                 .help("output the DDL to stdout, don't run it");
+
+        subparser.addArgument("-a", "--all")
+                 .action(Arguments.storeTrue())
+                 .dest("all")
+                 .setDefault(Boolean.FALSE)
+                 .help("mark all pending change sets as applied");
+
+        subparser.addArgument("-i", "--include")
+                 .action(Arguments.append())
+                 .dest("contexts")
+                 .help("include change sets from the given context");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace,
+                    Liquibase liquibase) throws Exception {
+        final String context = getContext(namespace);
+        if (namespace.getBoolean("all")) {
+            if (namespace.getBoolean("dry-run")) {
+                liquibase.changeLogSync(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.changeLogSync(context);
+            }
+        } else {
+            if (namespace.getBoolean("dry-run")) {
+                liquibase.markNextChangeSetRan(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.markNextChangeSetRan(context);
+            }
+        }
+    }
+
+    private String getContext(Namespace namespace) {
+        final List<Object> contexts = namespace.getList("contexts");
+        if (contexts == null) {
+            return "";
+        }
+        return Joiner.on(',').join(contexts);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbGenerateDocsCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbGenerateDocsCommand.java
new file mode 100644
index 0000000..bd709f3
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbGenerateDocsCommand.java
@@ -0,0 +1,25 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+public class DbGenerateDocsCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbGenerateDocsCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("generate-docs", "Generate documentation about the database state.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("output").nargs(1).help("output directory");
+    }
+
+    @Override
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        liquibase.generateDocumentation(namespace.<String>getList("output").get(0));
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbLocksCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbLocksCommand.java
new file mode 100644
index 0000000..9f1c595
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbLocksCommand.java
@@ -0,0 +1,46 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+public class DbLocksCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbLocksCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("locks", "Manage database migration locks", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-l", "--list")
+                 .dest("list")
+                 .action(Arguments.storeTrue())
+                 .setDefault(Boolean.FALSE)
+                 .help("list all open locks");
+
+        subparser.addArgument("-r", "--force-release")
+                 .dest("release")
+                 .action(Arguments.storeTrue())
+                 .setDefault(Boolean.FALSE)
+                 .help("forcibly release all open locks");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        final Boolean list = namespace.getBoolean("list");
+        final Boolean release = namespace.getBoolean("release");
+
+        if (!list && !release) {
+            throw new IllegalArgumentException("Must specify either --list or --force-release");
+        } else if (list) {
+            liquibase.reportLocks(System.out);
+        } else {
+            liquibase.forceReleaseLocks();
+        }
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbMigrateCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbMigrateCommand.java
new file mode 100644
index 0000000..a87915a
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbMigrateCommand.java
@@ -0,0 +1,69 @@
+package io.dropwizard.migrations;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+public class DbMigrateCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbMigrateCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("migrate", "Apply all pending change sets.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-n", "--dry-run")
+                 .action(Arguments.storeTrue())
+                 .dest("dry-run")
+                 .setDefault(Boolean.FALSE)
+                 .help("output the DDL to stdout, don't run it");
+
+        subparser.addArgument("-c", "--count")
+                 .type(Integer.class)
+                 .dest("count")
+                 .help("only apply the next N change sets");
+
+        subparser.addArgument("-i", "--include")
+                 .action(Arguments.append())
+                 .dest("contexts")
+                 .help("include change sets from the given context");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        final String context = getContext(namespace);
+        final Integer count = namespace.getInt("count");
+        final Boolean dryRun = namespace.getBoolean("dry-run");
+        if (count != null) {
+            if (dryRun) {
+                liquibase.update(count, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.update(count, context);
+            }
+        } else {
+            if (dryRun) {
+                liquibase.update(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.update(context);
+            }
+        }
+    }
+
+    private String getContext(Namespace namespace) {
+        final List<Object> contexts = namespace.getList("contexts");
+        if (contexts == null) {
+            return "";
+        }
+        return Joiner.on(',').join(contexts);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbPrepareRollbackCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbPrepareRollbackCommand.java
new file mode 100644
index 0000000..4132456
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbPrepareRollbackCommand.java
@@ -0,0 +1,54 @@
+package io.dropwizard.migrations;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+public class DbPrepareRollbackCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbPrepareRollbackCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("prepare-rollback", "Generate rollback DDL scripts for pending change sets.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-c", "--count")
+                 .dest("count")
+                 .type(Integer.class)
+                 .help("limit script to the specified number of pending change sets");
+
+        subparser.addArgument("-i", "--include")
+                 .action(Arguments.append())
+                 .dest("contexts")
+                 .help("include change sets from the given context");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        final String context = getContext(namespace);
+        final Integer count = namespace.getInt("count");
+        if (count != null) {
+            liquibase.futureRollbackSQL(count, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+        } else {
+            liquibase.futureRollbackSQL(context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+        }
+    }
+
+    private String getContext(Namespace namespace) {
+        final List<Object> contexts = namespace.getList("contexts");
+        if (contexts == null) {
+            return "";
+        }
+        return Joiner.on(',').join(contexts);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbRollbackCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbRollbackCommand.java
new file mode 100644
index 0000000..eab16ec
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbRollbackCommand.java
@@ -0,0 +1,92 @@
+package io.dropwizard.migrations;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.io.OutputStreamWriter;
+import java.util.Date;
+import java.util.List;
+
+public class DbRollbackCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbRollbackCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("rollback",
+              "Rollback the database schema to a previous version.",
+              strategy,
+              configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-n", "--dry-run")
+                 .action(Arguments.storeTrue())
+                 .dest("dry-run")
+                 .setDefault(Boolean.FALSE)
+                 .help("Output the DDL to stdout, don't run it");
+        subparser.addArgument("-t", "--tag").dest("tag").help("Rollback to the given tag");
+        subparser.addArgument("-d", "--date")
+                 .dest("date")
+                 .type(Date.class)
+                 .help("Rollback to the given date");
+        subparser.addArgument("-c", "--count")
+                 .dest("count")
+                 .type(Integer.class)
+                 .help("Rollback the specified number of change sets");
+        subparser.addArgument("-i", "--include")
+                 .action(Arguments.append())
+                 .dest("contexts")
+                 .help("include change sets from the given context");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        final String tag = namespace.getString("tag");
+        final Integer count = namespace.getInt("count");
+        final Date date = (Date) namespace.get("date");
+        final Boolean dryRun = namespace.getBoolean("dry-run");
+        final String context = getContext(namespace);
+
+        if (((count == null) && (tag == null) && (date == null)) ||
+                (((count != null) && (tag != null)) ||
+                        ((count != null) && (date != null)) ||
+                        ((tag != null) && (date != null)))) {
+            throw new IllegalArgumentException("Must specify either a count, a tag, or a date.");
+        }
+
+        if (count != null) {
+            if (dryRun) {
+                liquibase.rollback(count, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.rollback(count, context);
+            }
+        } else if (tag != null) {
+            if (dryRun) {
+                liquibase.rollback(tag, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.rollback(tag, context);
+            }
+        } else {
+            if (dryRun) {
+                liquibase.rollback(date, context, new OutputStreamWriter(System.out, Charsets.UTF_8));
+            } else {
+                liquibase.rollback(date, context);
+            }
+        }
+    }
+
+    private String getContext(Namespace namespace) {
+        final List<Object> contexts = namespace.getList("contexts");
+        if (contexts == null) {
+            return "";
+        }
+        return Joiner.on(',').join(contexts);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbStatusCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbStatusCommand.java
new file mode 100644
index 0000000..3445d89
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbStatusCommand.java
@@ -0,0 +1,49 @@
+package io.dropwizard.migrations;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Joiner;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.io.OutputStreamWriter;
+import java.util.List;
+
+public class DbStatusCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbStatusCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("status", "Check for pending change sets.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-v", "--verbose")
+                 .action(Arguments.storeTrue())
+                 .dest("verbose")
+                 .help("Output verbose information");
+        subparser.addArgument("-i", "--include")
+                 .action(Arguments.append())
+                 .dest("contexts")
+                 .help("include change sets from the given context");
+    }
+
+    @Override
+    @SuppressWarnings("UseOfSystemOutOrSystemErr")
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        liquibase.reportStatus(namespace.getBoolean("verbose"),
+                               getContext(namespace),
+                               new OutputStreamWriter(System.out, Charsets.UTF_8));
+    }
+
+    private String getContext(Namespace namespace) {
+        final List<Object> contexts = namespace.getList("contexts");
+        if (contexts == null) {
+            return "";
+        }
+        return Joiner.on(',').join(contexts);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTagCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTagCommand.java
new file mode 100644
index 0000000..316494a
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTagCommand.java
@@ -0,0 +1,25 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+public class DbTagCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbTagCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("tag", "Tag the database schema.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("tag-name").nargs(1).required(true).help("The tag name");
+    }
+
+    @Override
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        liquibase.tag(namespace.<String>getList("tag-name").get(0));
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTestCommand.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTestCommand.java
new file mode 100644
index 0000000..03dbdc1
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTestCommand.java
@@ -0,0 +1,40 @@
+package io.dropwizard.migrations;
+
+import com.google.common.base.Joiner;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import liquibase.Liquibase;
+import net.sourceforge.argparse4j.impl.Arguments;
+import net.sourceforge.argparse4j.inf.Namespace;
+import net.sourceforge.argparse4j.inf.Subparser;
+
+import java.util.List;
+
+public class DbTestCommand<T extends Configuration> extends AbstractLiquibaseCommand<T> {
+    public DbTestCommand(DatabaseConfiguration<T> strategy, Class<T> configurationClass) {
+        super("test", "Apply and rollback pending change sets.", strategy, configurationClass);
+    }
+
+    @Override
+    public void configure(Subparser subparser) {
+        super.configure(subparser);
+
+        subparser.addArgument("-i", "--include")
+                 .action(Arguments.append())
+                 .dest("contexts")
+                 .help("include change sets from the given context");
+    }
+
+    @Override
+    public void run(Namespace namespace, Liquibase liquibase) throws Exception {
+        liquibase.updateTestingRollback(getContext(namespace));
+    }
+
+    private String getContext(Namespace namespace) {
+        final List<Object> contexts = namespace.getList("contexts");
+        if (contexts == null) {
+            return "";
+        }
+        return Joiner.on(',').join(contexts);
+    }
+}
diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/MigrationsBundle.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/MigrationsBundle.java
new file mode 100644
index 0000000..88a4c6a
--- /dev/null
+++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/MigrationsBundle.java
@@ -0,0 +1,21 @@
+package io.dropwizard.migrations;
+
+import io.dropwizard.Bundle;
+import io.dropwizard.Configuration;
+import io.dropwizard.db.DatabaseConfiguration;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import io.dropwizard.util.Generics;
+
+public abstract class MigrationsBundle<T extends Configuration> implements Bundle, DatabaseConfiguration<T> {
+    @Override
+    public final void initialize(Bootstrap<?> bootstrap) {
+        final Class<T> klass = Generics.getTypeParameter(getClass(), Configuration.class);
+        bootstrap.addCommand(new DbCommand<>(this, klass));
+    }
+
+    @Override
+    public final void run(Environment environment) {
+        // nothing doing
+    }
+}
diff --git a/dropwizard-servlets/pom.xml b/dropwizard-servlets/pom.xml
new file mode 100644
index 0000000..2027f38
--- /dev/null
+++ b/dropwizard-servlets/pom.xml
@@ -0,0 +1,76 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-servlets</artifactId>
+    <name>Dropwizard Servlet Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>${slf4j.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-util</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-annotation</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.codahale.metrics</groupId>
+            <artifactId>metrics-core</artifactId>
+            <version>${metrics3.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.orbit</groupId>
+            <artifactId>javax.servlet</artifactId>
+            <version>${servlet.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-servlet</artifactId>
+            <version>${jetty.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>${logback.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <testResources>
+            <testResource>
+                <directory>src/test/more-resources</directory>
+            </testResource>
+            <testResource>
+                <directory>src/test/resources</directory>
+            </testResource>
+        </testResources>
+    </build>
+</project>
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/CacheBustingFilter.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/CacheBustingFilter.java
new file mode 100644
index 0000000..5e1df7e
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/CacheBustingFilter.java
@@ -0,0 +1,31 @@
+package io.dropwizard.servlets;
+
+import com.google.common.net.HttpHeaders;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Adds a no-cache header to all responses.
+ */
+public class CacheBustingFilter implements Filter {
+    private static final String CACHE_SETTINGS = "must-revalidate,no-cache,no-store";
+
+    @Override
+    public void doFilter(ServletRequest request,
+                         ServletResponse response,
+                         FilterChain chain) throws IOException, ServletException {
+        if (response instanceof HttpServletResponse) {
+            final HttpServletResponse resp = (HttpServletResponse) response;
+            resp.setHeader(HttpHeaders.CACHE_CONTROL, CACHE_SETTINGS);
+        }
+        chain.doFilter(request, response);
+    }
+
+    @Override
+    public void destroy() { /* unused */ }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException { /* unused */ }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/Servlets.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/Servlets.java
new file mode 100644
index 0000000..503236e
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/Servlets.java
@@ -0,0 +1,24 @@
+package io.dropwizard.servlets;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Utility functions for dealing with servlets.
+ */
+public class Servlets {
+    private Servlets() { /* singleton */ }
+
+    /**
+     * Returns the full URL of the given request.
+     *
+     * @param request    an HTTP servlet request
+     * @return the full URL, including the query string
+     */
+    public static String getFullUrl(HttpServletRequest request) {
+        final StringBuilder url = new StringBuilder(100).append(request.getRequestURI());
+        if (request.getQueryString() != null) {
+            url.append('?').append(request.getQueryString());
+        }
+        return url.toString();
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/SlowRequestFilter.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/SlowRequestFilter.java
new file mode 100644
index 0000000..1785b8a
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/SlowRequestFilter.java
@@ -0,0 +1,68 @@
+package io.dropwizard.servlets;
+
+import io.dropwizard.util.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+import static io.dropwizard.servlets.Servlets.getFullUrl;
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+/**
+ * A servlet filter which logs the methods and URIs of requests which take longer than a given
+ * duration of time to complete.
+ */
+ at SuppressWarnings("UnusedDeclaration")
+public class SlowRequestFilter implements Filter {
+    private static final Logger LOGGER = LoggerFactory.getLogger(SlowRequestFilter.class);
+    private final long threshold;
+
+    /**
+     * Creates a filter which logs requests which take longer than 1 second.
+     */
+    public SlowRequestFilter() {
+        this(Duration.seconds(1));
+    }
+
+    /**
+     * Creates a filter which logs requests which take longer than the given duration.
+     *
+     * @param threshold    the threshold for considering a request slow
+     */
+    public SlowRequestFilter(Duration threshold) {
+        this.threshold = threshold.toNanoseconds();
+    }
+
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException { /* unused */ }
+
+    @Override
+    public void destroy() { /* unused */ }
+
+    @Override
+    public void doFilter(ServletRequest request,
+                         ServletResponse response,
+                         FilterChain chain) throws IOException, ServletException {
+        final HttpServletRequest req = (HttpServletRequest) request;
+        final long startTime = System.nanoTime();
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            final long elapsedNS = System.nanoTime() - startTime;
+            final long elapsedMS = NANOSECONDS.toMillis(elapsedNS);
+            if (elapsedNS >= threshold) {
+                LOGGER.warn("Slow request: {} {} ({}ms)",
+                            req.getMethod(),
+                            getFullUrl(req), elapsedMS);
+            }
+        }
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/ThreadNameFilter.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/ThreadNameFilter.java
new file mode 100644
index 0000000..5e9874c
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/ThreadNameFilter.java
@@ -0,0 +1,38 @@
+package io.dropwizard.servlets;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+import static io.dropwizard.servlets.Servlets.getFullUrl;
+
+/**
+ * A servlet filter which adds the request method and URI to the thread name processing the request
+ * for the duration of the request.
+ */
+public class ThreadNameFilter implements Filter {
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException { /* unused */ }
+
+    @Override
+    public void destroy() { /* unused */ }
+
+    @Override
+    public void doFilter(ServletRequest request,
+                         ServletResponse response,
+                         FilterChain chain) throws IOException, ServletException {
+        final HttpServletRequest req = (HttpServletRequest) request;
+        final Thread current = Thread.currentThread();
+        final String oldName = current.getName();
+        try {
+            current.setName(formatName(req, oldName));
+            chain.doFilter(request, response);
+        } finally {
+            current.setName(oldName);
+        }
+    }
+
+    private static String formatName(HttpServletRequest req, String oldName) {
+        return oldName + " - " + req.getMethod() + ' ' + getFullUrl(req);
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/AssetServlet.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/AssetServlet.java
new file mode 100644
index 0000000..390435c
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/AssetServlet.java
@@ -0,0 +1,175 @@
+package io.dropwizard.servlets.assets;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.hash.Hashing;
+import com.google.common.io.Resources;
+import com.google.common.net.HttpHeaders;
+import com.google.common.net.MediaType;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+public class AssetServlet extends HttpServlet {
+    private static final long serialVersionUID = 6393345594784987908L;
+    private static final CharMatcher SLASHES = CharMatcher.is('/');
+
+    private static class CachedAsset {
+        private final byte[] resource;
+        private final String eTag;
+        private final long lastModifiedTime;
+
+        private CachedAsset(byte[] resource, long lastModifiedTime) {
+            this.resource = resource;
+            this.eTag = '"' + Hashing.murmur3_128().hashBytes(resource).toString() + '"';
+            this.lastModifiedTime = lastModifiedTime;
+        }
+
+        public byte[] getResource() {
+            return resource;
+        }
+
+        public String getETag() {
+            return eTag;
+        }
+
+        public long getLastModifiedTime() {
+            return lastModifiedTime;
+        }
+    }
+
+    private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.HTML_UTF_8;
+
+    private final String resourcePath;
+    private final String uriPath;
+    private final String indexFile;
+    private final Charset defaultCharset;
+
+    /**
+     * Creates a new {@code AssetServlet} that serves static assets loaded from {@code resourceURL}
+     * (typically a file: or jar: URL). The assets are served at URIs rooted at {@code uriPath}. For
+     * example, given a {@code resourceURL} of {@code "file:/data/assets"} and a {@code uriPath} of
+     * {@code "/js"}, an {@code AssetServlet} would serve the contents of {@code
+     * /data/assets/example.js} in response to a request for {@code /js/example.js}. If a directory
+     * is requested and {@code indexFile} is defined, then {@code AssetServlet} will attempt to
+     * serve a file with that name in that directory. If a directory is requested and {@code
+     * indexFile} is null, it will serve a 404.
+     *
+     * @param resourcePath   the base URL from which assets are loaded
+     * @param uriPath        the URI path fragment in which all requests are rooted
+     * @param indexFile      the filename to use when directories are requested, or null to serve no
+     *                       indexes
+     * @param defaultCharset the default character set
+     */
+    public AssetServlet(String resourcePath,
+                        String uriPath,
+                        String indexFile,
+                        Charset defaultCharset) {
+        final String trimmedPath = SLASHES.trimFrom(resourcePath);
+        this.resourcePath = trimmedPath.isEmpty() ? trimmedPath : trimmedPath + '/';
+        final String trimmedUri = SLASHES.trimTrailingFrom(uriPath);
+        this.uriPath = trimmedUri.isEmpty() ? "/" : trimmedUri;
+        this.indexFile = indexFile;
+        this.defaultCharset = defaultCharset;
+    }
+
+    public URL getResourceURL() {
+        return Resources.getResource(resourcePath);
+    }
+
+    public String getUriPath() {
+        return uriPath;
+    }
+
+    public String getIndexFile() {
+        return indexFile;
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest req,
+                         HttpServletResponse resp) throws ServletException, IOException {
+        try {
+            final StringBuilder builder = new StringBuilder(req.getServletPath());
+            if (req.getPathInfo() != null) {
+                builder.append(req.getPathInfo());
+            }
+            final CachedAsset cachedAsset = loadAsset(builder.toString());
+            if (cachedAsset == null) {
+                resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+                return;
+            }
+
+            if (isCachedClientSide(req, cachedAsset)) {
+                resp.sendError(HttpServletResponse.SC_NOT_MODIFIED);
+                return;
+            }
+
+            resp.setDateHeader(HttpHeaders.LAST_MODIFIED, cachedAsset.getLastModifiedTime());
+            resp.setHeader(HttpHeaders.ETAG, cachedAsset.getETag());
+
+            final String mimeTypeOfExtension = req.getServletContext()
+                                                  .getMimeType(req.getRequestURI());
+            MediaType mediaType = DEFAULT_MEDIA_TYPE;
+
+            if (mimeTypeOfExtension != null) {
+                try {
+                    mediaType = MediaType.parse(mimeTypeOfExtension);
+                    if (defaultCharset != null && mediaType.is(MediaType.ANY_TEXT_TYPE)) {
+                        mediaType = mediaType.withCharset(defaultCharset);
+                    }
+                } catch (IllegalArgumentException ignore) {}
+            }
+
+            resp.setContentType(mediaType.type() + '/' + mediaType.subtype());
+
+            if (mediaType.charset().isPresent()) {
+                resp.setCharacterEncoding(mediaType.charset().get().toString());
+            }
+
+            try (ServletOutputStream output = resp.getOutputStream()) {
+                output.write(cachedAsset.getResource());
+            }
+        } catch (RuntimeException | URISyntaxException ignored) {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    private CachedAsset loadAsset(String key) throws URISyntaxException, IOException {
+        checkArgument(key.startsWith(uriPath));
+        final String requestedResourcePath = SLASHES.trimFrom(key.substring(uriPath.length()));
+        final String absoluteRequestedResourcePath = SLASHES.trimFrom(this.resourcePath + requestedResourcePath);
+
+        URL requestedResourceURL = Resources.getResource(absoluteRequestedResourcePath);
+        if (ResourceURL.isDirectory(requestedResourceURL)) {
+            if (indexFile != null) {
+                requestedResourceURL = Resources.getResource(absoluteRequestedResourcePath + '/' + indexFile);
+            } else {
+                // directory requested but no index file defined
+                return null;
+            }
+        }
+
+        long lastModified = ResourceURL.getLastModified(requestedResourceURL);
+        if (lastModified < 1) {
+            // Something went wrong trying to get the last modified time: just use the current time
+            lastModified = System.currentTimeMillis();
+        }
+
+        // zero out the millis since the date we get back from If-Modified-Since will not have them
+        lastModified = (lastModified / 1000) * 1000;
+        return new CachedAsset(Resources.toByteArray(requestedResourceURL), lastModified);
+    }
+
+    private boolean isCachedClientSide(HttpServletRequest req, CachedAsset cachedAsset) {
+        return cachedAsset.getETag().equals(req.getHeader(HttpHeaders.IF_NONE_MATCH)) ||
+                (req.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE) >= cachedAsset.getLastModifiedTime());
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceNotFoundException.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceNotFoundException.java
new file mode 100644
index 0000000..7dc2b67
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceNotFoundException.java
@@ -0,0 +1,9 @@
+package io.dropwizard.servlets.assets;
+
+public class ResourceNotFoundException extends RuntimeException {
+    private static final long serialVersionUID = 7084957514695533766L;
+
+    public ResourceNotFoundException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceURL.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceURL.java
new file mode 100644
index 0000000..524686d
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceURL.java
@@ -0,0 +1,118 @@
+package io.dropwizard.servlets.assets;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.*;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+
+/**
+ * Helper methods for dealing with {@link URL} objects for local resources.
+ */
+public class ResourceURL {
+    private ResourceURL() { /* singleton */ }
+
+    /**
+     * Returns true if the URL passed to it corresponds to a directory.  This is slightly tricky due to some quirks
+     * of the {@link JarFile} API.  Only jar:// and file:// URLs are supported.
+     *
+     * @param resourceURL the URL to check
+     * @return true if resource is a directory
+     */
+    public static boolean isDirectory(URL resourceURL) throws URISyntaxException {
+        final String protocol = resourceURL.getProtocol();
+        switch (protocol) {
+            case "jar":
+                try {
+                    final JarURLConnection jarConnection = (JarURLConnection) resourceURL.openConnection();
+                    final JarEntry entry = jarConnection.getJarEntry();
+                    if (entry.isDirectory()) {
+                        return true;
+                    }
+
+                    // WARNING! Heuristics ahead.
+                    // It turns out that JarEntry#isDirectory() really just tests whether the filename ends in a '/'. If you try
+                    // to open the same URL without a trailing '/', it'll succeed — but the result won't be what you want.
+                    // We try to get around this by calling getInputStream() on the file inside the jar. This seems to return null
+                    // for directories (though that behavior is undocumented as far as I can tell). If you have a better idea,
+                    // please improve this.
+
+                    String filename = resourceURL.getFile();
+                    filename = filename.substring(filename.indexOf('!') + 2); // leaves just the relative file path inside the jar
+                    final JarFile jarFile = jarConnection.getJarFile();
+                    final ZipEntry zipEntry = jarFile.getEntry(filename);
+                    final InputStream inputStream = jarFile.getInputStream(zipEntry);
+
+                    return (inputStream == null);
+                } catch (IOException e) {
+                    throw new ResourceNotFoundException(e);
+                }
+            case "file":
+                return new File(resourceURL.toURI()).isDirectory();
+            default:
+                throw new IllegalArgumentException("Unsupported protocol " + resourceURL.getProtocol() + " for resource " + resourceURL);
+        }
+    }
+
+    /**
+     * Appends a trailing '/' to a {@link URL} object. Does not append a slash if one is already present.
+     *
+     * @param originalURL The URL to append a slash to
+     * @return a new URL object that ends in a slash
+     */
+    public static URL appendTrailingSlash(URL originalURL) {
+        try {
+            return originalURL.getPath().endsWith("/") ? originalURL :
+                    new URL(originalURL.getProtocol(),
+                            originalURL.getHost(),
+                            originalURL.getPort(),
+                            originalURL.getFile() + '/');
+        } catch (MalformedURLException ignored) { // shouldn't happen
+            throw new IllegalArgumentException("Invalid resource URL: " + originalURL);
+        }
+    }
+
+    /**
+     * Returns the last modified time for file:// and jar:// URLs.  This is slightly tricky for a couple of reasons:
+     * 1) calling getConnection on a {@link URLConnection} to a file opens an {@link InputStream} to that file that must
+     * then be closed — though this is not true for {@code URLConnection}s to jar resources
+     * 2) calling getLastModified on {@link JarURLConnection}s returns the last modified time of the jar file, rather
+     * than the file within
+     *
+     * @param resourceURL the URL to return the last modified time for
+     * @return the last modified time of the resource, expressed as the number of milliseconds since the epoch, or 0 if there was a problem
+     */
+    public static long getLastModified(URL resourceURL) {
+        final String protocol = resourceURL.getProtocol();
+        switch (protocol) {
+            case "jar":
+                try {
+                    final JarURLConnection jarConnection = (JarURLConnection) resourceURL.openConnection();
+                    final JarEntry entry = jarConnection.getJarEntry();
+                    return entry.getTime();
+                } catch (IOException ignored) {
+                    return 0;
+                }
+            case "file":
+                URLConnection connection = null;
+                try {
+                    connection = resourceURL.openConnection();
+                    return connection.getLastModified();
+                } catch (IOException ignored) {
+                    return 0;
+                } finally {
+                    if (connection != null) {
+                        try {
+                            connection.getInputStream().close();
+                        } catch (IOException ignored) {
+                            // do nothing.
+                        }
+                    }
+                }
+            default:
+                throw new IllegalArgumentException("Unsupported protocol " + resourceURL.getProtocol() + " for resource " + resourceURL);
+        }
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/GarbageCollectionTask.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/GarbageCollectionTask.java
new file mode 100644
index 0000000..20e4e50
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/GarbageCollectionTask.java
@@ -0,0 +1,58 @@
+package io.dropwizard.servlets.tasks;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+
+import java.io.PrintWriter;
+
+/**
+ * Performs a full JVM garbage collection (probably).
+ */
+public class GarbageCollectionTask extends Task {
+    private final Runtime runtime;
+
+    /**
+     * Creates a new GarbageCollectionTask.
+     */
+    public GarbageCollectionTask() {
+        this(Runtime.getRuntime());
+    }
+
+    /**
+     * Creates a new GarbageCollectionTask with the given {@link Runtime} instance.
+     * <p/>
+     * <b>Use {@link GarbageCollectionTask#GarbageCollectionTask()} instead.</b>
+     *
+     * @param runtime a {@link Runtime} instance
+     */
+    public GarbageCollectionTask(Runtime runtime) {
+        super("gc");
+        this.runtime = runtime;
+    }
+
+    @Override
+    @SuppressWarnings("CallToSystemGC")
+    public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) {
+        final int count = parseRuns(parameters);
+        for (int i = 0; i < count; i++) {
+            output.println("Running GC...");
+            output.flush();
+            runtime.gc();
+        }
+
+        output.println("Done!");
+    }
+
+    private static int parseRuns(ImmutableMultimap<String, String> parameters) {
+        final ImmutableList<String> runs = parameters.get("runs").asList();
+        if (runs.isEmpty()) {
+            return 1;
+        } else {
+            try {
+                return Integer.parseInt(runs.get(0));
+            } catch (NumberFormatException ignored) {
+                return 1;
+            }
+        }
+    }
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/Task.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/Task.java
new file mode 100644
index 0000000..64116e7
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/Task.java
@@ -0,0 +1,42 @@
+package io.dropwizard.servlets.tasks;
+
+import com.google.common.collect.ImmutableMultimap;
+
+import java.io.PrintWriter;
+
+/**
+ * An arbitrary administrative task which can be performed via the admin interface.
+ *
+ * @see TaskServlet
+ */
+public abstract class Task {
+    private final String name;
+
+    /**
+     * Create a new task with the given name.
+     *
+     * @param name the task's name
+     */
+    protected Task(String name) {
+        this.name = name;
+    }
+
+    /**
+     * Returns the task's name,
+     *
+     * @return the task's name
+     */
+    public String getName() {
+        return name;
+    }
+
+    /**
+     * Executes the task.
+     *
+     * @param parameters the query string parameters
+     * @param output     a {@link PrintWriter} wrapping the output stream of the task
+     * @throws Exception if something goes wrong
+     */
+    public abstract void execute(ImmutableMultimap<String, String> parameters,
+                                 PrintWriter output) throws Exception;
+}
diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/TaskServlet.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/TaskServlet.java
new file mode 100644
index 0000000..465500c
--- /dev/null
+++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/TaskServlet.java
@@ -0,0 +1,224 @@
+package io.dropwizard.servlets.tasks;
+
+import com.codahale.metrics.Meter;
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import com.codahale.metrics.annotation.ExceptionMetered;
+import com.codahale.metrics.annotation.Metered;
+import com.codahale.metrics.annotation.Timed;
+import static com.codahale.metrics.MetricRegistry.name;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.net.MediaType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Enumeration;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A servlet which provides access to administrative {@link Task}s. It only responds to {@code POST}
+ * requests, since most {@link Task}s aren't side-effect free, and passes along the query string
+ * parameters of the request to the task as a multimap.
+ *
+ * @see Task
+ */
+public class TaskServlet extends HttpServlet {
+    private static final long serialVersionUID = 7404713218661358124L;
+    private static final Logger LOGGER = LoggerFactory.getLogger(TaskServlet.class);
+    private final ConcurrentMap<String, Task> tasks;
+    private final ConcurrentMap<Task, TaskExecutor> taskExecutors;
+
+    private final MetricRegistry metricRegistry;
+
+    /**
+     * Creates a new TaskServlet.
+     */
+    public TaskServlet(MetricRegistry metricRegistry) {
+        this.metricRegistry = metricRegistry;
+        this.tasks = Maps.newConcurrentMap();
+        this.taskExecutors = Maps.newConcurrentMap();
+    }
+
+    public void add(Task task) {
+        tasks.put('/' + task.getName(), task);
+
+        TaskExecutor taskExecutor = new TaskExecutor(task);
+        try {
+            Method executeMethod = task.getClass().getMethod("execute",
+                    ImmutableMultimap.class, PrintWriter.class);
+
+            if(executeMethod.isAnnotationPresent(Timed.class)) {
+                Timed annotation = executeMethod.getAnnotation(Timed.class);
+                String name = chooseName(annotation.name(),
+                        annotation.absolute(),
+                        task);
+                Timer timer = metricRegistry.timer(name);
+                taskExecutor = new TimedTask(taskExecutor, timer);
+            }
+
+            if(executeMethod.isAnnotationPresent(Metered.class)) {
+                Metered annotation = executeMethod.getAnnotation(Metered.class);
+                String name = chooseName(annotation.name(),
+                                        annotation.absolute(),
+                                        task);
+                Meter meter = metricRegistry.meter(name);
+                taskExecutor = new MeteredTask(taskExecutor, meter);
+            }
+
+            if(executeMethod.isAnnotationPresent(ExceptionMetered.class)) {
+                ExceptionMetered annotation = executeMethod.getAnnotation(ExceptionMetered.class);
+                String name = chooseName(annotation.name(),
+                                        annotation.absolute(),
+                                        task,
+                                        ExceptionMetered.DEFAULT_NAME_SUFFIX);
+                Meter exceptionMeter = metricRegistry.meter(name);
+                taskExecutor = new ExceptionMeteredTask(taskExecutor, exceptionMeter, annotation.cause());
+            }
+        } catch (NoSuchMethodException e) {
+        }
+
+        taskExecutors.put(task, taskExecutor);
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req,
+                          HttpServletResponse resp) throws ServletException, IOException {
+        final Task task = tasks.get(req.getPathInfo());
+        if (task != null) {
+            resp.setContentType(MediaType.PLAIN_TEXT_UTF_8.toString());
+            final PrintWriter output = resp.getWriter();
+            try {
+                TaskExecutor taskExecutor = taskExecutors.get(task);
+                taskExecutor.executeTask(getParams(req), output);
+            } catch (Exception e) {
+                LOGGER.error("Error running {}", task.getName(), e);
+                resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+                output.println();
+                output.println(e.getMessage());
+                e.printStackTrace(output);
+            } finally {
+                output.close();
+            }
+        } else {
+            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    private static ImmutableMultimap<String, String> getParams(HttpServletRequest req) {
+        final ImmutableMultimap.Builder<String, String> results = ImmutableMultimap.builder();
+        final Enumeration<String> names = req.getParameterNames();
+        while (names.hasMoreElements()) {
+            final String name = names.nextElement();
+            final String[] values = req.getParameterValues(name);
+            results.putAll(name, values);
+        }
+        return results.build();
+    }
+
+    public Collection<Task> getTasks() {
+        return tasks.values();
+    }
+
+    private String chooseName(String explicitName, boolean absolute, Task task, String... suffixes) {
+        if (explicitName != null && !explicitName.isEmpty()) {
+            if (absolute) {
+                return explicitName;
+            }
+            return name(task.getClass(), explicitName);
+        }
+
+        return name(task.getClass(), suffixes);
+    }
+
+    private static class TaskExecutor {
+        private final Task task;
+
+        private TaskExecutor(Task task) {
+            this.task = task;
+        }
+
+        public void executeTask(ImmutableMultimap<String, String> params, PrintWriter output) throws Exception {
+            try {
+                task.execute(params, output);
+            } catch (Exception e) {
+                throw e;
+            }
+        }
+    }
+
+    private static class TimedTask extends TaskExecutor {
+        private TaskExecutor underlying;
+        private final Timer timer;
+
+        private TimedTask(TaskExecutor underlying, Timer timer) {
+            super(underlying.task);
+            this.underlying = underlying;
+            this.timer = timer;
+        }
+
+        @Override
+        public void executeTask(ImmutableMultimap<String, String> params, PrintWriter output) throws Exception {
+            final Timer.Context context = timer.time();
+            try {
+                underlying.executeTask(params, output);
+            } finally {
+                context.stop();
+            }
+        }
+    }
+
+    private static class MeteredTask extends TaskExecutor {
+        private TaskExecutor underlying;
+        private final Meter meter;
+
+        private MeteredTask(TaskExecutor underlying, Meter meter) {
+            super(underlying.task);
+            this.meter = meter;
+            this.underlying = underlying;
+        }
+
+        @Override
+        public void executeTask(ImmutableMultimap<String, String> params, PrintWriter output) throws Exception {
+            meter.mark();
+            underlying.executeTask(params, output);
+        }
+    }
+
+    private static class ExceptionMeteredTask extends TaskExecutor {
+        private TaskExecutor underlying;
+        private final Meter exceptionMeter;
+        private final Class<?> exceptionClass;
+
+        private ExceptionMeteredTask(TaskExecutor underlying,
+                                     Meter exceptionMeter, Class<? extends Throwable> exceptionClass) {
+            super(underlying.task);
+            this.underlying = underlying;
+            this.exceptionMeter = exceptionMeter;
+            this.exceptionClass = exceptionClass;
+        }
+
+        @Override
+        public void executeTask(ImmutableMultimap<String, String> params, PrintWriter output) throws Exception {
+            try {
+                underlying.executeTask(params, output);
+            } catch(Exception e) {
+                if (exceptionMeter != null && exceptionClass.isAssignableFrom(e.getClass()) ||
+                        (e.getCause() != null && exceptionClass.isAssignableFrom(e.getCause().getClass()))) {
+                    exceptionMeter.mark();
+                }
+
+                throw e;
+            }
+        }
+    }
+
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/CacheBustingFilterTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/CacheBustingFilterTest.java
new file mode 100644
index 0000000..dd281b4
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/CacheBustingFilterTest.java
@@ -0,0 +1,39 @@
+package io.dropwizard.servlets;
+
+import org.junit.Test;
+import org.mockito.InOrder;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import static org.mockito.Mockito.*;
+
+public class CacheBustingFilterTest {
+    private final HttpServletRequest request = mock(HttpServletRequest.class);
+    private final HttpServletResponse response = mock(HttpServletResponse.class);
+    private final FilterChain chain = mock(FilterChain.class);
+    private final CacheBustingFilter filter = new CacheBustingFilter();
+
+    @Test
+    public void passesThroughNonHttpRequests() throws Exception {
+        final ServletRequest req = mock(ServletRequest.class);
+        final ServletResponse res = mock(ServletResponse.class);
+
+        filter.doFilter(req, res, chain);
+
+        verify(chain).doFilter(req, res);
+        verifyZeroInteractions(res);
+    }
+
+    @Test
+    public void setsACacheHeaderOnTheResponse() throws Exception {
+        filter.doFilter(request, response, chain);
+
+        final InOrder inOrder = inOrder(response, chain);
+        inOrder.verify(response).setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+        inOrder.verify(chain).doFilter(request, response);
+    }
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/ServletsTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/ServletsTest.java
new file mode 100644
index 0000000..a27f2e5
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/ServletsTest.java
@@ -0,0 +1,34 @@
+package io.dropwizard.servlets;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ServletsTest {
+    private final HttpServletRequest request = mock(HttpServletRequest.class);
+    private final HttpServletRequest fullRequest = mock(HttpServletRequest.class);
+
+    @Before
+    public void setUp() throws Exception {
+        when(request.getRequestURI()).thenReturn("/one/two");
+        when(fullRequest.getRequestURI()).thenReturn("/one/two");
+        when(fullRequest.getQueryString()).thenReturn("one=two&three=four");
+    }
+
+    @Test
+    public void formatsBasicURIs() throws Exception {
+        assertThat(Servlets.getFullUrl(request))
+                .isEqualTo("/one/two");
+    }
+
+    @Test
+    public void formatsFullURIs() throws Exception {
+        assertThat(Servlets.getFullUrl(fullRequest))
+                .isEqualTo("/one/two?one=two&three=four");
+    }
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/AssetServletTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/AssetServletTest.java
new file mode 100644
index 0000000..9ca9f1d
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/AssetServletTest.java
@@ -0,0 +1,306 @@
+package io.dropwizard.servlets.assets;
+
+import com.google.common.base.Charsets;
+import com.google.common.net.HttpHeaders;
+import org.eclipse.jetty.http.*;
+import org.eclipse.jetty.servlet.ServletTester;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class AssetServletTest {
+    private static final String DUMMY_SERVLET = "/dummy_servlet/";
+    private static final String NOINDEX_SERVLET = "/noindex_servlet/";
+    private static final String NOCHARSET_SERVLET = "/nocharset_servlet/";
+    private static final String ROOT_SERVLET = "/";
+    private static final String RESOURCE_PATH = "/assets";
+
+    // ServletTester expects to be able to instantiate the servlet with zero arguments
+
+    public static class DummyAssetServlet extends AssetServlet {
+        private static final long serialVersionUID = -1L;
+
+        public DummyAssetServlet() {
+            super(RESOURCE_PATH, DUMMY_SERVLET, "index.htm", Charsets.UTF_8);
+        }
+    }
+
+    public static class NoIndexAssetServlet extends AssetServlet {
+        private static final long serialVersionUID = -1L;
+
+        public NoIndexAssetServlet() {
+            super(RESOURCE_PATH, DUMMY_SERVLET, null, Charsets.UTF_8);
+        }
+    }
+
+    public static class RootAssetServlet extends AssetServlet {
+        public RootAssetServlet() {
+            super("/", ROOT_SERVLET, null, Charsets.UTF_8);
+        }
+    }
+
+    public static class NoCharsetAssetServlet extends AssetServlet {
+        public NoCharsetAssetServlet() {
+            super(RESOURCE_PATH, NOCHARSET_SERVLET, null, null);
+        }
+    }
+
+    private final ServletTester servletTester = new ServletTester();
+    private final HttpTester.Request request = HttpTester.newRequest();
+    private HttpTester.Response response;
+
+    @Before
+    public void setup() throws Exception {
+        servletTester.addServlet(DummyAssetServlet.class, DUMMY_SERVLET + '*');
+        servletTester.addServlet(NoIndexAssetServlet.class, NOINDEX_SERVLET + '*');
+        servletTester.addServlet(NoCharsetAssetServlet.class, NOCHARSET_SERVLET + '*');
+        servletTester.addServlet(RootAssetServlet.class, ROOT_SERVLET + '*');
+        servletTester.start();
+
+        request.setMethod("GET");
+        request.setURI(DUMMY_SERVLET + "example.txt");
+        request.setVersion(HttpVersion.HTTP_1_0);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        servletTester.stop();
+    }
+
+    @Test
+    public void servesFilesMappedToRoot() throws Exception {
+        request.setURI(ROOT_SERVLET + "assets/example.txt");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.getContent())
+                .isEqualTo("HELLO THERE");
+    }
+
+    @Test
+    public void servesCharset() throws Exception {
+        request.setURI(DUMMY_SERVLET + "example.txt");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
+                .isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8);
+
+        request.setURI(NOCHARSET_SERVLET + "example.txt");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.get(HttpHeader.CONTENT_TYPE))
+                .isEqualTo(MimeTypes.Type.TEXT_PLAIN.toString());
+    }
+
+    @Test
+    public void servesFilesFromRootsWithSameName() throws Exception {
+        request.setURI(DUMMY_SERVLET + "example2.txt");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.getContent())
+                .isEqualTo("HELLO THERE 2");
+    }
+
+    @Test
+    public void servesFilesWithA200() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.getContent())
+                .isEqualTo("HELLO THERE");
+    }
+
+    @Test
+    public void throws404IfTheAssetIsMissing() throws Exception {
+        request.setURI(DUMMY_SERVLET + "doesnotexist.txt");
+
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(404);
+    }
+
+    @Test
+    public void consistentlyAssignsETags() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final String firstEtag = response.get(HttpHeaders.ETAG);
+
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final String secondEtag = response.get(HttpHeaders.ETAG);
+
+        System.out.println(firstEtag);
+
+        assertThat(firstEtag)
+                .isEqualTo("\"174a6dd7325e64c609eab14ab1d30b86\"")
+                .isEqualTo(secondEtag);
+    }
+
+    @Test
+    public void assignsDifferentETagsForDifferentFiles() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final String firstEtag = response.get(HttpHeaders.ETAG);
+
+        request.setURI(DUMMY_SERVLET + "foo.bar");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final String secondEtag = response.get(HttpHeaders.ETAG);
+
+        assertThat(firstEtag)
+                .isEqualTo("\"174a6dd7325e64c609eab14ab1d30b86\"");
+        assertThat(secondEtag)
+                .isEqualTo("\"378521448e0a3893a209edcc686d91ce\"");
+    }
+
+    @Test
+    public void supportsIfNoneMatchRequests() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final String correctEtag = response.get(HttpHeaders.ETAG);
+
+        request.setHeader(HttpHeaders.IF_NONE_MATCH, correctEtag);
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final int statusWithMatchingEtag = response.getStatus();
+
+        request.setHeader(HttpHeaders.IF_NONE_MATCH, correctEtag + "FOO");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final int statusWithNonMatchingEtag = response.getStatus();
+
+        assertThat(statusWithMatchingEtag)
+                .isEqualTo(304);
+        assertThat(statusWithNonMatchingEtag)
+                .isEqualTo(200);
+    }
+
+    @Test
+    public void consistentlyAssignsLastModifiedTimes() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final long firstLastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED);
+
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final long secondLastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED);
+
+        assertThat(firstLastModifiedTime)
+                .isEqualTo(secondLastModifiedTime);
+    }
+
+    @Test
+    public void supportsIfModifiedSinceRequests() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final long lastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED);
+
+
+        request.setHeader(HttpHeaders.IF_MODIFIED_SINCE, HttpFields.formatDate(lastModifiedTime));
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final int statusWithMatchingLastModifiedTime = response.getStatus();
+
+        request.setHeader(HttpHeaders.IF_MODIFIED_SINCE,
+                          HttpFields.formatDate(lastModifiedTime - 100));
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final int statusWithStaleLastModifiedTime = response.getStatus();
+
+        request.setHeader(HttpHeaders.IF_MODIFIED_SINCE,
+                          HttpFields.formatDate(lastModifiedTime + 100));
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        final int statusWithRecentLastModifiedTime = response.getStatus();
+
+        assertThat(statusWithMatchingLastModifiedTime)
+                .isEqualTo(304);
+        assertThat(statusWithStaleLastModifiedTime)
+                .isEqualTo(200);
+        assertThat(statusWithRecentLastModifiedTime)
+                .isEqualTo(304);
+    }
+
+    @Test
+    public void guessesMimeTypes() throws Exception {
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
+                .isEqualTo(MimeTypes.Type.TEXT_PLAIN_UTF_8);
+    }
+
+    @Test
+    public void defaultsToHtml() throws Exception {
+        request.setURI(DUMMY_SERVLET + "foo.bar");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(MimeTypes.CACHE.get(response.get(HttpHeader.CONTENT_TYPE)))
+                .isEqualTo(MimeTypes.Type.TEXT_HTML_UTF_8);
+    }
+
+    @Test
+    public void servesIndexFilesByDefault() throws Exception {
+        // Root directory listing:
+        request.setURI(DUMMY_SERVLET);
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.getContent())
+                .contains("/assets Index File");
+
+        // Subdirectory listing:
+        request.setURI(DUMMY_SERVLET + "some_directory");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.getContent())
+                .contains("/assets/some_directory Index File");
+
+        // Subdirectory listing with slash:
+        request.setURI(DUMMY_SERVLET + "some_directory/");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+        assertThat(response.getContent())
+                .contains("/assets/some_directory Index File");
+    }
+
+    @Test
+    public void throwsA404IfNoIndexFileIsDefined() throws Exception {
+        // Root directory listing:
+        request.setURI(NOINDEX_SERVLET + '/');
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(404);
+
+        // Subdirectory listing:
+        request.setURI(NOINDEX_SERVLET + "some_directory");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(404);
+
+        // Subdirectory listing with slash:
+        request.setURI(NOINDEX_SERVLET + "some_directory/");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(404);
+    }
+
+    @Test
+    public void doesNotAllowOverridingUrls() throws Exception {
+        request.setURI(DUMMY_SERVLET + "file:/etc/passwd");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(404);
+    }
+
+    @Test
+    public void doesNotAllowOverridingPaths() throws Exception {
+        request.setURI(DUMMY_SERVLET + "/etc/passwd");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(404);
+    }
+
+    @Test
+    public void allowsEncodedAssetNames() throws Exception {
+        request.setURI(DUMMY_SERVLET + "encoded%20example.txt");
+        response = HttpTester.parseResponse(servletTester.getResponses(request.generate()));
+        assertThat(response.getStatus())
+                .isEqualTo(200);
+    }
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ResourceURLTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ResourceURLTest.java
new file mode 100644
index 0000000..4e80461
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ResourceURLTest.java
@@ -0,0 +1,147 @@
+package io.dropwizard.servlets.assets;
+
+import com.google.common.io.Files;
+import com.google.common.io.Resources;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.File;
+import java.net.JarURLConnection;
+import java.net.URL;
+import java.util.jar.JarEntry;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.fest.assertions.api.Assertions.fail;
+
+public class ResourceURLTest {
+    private File directory;
+    private File file;
+
+    @Before
+    public void setup() throws Exception {
+        file = File.createTempFile("resource_url_test", null);
+        file.deleteOnExit();
+
+        directory = Files.createTempDir();
+        directory.deleteOnExit();
+    }
+
+    @Test
+    public void isDirectoryReturnsTrueForPlainDirectories() throws Exception {
+        final URL url = directory.toURI().toURL();
+
+        assertThat(url.getProtocol())
+                .isEqualTo("file");
+        assertThat(ResourceURL.isDirectory(url))
+                .isTrue();
+    }
+
+    @Test
+    public void isDirectoryReturnsFalseForPlainFiles() throws Exception {
+        final URL url = file.toURI().toURL();
+
+        assertThat(url.getProtocol())
+                .isEqualTo("file");
+        assertThat(ResourceURL.isDirectory(url))
+                .isFalse();
+    }
+
+    @Test
+    public void isDirectoryReturnsTrueForDirectoriesInJars() throws Exception {
+        final URL url = Resources.getResource("META-INF/");
+
+        assertThat(url.getProtocol())
+                .isEqualTo("jar");
+        assertThat(ResourceURL.isDirectory(url))
+                .isTrue();
+    }
+
+    @Test
+    public void isDirectoryReturnsFalseForFilesInJars() throws Exception {
+        final URL url = Resources.getResource("META-INF/MANIFEST.MF");
+
+        assertThat(url.getProtocol())
+                .isEqualTo("jar");
+        assertThat(ResourceURL.isDirectory(url))
+                .isFalse();
+    }
+
+    @Test
+    public void isDirectoryReturnsTrueForDirectoriesInJarsWithoutTrailingSlashes() throws Exception {
+        final URL url = Resources.getResource("META-INF");
+
+        assertThat(url.getProtocol())
+                .isEqualTo("jar");
+        assertThat(ResourceURL.isDirectory(url))
+                .isTrue();
+    }
+
+    @Test
+    public void isDirectoryThrowsResourceNotFoundExceptionForMissingDirectories() throws Exception {
+        URL url = Resources.getResource("META-INF/");
+        url = new URL(url.toExternalForm() + "missing");
+        try {
+            ResourceURL.isDirectory(url);
+            fail("should have thrown an exception");
+        }
+        catch (ResourceNotFoundException ignored) {
+            // expected
+        }
+    }
+
+    @Test
+    public void appendTrailingSlashAddsASlash() throws Exception {
+        final URL url = Resources.getResource("META-INF");
+
+        assertThat(url.toExternalForm())
+                .doesNotMatch(".*/$");
+        assertThat(ResourceURL.appendTrailingSlash(url).toExternalForm())
+                .endsWith("/");
+    }
+
+    @Test
+    public void appendTrailingSlashDoesntASlashWhenOneIsAlreadyPresent() throws Exception {
+        final URL url = Resources.getResource("META-INF/");
+
+        assertThat(url.toExternalForm())
+                .endsWith("/");
+        assertThat(ResourceURL.appendTrailingSlash(url).toExternalForm())
+                .doesNotMatch(".*//$");
+        assertThat(url)
+                .isEqualTo(ResourceURL.appendTrailingSlash(url));
+    }
+
+    @Test
+    public void getLastModifiedReturnsTheLastModifiedTimeOfAFile() throws Exception {
+        final URL url = file.toURI().toURL();
+        final long lastModified = ResourceURL.getLastModified(url);
+
+        assertThat(lastModified)
+                .isGreaterThan(0);
+        assertThat(lastModified)
+                .isEqualTo(file.lastModified());
+    }
+
+    @Test
+    public void getLastModifiedReturnsTheLastModifiedTimeOfAJarEntry() throws Exception {
+        final URL url = Resources.getResource("META-INF/MANIFEST.MF");
+        final long lastModified = ResourceURL.getLastModified(url);
+
+        final JarURLConnection jarConnection = (JarURLConnection) url.openConnection();
+        final JarEntry entry = jarConnection.getJarEntry();
+
+        assertThat(lastModified)
+                .isGreaterThan(0);
+        assertThat(lastModified)
+                .isEqualTo(entry.getTime());
+    }
+
+    @Test
+    public void getLastModifiedReturnsZeroIfAnErrorOccurs() throws Exception {
+        final URL url = new URL("file:/some/path/that/doesnt/exist");
+        final long lastModified = ResourceURL.getLastModified(url);
+
+        assertThat(lastModified)
+                .isZero();
+    }
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/GarbageCollectionTaskTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/GarbageCollectionTaskTest.java
new file mode 100644
index 0000000..bd85e5b
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/GarbageCollectionTaskTest.java
@@ -0,0 +1,36 @@
+package io.dropwizard.servlets.tasks;
+
+import com.google.common.collect.ImmutableMultimap;
+import org.junit.Test;
+
+import java.io.PrintWriter;
+
+import static org.mockito.Mockito.*;
+
+ at SuppressWarnings("CallToSystemGC")
+public class GarbageCollectionTaskTest {
+    private final Runtime runtime = mock(Runtime.class);
+    private final PrintWriter output = mock(PrintWriter.class);
+    private final Task task = new GarbageCollectionTask(runtime);
+
+    @Test
+    public void runsOnceWithNoParameters() throws Exception {
+        task.execute(ImmutableMultimap.<String, String>of(), output);
+
+        verify(runtime, times(1)).gc();
+    }
+
+    @Test
+    public void usesTheFirstRunsParameter() throws Exception {
+        task.execute(ImmutableMultimap.of("runs", "3", "runs", "2"), output);
+
+        verify(runtime, times(3)).gc();
+    }
+
+    @Test
+    public void defaultsToOneRunIfTheQueryParamDoesNotParse() throws Exception {
+        task.execute(ImmutableMultimap.of("runs", "$"), output);
+
+        verify(runtime, times(1)).gc();
+    }
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskServletTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskServletTest.java
new file mode 100644
index 0000000..672a486
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskServletTest.java
@@ -0,0 +1,106 @@
+package io.dropwizard.servlets.tasks;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMultimap;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.PrintWriter;
+import java.util.Collections;
+
+import static org.mockito.Mockito.*;
+
+public class TaskServletTest {
+    private final Task gc = mock(Task.class);
+    private final Task clearCache = mock(Task.class);
+
+    {
+        when(gc.getName()).thenReturn("gc");
+        when(clearCache.getName()).thenReturn("clear-cache");
+    }
+
+    private final TaskServlet servlet = new TaskServlet(new MetricRegistry());
+    private final HttpServletRequest request = mock(HttpServletRequest.class);
+    private final HttpServletResponse response = mock(HttpServletResponse.class);
+
+    @Before
+    public void setUp() throws Exception {
+        servlet.add(gc);
+        servlet.add(clearCache);
+    }
+
+    @Test
+    public void returnsA404WhenNotFound() throws Exception {
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getPathInfo()).thenReturn("/test");
+
+        servlet.service(request, response);
+
+        verify(response).sendError(404);
+    }
+
+    @Test
+    public void runsATaskWhenFound() throws Exception {
+        final PrintWriter output = mock(PrintWriter.class);
+
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getPathInfo()).thenReturn("/gc");
+        when(request.getParameterNames()).thenReturn(Collections.enumeration(ImmutableList.<String>of()));
+        when(response.getWriter()).thenReturn(output);
+
+        servlet.service(request, response);
+
+        verify(gc).execute(ImmutableMultimap.<String, String>of(), output);
+    }
+
+    @Test
+    public void passesQueryStringParamsAlong() throws Exception {
+        final PrintWriter output = mock(PrintWriter.class);
+
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getPathInfo()).thenReturn("/gc");
+        when(request.getParameterNames()).thenReturn(Collections.enumeration(ImmutableList.of("runs")));
+        when(request.getParameterValues("runs")).thenReturn(new String[]{ "1" });
+        when(response.getWriter()).thenReturn(output);
+
+        servlet.service(request, response);
+
+        verify(gc).execute(ImmutableMultimap.of("runs", "1"), output);
+    }
+
+    @Test
+    @SuppressWarnings("unchecked")
+    public void returnsA500OnExceptions() throws Exception {
+        when(request.getMethod()).thenReturn("POST");
+        when(request.getPathInfo()).thenReturn("/gc");
+        when(request.getParameterNames()).thenReturn(Collections.enumeration(ImmutableList.<String>of()));
+
+        final PrintWriter output = mock(PrintWriter.class);
+        when(response.getWriter()).thenReturn(output);
+
+        final RuntimeException ex = new RuntimeException("whoops");
+
+        doThrow(ex).when(gc).execute(any(ImmutableMultimap.class), any(PrintWriter.class));
+
+        servlet.service(request, response);
+
+        verify(response).setStatus(500);
+    }
+
+    /**
+     * Add a test to make sure the signature of the Task class does not change as the TaskServlet
+     * depends on this to perform record metrics on Tasks
+     */
+    @Test
+    public void verifyTaskExecuteMethod() {
+        try {
+            Task.class.getMethod("execute", ImmutableMultimap.class, PrintWriter.class);
+        } catch (NoSuchMethodException e) {
+            Assert.fail("Execute method for " + Task.class.getName() + " not found");
+        }
+    }
+}
diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskTest.java
new file mode 100644
index 0000000..cf53495
--- /dev/null
+++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskTest.java
@@ -0,0 +1,24 @@
+package io.dropwizard.servlets.tasks;
+
+import com.google.common.collect.ImmutableMultimap;
+import org.junit.Test;
+
+import java.io.PrintWriter;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class TaskTest {
+    private final Task task = new Task("test") {
+        @Override
+        public void execute(ImmutableMultimap<String, String> parameters,
+                            PrintWriter output) throws Exception {
+
+        }
+    };
+
+    @Test
+    public void hasAName() throws Exception {
+        assertThat(task.getName())
+                .isEqualTo("test");
+    }
+}
diff --git a/dropwizard-servlets/src/test/more-resources/assets/example2.txt b/dropwizard-servlets/src/test/more-resources/assets/example2.txt
new file mode 100644
index 0000000..df81343
--- /dev/null
+++ b/dropwizard-servlets/src/test/more-resources/assets/example2.txt
@@ -0,0 +1 @@
+HELLO THERE 2
\ No newline at end of file
diff --git a/dropwizard-servlets/src/test/resources/assets/encoded example.txt b/dropwizard-servlets/src/test/resources/assets/encoded example.txt
new file mode 100644
index 0000000..8435ed0
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/assets/encoded example.txt	
@@ -0,0 +1 @@
+yay
diff --git a/dropwizard-servlets/src/test/resources/assets/example.txt b/dropwizard-servlets/src/test/resources/assets/example.txt
new file mode 100644
index 0000000..25cd1c9
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/assets/example.txt
@@ -0,0 +1 @@
+HELLO THERE
\ No newline at end of file
diff --git a/dropwizard-servlets/src/test/resources/assets/foo.bar b/dropwizard-servlets/src/test/resources/assets/foo.bar
new file mode 100644
index 0000000..f497738
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/assets/foo.bar
@@ -0,0 +1 @@
+BAZOMATIX
\ No newline at end of file
diff --git a/dropwizard-servlets/src/test/resources/assets/index.htm b/dropwizard-servlets/src/test/resources/assets/index.htm
new file mode 100644
index 0000000..bace7b4
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/assets/index.htm
@@ -0,0 +1,5 @@
+<html>
+<body>
+/assets Index File
+</body>
+</html>
\ No newline at end of file
diff --git a/dropwizard-servlets/src/test/resources/assets/some_directory/example.txt b/dropwizard-servlets/src/test/resources/assets/some_directory/example.txt
new file mode 100644
index 0000000..0dc44dd
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/assets/some_directory/example.txt
@@ -0,0 +1 @@
+WRONG FILE FELLA!
\ No newline at end of file
diff --git a/dropwizard-servlets/src/test/resources/assets/some_directory/index.htm b/dropwizard-servlets/src/test/resources/assets/some_directory/index.htm
new file mode 100644
index 0000000..f0a8ea3
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/assets/some_directory/index.htm
@@ -0,0 +1,5 @@
+<html>
+<body>
+/assets/some_directory Index File
+</body>
+</html>
\ No newline at end of file
diff --git a/dropwizard-servlets/src/test/resources/logback-test.xml b/dropwizard-servlets/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-servlets/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-spdy/pom.xml b/dropwizard-spdy/pom.xml
new file mode 100644
index 0000000..c7afe3d
--- /dev/null
+++ b/dropwizard-spdy/pom.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-spdy</artifactId>
+    <name>Dropwizard SPDY Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-jetty</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.jetty.spdy</groupId>
+            <artifactId>spdy-http-server</artifactId>
+            <version>${jetty.version}</version>
+        </dependency>
+        <!-- this needs to be on your JVM's bootpath for SPDY to work -->
+        <!-- -Xbootclasspath/p:/blah/npn-boot-1.1.6.v20130911.jar -->
+        <dependency>
+            <groupId>org.mortbay.jetty.npn</groupId>
+            <artifactId>npn-boot</artifactId>
+            <version>1.1.6.v20130911</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-spdy/src/main/java/io/dropwizard/spdy/NonePushStrategyFactory.java b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/NonePushStrategyFactory.java
new file mode 100644
index 0000000..b03b5fa
--- /dev/null
+++ b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/NonePushStrategyFactory.java
@@ -0,0 +1,17 @@
+package io.dropwizard.spdy;
+
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+
+/**
+ * Disables server-initiated pushes.
+ *
+ * @see PushStrategyFactory
+ */
+ at JsonTypeName("none")
+public class NonePushStrategyFactory implements PushStrategyFactory {
+    @Override
+    public PushStrategy build() {
+        return new PushStrategy.None();
+    }
+}
diff --git a/dropwizard-spdy/src/main/java/io/dropwizard/spdy/PushStrategyFactory.java b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/PushStrategyFactory.java
new file mode 100644
index 0000000..f350486
--- /dev/null
+++ b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/PushStrategyFactory.java
@@ -0,0 +1,13 @@
+package io.dropwizard.spdy;
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import io.dropwizard.jackson.Discoverable;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+
+/**
+ * Builds {@link PushStrategy} instances for SPDY connectors.
+ */
+ at JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
+public interface PushStrategyFactory extends Discoverable {
+    PushStrategy build();
+}
diff --git a/dropwizard-spdy/src/main/java/io/dropwizard/spdy/ReferrerPushStrategyFactory.java b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/ReferrerPushStrategyFactory.java
new file mode 100644
index 0000000..648e5bd
--- /dev/null
+++ b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/ReferrerPushStrategyFactory.java
@@ -0,0 +1,176 @@
+package io.dropwizard.spdy;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.util.Duration;
+import io.dropwizard.validation.MinDuration;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+import org.eclipse.jetty.spdy.server.http.ReferrerPushStrategy;
+
+import javax.validation.constraints.Min;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A SPDY push strategy that auto-populates push metadata based on referrer URLs.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code pushableOriginPatterns}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The list of origin patterns to which pushes are allowed. If not specified, all
+ *             origins are allowed.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code maxAssociatedResources}</td>
+ *         <td>32</td>
+ *         <td>The maximum number of associated resources to push.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code referrerPushPeriod}</td>
+ *         <td>5 seconds</td>
+ *         <td>The amount of time after a request to consider following requests as secondary.</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code pushableFilenamePatterns}</td>
+ *         <td>
+ *             {@code *.css}, {@code *.js}, {@code *.png}, {@code *.jpeg}, {@code *.jpg},
+ *             {@code *.gif}, {@code *.ico}
+ *         </td>
+ *         <td>
+ *             The list of regular expressions which determine which secondary requests are pushable
+ *             resources.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code pushableContentTypes}</td>
+ *         <td>
+ *             {@code text/css}, {@code text/javascript}, {@code application/javascript},
+ *             {@code application/x-javascript}, {@code image/png}, {@code image/x-png},
+ *             {@code image/jpeg}, {@code image/gif}, {@code image/x-icon},
+ *             {@code image/vnd.microsoft.icon}
+ *         </td>
+ *         <td>
+ *             The list of MIME types of pushable resources.
+ *         </td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code nonPushableUserAgentPatterns}</td>
+ *         <td>(none)</td>
+ *         <td>The list of user-agent patterns to which patterns are <b>not</b> allowed.</td>
+ *     </tr>
+ * </table>
+ *
+ * @see PushStrategyFactory
+ * @see ReferrerPushStrategy
+ */
+ at JsonTypeName("referrer")
+public class ReferrerPushStrategyFactory implements PushStrategyFactory {
+    private List<String> pushableOriginPatterns;
+
+    @Min(1)
+    private int maxAssociatedResources = 32;
+
+    @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS)
+    private Duration referrerPushPeriod = Duration.seconds(5);
+
+    private List<String> pushableFilenamePatterns;
+
+    private List<String> pushableContentTypes;
+
+    private List<String> nonPushableUserAgentPatterns;
+
+    @JsonProperty
+    public List<String> getPushableOriginPatterns() {
+        return pushableOriginPatterns;
+    }
+
+    @JsonProperty
+    public void setPushableOriginPatterns(List<String> origins) {
+        this.pushableOriginPatterns = origins;
+    }
+
+    @JsonProperty
+    public int getMaxAssociatedResources() {
+        return maxAssociatedResources;
+    }
+
+    @JsonProperty
+    public void setMaxAssociatedResources(int maxAssociatedResources) {
+        this.maxAssociatedResources = maxAssociatedResources;
+    }
+
+    @JsonProperty
+    public Duration getReferrerPushPeriod() {
+        return referrerPushPeriod;
+    }
+
+    @JsonProperty
+    public void setReferrerPushPeriod(Duration referrerPushPeriod) {
+        this.referrerPushPeriod = referrerPushPeriod;
+    }
+
+    @JsonProperty
+    public List<String> getPushableFilenamePatterns() {
+        return pushableFilenamePatterns;
+    }
+
+    @JsonProperty
+    public void setPushableFilenamePatterns(List<String> pushableFilenamePatterns) {
+        this.pushableFilenamePatterns = pushableFilenamePatterns;
+    }
+
+    @JsonProperty
+    public List<String> getPushableContentTypes() {
+        return pushableContentTypes;
+    }
+
+    @JsonProperty
+    public void setPushableContentTypes(List<String> pushableContentTypes) {
+        this.pushableContentTypes = pushableContentTypes;
+    }
+
+    @JsonProperty
+    public List<String> getNonPushableUserAgentPatterns() {
+        return nonPushableUserAgentPatterns;
+    }
+
+    @JsonProperty
+    public void setNonPushableUserAgentPatterns(List<String> nonPushableUserAgentPatterns) {
+        this.nonPushableUserAgentPatterns = nonPushableUserAgentPatterns;
+    }
+
+    @Override
+    public PushStrategy build() {
+        final ReferrerPushStrategy strategy = new ReferrerPushStrategy();
+
+        if (pushableOriginPatterns != null) {
+            strategy.setAllowedPushOrigins(pushableOriginPatterns);
+        }
+
+        if (pushableContentTypes != null) {
+            strategy.setPushContentTypes(pushableContentTypes);
+        }
+
+        if (pushableFilenamePatterns != null) {
+            strategy.setPushRegexps(pushableFilenamePatterns);
+        }
+
+        if (nonPushableUserAgentPatterns != null) {
+            strategy.setUserAgentBlacklist(nonPushableUserAgentPatterns);
+        }
+
+        strategy.setMaxAssociatedResources(maxAssociatedResources);
+        strategy.setReferrerPushPeriod((int) referrerPushPeriod.toMilliseconds());
+
+        return strategy;
+    }
+}
diff --git a/dropwizard-spdy/src/main/java/io/dropwizard/spdy/Spdy3ConnectorFactory.java b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/Spdy3ConnectorFactory.java
new file mode 100644
index 0000000..3584a58
--- /dev/null
+++ b/dropwizard-spdy/src/main/java/io/dropwizard/spdy/Spdy3ConnectorFactory.java
@@ -0,0 +1,102 @@
+package io.dropwizard.spdy;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.jetty9.InstrumentedConnectionFactory;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonTypeName;
+import io.dropwizard.jetty.HttpsConnectorFactory;
+import org.eclipse.jetty.io.ByteBufferPool;
+import org.eclipse.jetty.server.*;
+import org.eclipse.jetty.spdy.api.SPDY;
+import org.eclipse.jetty.spdy.server.NPNServerConnectionFactory;
+import org.eclipse.jetty.spdy.server.http.HTTPSPDYServerConnectionFactory;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
+import org.eclipse.jetty.util.thread.Scheduler;
+import org.eclipse.jetty.util.thread.ThreadPool;
+
+import javax.validation.Valid;
+import javax.validation.constraints.NotNull;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+/**
+ * Builds SPDY v3 connectors.
+ * <p/>
+ * <b>Configuration Parameters:</b>
+ * <table>
+ *     <tr>
+ *         <td>Name</td>
+ *         <td>Default</td>
+ *         <td>Description</td>
+ *     </tr>
+ *     <tr>
+ *         <td>{@code pushStrategy}</td>
+ *         <td>(none)</td>
+ *         <td>
+ *             The {@link PushStrategyFactory push strategy} to use for server-initiated SPDY
+ *             pushes.
+ *         </td>
+ *     </tr>
+ * </table>
+ * <p/>
+ * For more configuration parameters, see {@link HttpsConnectorFactory}.
+ *
+ * @see HttpsConnectorFactory
+ */
+ at JsonTypeName("spdy3")
+public class Spdy3ConnectorFactory extends HttpsConnectorFactory {
+    @Valid
+    @NotNull
+    private PushStrategyFactory pushStrategy = new NonePushStrategyFactory();
+
+    @JsonProperty
+    public PushStrategyFactory getPushStrategy() {
+        return pushStrategy;
+    }
+
+    @JsonProperty
+    public void setPushStrategy(PushStrategyFactory pushStrategy) {
+        this.pushStrategy = pushStrategy;
+    }
+
+    @Override
+    public Connector build(Server server, MetricRegistry metrics, String name, ThreadPool threadPool) {
+        logSupportedParameters();
+
+        final HttpConfiguration httpConfig = buildHttpConfiguration();
+
+        final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig);
+
+        final SslContextFactory sslContextFactory = buildSslContextFactory();
+        server.addBean(sslContextFactory);
+
+        final PushStrategy pushStrategy = this.pushStrategy.build();
+        final HTTPSPDYServerConnectionFactory spdy3Factory =
+                new HTTPSPDYServerConnectionFactory(SPDY.V3, httpConfig, pushStrategy);
+
+        final NPNServerConnectionFactory npnFactory =
+                new NPNServerConnectionFactory("spdy/3", "spdy/2", "http/1.1");
+        npnFactory.setDefaultProtocol("http/1.1");
+
+        final HTTPSPDYServerConnectionFactory spdy2Factory =
+                new HTTPSPDYServerConnectionFactory(SPDY.V2, httpConfig, pushStrategy);
+
+        final SslConnectionFactory sslConnectionFactory =
+                new SslConnectionFactory(sslContextFactory, "npn");
+
+        final Scheduler scheduler = new ScheduledExecutorScheduler();
+
+        final ByteBufferPool bufferPool = buildBufferPool();
+
+        final String timerName = name(HttpConnectionFactory.class, getBindHost(), Integer.toString(getPort()), "connections");
+
+        return buildConnector(server, scheduler, bufferPool, name, threadPool,
+                              new InstrumentedConnectionFactory(sslConnectionFactory, metrics.timer(timerName)),
+                              npnFactory,
+                              spdy3Factory,
+                              spdy2Factory,
+                              httpConnectionFactory);
+    }
+}
diff --git a/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable b/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
new file mode 100644
index 0000000..cf04b08
--- /dev/null
+++ b/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable
@@ -0,0 +1 @@
+io.dropwizard.spdy.PushStrategyFactory
diff --git a/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory b/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory
new file mode 100644
index 0000000..1a2d4c9
--- /dev/null
+++ b/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory
@@ -0,0 +1 @@
+io.dropwizard.spdy.Spdy3ConnectorFactory
diff --git a/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.spdy.PushStrategyFactory b/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.spdy.PushStrategyFactory
new file mode 100644
index 0000000..e0007b5
--- /dev/null
+++ b/dropwizard-spdy/src/main/resources/META-INF/services/io.dropwizard.spdy.PushStrategyFactory
@@ -0,0 +1,2 @@
+io.dropwizard.spdy.NonePushStrategyFactory
+io.dropwizard.spdy.ReferrerPushStrategyFactory
diff --git a/dropwizard-spdy/src/test/java/io/dropwizard/spdy/NonePushStrategyFactoryTest.java b/dropwizard-spdy/src/test/java/io/dropwizard/spdy/NonePushStrategyFactoryTest.java
new file mode 100644
index 0000000..6c0c2d4
--- /dev/null
+++ b/dropwizard-spdy/src/test/java/io/dropwizard/spdy/NonePushStrategyFactoryTest.java
@@ -0,0 +1,23 @@
+package io.dropwizard.spdy;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.eclipse.jetty.spdy.server.http.PushStrategy;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class NonePushStrategyFactoryTest {
+    private final NonePushStrategyFactory factory = new NonePushStrategyFactory();
+
+    @Test
+    public void returnsAPushStrategyWhichNeverPushesAnything() throws Exception {
+        assertThat(factory.build())
+                .isInstanceOf(PushStrategy.None.class);
+    }
+
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(NonePushStrategyFactory.class);
+    }
+}
diff --git a/dropwizard-spdy/src/test/java/io/dropwizard/spdy/ReferrerPushStrategyFactoryTest.java b/dropwizard-spdy/src/test/java/io/dropwizard/spdy/ReferrerPushStrategyFactoryTest.java
new file mode 100644
index 0000000..863459b
--- /dev/null
+++ b/dropwizard-spdy/src/test/java/io/dropwizard/spdy/ReferrerPushStrategyFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.spdy;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class ReferrerPushStrategyFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(ReferrerPushStrategyFactory.class);
+    }
+}
diff --git a/dropwizard-spdy/src/test/java/io/dropwizard/spdy/Spdy3ConnectorFactoryTest.java b/dropwizard-spdy/src/test/java/io/dropwizard/spdy/Spdy3ConnectorFactoryTest.java
new file mode 100644
index 0000000..f8d0372
--- /dev/null
+++ b/dropwizard-spdy/src/test/java/io/dropwizard/spdy/Spdy3ConnectorFactoryTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.spdy;
+
+import io.dropwizard.jackson.DiscoverableSubtypeResolver;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class Spdy3ConnectorFactoryTest {
+    @Test
+    public void isDiscoverable() throws Exception {
+        assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes())
+                .contains(Spdy3ConnectorFactory.class);
+    }
+}
diff --git a/dropwizard-testing/pom.xml b/dropwizard-testing/pom.xml
new file mode 100644
index 0000000..4661d69
--- /dev/null
+++ b/dropwizard-testing/pom.xml
@@ -0,0 +1,73 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-testing</artifactId>
+    <name>Dropwizard Test Helpers</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${jersey.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-inmemory</artifactId>
+            <version>${jersey.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>1.9.5</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.objenesis</groupId>
+                    <artifactId>objenesis</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.hamcrest</groupId>
+                    <artifactId>hamcrest-core</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.objenesis</groupId>
+            <artifactId>objenesis</artifactId>
+            <version>2.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.easytesting</groupId>
+            <artifactId>fest-assert-core</artifactId>
+            <version>2.0M10</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/FixtureHelpers.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/FixtureHelpers.java
new file mode 100644
index 0000000..0da903a
--- /dev/null
+++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/FixtureHelpers.java
@@ -0,0 +1,39 @@
+package io.dropwizard.testing;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+
+/**
+ * A set of helper method for fixture files.
+ */
+public class FixtureHelpers {
+    private FixtureHelpers() { /* singleton */ }
+
+    /**
+     * Reads the given fixture file from {@code src/test/resources} and returns its contents as a
+     * UTF-8 string.
+     *
+     * @param filename    the filename of the fixture file
+     * @return the contents of {@code src/test/resources/{filename}}
+     * @throws IOException if {@code filename} doesn't exist or can't be opened
+     */
+    public static String fixture(String filename) throws IOException {
+        return fixture(filename, Charsets.UTF_8);
+    }
+
+    /**
+     * Reads the given fixture file from {@code src/test/resources} and returns its contents as a
+     * string.
+     *
+     * @param filename    the filename of the fixture file
+     * @param charset     the character set of {@code filename}
+     * @return the contents of {@code src/test/resources/{filename}}
+     * @throws IOException if {@code filename} doesn't exist or can't be opened
+     */
+    private static String fixture(String filename, Charset charset) throws IOException {
+        return Resources.toString(Resources.getResource(filename), charset).trim();
+    }
+}
diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ConfigOverride.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ConfigOverride.java
new file mode 100644
index 0000000..a09bd07
--- /dev/null
+++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ConfigOverride.java
@@ -0,0 +1,20 @@
+package io.dropwizard.testing.junit;
+
+public class ConfigOverride {
+
+    private final String key;
+    private final String value;
+
+    private ConfigOverride(String key, String value) {
+        this.key = key;
+        this.value = value;
+    }
+
+    public static ConfigOverride config(String key, String value) {
+        return new ConfigOverride(key, value);
+    }
+
+    public void addToSystemProperties() {
+        System.setProperty("dw." + key, value);
+    }
+}
diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardAppRule.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardAppRule.java
new file mode 100644
index 0000000..a4986d0
--- /dev/null
+++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardAppRule.java
@@ -0,0 +1,141 @@
+package io.dropwizard.testing.junit;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.cli.ServerCommand;
+import io.dropwizard.lifecycle.ServerLifecycleListener;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import net.sourceforge.argparse4j.inf.Namespace;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.ServerConnector;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import javax.annotation.Nullable;
+import java.util.Enumeration;
+
+/**
+ * A JUnit rule for starting and stopping your application at the start and end of a test class.
+ * <p/>
+ * By default, the {@link Application} will be constructed using reflection to invoke the nullary
+ * constructor. If your application does not provide a public nullary constructor, you will need to
+ * override the {@link #newApplication()} method to provide your application instance(s).
+ *
+ * @param <C> the configuration type
+ */
+public class DropwizardAppRule<C extends Configuration> implements TestRule {
+
+    private final Class<? extends Application<C>> applicationClass;
+    private final String configPath;
+
+    private C configuration;
+    private Application<C> application;
+    private Environment environment;
+    private Server jettyServer;
+
+    public DropwizardAppRule(Class<? extends Application<C>> applicationClass,
+                             @Nullable String configPath,
+                             ConfigOverride... configOverrides) {
+        this.applicationClass = applicationClass;
+        this.configPath = configPath;
+        for (ConfigOverride configOverride: configOverrides) {
+            configOverride.addToSystemProperties();
+        }
+    }
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                startIfRequired();
+                try {
+                    base.evaluate();
+                } finally {
+                    resetConfigOverrides();
+                    jettyServer.stop();
+                }
+            }
+        };
+    }
+
+    private void resetConfigOverrides() {
+        for (Enumeration<?> props = System.getProperties().propertyNames(); props.hasMoreElements();) {
+            String keyString = (String) props.nextElement();
+            if (keyString.startsWith("dw.")) {
+                System.clearProperty(keyString);
+            }
+        }
+    }
+
+    private void startIfRequired() {
+        if (jettyServer != null) {
+            return;
+        }
+
+        try {
+            application = newApplication();
+
+            final Bootstrap<C> bootstrap = new Bootstrap<C>(application) {
+                @Override
+                public void run(C configuration, Environment environment) throws Exception {
+                    environment.lifecycle().addServerLifecycleListener(new ServerLifecycleListener() {
+                                    @Override
+                                    public void serverStarted(Server server) {
+                                        jettyServer = server;
+                                    }
+                                });
+                    DropwizardAppRule.this.configuration = configuration;
+                    DropwizardAppRule.this.environment = environment;
+                    super.run(configuration, environment);
+                }
+            };
+
+            application.initialize(bootstrap);
+            final ServerCommand<C> command = new ServerCommand<>(application);
+
+            ImmutableMap.Builder<String, Object> file = ImmutableMap.builder();
+            if (!Strings.isNullOrEmpty(configPath)) {
+                file.put("file", configPath);
+            }
+            final Namespace namespace = new Namespace(file.build());
+
+            command.run(bootstrap, namespace);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public C getConfiguration() {
+        return configuration;
+    }
+
+    public int getLocalPort() {
+        return ((ServerConnector) jettyServer.getConnectors()[0]).getLocalPort();
+    }
+
+    public int getAdminPort() {
+        return ((ServerConnector) jettyServer.getConnectors()[1]).getLocalPort();
+    }
+
+    public Application<C> newApplication() {
+        try {
+            return applicationClass.newInstance();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    public <A extends Application<C>> A getApplication() {
+        return (A) application;
+    }
+
+    public Environment getEnvironment() {
+        return environment;
+    }
+}
diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestRule.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestRule.java
new file mode 100755
index 0000000..db5c02b
--- /dev/null
+++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestRule.java
@@ -0,0 +1,159 @@
+package io.dropwizard.testing.junit;
+
+import com.codahale.metrics.MetricRegistry;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import io.dropwizard.jackson.Jackson;
+import io.dropwizard.jersey.DropwizardResourceConfig;
+import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider;
+import io.dropwizard.logging.LoggingFactory;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A JUnit {@link TestRule} for testing Jersey resources.
+ */
+public class ResourceTestRule implements TestRule {
+
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    public static class Builder {
+
+        private final Set<Object> singletons = Sets.newHashSet();
+        private final Set<Class<?>> providers = Sets.newHashSet();
+        private final Map<String, Boolean> features = Maps.newHashMap();
+        private final Map<String, Object> properties = Maps.newHashMap();
+        private ObjectMapper mapper = Jackson.newObjectMapper();
+        private Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+        public Builder setMapper(ObjectMapper mapper) {
+            this.mapper = mapper;
+            return this;
+        }
+
+        public Builder setValidator(Validator validator) {
+            this.validator = validator;
+            return this;
+        }
+
+        public Builder addResource(Object resource) {
+            singletons.add(resource);
+            return this;
+        }
+
+        public Builder addProvider(Class<?> klass) {
+            providers.add(klass);
+            return this;
+        }
+
+        public Builder addProvider(Object provider) {
+            singletons.add(provider);
+            return this;
+        }
+
+        public Builder addFeature(String feature, Boolean value) {
+            features.put(feature, value);
+            return this;
+        }
+
+        public Builder addProperty(String property, Object value) {
+            properties.put(property, value);
+            return this;
+        }
+
+        public ResourceTestRule build() {
+            return new ResourceTestRule(singletons, providers, features, properties, mapper, validator);
+        }
+    }
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private final Set<Object> singletons;
+    private final Set<Class<?>> providers;
+    private final Map<String, Boolean> features;
+    private final Map<String, Object> properties;
+    private final ObjectMapper mapper;
+    private final Validator validator;
+
+    private JerseyTest test;
+
+    private ResourceTestRule(Set<Object> singletons,
+                             Set<Class<?>> providers,
+                             Map<String, Boolean> features,
+                             Map<String, Object> properties,
+                             ObjectMapper mapper,
+                             Validator validator) {
+        this.singletons = singletons;
+        this.providers = providers;
+        this.features = features;
+        this.properties = properties;
+        this.mapper = mapper;
+        this.validator = validator;
+    }
+
+    public Validator getValidator() {
+        return validator;
+    }
+
+    public ObjectMapper getObjectMapper() {
+        return mapper;
+    }
+
+    public Client client() {
+        return test.client();
+    }
+
+    public JerseyTest getJerseyTest() {
+        return test;
+    }
+
+    @Override
+    public Statement apply(final Statement base, Description description) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                try {
+                    test = new JerseyTest() {
+                        @Override
+                        protected AppDescriptor configure() {
+                            final DropwizardResourceConfig config = DropwizardResourceConfig.forTesting(new MetricRegistry());
+                            for (Class<?> provider : providers) {
+                                config.getClasses().add(provider);
+                            }
+                            for (Map.Entry<String, Boolean> feature : features.entrySet()) {
+                                config.getFeatures().put(feature.getKey(), feature.getValue());
+                            }
+                            for (Map.Entry<String, Object> property : properties.entrySet()) {
+                                config.getProperties().put(property.getKey(), property.getValue());
+                            }
+                            config.getSingletons().add(new JacksonMessageBodyProvider(mapper, validator));
+                            config.getSingletons().addAll(singletons);
+                            return new LowLevelAppDescriptor.Builder(config).build();
+                        }
+                    };
+                    test.setUp();
+                    base.evaluate();
+                } finally {
+                    if (test != null) {
+                        test.tearDown();
+                    }
+                }
+            }
+        };
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/FixtureHelpersTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/FixtureHelpersTest.java
new file mode 100644
index 0000000..3182751
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/FixtureHelpersTest.java
@@ -0,0 +1,14 @@
+package io.dropwizard.testing;
+
+import org.junit.Test;
+
+import static io.dropwizard.testing.FixtureHelpers.fixture;
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class FixtureHelpersTest {
+    @Test
+    public void readsTheFileAsAString() throws Exception {
+        assertThat(fixture("fixtures/fixture.txt"))
+                .isEqualTo("YAY FOR ME");
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/Person.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/Person.java
new file mode 100644
index 0000000..a84ed09
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/Person.java
@@ -0,0 +1,53 @@
+package io.dropwizard.testing;
+
+import com.google.common.base.Objects;
+
+public class Person {
+    private String name;
+    private String email;
+
+    public Person() { /* Jackson deserialization */ }
+
+    public Person(String name, String email) {
+        this.name = name;
+        this.email = email;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) { return true; }
+        if ((obj == null) || (getClass() != obj.getClass())) { return false; }
+
+        final Person person = (Person) obj;
+        return !((email != null) ? !email.equals(person.email) : (person.email != null)) &&
+                !((name != null) ? !name.equals(person.name) : (person.name != null));
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (name != null) ? name.hashCode() : 0;
+        result = (31 * result) + ((email != null) ? email.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return Objects.toStringHelper(this).add("name", name).add("email", email).toString();
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/GzipDefaultVaryBehaviourTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/GzipDefaultVaryBehaviourTest.java
new file mode 100644
index 0000000..769a579
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/GzipDefaultVaryBehaviourTest.java
@@ -0,0 +1,35 @@
+package io.dropwizard.testing.app;
+
+import com.sun.jersey.api.client.Client;
+import com.sun.jersey.api.client.ClientResponse;
+import io.dropwizard.testing.junit.DropwizardAppRule;
+import io.dropwizard.testing.junit.DropwizardAppRuleTest;
+import io.dropwizard.testing.junit.TestApplication;
+import io.dropwizard.testing.junit.TestConfiguration;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static javax.ws.rs.core.HttpHeaders.ACCEPT_ENCODING;
+import static javax.ws.rs.core.HttpHeaders.CONTENT_ENCODING;
+import static javax.ws.rs.core.HttpHeaders.VARY;
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class GzipDefaultVaryBehaviourTest {
+
+    @ClassRule
+    public static final DropwizardAppRule<TestConfiguration> RULE =
+            new DropwizardAppRule<>(TestApplication.class, DropwizardAppRuleTest.resourceFilePath("test-config.yaml"));
+
+    @Test
+    public void testDefaultVaryHeader() {
+        final ClientResponse clientResponse = new Client().resource("http://localhost:" +
+                RULE.getLocalPort()
+                +"/test")
+                .header(ACCEPT_ENCODING, "gzip")
+                .get(ClientResponse.class);
+
+        assertThat(clientResponse.getHeaders().get(VARY)).isEqualTo(asList(ACCEPT_ENCODING));
+        assertThat(clientResponse.getHeaders().get(CONTENT_ENCODING)).isEqualTo(asList("gzip"));
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PeopleStore.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PeopleStore.java
new file mode 100644
index 0000000..a258750
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PeopleStore.java
@@ -0,0 +1,7 @@
+package io.dropwizard.testing.app;
+
+import io.dropwizard.testing.Person;
+
+public interface PeopleStore {
+    Person fetchPerson(String name);
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResource.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResource.java
new file mode 100644
index 0000000..247e332
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResource.java
@@ -0,0 +1,26 @@
+package io.dropwizard.testing.app;
+
+import com.codahale.metrics.annotation.Timed;
+import io.dropwizard.testing.Person;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+ at Path("/person/{name}")
+ at Produces(MediaType.APPLICATION_JSON)
+public class PersonResource {
+    private final PeopleStore store;
+
+    public PersonResource(PeopleStore store) {
+        this.store = store;
+    }
+
+    @GET
+    @Timed
+    public Person getPerson(@PathParam("name") String name) {
+        return store.fetchPerson(name);
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceTest.java
new file mode 100644
index 0000000..3e291a9
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceTest.java
@@ -0,0 +1,44 @@
+package io.dropwizard.testing.app;
+
+import io.dropwizard.testing.Person;
+import io.dropwizard.testing.junit.ResourceTestRule;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static org.mockito.Mockito.*;
+import static org.fest.assertions.api.Assertions.assertThat;
+
+/**
+ * Tests {@link ResourceTestRule}.
+ */
+public class PersonResourceTest {
+
+    static {
+        Logger.getLogger("com.sun.jersey").setLevel(Level.OFF);
+    }
+
+    private static final PeopleStore dao = mock(PeopleStore.class);
+
+    @ClassRule
+    public static final ResourceTestRule resources = ResourceTestRule.builder()
+            .addResource(new PersonResource(dao))
+            .build();
+
+    private final Person person = new Person("blah", "blah at example.com");
+
+    @Before
+    public void setup() {
+        when(dao.fetchPerson(eq("blah"))).thenReturn(person);
+    }
+
+    @Test
+    public void testGetPerson() {
+        assertThat(resources.client().resource("/person/blah").get(Person.class))
+                .isEqualTo(person);
+        verify(dao).fetchPerson("blah");
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleTest.java
new file mode 100644
index 0000000..395e16e
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleTest.java
@@ -0,0 +1,127 @@
+package io.dropwizard.testing.junit;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.io.Resources;
+import com.sun.jersey.api.client.Client;
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.servlets.tasks.Task;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import org.hibernate.validator.constraints.NotEmpty;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import java.io.File;
+import java.io.PrintWriter;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+public class DropwizardAppRuleTest {
+
+    @ClassRule
+    public static final DropwizardAppRule<TestConfiguration> RULE =
+            new DropwizardAppRule<>(TestApplication.class, resourceFilePath("test-config.yaml"));
+
+    @Test
+    public void canGetExpectedResourceOverHttp() {
+        final String content = new Client().resource("http://localhost:" +
+                                                     RULE.getLocalPort()
+                                                     +"/test").get(String.class);
+
+        assertThat(content, is("Yes, it's here"));
+    }
+
+    @Test
+    public void returnsConfiguration() {
+        final TestConfiguration config = RULE.getConfiguration();
+        assertThat(config.getMessage(), is("Yes, it's here"));
+    }
+
+    @Test
+    public void returnsApplication() {
+        final TestApplication application = RULE.getApplication();
+        assertNotNull(application);
+    }
+
+    @Test
+    public void returnsEnvironment() {
+        final Environment environment = RULE.getEnvironment();
+        assertThat(environment.getName(), is("TestApplication"));
+    }
+
+    @Test
+    public void canPerformAdminTask() {
+        final String response = new Client().resource("http://localhost:" +
+                RULE.getAdminPort() + "/tasks/hello?name=test_user")
+                .post(String.class);
+        assertThat(response, is("Hello has been said to test_user"));
+    }
+
+    public static class TestApplication extends Application<TestConfiguration> {
+        @Override
+        public void initialize(Bootstrap<TestConfiguration> bootstrap) {
+        }
+
+        @Override
+        public void run(TestConfiguration configuration, Environment environment) throws Exception {
+            environment.jersey().register(new TestResource(configuration.getMessage()));
+            environment.admin().addTask(new HelloTask());
+        }
+    }
+
+    @Path("/")
+    public static class TestResource {
+
+        private final String message;
+
+        public TestResource(String message) {
+            this.message = message;
+        }
+
+        @Path("test")
+        @GET
+        public String test() {
+            return message;
+        }
+    }
+
+    public static class TestConfiguration extends Configuration {
+        @NotEmpty
+        @JsonProperty
+        private String message;
+
+        public String getMessage() {
+            return message;
+        }
+    }
+
+    public static String resourceFilePath(String resourceClassPathLocation) {
+        try {
+            return new File(Resources.getResource(resourceClassPathLocation).toURI()).getAbsolutePath();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static class HelloTask extends Task {
+
+        public HelloTask() {
+            super("hello");
+        }
+
+        @Override
+        public void execute(ImmutableMultimap<String, String> parameters, PrintWriter output) throws Exception {
+            ImmutableCollection<String> names = parameters.get("name");
+            String name = !names.isEmpty() ? names.asList().get(0) : "Anonymous";
+            output.print("Hello has been said to " + name);
+            output.flush();
+        }
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithoutConfigTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithoutConfigTest.java
new file mode 100644
index 0000000..e505cff
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithoutConfigTest.java
@@ -0,0 +1,52 @@
+package io.dropwizard.testing.junit;
+
+import com.google.common.collect.ImmutableMap;
+import com.sun.jersey.api.client.Client;
+import io.dropwizard.Application;
+import io.dropwizard.Configuration;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.Map;
+
+public class DropwizardAppRuleWithoutConfigTest {
+
+    @ClassRule
+    public static final DropwizardAppRule<Configuration> RULE = new DropwizardAppRule<>(TestApplication.class, null);
+
+    Client client = new Client();
+
+    @Test
+    public void runWithoutConfigFile() {
+        Map response = client.resource("http://localhost:" + RULE.getLocalPort() + "/test").get(Map.class);
+        Assert.assertEquals(ImmutableMap.of("color", "orange"), response);
+    }
+
+    static class TestApplication extends Application<Configuration> {
+        @Override
+        public void initialize(Bootstrap<Configuration> bootstrap) {
+        }
+
+        @Override
+        public void run(Configuration configuration, Environment environment) throws Exception {
+            environment.jersey().register(new TestResource());
+        }
+    }
+
+    @Path("test")
+    @Produces(MediaType.APPLICATION_JSON)
+    public static class TestResource {
+        @GET
+        public Response get() {
+            return Response.ok(ImmutableMap.of("color", "orange")).build();
+        }
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardServiceRuleConfigOverrideTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardServiceRuleConfigOverrideTest.java
new file mode 100644
index 0000000..7be7156
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardServiceRuleConfigOverrideTest.java
@@ -0,0 +1,27 @@
+package io.dropwizard.testing.junit;
+
+import com.sun.jersey.api.client.Client;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import static io.dropwizard.testing.junit.ConfigOverride.config;
+import static io.dropwizard.testing.junit.DropwizardAppRuleTest.resourceFilePath;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+public class DropwizardServiceRuleConfigOverrideTest {
+
+    @ClassRule
+    public static final DropwizardAppRule<TestConfiguration> RULE =
+            new DropwizardAppRule<TestConfiguration>(TestApplication.class,
+                                                     resourceFilePath("test-config.yaml"),
+                                                     config("message", "A new way to say Hooray!"));
+
+    @Test
+    public void supportsConfigAttributeOverrides() {
+        final String content = new Client().resource("http://localhost:" + RULE.getLocalPort() + "/test")
+                                           .get(String.class);
+
+        assertThat(content, is("A new way to say Hooray!"));
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestApplication.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestApplication.java
new file mode 100644
index 0000000..2d09522
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestApplication.java
@@ -0,0 +1,17 @@
+package io.dropwizard.testing.junit;
+
+import io.dropwizard.Application;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+public class TestApplication extends Application<TestConfiguration> {
+
+    @Override
+    public void initialize(Bootstrap<TestConfiguration> bootstrap) {
+    }
+
+    @Override
+    public void run(TestConfiguration configuration, Environment environment) throws Exception {
+        environment.jersey().register(new TestResource(configuration.getMessage()));
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestConfiguration.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestConfiguration.java
new file mode 100644
index 0000000..67bca9a
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestConfiguration.java
@@ -0,0 +1,16 @@
+package io.dropwizard.testing.junit;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.dropwizard.Configuration;
+import org.hibernate.validator.constraints.NotEmpty;
+
+public class TestConfiguration extends Configuration {
+
+    @JsonProperty
+    @NotEmpty
+    private String message;
+
+    public String getMessage() {
+        return message;
+    }
+}
diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestResource.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestResource.java
new file mode 100644
index 0000000..e8a8de2
--- /dev/null
+++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestResource.java
@@ -0,0 +1,20 @@
+package io.dropwizard.testing.junit;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+
+ at Path("/")
+public class TestResource {
+
+    private final String message;
+
+    public TestResource(String message) {
+        this.message = message;
+    }
+
+    @Path("test")
+    @GET
+    public String test() {
+        return message;
+    }
+}
diff --git a/dropwizard-testing/src/test/resources/fixtures/fixture.txt b/dropwizard-testing/src/test/resources/fixtures/fixture.txt
new file mode 100644
index 0000000..cc6ef95
--- /dev/null
+++ b/dropwizard-testing/src/test/resources/fixtures/fixture.txt
@@ -0,0 +1 @@
+YAY FOR ME
diff --git a/dropwizard-testing/src/test/resources/fixtures/person.json b/dropwizard-testing/src/test/resources/fixtures/person.json
new file mode 100644
index 0000000..542f91d
--- /dev/null
+++ b/dropwizard-testing/src/test/resources/fixtures/person.json
@@ -0,0 +1,4 @@
+{
+    "name": "Coda",
+    "email": "coda at example.com"
+}
diff --git a/dropwizard-testing/src/test/resources/logback-test.xml b/dropwizard-testing/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-testing/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-testing/src/test/resources/test-config.yaml b/dropwizard-testing/src/test/resources/test-config.yaml
new file mode 100644
index 0000000..a639e87
--- /dev/null
+++ b/dropwizard-testing/src/test/resources/test-config.yaml
@@ -0,0 +1,12 @@
+message: "Yes, it's here"
+server:
+  applicationConnectors:
+    - type: http
+      port: 0
+  adminConnectors:
+    - type: http
+      port: 0
+  requestLog:
+    appenders: []
+logging:
+  appenders: []
diff --git a/dropwizard-util/pom.xml b/dropwizard-util/pom.xml
new file mode 100644
index 0000000..0f3817b
--- /dev/null
+++ b/dropwizard-util/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-util</artifactId>
+    <name>Dropwizard Utility Classes</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <version>${jackson.api.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>${guava.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.findbugs</groupId>
+            <artifactId>jsr305</artifactId>
+            <version>2.0.3</version>
+        </dependency>
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+            <version>2.3</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-util/src/main/java/io/dropwizard/util/Duration.java b/dropwizard-util/src/main/java/io/dropwizard/util/Duration.java
new file mode 100644
index 0000000..48aa541
--- /dev/null
+++ b/dropwizard-util/src/main/java/io/dropwizard/util/Duration.java
@@ -0,0 +1,152 @@
+package io.dropwizard.util;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class Duration {
+    private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+)\\s*(\\S+)");
+
+    private static final Map<String, TimeUnit> SUFFIXES = new ImmutableMap.Builder<String, TimeUnit>()
+            .put("ns", TimeUnit.NANOSECONDS)
+            .put("nanosecond", TimeUnit.NANOSECONDS)
+            .put("nanoseconds", TimeUnit.NANOSECONDS)
+            .put("us", TimeUnit.MICROSECONDS)
+            .put("microsecond", TimeUnit.MICROSECONDS)
+            .put("microseconds", TimeUnit.MICROSECONDS)
+            .put("ms", TimeUnit.MILLISECONDS)
+            .put("millisecond", TimeUnit.MILLISECONDS)
+            .put("milliseconds", TimeUnit.MILLISECONDS)
+            .put("s", TimeUnit.SECONDS)
+            .put("second", TimeUnit.SECONDS)
+            .put("seconds", TimeUnit.SECONDS)
+            .put("m", TimeUnit.MINUTES)
+            .put("minute", TimeUnit.MINUTES)
+            .put("minutes", TimeUnit.MINUTES)
+            .put("h", TimeUnit.HOURS)
+            .put("hour", TimeUnit.HOURS)
+            .put("hours", TimeUnit.HOURS)
+            .put("d", TimeUnit.DAYS)
+            .put("day", TimeUnit.DAYS)
+            .put("days", TimeUnit.DAYS)
+            .build();
+
+    public static Duration nanoseconds(long count) {
+        return new Duration(count, TimeUnit.NANOSECONDS);
+    }
+
+    public static Duration microseconds(long count) {
+        return new Duration(count, TimeUnit.MICROSECONDS);
+    }
+
+    public static Duration milliseconds(long count) {
+        return new Duration(count, TimeUnit.MILLISECONDS);
+    }
+
+    public static Duration seconds(long count) {
+        return new Duration(count, TimeUnit.SECONDS);
+    }
+
+    public static Duration minutes(long count) {
+        return new Duration(count, TimeUnit.MINUTES);
+    }
+
+    public static Duration hours(long count) {
+        return new Duration(count, TimeUnit.HOURS);
+    }
+
+    public static Duration days(long count) {
+        return new Duration(count, TimeUnit.DAYS);
+    }
+
+    @JsonCreator
+    public static Duration parse(String duration) {
+        final Matcher matcher = DURATION_PATTERN.matcher(duration);
+        checkArgument(matcher.matches(), "Invalid duration: " + duration);
+
+        final long count = Long.valueOf(matcher.group(1));
+        final TimeUnit unit = SUFFIXES.get(matcher.group(2));
+        if (unit == null) {
+            throw new IllegalArgumentException("Invalid duration: " + duration + ". Wrong time unit");
+        }
+
+        return new Duration(count, unit);
+    }
+
+    private final long count;
+    private final TimeUnit unit;
+
+    private Duration(long count, TimeUnit unit) {
+        this.count = count;
+        this.unit = checkNotNull(unit);
+    }
+
+    public long getQuantity() {
+        return count;
+    }
+
+    public TimeUnit getUnit() {
+        return unit;
+    }
+
+    public long toNanoseconds() {
+        return TimeUnit.NANOSECONDS.convert(count, unit);
+    }
+
+    public long toMicroseconds() {
+        return TimeUnit.MICROSECONDS.convert(count, unit);
+    }
+
+    public long toMilliseconds() {
+        return TimeUnit.MILLISECONDS.convert(count, unit);
+    }
+
+    public long toSeconds() {
+        return TimeUnit.SECONDS.convert(count, unit);
+    }
+
+    public long toMinutes() {
+        return TimeUnit.MINUTES.convert(count, unit);
+    }
+
+    public long toHours() {
+        return TimeUnit.HOURS.convert(count, unit);
+    }
+
+    public long toDays() {
+        return TimeUnit.DAYS.convert(count, unit);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) { return true; }
+        if ((obj == null) || (getClass() != obj.getClass())) { return false; }
+        final Duration duration = (Duration) obj;
+        return (count == duration.count) && (unit == duration.unit);
+
+    }
+
+    @Override
+    public int hashCode() {
+        return (31 * (int) (count ^ (count >>> 32))) + unit.hashCode();
+    }
+
+    @Override
+    @JsonValue
+    public String toString() {
+        String units = unit.toString().toLowerCase(Locale.ENGLISH);
+        if (count == 1) {
+            units = units.substring(0, units.length() - 1);
+        }
+        return Long.toString(count) + ' ' + units;
+    }
+}
diff --git a/dropwizard-util/src/main/java/io/dropwizard/util/Generics.java b/dropwizard-util/src/main/java/io/dropwizard/util/Generics.java
new file mode 100644
index 0000000..1941927
--- /dev/null
+++ b/dropwizard-util/src/main/java/io/dropwizard/util/Generics.java
@@ -0,0 +1,76 @@
+package io.dropwizard.util;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+/**
+ * Helper methods for class type parameters.
+ * @see <a href="http://gafter.blogspot.com/2006/12/super-type-tokens.html">Super Type Tokens</a>
+ */
+public class Generics {
+    private Generics() { /* singleton */ }
+
+    /**
+     * Finds the type parameter for the given class.
+     *
+     * @param klass    a parameterized class
+     * @return the class's type parameter
+     */
+    public static Class<?> getTypeParameter(Class<?> klass) {
+        return getTypeParameter(klass, Object.class);
+    }
+
+    /**
+     * Finds the type parameter for the given class which is assignable to the bound class.
+     *
+     * @param klass    a parameterized class
+     * @param bound    the type bound
+     * @param <T>      the type bound
+     * @return the class's type parameter
+     */
+    @SuppressWarnings("unchecked")
+    public static <T> Class<T> getTypeParameter(Class<?> klass, Class<? super T> bound) {
+        Type t = checkNotNull(klass);
+        while (t instanceof Class<?>) {
+            t = ((Class<?>) t).getGenericSuperclass();
+        }
+        /* This is not guaranteed to work for all cases with convoluted piping
+         * of type parameters: but it can at least resolve straight-forward
+         * extension with single type parameter (as per [Issue-89]).
+         * And when it fails to do that, will indicate with specific exception.
+         */
+        if (t instanceof ParameterizedType) {
+            // should typically have one of type parameters (first one) that matches:
+            for (Type param : ((ParameterizedType) t).getActualTypeArguments()) {
+                if (param instanceof Class<?>) {
+                    final Class<T> cls = determineClass(bound, param);
+                    if (cls != null) { return cls; }
+                }
+                else if (param instanceof TypeVariable) {
+                    for (Type paramBound : ((TypeVariable<?>) param).getBounds()) {
+                        if (paramBound instanceof Class<?>) {
+                            final Class<T> cls = determineClass(bound, paramBound);
+                            if (cls != null) { return cls; }
+                        }
+                    }
+                }
+            }
+        }
+        throw new IllegalStateException("Cannot figure out type parameterization for " + klass.getName());
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T> Class<T> determineClass(Class<? super T> bound, Type candidate) {
+        if (candidate instanceof Class<?>) {
+            final Class<?> cls = (Class<?>) candidate;
+            if (bound.isAssignableFrom(cls)) {
+                return (Class<T>) cls;
+            }
+        }
+
+        return null;
+    }
+}
diff --git a/dropwizard-util/src/main/java/io/dropwizard/util/JarLocation.java b/dropwizard-util/src/main/java/io/dropwizard/util/JarLocation.java
new file mode 100644
index 0000000..98f788f
--- /dev/null
+++ b/dropwizard-util/src/main/java/io/dropwizard/util/JarLocation.java
@@ -0,0 +1,40 @@
+package io.dropwizard.util;
+
+import com.google.common.base.Optional;
+
+import java.io.File;
+import java.net.URL;
+
+/**
+ * A class which encapsulates the location on the local filesystem of the JAR in which the current
+ * code is executing.
+ */
+public class JarLocation {
+    private final Class<?> klass;
+
+    public JarLocation(Class<?> klass) {
+        this.klass = klass;
+    }
+
+    public Optional<String> getVersion() {
+        final Package pkg = klass.getPackage();
+        if (pkg == null) {
+            return Optional.absent();
+        }
+        return Optional.fromNullable(pkg.getImplementationVersion());
+    }
+
+    @Override
+    public String toString() {
+        final URL location = klass.getProtectionDomain().getCodeSource().getLocation();
+        try {
+            final String jar = new File(location.toURI()).getName();
+            if (jar.endsWith(".jar")) {
+                return jar;
+            }
+            return "project.jar";
+        } catch (Exception ignored) {
+            return "project.jar";
+        }
+    }
+}
diff --git a/dropwizard-util/src/main/java/io/dropwizard/util/Size.java b/dropwizard-util/src/main/java/io/dropwizard/util/Size.java
new file mode 100644
index 0000000..35c60dc
--- /dev/null
+++ b/dropwizard-util/src/main/java/io/dropwizard/util/Size.java
@@ -0,0 +1,132 @@
+package io.dropwizard.util;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.google.common.collect.ImmutableMap;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+public class Size {
+    private static final Pattern SIZE_PATTERN = Pattern.compile("(\\d+)\\s*(\\S+)");
+
+    private static final Map<String, SizeUnit> SUFFIXES = new ImmutableMap.Builder<String, SizeUnit>()
+            .put("B", SizeUnit.BYTES)
+            .put("byte", SizeUnit.BYTES)
+            .put("bytes", SizeUnit.BYTES)
+            .put("KB", SizeUnit.KILOBYTES)
+            .put("KiB", SizeUnit.KILOBYTES)
+            .put("kilobyte", SizeUnit.KILOBYTES)
+            .put("kilobytes", SizeUnit.KILOBYTES)
+            .put("MB", SizeUnit.MEGABYTES)
+            .put("MiB", SizeUnit.MEGABYTES)
+            .put("megabyte", SizeUnit.MEGABYTES)
+            .put("megabytes", SizeUnit.MEGABYTES)
+            .put("GB", SizeUnit.GIGABYTES)
+            .put("GiB", SizeUnit.GIGABYTES)
+            .put("gigabyte", SizeUnit.GIGABYTES)
+            .put("gigabytes", SizeUnit.GIGABYTES)
+            .put("TB", SizeUnit.TERABYTES)
+            .put("TiB", SizeUnit.TERABYTES)
+            .put("terabyte", SizeUnit.TERABYTES)
+            .put("terabytes", SizeUnit.TERABYTES)
+            .build();
+
+    public static Size bytes(long count) {
+        return new Size(count, SizeUnit.BYTES);
+    }
+
+    public static Size kilobytes(long count) {
+        return new Size(count, SizeUnit.KILOBYTES);
+    }
+
+    public static Size megabytes(long count) {
+        return new Size(count, SizeUnit.MEGABYTES);
+    }
+
+    public static Size gigabytes(long count) {
+        return new Size(count, SizeUnit.GIGABYTES);
+    }
+
+    public static Size terabytes(long count) {
+        return new Size(count, SizeUnit.TERABYTES);
+    }
+
+    @JsonCreator
+    public static Size parse(String size) {
+        final Matcher matcher = SIZE_PATTERN.matcher(size);
+        checkArgument(matcher.matches(), "Invalid size: " + size);
+
+        final long count = Long.valueOf(matcher.group(1));
+        final SizeUnit unit = SUFFIXES.get(matcher.group(2));
+        if (unit == null) {
+            throw new IllegalArgumentException("Invalid size: " + size + ". Wrong size unit");
+        }
+
+        return new Size(count, unit);
+    }
+
+    private final long count;
+    private final SizeUnit unit;
+
+    private Size(long count, SizeUnit unit) {
+        this.count = count;
+        this.unit = checkNotNull(unit);
+    }
+
+    public long getQuantity() {
+        return count;
+    }
+
+    public SizeUnit getUnit() {
+        return unit;
+    }
+
+    public long toBytes() {
+        return SizeUnit.BYTES.convert(count, unit);
+    }
+
+    public long toKilobytes() {
+        return SizeUnit.KILOBYTES.convert(count, unit);
+    }
+
+    public long toMegabytes() {
+        return SizeUnit.MEGABYTES.convert(count, unit);
+    }
+
+    public long toGigabytes() {
+        return SizeUnit.GIGABYTES.convert(count, unit);
+    }
+
+    public long toTerabytes() {
+        return SizeUnit.TERABYTES.convert(count, unit);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) { return true; }
+        if ((obj == null) || (getClass() != obj.getClass())) { return false; }
+        final Size size = (Size) obj;
+        return (count == size.count) && (unit == size.unit);
+    }
+
+    @Override
+    public int hashCode() {
+        return (31 * (int) (count ^ (count >>> 32))) + unit.hashCode();
+    }
+
+    @Override
+    @JsonValue
+    public String toString() {
+        String units = unit.toString().toLowerCase(Locale.ENGLISH);
+        if (count == 1) {
+            units = units.substring(0, units.length() - 1);
+        }
+        return Long.toString(count) + ' ' + units;
+    }
+}
diff --git a/dropwizard-util/src/main/java/io/dropwizard/util/SizeUnit.java b/dropwizard-util/src/main/java/io/dropwizard/util/SizeUnit.java
new file mode 100644
index 0000000..7837106
--- /dev/null
+++ b/dropwizard-util/src/main/java/io/dropwizard/util/SizeUnit.java
@@ -0,0 +1,98 @@
+package io.dropwizard.util;
+
+/**
+ * A unit of size.
+ */
+public enum SizeUnit {
+    /**
+     * Bytes.
+     */
+    BYTES(8),
+
+    /**
+     * Kilobytes.
+     */
+    KILOBYTES(8L * 1024),
+
+    /**
+     * Megabytes.
+     */
+    MEGABYTES(8L * 1024 * 1024),
+
+    /**
+     * Gigabytes.
+     */
+    GIGABYTES(8L * 1024 * 1024 * 1024),
+
+    /**
+     * Megabytes.
+     */
+    TERABYTES(8L * 1024 * 1024 * 1024 * 1024);
+
+    private final long bits;
+
+    SizeUnit(long bits) {
+        this.bits = bits;
+    }
+
+    /**
+     * Converts a size of the given unit into the current unit.
+     *
+     * @param size    the magnitude of the size
+     * @param unit    the unit of the size
+     * @return the given size in the current unit.
+     */
+    public long convert(long size, SizeUnit unit) {
+        return (size * unit.bits) / bits;
+    }
+
+    /**
+     * Converts the given number of the current units into bytes.
+     *
+     * @param l    the magnitude of the size in the current unit
+     * @return {@code l} of the current units in bytes
+     */
+    public long toBytes(long l) {
+        return BYTES.convert(l, this);
+    }
+
+    /**
+     * Converts the given number of the current units into kilobytes.
+     *
+     * @param l    the magnitude of the size in the current unit
+     * @return {@code l} of the current units in kilobytes
+     */
+    public long toKilobytes(long l) {
+        return KILOBYTES.convert(l, this);
+    }
+
+    /**
+     * Converts the given number of the current units into megabytes.
+     *
+     * @param l    the magnitude of the size in the current unit
+     * @return {@code l} of the current units in megabytes
+     */
+    public long toMegabytes(long l) {
+        return MEGABYTES.convert(l, this);
+    }
+
+    /**
+     * Converts the given number of the current units into gigabytes.
+     *
+     * @param l    the magnitude of the size in the current unit
+     * @return {@code l} of the current units in bytes
+     */
+    public long toGigabytes(long l) {
+        return GIGABYTES.convert(l, this);
+    }
+
+    /**
+     * Converts the given number of the current units into terabytes.
+     *
+     * @param l    the magnitude of the size in the current unit
+     * @return {@code l} of the current units in terabytes
+     */
+    public long toTerabytes(long l) {
+        return TERABYTES.convert(l, this);
+    }
+}
diff --git a/dropwizard-util/src/test/java/io/dropwizard/util/DurationTest.java b/dropwizard-util/src/test/java/io/dropwizard/util/DurationTest.java
new file mode 100644
index 0000000..cc29278
--- /dev/null
+++ b/dropwizard-util/src/test/java/io/dropwizard/util/DurationTest.java
@@ -0,0 +1,179 @@
+package io.dropwizard.util;
+
+import org.junit.Test;
+
+import java.util.concurrent.TimeUnit;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class DurationTest {
+    @Test
+    public void convertsDays() throws Exception {
+        assertThat(Duration.days(2).toDays())
+                .isEqualTo(2);
+        assertThat(Duration.days(2).toHours())
+                .isEqualTo(48);
+    }
+
+    @Test
+    public void convertsHours() throws Exception {
+        assertThat(Duration.hours(2).toMinutes())
+                .isEqualTo(120);
+    }
+
+    @Test
+    public void convertsMinutes() throws Exception {
+        assertThat(Duration.minutes(3).toSeconds())
+                .isEqualTo(180);
+    }
+
+    @Test
+    public void convertsSeconds() throws Exception {
+        assertThat(Duration.seconds(2).toMilliseconds())
+                .isEqualTo(2000);
+    }
+
+    @Test
+    public void convertsMilliseconds() throws Exception {
+        assertThat(Duration.milliseconds(2).toMicroseconds())
+                .isEqualTo(2000);
+    }
+
+    @Test
+    public void convertsMicroseconds() throws Exception {
+        assertThat(Duration.microseconds(2).toNanoseconds())
+                .isEqualTo(2000);
+    }
+
+    @Test
+    public void convertsNanoseconds() throws Exception {
+        assertThat(Duration.nanoseconds(2).toNanoseconds())
+                .isEqualTo(2);
+    }
+
+    @Test
+    public void parsesDays() throws Exception {
+        assertThat(Duration.parse("1d"))
+                .isEqualTo(Duration.days(1));
+
+        assertThat(Duration.parse("1 day"))
+                .isEqualTo(Duration.days(1));
+
+        assertThat(Duration.parse("2 days"))
+                .isEqualTo(Duration.days(2));
+    }
+
+    @Test
+    public void parsesHours() throws Exception {
+        assertThat(Duration.parse("1h"))
+                .isEqualTo(Duration.hours(1));
+
+        assertThat(Duration.parse("1 hour"))
+                .isEqualTo(Duration.hours(1));
+
+        assertThat(Duration.parse("2 hours"))
+                .isEqualTo(Duration.hours(2));
+    }
+
+    @Test
+    public void parsesMinutes() throws Exception {
+        assertThat(Duration.parse("1m"))
+                .isEqualTo(Duration.minutes(1));
+
+        assertThat(Duration.parse("1 minute"))
+                .isEqualTo(Duration.minutes(1));
+
+        assertThat(Duration.parse("2 minutes"))
+                .isEqualTo(Duration.minutes(2));
+    }
+
+    @Test
+    public void parsesSeconds() throws Exception {
+        assertThat(Duration.parse("1s"))
+                .isEqualTo(Duration.seconds(1));
+
+        assertThat(Duration.parse("1 second"))
+                .isEqualTo(Duration.seconds(1));
+
+        assertThat(Duration.parse("2 seconds"))
+                .isEqualTo(Duration.seconds(2));
+    }
+
+    @Test
+    public void parsesMilliseconds() throws Exception {
+        assertThat(Duration.parse("1ms"))
+                .isEqualTo(Duration.milliseconds(1));
+
+        assertThat(Duration.parse("1 millisecond"))
+                .isEqualTo(Duration.milliseconds(1));
+
+        assertThat(Duration.parse("2 milliseconds"))
+                .isEqualTo(Duration.milliseconds(2));
+    }
+
+    @Test
+    public void parsesMicroseconds() throws Exception {
+        assertThat(Duration.parse("1us"))
+                .isEqualTo(Duration.microseconds(1));
+
+        assertThat(Duration.parse("1 microsecond"))
+                .isEqualTo(Duration.microseconds(1));
+
+        assertThat(Duration.parse("2 microseconds"))
+                .isEqualTo(Duration.microseconds(2));
+    }
+
+    @Test
+    public void parsesNanoseconds() throws Exception {
+        assertThat(Duration.parse("1ns"))
+                .isEqualTo(Duration.nanoseconds(1));
+
+        assertThat(Duration.parse("1 nanosecond"))
+                .isEqualTo(Duration.nanoseconds(1));
+
+        assertThat(Duration.parse("2 nanoseconds"))
+                .isEqualTo(Duration.nanoseconds(2));
+    }
+
+    @Test
+    public void parseDurationWithWhiteSpaces() {
+        assertThat(Duration.parse("5   seconds"))
+                .isEqualTo(Duration.seconds(5));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void unableParseWrongDurationCount() {
+        Duration.parse("five seconds");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void unableParseWrongDurationTimeUnit() {
+        Duration.parse("1gs");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void unableParseWrongDurationFormat() {
+        Duration.parse("1 milli second");
+    }
+
+    @Test
+    public void isHumanReadable() throws Exception {
+        assertThat(Duration.microseconds(1).toString())
+                .isEqualTo("1 microsecond");
+
+        assertThat(Duration.microseconds(3).toString())
+                .isEqualTo("3 microseconds");
+    }
+
+    @Test
+    public void hasAQuantity() throws Exception {
+        assertThat(Duration.microseconds(12).getQuantity())
+                .isEqualTo(12);
+    }
+
+    @Test
+    public void hasAUnit() throws Exception {
+        assertThat(Duration.microseconds(1).getUnit())
+                .isEqualTo(TimeUnit.MICROSECONDS);
+    }
+}
diff --git a/dropwizard-util/src/test/java/io/dropwizard/util/JarLocationTest.java b/dropwizard-util/src/test/java/io/dropwizard/util/JarLocationTest.java
new file mode 100644
index 0000000..31020c4
--- /dev/null
+++ b/dropwizard-util/src/test/java/io/dropwizard/util/JarLocationTest.java
@@ -0,0 +1,20 @@
+package io.dropwizard.util;
+
+import com.google.common.base.Optional;
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class JarLocationTest {
+    @Test
+    public void isHumanReadable() throws Exception {
+        assertThat(new JarLocation(JarLocationTest.class).toString())
+                .isEqualTo("project.jar");
+    }
+
+    @Test
+    public void hasAVersion() throws Exception {
+        assertThat(new JarLocation(JarLocationTest.class).getVersion())
+                .isEqualTo(Optional.<String>absent());
+    }
+}
diff --git a/dropwizard-util/src/test/java/io/dropwizard/util/SizeTest.java b/dropwizard-util/src/test/java/io/dropwizard/util/SizeTest.java
new file mode 100644
index 0000000..5ed94eb
--- /dev/null
+++ b/dropwizard-util/src/test/java/io/dropwizard/util/SizeTest.java
@@ -0,0 +1,151 @@
+package io.dropwizard.util;
+
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class SizeTest {
+    @Test
+    public void convertsToTerabytes() throws Exception {
+        assertThat(Size.terabytes(2).toTerabytes())
+                .isEqualTo(2);
+    }
+
+    @Test
+    public void convertsToGigabytes() throws Exception {
+        assertThat(Size.terabytes(2).toGigabytes())
+                .isEqualTo(2048);
+    }
+
+    @Test
+    public void convertsToMegabytes() throws Exception {
+        assertThat(Size.gigabytes(2).toMegabytes())
+                .isEqualTo(2048);
+    }
+
+    @Test
+    public void convertsToKilobytes() throws Exception {
+        assertThat(Size.megabytes(2).toKilobytes())
+                .isEqualTo(2048);
+    }
+
+    @Test
+    public void convertsToBytes() throws Exception {
+        assertThat(Size.kilobytes(2).toBytes())
+                .isEqualTo(2048L);
+    }
+
+    @Test
+    public void parsesTerabytes() throws Exception {
+        assertThat(Size.parse("2TB"))
+                .isEqualTo(Size.terabytes(2));
+
+        assertThat(Size.parse("2TiB"))
+                .isEqualTo(Size.terabytes(2));
+
+        assertThat(Size.parse("1 terabyte"))
+                .isEqualTo(Size.terabytes(1));
+
+        assertThat(Size.parse("2 terabytes"))
+                .isEqualTo(Size.terabytes(2));
+    }
+
+    @Test
+    public void parsesGigabytes() throws Exception {
+        assertThat(Size.parse("2GB"))
+                .isEqualTo(Size.gigabytes(2));
+
+        assertThat(Size.parse("2GiB"))
+                .isEqualTo(Size.gigabytes(2));
+
+        assertThat(Size.parse("1 gigabyte"))
+                .isEqualTo(Size.gigabytes(1));
+
+        assertThat(Size.parse("2 gigabytes"))
+                .isEqualTo(Size.gigabytes(2));
+    }
+
+    @Test
+    public void parsesMegabytes() throws Exception {
+        assertThat(Size.parse("2MB"))
+                .isEqualTo(Size.megabytes(2));
+
+        assertThat(Size.parse("2MiB"))
+                .isEqualTo(Size.megabytes(2));
+
+        assertThat(Size.parse("1 megabyte"))
+                .isEqualTo(Size.megabytes(1));
+
+        assertThat(Size.parse("2 megabytes"))
+                .isEqualTo(Size.megabytes(2));
+    }
+
+    @Test
+    public void parsesKilobytes() throws Exception {
+        assertThat(Size.parse("2KB"))
+                .isEqualTo(Size.kilobytes(2));
+
+        assertThat(Size.parse("2KiB"))
+                .isEqualTo(Size.kilobytes(2));
+
+        assertThat(Size.parse("1 kilobyte"))
+                .isEqualTo(Size.kilobytes(1));
+
+        assertThat(Size.parse("2 kilobytes"))
+                .isEqualTo(Size.kilobytes(2));
+    }
+
+    @Test
+    public void parsesBytes() throws Exception {
+        assertThat(Size.parse("2B"))
+                .isEqualTo(Size.bytes(2));
+
+        assertThat(Size.parse("1 byte"))
+                .isEqualTo(Size.bytes(1));
+
+        assertThat(Size.parse("2 bytes"))
+                .isEqualTo(Size.bytes(2));
+    }
+
+    @Test
+    public void parseSizeWithWhiteSpaces() {
+        assertThat(Size.parse("64   kilobytes"))
+                .isEqualTo(Size.kilobytes(64));
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void unableParseWrongSizeCount() {
+        Size.parse("three bytes");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void unableParseWrongSizeUnit() {
+        Size.parse("1EB");
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void unableParseWrongSizeFormat() {
+        Size.parse("1 mega byte");
+    }
+
+    @Test
+    public void isHumanReadable() throws Exception {
+        assertThat(Size.gigabytes(3).toString())
+                .isEqualTo("3 gigabytes");
+
+        assertThat(Size.kilobytes(1).toString())
+                .isEqualTo("1 kilobyte");
+    }
+
+    @Test
+    public void hasAQuantity() throws Exception {
+        assertThat(Size.gigabytes(3).getQuantity())
+                .isEqualTo(3);
+    }
+
+    @Test
+    public void hasAUnit() throws Exception {
+        assertThat(Size.gigabytes(3).getUnit())
+                .isEqualTo(SizeUnit.GIGABYTES);
+    }
+}
diff --git a/dropwizard-util/src/test/java/io/dropwizard/util/SizeUnitTest.java b/dropwizard-util/src/test/java/io/dropwizard/util/SizeUnitTest.java
new file mode 100644
index 0000000..650efba
--- /dev/null
+++ b/dropwizard-util/src/test/java/io/dropwizard/util/SizeUnitTest.java
@@ -0,0 +1,243 @@
+package io.dropwizard.util;
+
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class SizeUnitTest {
+    // BYTES
+
+    @Test
+    public void oneByteInBytes() throws Exception {
+        assertThat(SizeUnit.BYTES.convert(1, SizeUnit.BYTES))
+                .isEqualTo(1);
+
+        assertThat(SizeUnit.BYTES.toBytes(1))
+                .isEqualTo(1);
+    }
+
+
+    @Test
+    public void oneByteInKilobytes() throws Exception {
+        assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.BYTES))
+                .isZero();
+
+        assertThat(SizeUnit.BYTES.toKilobytes(1))
+                .isZero();
+    }
+
+    @Test
+    public void oneByteInMegabytes() throws Exception {
+        assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.BYTES))
+                .isZero();
+
+        assertThat(SizeUnit.BYTES.toMegabytes(1))
+                .isZero();
+    }
+
+    @Test
+    public void oneByteInGigabytes() throws Exception {
+        assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.BYTES))
+                .isZero();
+
+        assertThat(SizeUnit.BYTES.toGigabytes(1))
+                .isZero();
+    }
+
+    @Test
+    public void oneByteInTerabytes() throws Exception {
+        assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.BYTES))
+                .isZero();
+
+        assertThat(SizeUnit.BYTES.toTerabytes(1))
+                .isZero();
+    }
+
+    // KILOBYTES
+
+    @Test
+    public void oneKilobyteInBytes() throws Exception {
+        assertThat(SizeUnit.BYTES.convert(1, SizeUnit.KILOBYTES))
+                .isEqualTo(1024);
+
+        assertThat(SizeUnit.KILOBYTES.toBytes(1))
+                .isEqualTo(1024);
+    }
+
+    @Test
+    public void oneKilobyteInKilobytes() throws Exception {
+        assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.KILOBYTES))
+                .isEqualTo(1);
+
+        assertThat(SizeUnit.KILOBYTES.toKilobytes(1))
+                .isEqualTo(1L);
+    }
+
+    @Test
+    public void oneKilobyteInMegabytes() throws Exception {
+        assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.KILOBYTES))
+                .isZero();
+
+        assertThat(SizeUnit.KILOBYTES.toMegabytes(1))
+                .isZero();
+    }
+
+    @Test
+    public void oneKilobyteInGigabytes() throws Exception {
+        assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.KILOBYTES))
+                .isZero();
+
+        assertThat(SizeUnit.KILOBYTES.toGigabytes(1))
+                .isZero();
+    }
+
+    @Test
+    public void oneKilobyteInTerabytes() throws Exception {
+        assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.KILOBYTES))
+                .isZero();
+
+        assertThat(SizeUnit.KILOBYTES.toTerabytes(1))
+                .isZero();
+    }
+
+    // MEGABYTES
+
+    @Test
+    public void oneMegabyteInBytes() throws Exception {
+        assertThat(SizeUnit.BYTES.convert(1, SizeUnit.MEGABYTES))
+                .isEqualTo(1048576);
+
+        assertThat(SizeUnit.MEGABYTES.toBytes(1))
+                .isEqualTo(1048576L);
+    }
+
+    @Test
+    public void oneMegabyteInKilobytes() throws Exception {
+        assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.MEGABYTES))
+                .isEqualTo(1024);
+
+        assertThat(SizeUnit.MEGABYTES.toKilobytes(1))
+                .isEqualTo(1024);
+    }
+
+    @Test
+    public void oneMegabyteInMegabytes() throws Exception {
+        assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.MEGABYTES))
+                .isEqualTo(1);
+
+        assertThat(SizeUnit.MEGABYTES.toMegabytes(1))
+                .isEqualTo(1);
+    }
+
+    @Test
+    public void oneMegabyteInGigabytes() throws Exception {
+        assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.MEGABYTES))
+                .isZero();
+
+        assertThat(SizeUnit.MEGABYTES.toGigabytes(1))
+                .isZero();
+    }
+
+    @Test
+    public void oneMegabyteInTerabytes() throws Exception {
+        assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.MEGABYTES))
+                .isZero();
+
+        assertThat(SizeUnit.MEGABYTES.toTerabytes(1))
+                .isZero();
+    }
+
+    // GIGABYTES
+
+    @Test
+    public void oneGigabyteInBytes() throws Exception {
+        assertThat(SizeUnit.BYTES.convert(1, SizeUnit.GIGABYTES))
+                .isEqualTo(1073741824);
+
+        assertThat(SizeUnit.GIGABYTES.toBytes(1))
+                .isEqualTo(1073741824);
+    }
+
+    @Test
+    public void oneGigabyteInKilobytes() throws Exception {
+        assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.GIGABYTES))
+                .isEqualTo(1048576);
+
+        assertThat(SizeUnit.GIGABYTES.toKilobytes(1))
+                .isEqualTo(1048576);
+    }
+
+    @Test
+    public void oneGigabyteInMegabytes() throws Exception {
+        assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.GIGABYTES))
+                .isEqualTo(1024);
+
+        assertThat(SizeUnit.GIGABYTES.toMegabytes(1))
+                .isEqualTo(1024);
+    }
+
+    @Test
+    public void oneGigabyteInGigabytes() throws Exception {
+        assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.GIGABYTES))
+                .isEqualTo(1L);
+
+        assertThat(SizeUnit.GIGABYTES.toGigabytes(1))
+                .isEqualTo(1L);
+    }
+
+    @Test
+    public void oneGigabyteInTerabytes() throws Exception {
+        assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.GIGABYTES))
+                .isZero();
+
+        assertThat(SizeUnit.GIGABYTES.toTerabytes(1))
+                .isZero();
+    }
+
+    // TERABYTES
+
+    @Test
+    public void oneTerabyteInBytes() throws Exception {
+        assertThat(SizeUnit.BYTES.convert(1, SizeUnit.TERABYTES))
+                .isEqualTo(1099511627776L);
+
+        assertThat(SizeUnit.TERABYTES.toBytes(1))
+                .isEqualTo(1099511627776L);
+    }
+
+    @Test
+    public void oneTerabyteInKilobytes() throws Exception {
+        assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.TERABYTES))
+                .isEqualTo(1073741824L);
+
+        assertThat(SizeUnit.TERABYTES.toKilobytes(1))
+                .isEqualTo(1073741824L);
+    }
+
+    @Test
+    public void oneTerabyteInMegabytes() throws Exception {
+        assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.TERABYTES))
+                .isEqualTo(1048576);
+
+        assertThat(SizeUnit.TERABYTES.toMegabytes(1))
+                .isEqualTo(1048576L);
+    }
+
+    @Test
+    public void oneTerabyteInGigabytes() throws Exception {
+        assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.TERABYTES))
+                .isEqualTo(1024);
+
+        assertThat(SizeUnit.TERABYTES.toGigabytes(1))
+                .isEqualTo(1024);
+    }
+
+    @Test
+    public void oneTerabyteInTerabytes() throws Exception {
+        assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.TERABYTES))
+                .isEqualTo(1);
+
+        assertThat(SizeUnit.TERABYTES.toTerabytes(1))
+                .isEqualTo(1);
+    }
+}
diff --git a/dropwizard-validation/pom.xml b/dropwizard-validation/pom.xml
new file mode 100644
index 0000000..21556a2
--- /dev/null
+++ b/dropwizard-validation/pom.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-validation</artifactId>
+    <name>Dropwizard Validation Support</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-util</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate</groupId>
+            <artifactId>hibernate-validator</artifactId>
+            <version>5.1.1.Final</version>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish.web</groupId>
+            <artifactId>javax.el</artifactId>
+            <version>2.2.6</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/ConstraintViolations.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/ConstraintViolations.java
new file mode 100644
index 0000000..e26b5ec
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/ConstraintViolations.java
@@ -0,0 +1,55 @@
+package io.dropwizard.validation;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Path;
+import java.util.Set;
+
+public class ConstraintViolations {
+    private ConstraintViolations() { /* singleton */ }
+
+    public static <T> String format(ConstraintViolation<T> v) {
+        if (v.getConstraintDescriptor().getAnnotation() instanceof ValidationMethod) {
+            final ImmutableList<Path.Node> nodes = ImmutableList.copyOf(v.getPropertyPath());
+            final ImmutableList<Path.Node> usefulNodes = nodes.subList(0, nodes.size() - 1);
+            final String msg = v.getMessage().startsWith(".") ? "%s%s" : "%s %s";
+            return String.format(msg,
+                                 Joiner.on('.').join(usefulNodes),
+                                 v.getMessage()).trim();
+        } else {
+            return String.format("%s %s (was %s)",
+                                 v.getPropertyPath(),
+                                 v.getMessage(),
+                                 v.getInvalidValue());
+        }
+    }
+
+    public static <T> ImmutableList<String> format(Set<ConstraintViolation<T>> violations) {
+        final Set<String> errors = Sets.newHashSet();
+        for (ConstraintViolation<?> v : violations) {
+            errors.add(format(v));
+        }
+        return ImmutableList.copyOf(Ordering.natural().sortedCopy(errors));
+    }
+
+    public static ImmutableList<String> formatUntyped(Set<ConstraintViolation<?>> violations) {
+        final Set<String> errors = Sets.newHashSet();
+        for (ConstraintViolation<?> v : violations) {
+            errors.add(format(v));
+        }
+        return ImmutableList.copyOf(Ordering.natural().sortedCopy(errors));
+    }
+
+    public static <T> ImmutableSet<ConstraintViolation<?>> copyOf(Set<ConstraintViolation<T>> violations) {
+        final ImmutableSet.Builder<ConstraintViolation<?>> builder = ImmutableSet.builder();
+        for (ConstraintViolation<T> violation : violations) {
+            builder.add(violation);
+        }
+        return builder.build();
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/DurationRange.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/DurationRange.java
new file mode 100644
index 0000000..128833d
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/DurationRange.java
@@ -0,0 +1,54 @@
+package io.dropwizard.validation;
+
+import javax.validation.Constraint;
+import javax.validation.OverridesAttribute;
+import javax.validation.Payload;
+import javax.validation.ReportAsSingleViolation;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * The annotated element has to be in the appropriate range. Apply on
+ * {@link io.dropwizard.util.Duration} instances.
+ */
+ at Documented
+ at Constraint(validatedBy = { })
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+ at Retention(RUNTIME)
+ at MinDuration(0)
+ at MaxDuration(value = Long.MAX_VALUE, unit = TimeUnit.DAYS)
+ at ReportAsSingleViolation
+public @interface DurationRange {
+    @OverridesAttribute(constraint = MinDuration.class, name = "value")
+    long min() default 0;
+
+    @OverridesAttribute(constraint = MaxDuration.class, name = "value")
+    long max() default Long.MAX_VALUE;
+
+    @OverridesAttribute.List({
+        @OverridesAttribute(constraint = MinDuration.class, name = "unit"),
+        @OverridesAttribute(constraint = MaxDuration.class, name = "unit")
+    })
+    TimeUnit unit() default TimeUnit.SECONDS;
+
+    String message() default "must be between {min} {unit} and {max} {unit}";
+
+    Class<?>[] groups() default { };
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+
+    /**
+     * Defines several {@code @DurationRange} annotations on the same element.
+     */
+    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+    @Retention(RUNTIME)
+    @Documented
+    public @interface List {
+        DurationRange[] value();
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDuration.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDuration.java
new file mode 100644
index 0000000..c2def92
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDuration.java
@@ -0,0 +1,39 @@
+package io.dropwizard.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * The annotated element must be a {@link io.dropwizard.util.Duration}
+ * whose value must be higher or equal to the specified minimum.
+ * <p/>
+ * <code>null</code> elements are considered valid
+ */
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+ at Retention(RUNTIME)
+ at Documented
+ at Constraint(validatedBy = MaxDurationValidator.class)
+public @interface MaxDuration {
+    String message() default "must be less than or equal to {value} {unit}";
+
+    Class<?>[] groups() default { };
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+
+    /**
+     * @return value the element must be higher or equal to
+     */
+    long value();
+
+    /**
+     * @return unit of the value the element must be higher or equal to
+     */
+    TimeUnit unit() default TimeUnit.SECONDS;
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDurationValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDurationValidator.java
new file mode 100644
index 0000000..b84681f
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDurationValidator.java
@@ -0,0 +1,28 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.Duration;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Check that a {@link Duration} being validated is less than or equal to the
+ * minimum value specified.
+ */
+public class MaxDurationValidator implements ConstraintValidator<MaxDuration, Duration> {
+
+    private long maxQty;
+    private TimeUnit maxUnit;
+
+    @Override
+    public void initialize(MaxDuration constraintAnnotation) {
+        this.maxQty = constraintAnnotation.value();
+        this.maxUnit = constraintAnnotation.unit();
+    }
+
+    @Override
+    public boolean isValid(Duration value, ConstraintValidatorContext context) {
+        return (value == null) || (value.toNanoseconds() <= maxUnit.toNanos(maxQty));
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSize.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSize.java
new file mode 100644
index 0000000..10bc928
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSize.java
@@ -0,0 +1,40 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.SizeUnit;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * The annotated element must be a {@link io.dropwizard.util.Size}
+ * whose value must be higher or equal to the specified minimum.
+ * <p/>
+ * <code>null</code> elements are considered valid
+ */
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+ at Retention(RUNTIME)
+ at Documented
+ at Constraint(validatedBy = MaxSizeValidator.class)
+public @interface MaxSize {
+    String message() default "must be less than or equal to {value} {unit}";
+
+    Class<?>[] groups() default { };
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+
+    /**
+     * @return value the element must be higher or equal to
+     */
+    long value();
+
+    /**
+     * @return unit of the value the element must be higher or equal to
+     */
+    SizeUnit unit() default SizeUnit.BYTES;
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSizeValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSizeValidator.java
new file mode 100644
index 0000000..7eb6a67
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSizeValidator.java
@@ -0,0 +1,28 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.Size;
+import io.dropwizard.util.SizeUnit;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * Check that a {@link Size} being validated is less than or equal to the
+ * minimum value specified.
+ */
+public class MaxSizeValidator implements ConstraintValidator<MaxSize, Size> {
+
+    private long maxQty;
+    private SizeUnit maxUnit;
+
+    @Override
+    public void initialize(MaxSize constraintAnnotation) {
+        this.maxQty = constraintAnnotation.value();
+        this.maxUnit = constraintAnnotation.unit();
+    }
+
+    @Override
+    public boolean isValid(Size value, ConstraintValidatorContext context) {
+        return (value == null) || (value.toBytes() <= maxUnit.toBytes(maxQty));
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MethodValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MethodValidator.java
new file mode 100644
index 0000000..63dcdc8
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MethodValidator.java
@@ -0,0 +1,19 @@
+package io.dropwizard.validation;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * A validator for {@link ValidationMethod}-annotated methods.
+ */
+public class MethodValidator implements ConstraintValidator<ValidationMethod, Boolean> {
+    @Override
+    public void initialize(ValidationMethod constraintAnnotation) {
+
+    }
+
+    @Override
+    public boolean isValid(Boolean value, ConstraintValidatorContext context) {
+        return (value == null) || value;
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDuration.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDuration.java
new file mode 100644
index 0000000..0162efa
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDuration.java
@@ -0,0 +1,39 @@
+package io.dropwizard.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.concurrent.TimeUnit;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * The annotated element must be a {@link io.dropwizard.util.Duration}
+ * whose value must be higher or equal to the specified minimum.
+ * <p/>
+ * <code>null</code> elements are considered valid
+ */
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+ at Retention(RUNTIME)
+ at Documented
+ at Constraint(validatedBy = MinDurationValidator.class)
+public @interface MinDuration {
+    String message() default "must be greater than or equal to {value} {unit}";
+
+    Class<?>[] groups() default { };
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+
+    /**
+     * @return value the element must be higher or equal to
+     */
+    long value();
+
+    /**
+     * @return unit of the value the element must be higher or equal to
+     */
+    TimeUnit unit() default TimeUnit.SECONDS;
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDurationValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDurationValidator.java
new file mode 100644
index 0000000..48087ef
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDurationValidator.java
@@ -0,0 +1,28 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.Duration;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Check that a {@link Duration} being validated is greater than or equal to the
+ * minimum value specified.
+ */
+public class MinDurationValidator implements ConstraintValidator<MinDuration, Duration> {
+
+    private long minQty;
+    private TimeUnit minUnit;
+
+    @Override
+    public void initialize(MinDuration constraintAnnotation) {
+        this.minQty = constraintAnnotation.value();
+        this.minUnit = constraintAnnotation.unit();
+    }
+
+    @Override
+    public boolean isValid(Duration value, ConstraintValidatorContext context) {
+        return (value == null) || (value.toNanoseconds() >= minUnit.toNanos(minQty));
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSize.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSize.java
new file mode 100644
index 0000000..bae7ae3
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSize.java
@@ -0,0 +1,40 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.SizeUnit;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * The annotated element must be a {@link io.dropwizard.util.Size}
+ * whose value must be higher or equal to the specified minimum.
+ * <p/>
+ * <code>null</code> elements are considered valid
+ */
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+ at Retention(RUNTIME)
+ at Documented
+ at Constraint(validatedBy = MinSizeValidator.class)
+public @interface MinSize {
+    String message() default "must be greater than or equal to {value} {unit}";
+
+    Class<?>[] groups() default { };
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+
+    /**
+     * @return value the element must be higher or equal to
+     */
+    long value();
+
+    /**
+     * @return unit of the value the element must be higher or equal to
+     */
+    SizeUnit unit() default SizeUnit.BYTES;
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSizeValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSizeValidator.java
new file mode 100644
index 0000000..ffd5e22
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSizeValidator.java
@@ -0,0 +1,28 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.Size;
+import io.dropwizard.util.SizeUnit;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * Check that a {@link Size} being validated is greater than or equal to the
+ * minimum value specified.
+ */
+public class MinSizeValidator implements ConstraintValidator<MinSize, Size> {
+
+    private long minQty;
+    private SizeUnit minUnit;
+
+    @Override
+    public void initialize(MinSize constraintAnnotation) {
+        this.minQty = constraintAnnotation.value();
+        this.minUnit = constraintAnnotation.unit();
+    }
+
+    @Override
+    public boolean isValid(Size value, ConstraintValidatorContext context) {
+        return (value == null) || (value.toBytes() >= minUnit.toBytes(minQty));
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOf.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOf.java
new file mode 100644
index 0000000..12df112
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOf.java
@@ -0,0 +1,40 @@
+package io.dropwizard.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Checks to see that the value is one of a set of elements.
+ */
+ at Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
+ at Retention(RUNTIME)
+ at Documented
+ at Constraint(validatedBy = OneOfValidator.class)
+public @interface OneOf {
+    String message() default "must be one of {value}";
+
+    Class<?>[] groups() default {};
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default {};
+
+    /**
+     * The set of valid values.
+     */
+    String[] value();
+
+    /**
+     * Whether or not to ignore case.
+     */
+    boolean ignoreCase() default false;
+
+    /**
+     * Whether or not to ignore leading and trailing whitespace.
+     */
+    boolean ignoreWhitespace() default false;
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOfValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOfValidator.java
new file mode 100644
index 0000000..0da7297
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOfValidator.java
@@ -0,0 +1,39 @@
+package io.dropwizard.validation;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class OneOfValidator implements ConstraintValidator<OneOf, Object> {
+    private String[] values;
+    private boolean caseInsensitive;
+    private boolean ignoreWhitespace;
+
+    @Override
+    public void initialize(OneOf constraintAnnotation) {
+        this.values = constraintAnnotation.value();
+        this.caseInsensitive = constraintAnnotation.ignoreCase();
+        this.ignoreWhitespace = constraintAnnotation.ignoreWhitespace();
+    }
+
+    @Override
+    public boolean isValid(Object value, ConstraintValidatorContext context) {
+        if (value == null) {
+            return true;
+        }
+        final String v = ignoreWhitespace ? value.toString().trim() : value.toString();
+        if (caseInsensitive) {
+            for (String s : values) {
+                if (s.equalsIgnoreCase(v)) {
+                    return true;
+                }
+            }
+        } else {
+            for (String s : values) {
+                if (s.equals(v)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRange.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRange.java
new file mode 100644
index 0000000..20fa56a
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRange.java
@@ -0,0 +1,31 @@
+package io.dropwizard.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * A constraint that allows one to specify a port range, but still allow 0 as the port value to
+ * indicate dynamically allocated ports.
+ *
+ */
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE })
+ at Retention(RUNTIME)
+ at Constraint(validatedBy = PortRangeValidator.class)
+ at Documented
+public @interface PortRange {
+    int min() default 1;
+
+    int max() default 65535;
+
+    String message() default "{org.hibernate.validator.constraints.Range.message}";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRangeValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRangeValidator.java
new file mode 100644
index 0000000..257b9d9
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRangeValidator.java
@@ -0,0 +1,24 @@
+package io.dropwizard.validation;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+/**
+ * Allow 0 to indicate dynamic port range allocation. If not zero, it must be within the {min,max}
+ * range, inclusive.
+ */
+public class PortRangeValidator implements ConstraintValidator<PortRange, Integer> {
+    private int min;
+    private int max;
+
+    @Override
+    public void initialize(PortRange constraintAnnotation) {
+        this.min = constraintAnnotation.min();
+        this.max = constraintAnnotation.max();
+    }
+
+    @Override
+    public boolean isValid(Integer value, ConstraintValidatorContext context) {
+        return value == 0 || (value >= min && value <= max);
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/SizeRange.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/SizeRange.java
new file mode 100644
index 0000000..eda8cf4
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/SizeRange.java
@@ -0,0 +1,55 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.SizeUnit;
+
+import javax.validation.Constraint;
+import javax.validation.OverridesAttribute;
+import javax.validation.Payload;
+import javax.validation.ReportAsSingleViolation;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * The annotated element has to be in the appropriate range. Apply on
+ * {@link io.dropwizard.util.Size} instances.
+ */
+ at Documented
+ at Constraint(validatedBy = { })
+ at Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+ at Retention(RUNTIME)
+ at MinSize(0)
+ at MaxSize(value = Long.MAX_VALUE, unit = SizeUnit.TERABYTES)
+ at ReportAsSingleViolation
+public @interface SizeRange {
+    @OverridesAttribute(constraint = MinSize.class, name = "value")
+    long min() default 0;
+
+    @OverridesAttribute(constraint = MaxSize.class, name = "value")
+    long max() default Long.MAX_VALUE;
+
+    @OverridesAttribute.List({
+        @OverridesAttribute(constraint = MinSize.class, name = "unit"),
+        @OverridesAttribute(constraint = MaxSize.class, name = "unit")
+    })
+    SizeUnit unit() default SizeUnit.BYTES;
+
+    String message() default "must be between {min} {unit} and {max} {unit}";
+
+    Class<?>[] groups() default { };
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+
+    /**
+     * Defines several {@code @SizeRange} annotations on the same element.
+     */
+    @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
+    @Retention(RUNTIME)
+    @Documented
+    public @interface List {
+        SizeRange[] value();
+    }
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/Validated.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/Validated.java
new file mode 100755
index 0000000..552b0b5
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/Validated.java
@@ -0,0 +1,22 @@
+package io.dropwizard.validation;
+
+import javax.validation.groups.Default;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Due to limit of @see javax.validation.Valid Annotation for validation groups and ordered validations,
+ * this annotation is serving supplementary purposes to validation process.
+ */
+ at Target(PARAMETER)
+ at Retention(RUNTIME)
+public @interface Validated {
+   /**
+    * Specify one or more validation groups to apply to the validation.
+    * @return Validation groups
+    */
+   Class<?>[] value() default {Default.class};
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/ValidationMethod.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/ValidationMethod.java
new file mode 100644
index 0000000..7f7a0a2
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/ValidationMethod.java
@@ -0,0 +1,26 @@
+package io.dropwizard.validation;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Validates a bean predicate method as returning true. Bean predicates must be of the form
+ * {@code isSomething} or they'll be silently ignored.
+ */
+ at Target({TYPE, ANNOTATION_TYPE, METHOD})
+ at Retention(RUNTIME)
+ at Constraint(validatedBy = MethodValidator.class)
+ at Documented
+public @interface ValidationMethod {
+    String message() default "is not valid";
+
+    Class<?>[] groups() default {};
+
+    @SuppressWarnings("UnusedDeclaration") Class<? extends Payload>[] payload() default { };
+}
diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapper.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapper.java
new file mode 100644
index 0000000..c68c805
--- /dev/null
+++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapper.java
@@ -0,0 +1,29 @@
+package io.dropwizard.validation.valuehandling;
+
+import com.fasterxml.classmate.ResolvedType;
+import com.fasterxml.classmate.TypeResolver;
+import com.google.common.base.Optional;
+import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper;
+
+import java.lang.reflect.Type;
+
+/**
+ * A {@link ValidatedValueUnwrapper} for Guava's {@link Optional}.
+ * <p/>
+ * Extracts the value contained by the {@link Optional} for validation, or produces {@code null}.
+ */
+public class OptionalValidatedValueUnwrapper extends ValidatedValueUnwrapper<Optional<?>> {
+
+    private final TypeResolver resolver = new TypeResolver();
+
+    @Override
+    public Object handleValidatedValue(final Optional<?> optional) {
+        return optional.orNull();
+    }
+
+    @Override
+    public Type getValidatedValueType(final Type type) {
+        ResolvedType resolvedType = resolver.resolve(type);
+        return resolvedType.typeParametersFor(Optional.class).get(0).getErasedType();
+    }
+}
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/DurationValidatorTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/DurationValidatorTest.java
new file mode 100644
index 0000000..dce4675
--- /dev/null
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/DurationValidatorTest.java
@@ -0,0 +1,63 @@
+package io.dropwizard.validation;
+
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.util.Duration;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class DurationValidatorTest {
+    @SuppressWarnings("unused")
+    public static class Example {
+        @MaxDuration(value = 30, unit = TimeUnit.SECONDS)
+        private Duration tooBig = Duration.minutes(10);
+
+        @MinDuration(value = 30, unit = TimeUnit.SECONDS)
+        private Duration tooSmall = Duration.milliseconds(100);
+        
+        @DurationRange(min = 10, max = 30, unit = TimeUnit.MINUTES)
+        private Duration outOfRange = Duration.minutes(60);
+
+        public void setTooBig(Duration tooBig) {
+            this.tooBig = tooBig;
+        }
+        public void setTooSmall(Duration tooSmall) {
+            this.tooSmall = tooSmall;
+        }
+        public void setOutOfRange(Duration outOfRange) {
+            this.outOfRange = outOfRange;
+        }
+    }
+
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    public void returnsASetOfErrorsForAnObject() throws Exception {
+        if ("en".equals(Locale.getDefault().getLanguage())) {
+            final ImmutableList<String> errors =
+                    ConstraintViolations.format(validator.validate(new Example()));
+
+            assertThat(errors)
+                    .containsOnly(
+                            "outOfRange must be between 10 MINUTES and 30 MINUTES (was 60 minutes)",
+                            "tooBig must be less than or equal to 30 SECONDS (was 10 minutes)",
+                            "tooSmall must be greater than or equal to 30 SECONDS (was 100 milliseconds)");
+        }
+    }
+
+    @Test
+    public void returnsAnEmptySetForAValidObject() throws Exception {
+        final Example example = new Example();
+        example.setTooBig(Duration.seconds(10));
+        example.setTooSmall(Duration.seconds(100));
+        example.setOutOfRange(Duration.minutes(15));
+
+        assertThat(validator.validate(example))
+                .isEmpty();
+    }
+}
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/MethodValidatorTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/MethodValidatorTest.java
new file mode 100644
index 0000000..3a179a5
--- /dev/null
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/MethodValidatorTest.java
@@ -0,0 +1,47 @@
+package io.dropwizard.validation;
+
+import com.google.common.collect.ImmutableList;
+import org.junit.Test;
+
+import javax.validation.Valid;
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+ at SuppressWarnings({"FieldMayBeFinal","MethodMayBeStatic","UnusedDeclaration"})
+public class MethodValidatorTest {
+    public static class SubExample {
+        @ValidationMethod(message = "also needs something special")
+        public boolean isOK() {
+            return false;
+        }
+    }
+
+    public static class Example {
+        @Valid
+        private SubExample subExample = new SubExample();
+
+        @ValidationMethod(message = "must have a false thing")
+        public boolean isFalse() {
+            return false;
+        }
+
+        @ValidationMethod(message = "must have a true thing")
+        public boolean isTrue() {
+            return true;
+        }
+    }
+
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    public void complainsAboutMethodsWhichReturnFalse() throws Exception {
+        final ImmutableList<String> errors =
+                ConstraintViolations.format(validator.validate(new Example()));
+
+        assertThat(errors)
+                .containsOnly("must have a false thing",
+                              "subExample also needs something special");
+    }
+}
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/OneOfValidatorTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/OneOfValidatorTest.java
new file mode 100644
index 0000000..18f37de
--- /dev/null
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/OneOfValidatorTest.java
@@ -0,0 +1,62 @@
+package io.dropwizard.validation;
+
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Locale;
+
+import static io.dropwizard.validation.ConstraintViolations.format;
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.junit.Assume.assumeTrue;
+
+public class OneOfValidatorTest {
+    @SuppressWarnings("UnusedDeclaration")
+    public static class Example {
+        @OneOf({"one", "two", "three"})
+        private String basic = "one";
+
+        @OneOf(value = {"one", "two", "three"}, ignoreCase = true)
+        private String caseInsensitive = "one";
+
+        @OneOf(value = {"one", "two", "three"}, ignoreWhitespace = true)
+        private String whitespaceInsensitive = "one";
+    }
+
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    public void allowsExactElements() throws Exception {
+        assertThat(format(validator.validate(new Example())))
+                .isEmpty();
+    }
+
+    @Test
+    public void doesNotAllowOtherElements() throws Exception {
+        assumeTrue("en".equals(Locale.getDefault().getLanguage()));
+
+        final Example example = new Example();
+        example.basic = "four";
+
+        assertThat(format(validator.validate(example)))
+                .containsOnly("basic must be one of [one, two, three] (was four)");
+    }
+
+    @Test
+    public void optionallyIgnoresCase() throws Exception {
+        final Example example = new Example();
+        example.caseInsensitive = "ONE";
+
+        assertThat(format(validator.validate(example)))
+                .isEmpty();
+    }
+
+    @Test
+    public void optionallyIgnoresWhitespace() throws Exception {
+        final Example example = new Example();
+        example.whitespaceInsensitive = "   one  ";
+
+        assertThat(format(validator.validate(example)))
+                .isEmpty();
+    }
+}
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/PortRangeValidatorTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/PortRangeValidatorTest.java
new file mode 100644
index 0000000..2e26392
--- /dev/null
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/PortRangeValidatorTest.java
@@ -0,0 +1,72 @@
+package io.dropwizard.validation;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Locale;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assume.assumeThat;
+
+public class PortRangeValidatorTest {
+    @SuppressWarnings("PublicField")
+    public static class Example {
+        @PortRange
+        public int port = 8080;
+
+        @PortRange(min = 10000, max = 15000)
+        public int otherPort = 10001;
+    }
+
+
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+    private final Example example = new Example();
+
+    @Before
+    public void setUp() throws Exception {
+        assumeThat(Locale.getDefault().getLanguage(), is("en"));
+    }
+
+    @Test
+    public void acceptsNonPrivilegedPorts() throws Exception {
+        example.port = 2048;
+
+        assertThat(validator.validate(example))
+                .isEmpty();
+    }
+
+    @Test
+    public void acceptsDynamicPorts() throws Exception {
+        example.port = 0;
+
+        assertThat(validator.validate(example))
+                .isEmpty();
+    }
+
+    @Test
+    public void rejectsNegativePorts() throws Exception {
+        example.port = -1;
+
+        assertThat(ConstraintViolations.format(validator.validate(example)))
+                .containsOnly("port must be between 1 and 65535 (was -1)");
+    }
+
+    @Test
+    public void allowsForCustomMinimumPorts() throws Exception {
+        example.otherPort = 8080;
+
+        assertThat(ConstraintViolations.format(validator.validate(example)))
+                .containsOnly("otherPort must be between 10000 and 15000 (was 8080)");
+    }
+
+    @Test
+    public void allowsForCustomMaximumPorts() throws Exception {
+        example.otherPort = 16000;
+
+        assertThat(ConstraintViolations.format(validator.validate(example)))
+                .containsOnly("otherPort must be between 10000 and 15000 (was 16000)");
+    }
+}
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/SizeValidatorTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/SizeValidatorTest.java
new file mode 100644
index 0000000..708386d
--- /dev/null
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/SizeValidatorTest.java
@@ -0,0 +1,58 @@
+package io.dropwizard.validation;
+
+import io.dropwizard.util.Size;
+import io.dropwizard.util.SizeUnit;
+import org.junit.Test;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Locale;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class SizeValidatorTest {
+    @SuppressWarnings("unused")
+    public static class Example {
+        @MaxSize(value = 30, unit = SizeUnit.KILOBYTES)
+        private Size tooBig = Size.gigabytes(2);
+
+        @MinSize(value = 30, unit = SizeUnit.KILOBYTES)
+        private Size tooSmall = Size.bytes(100);
+        
+        @SizeRange(min = 10, max = 100, unit = SizeUnit.KILOBYTES)
+        private Size outOfRange = Size.megabytes(2);
+
+        public void setTooBig(Size tooBig) {
+            this.tooBig = tooBig;
+        }
+        public void setTooSmall(Size tooSmall) {
+            this.tooSmall = tooSmall;
+        }
+        public void setOutOfRange(Size outOfRange) {
+            this.outOfRange = outOfRange;
+        }
+    }
+
+    private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
+
+    @Test
+    public void returnsASetOfErrorsForAnObject() throws Exception {
+        if ("en".equals(Locale.getDefault().getLanguage())) {
+            assertThat(ConstraintViolations.format(validator.validate(new Example())))
+                    .containsOnly("outOfRange must be between 10 KILOBYTES and 100 KILOBYTES (was 2 megabytes)",
+                                  "tooBig must be less than or equal to 30 KILOBYTES (was 2 gigabytes)",
+                                  "tooSmall must be greater than or equal to 30 KILOBYTES (was 100 bytes)");
+        }
+    }
+
+    @Test
+    public void returnsAnEmptySetForAValidObject() throws Exception {
+        final Example example = new Example();
+        example.setTooBig(Size.bytes(10));
+        example.setTooSmall(Size.megabytes(10));
+        example.setOutOfRange(Size.kilobytes(64));
+
+        assertThat(validator.validate(example))
+                .isEmpty();
+    }
+}
diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapperTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapperTest.java
new file mode 100644
index 0000000..614fe49
--- /dev/null
+++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapperTest.java
@@ -0,0 +1,84 @@
+package io.dropwizard.validation.valuehandling;
+
+import com.google.common.base.Optional;
+import org.hibernate.validator.HibernateValidator;
+import org.hibernate.validator.valuehandling.UnwrapValidatedValue;
+import org.junit.Test;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.NotNull;
+
+import java.util.Set;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class OptionalValidatedValueUnwrapperTest {
+
+    public static class Example {
+
+        @Min(3)
+        @UnwrapValidatedValue
+        public Optional<Integer> three = Optional.absent();
+
+        @NotNull
+        @UnwrapValidatedValue
+        public Optional<Integer> notNull = Optional.of(123);
+    }
+
+    private final Validator validator = Validation
+            .byProvider(HibernateValidator.class)
+            .configure()
+            .addValidatedValueHandler(new OptionalValidatedValueUnwrapper())
+            .buildValidatorFactory()
+            .getValidator();
+
+    @Test
+    public void succeedsWhenAbsent() {
+        Example example = new Example();
+        Set<ConstraintViolation<Example>> violations = validator.validate(example);
+        assertThat(violations).isEmpty();
+    }
+
+    @Test
+    public void failsWhenFailingConstraint() {
+        Example example = new Example();
+        example.three = Optional.of(2);
+        Set<ConstraintViolation<Example>> violations = validator.validate(example);
+        assertThat(violations).hasSize(1);
+    }
+
+    @Test
+    public void succeedsWhenPresentButNull() {
+        Example example = new Example();
+        example.three = Optional.fromNullable(null);
+        Set<ConstraintViolation<Example>> violations = validator.validate(example);
+        assertThat(violations).isEmpty();
+    }
+
+    @Test
+    public void succeedsWhenConstraintsMet() {
+        Example example = new Example();
+        example.three = Optional.of(10);
+        Set<ConstraintViolation<Example>> violations = validator.validate(example);
+        assertThat(violations).isEmpty();
+    }
+
+    @Test
+    public void notNullFailsWhenAbsent() {
+        Example example = new Example();
+        example.notNull = Optional.absent();
+        Set<ConstraintViolation<Example>> violations = validator.validate(example);
+        assertThat(violations).hasSize(1);
+    }
+
+    @Test
+    public void notNullFailsWhenPresentButNull() {
+        Example example = new Example();
+        example.notNull = Optional.fromNullable(null);
+        Set<ConstraintViolation<Example>> violations = validator.validate(example);
+        assertThat(violations).hasSize(1);
+    }
+}
diff --git a/dropwizard-views-freemarker/pom.xml b/dropwizard-views-freemarker/pom.xml
new file mode 100644
index 0000000..b37459c
--- /dev/null
+++ b/dropwizard-views-freemarker/pom.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-views-freemarker</artifactId>
+    <name>Dropwizard Freemarker Views</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-views</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.freemarker</groupId>
+            <artifactId>freemarker</artifactId>
+            <version>2.3.20</version>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-http</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-views-freemarker/src/main/java/io/dropwizard/views/freemarker/FreemarkerViewRenderer.java b/dropwizard-views-freemarker/src/main/java/io/dropwizard/views/freemarker/FreemarkerViewRenderer.java
new file mode 100644
index 0000000..fabd646
--- /dev/null
+++ b/dropwizard-views-freemarker/src/main/java/io/dropwizard/views/freemarker/FreemarkerViewRenderer.java
@@ -0,0 +1,65 @@
+package io.dropwizard.views.freemarker;
+
+import com.google.common.base.Charsets;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.sun.jersey.api.container.MappableContainerException;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
+import freemarker.template.Template;
+import freemarker.template.TemplateException;
+import io.dropwizard.views.View;
+import io.dropwizard.views.ViewRenderer;
+
+import javax.ws.rs.WebApplicationException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Locale;
+
+/**
+ * A {@link ViewRenderer} which renders Freemarker ({@code .ftl}) templates.
+ */
+public class FreemarkerViewRenderer implements ViewRenderer {
+    private static class TemplateLoader extends CacheLoader<Class<?>, Configuration> {
+        @Override
+        public Configuration load(Class<?> key) throws Exception {
+            final Configuration configuration = new Configuration();
+            configuration.setObjectWrapper(new DefaultObjectWrapper());
+            configuration.loadBuiltInEncodingMap();
+            configuration.setDefaultEncoding(Charsets.UTF_8.name());
+            configuration.setClassForTemplateLoading(key, "/");
+            return configuration;
+        }
+    }
+
+    private final LoadingCache<Class<?>, Configuration> configurationCache;
+
+    public FreemarkerViewRenderer() {
+        this.configurationCache = CacheBuilder.newBuilder()
+                                              .concurrencyLevel(128)
+                                              .build(new TemplateLoader());
+    }
+
+    @Override
+    public boolean isRenderable(View view) {
+        return view.getTemplateName().endsWith(".ftl");
+    }
+
+    @Override
+    public void render(View view,
+                       Locale locale,
+                       OutputStream output) throws IOException, WebApplicationException {
+        try {
+            final Configuration configuration = configurationCache.getUnchecked(view.getClass());
+            final Charset charset = view.getCharset().or(Charset.forName(configuration.getEncoding(locale)));
+            final Template template = configuration.getTemplate(view.getTemplateName(), locale, charset.name());
+            template.process(view, new OutputStreamWriter(output, template.getEncoding()));
+        } catch (TemplateException e) {
+            throw new MappableContainerException(e);
+        }
+    }
+
+}
diff --git a/dropwizard-views-freemarker/src/main/resources/META-INF/services/io.dropwizard.views.ViewRenderer b/dropwizard-views-freemarker/src/main/resources/META-INF/services/io.dropwizard.views.ViewRenderer
new file mode 100644
index 0000000..7db7e8d
--- /dev/null
+++ b/dropwizard-views-freemarker/src/main/resources/META-INF/services/io.dropwizard.views.ViewRenderer
@@ -0,0 +1 @@
+io.dropwizard.views.freemarker.FreemarkerViewRenderer
diff --git a/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/AbsoluteView.java b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/AbsoluteView.java
new file mode 100644
index 0000000..288b4c2
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/AbsoluteView.java
@@ -0,0 +1,16 @@
+package io.dropwizard.views.freemarker;
+
+import io.dropwizard.views.View;
+
+public class AbsoluteView extends View {
+    private final String name;
+
+    public AbsoluteView(String name) {
+        super("/example.ftl");
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
diff --git a/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/BadView.java b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/BadView.java
new file mode 100644
index 0000000..20a23c9
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/BadView.java
@@ -0,0 +1,9 @@
+package io.dropwizard.views.freemarker;
+
+import io.dropwizard.views.View;
+
+public class BadView extends View {
+    public BadView() {
+        super("/woo-oo-ahh.txt.ftl");
+    }
+}
diff --git a/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/FreemarkerViewRendererTest.java b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/FreemarkerViewRendererTest.java
new file mode 100644
index 0000000..62e6676
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/FreemarkerViewRendererTest.java
@@ -0,0 +1,84 @@
+package io.dropwizard.views.freemarker;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.collect.ImmutableList;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.api.core.DefaultResourceConfig;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.views.ViewMessageBodyWriter;
+import io.dropwizard.views.ViewRenderer;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class FreemarkerViewRendererTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Path("/test/")
+    @Produces(MediaType.TEXT_HTML)
+    public static class ExampleResource {
+        @GET
+        @Path("/absolute")
+        public AbsoluteView showAbsolute() {
+            return new AbsoluteView("yay");
+        }
+
+        @GET
+        @Path("/relative")
+        public RelativeView showRelative() {
+            return new RelativeView();
+        }
+
+        @GET
+        @Path("/bad")
+        public BadView showBad() {
+            return new BadView();
+        }
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        final DefaultResourceConfig config = new DefaultResourceConfig();
+        final ViewRenderer renderer = new FreemarkerViewRenderer();
+        config.getSingletons().add(new ViewMessageBodyWriter(new MetricRegistry(), ImmutableList.of(renderer)));
+        config.getSingletons().add(new ExampleResource());
+        return new LowLevelAppDescriptor.Builder(config).build();
+    }
+
+    @Test
+    public void rendersViewsWithAbsoluteTemplatePaths() throws Exception {
+        final String response = client().resource(getBaseURI() + "test/absolute").get(String.class);
+        assertThat(response)
+                .isEqualToIgnoringCase("Woop woop. yay" + System.lineSeparator());
+    }
+
+    @Test
+    public void rendersViewsWithRelativeTemplatePaths() throws Exception {
+        final String response = client().resource(getBaseURI() + "test/relative").get(String.class);
+        assertThat(response)
+                .isEqualToIgnoringCase("Ok." + System.lineSeparator());
+    }
+
+    @Test
+    public void returnsA500ForViewsWithBadTemplatePaths() throws Exception {
+        try {
+            client().resource(getBaseURI() + "test/bad").get(String.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(500);
+
+            assertThat(e.getResponse().getEntity(String.class))
+                    .isEqualTo("<html><head><title>Missing Template</title></head><body><h1>Missing Template</h1><p>Template \"/woo-oo-ahh.txt.ftl\" not found.</p></body></html>");
+        }
+    }
+}
diff --git a/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/RelativeView.java b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/RelativeView.java
new file mode 100644
index 0000000..709fd4e
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/RelativeView.java
@@ -0,0 +1,10 @@
+package io.dropwizard.views.freemarker;
+
+import io.dropwizard.views.View;
+
+public class RelativeView extends View {
+    public RelativeView() {
+        super("relative.ftl");
+    }
+}
+
diff --git a/dropwizard-views-freemarker/src/test/resources/example.ftl b/dropwizard-views-freemarker/src/test/resources/example.ftl
new file mode 100644
index 0000000..d8d6a80
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/resources/example.ftl
@@ -0,0 +1,2 @@
+<#-- @ftlvariable name="" type="io.dropwizard.views.freemarker.AbsoluteView" -->
+Woop woop. ${name?html}
diff --git a/dropwizard-views-freemarker/src/test/resources/io/dropwizard/views/freemarker/relative.ftl b/dropwizard-views-freemarker/src/test/resources/io/dropwizard/views/freemarker/relative.ftl
new file mode 100644
index 0000000..f205466
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/resources/io/dropwizard/views/freemarker/relative.ftl
@@ -0,0 +1,2 @@
+<#-- @ftlvariable name="" type="io.dropwizard.views.freemarker.AbsoluteView" -->
+Ok.
diff --git a/dropwizard-views-freemarker/src/test/resources/logback-test.xml b/dropwizard-views-freemarker/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-views-freemarker/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-views-mustache/pom.xml b/dropwizard-views-mustache/pom.xml
new file mode 100644
index 0000000..e997c9c
--- /dev/null
+++ b/dropwizard-views-mustache/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-views-mustache</artifactId>
+    <name>Dropwizard Mustache Views</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-views</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>com.github.spullara.mustache.java</groupId>
+            <artifactId>compiler</artifactId>
+            <version>0.8.15</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.google.guava</groupId>
+                    <artifactId>guava</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-core</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.jersey.jersey-test-framework</groupId>
+            <artifactId>jersey-test-framework-http</artifactId>
+            <version>${jersey.version}</version>
+            <scope>test</scope>
+            <exclusions>
+                <exclusion>
+                    <groupId>junit</groupId>
+                    <artifactId>junit</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/MustacheViewRenderer.java b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/MustacheViewRenderer.java
new file mode 100644
index 0000000..59c4e24
--- /dev/null
+++ b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/MustacheViewRenderer.java
@@ -0,0 +1,57 @@
+package io.dropwizard.views.mustache;
+
+import com.github.mustachejava.Mustache;
+import com.github.mustachejava.MustacheException;
+import com.github.mustachejava.MustacheFactory;
+import com.google.common.base.Charsets;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.util.concurrent.UncheckedExecutionException;
+import io.dropwizard.views.View;
+import io.dropwizard.views.ViewRenderer;
+
+import javax.ws.rs.WebApplicationException;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * A {@link ViewRenderer} which renders Mustache ({@code .mustache}) templates.
+ */
+public class MustacheViewRenderer implements ViewRenderer {
+    private final LoadingCache<Class<? extends View>, MustacheFactory> factories;
+
+    public MustacheViewRenderer() {
+        this.factories = CacheBuilder.newBuilder()
+                                     .build(new CacheLoader<Class<? extends View>, MustacheFactory>() {
+                                         @Override
+                                         public MustacheFactory load(Class<? extends View> key) throws Exception {
+                                             return new PerClassMustacheFactory(key);
+                                         }
+                                     });
+    }
+
+    @Override
+    public boolean isRenderable(View view) {
+        return view.getTemplateName().endsWith(".mustache");
+    }
+
+    @Override
+    public void render(View view, Locale locale, OutputStream output) throws IOException, WebApplicationException {
+        try {
+            final Mustache template = factories.get(view.getClass())
+                                               .compile(view.getTemplateName());
+            final Charset charset = view.getCharset().or(Charsets.UTF_8);
+            try (OutputStreamWriter writer = new OutputStreamWriter(output, charset)) {
+                template.execute(writer, view);
+            }
+        } catch (ExecutionException | UncheckedExecutionException | MustacheException ignored) {
+            throw new FileNotFoundException("Template " + view.getTemplateName() + " not found.");
+        }
+    }
+}
diff --git a/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/PerClassMustacheFactory.java b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/PerClassMustacheFactory.java
new file mode 100644
index 0000000..bd08533
--- /dev/null
+++ b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/PerClassMustacheFactory.java
@@ -0,0 +1,31 @@
+package io.dropwizard.views.mustache;
+
+import com.github.mustachejava.DefaultMustacheFactory;
+import com.github.mustachejava.MustacheException;
+import com.google.common.base.Charsets;
+import io.dropwizard.views.View;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+/**
+ * A class-specific Mustache factory which caches the parsed/compiled templates.
+ */
+class PerClassMustacheFactory extends DefaultMustacheFactory {
+    private final Class<? extends View> klass;
+
+    PerClassMustacheFactory(Class<? extends View> klass) {
+        this.klass = klass;
+    }
+
+    @Override
+    public Reader getReader(String resourceName) {
+        final InputStream is = klass.getResourceAsStream(resourceName);
+        if (is == null) {
+            throw new MustacheException("Template " + resourceName + " not found");
+        }
+        return new BufferedReader(new InputStreamReader(is, Charsets.UTF_8));
+    }
+}
diff --git a/dropwizard-views-mustache/src/main/resources/META-INF/services/io.dropwizard.views.ViewRenderer b/dropwizard-views-mustache/src/main/resources/META-INF/services/io.dropwizard.views.ViewRenderer
new file mode 100644
index 0000000..cdcd583
--- /dev/null
+++ b/dropwizard-views-mustache/src/main/resources/META-INF/services/io.dropwizard.views.ViewRenderer
@@ -0,0 +1 @@
+io.dropwizard.views.mustache.MustacheViewRenderer
diff --git a/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/AbsoluteView.java b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/AbsoluteView.java
new file mode 100644
index 0000000..7855345
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/AbsoluteView.java
@@ -0,0 +1,16 @@
+package io.dropwizard.views.mustache;
+
+import io.dropwizard.views.View;
+
+public class AbsoluteView extends View {
+    private final String name;
+
+    public AbsoluteView(String name) {
+        super("/example.mustache");
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
diff --git a/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/BadView.java b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/BadView.java
new file mode 100644
index 0000000..0749241
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/BadView.java
@@ -0,0 +1,9 @@
+package io.dropwizard.views.mustache;
+
+import io.dropwizard.views.View;
+
+public class BadView extends View {
+    public BadView() {
+        super("/woo-oo-ahh.txt.mustache");
+    }
+}
diff --git a/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/MustacheViewRendererTest.java b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/MustacheViewRendererTest.java
new file mode 100644
index 0000000..c0824bc
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/MustacheViewRendererTest.java
@@ -0,0 +1,84 @@
+package io.dropwizard.views.mustache;
+
+import com.codahale.metrics.MetricRegistry;
+import com.google.common.collect.ImmutableList;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.api.core.DefaultResourceConfig;
+import com.sun.jersey.test.framework.AppDescriptor;
+import com.sun.jersey.test.framework.JerseyTest;
+import com.sun.jersey.test.framework.LowLevelAppDescriptor;
+import io.dropwizard.logging.LoggingFactory;
+import io.dropwizard.views.ViewMessageBodyWriter;
+import io.dropwizard.views.ViewRenderer;
+import org.junit.Test;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class MustacheViewRendererTest extends JerseyTest {
+    static {
+        LoggingFactory.bootstrap();
+    }
+
+    @Path("/test/")
+    @Produces(MediaType.TEXT_HTML)
+    public static class ExampleResource {
+        @GET
+        @Path("/absolute")
+        public AbsoluteView showAbsolute() {
+            return new AbsoluteView("yay");
+        }
+
+        @GET
+        @Path("/relative")
+        public RelativeView showRelative() {
+            return new RelativeView();
+        }
+
+        @GET
+        @Path("/bad")
+        public BadView showBad() {
+            return new BadView();
+        }
+    }
+
+    @Override
+    protected AppDescriptor configure() {
+        final DefaultResourceConfig config = new DefaultResourceConfig();
+        final ViewRenderer renderer = new MustacheViewRenderer();
+        config.getSingletons().add(new ViewMessageBodyWriter(new MetricRegistry(), ImmutableList.of(renderer)));
+        config.getSingletons().add(new ExampleResource());
+        return new LowLevelAppDescriptor.Builder(config).build();
+    }
+
+    @Test
+    public void rendersViewsWithAbsoluteTemplatePaths() throws Exception {
+        final String response = client().resource(getBaseURI() + "test/absolute").get(String.class);
+        assertThat(response)
+                .isEqualTo("Woop woop. yay" + System.lineSeparator());
+    }
+
+    @Test
+    public void rendersViewsWithRelativeTemplatePaths() throws Exception {
+        final String response = client().resource(getBaseURI() + "test/relative").get(String.class);
+        assertThat(response)
+                .isEqualTo("Ok." + System.lineSeparator());
+    }
+
+    @Test
+    public void returnsA500ForViewsWithBadTemplatePaths() throws Exception {
+        try {
+            client().resource(getBaseURI() + "test/bad").get(String.class);
+        } catch (UniformInterfaceException e) {
+            assertThat(e.getResponse().getStatus())
+                    .isEqualTo(500);
+
+            assertThat(e.getResponse().getEntity(String.class))
+                    .isEqualTo("<html><head><title>Missing Template</title></head><body><h1>Missing Template</h1><p>Template /woo-oo-ahh.txt.mustache not found.</p></body></html>");
+        }
+    }
+}
diff --git a/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/RelativeView.java b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/RelativeView.java
new file mode 100644
index 0000000..4b09997
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/RelativeView.java
@@ -0,0 +1,9 @@
+package io.dropwizard.views.mustache;
+
+import io.dropwizard.views.View;
+
+public class RelativeView extends View {
+    public RelativeView() {
+        super("relative.mustache");
+    }
+}
diff --git a/dropwizard-views-mustache/src/test/resources/example.mustache b/dropwizard-views-mustache/src/test/resources/example.mustache
new file mode 100644
index 0000000..cb60981
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/resources/example.mustache
@@ -0,0 +1 @@
+Woop woop. {{name}}
diff --git a/dropwizard-views-mustache/src/test/resources/io/dropwizard/views/mustache/relative.mustache b/dropwizard-views-mustache/src/test/resources/io/dropwizard/views/mustache/relative.mustache
new file mode 100644
index 0000000..587579a
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/resources/io/dropwizard/views/mustache/relative.mustache
@@ -0,0 +1 @@
+Ok.
diff --git a/dropwizard-views-mustache/src/test/resources/logback-test.xml b/dropwizard-views-mustache/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-views-mustache/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/dropwizard-views/pom.xml b/dropwizard-views/pom.xml
new file mode 100644
index 0000000..6cc3ed4
--- /dev/null
+++ b/dropwizard-views/pom.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.dropwizard</groupId>
+        <artifactId>dropwizard-parent</artifactId>
+        <version>0.7.1</version>
+    </parent>
+
+    <artifactId>dropwizard-views</artifactId>
+    <name>Dropwizard Views</name>
+    
+    <dependencies>
+        <dependency>
+            <groupId>io.dropwizard</groupId>
+            <artifactId>dropwizard-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/dropwizard-views/src/main/java/io/dropwizard/views/View.java b/dropwizard-views/src/main/java/io/dropwizard/views/View.java
new file mode 100644
index 0000000..8c7d9e5
--- /dev/null
+++ b/dropwizard-views/src/main/java/io/dropwizard/views/View.java
@@ -0,0 +1,62 @@
+package io.dropwizard.views;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.google.common.base.Optional;
+
+import java.nio.charset.Charset;
+
+/**
+ * A Dropwizard view class.
+ */
+public abstract class View {
+    private final String templateName;
+    private final Charset charset;
+
+    /**
+     * Creates a new view.
+     *
+     * @param templateName the name of the template resource
+     */
+    protected View(String templateName) {
+        this(templateName, null);
+    }
+
+    /**
+     * Creates a new view.
+     *
+     * @param templateName the name of the template resource
+     * @param charset      the character set for {@code templateName}
+     */
+    protected View(String templateName, Charset charset) {
+        this.templateName = resolveName(templateName);
+        this.charset = charset;
+    }
+
+    /**
+     * Returns the template name.
+     *
+     * @return the template name
+     */
+    @JsonIgnore
+    public String getTemplateName() {
+        return templateName;
+    }
+
+    /**
+     * Returns the character set of the template.
+     *
+     * @return the character set of the template
+     */
+    @JsonIgnore
+    public Optional<Charset> getCharset() {
+        return Optional.fromNullable(charset);
+    }
+
+    private String resolveName(String templateName) {
+        if (templateName.startsWith("/")) {
+            return templateName;
+        }
+        final String packagePath = getClass().getPackage().getName().replace('.', '/');
+        return String.format("/%s/%s", packagePath, templateName);
+    }
+}
diff --git a/dropwizard-views/src/main/java/io/dropwizard/views/ViewBundle.java b/dropwizard-views/src/main/java/io/dropwizard/views/ViewBundle.java
new file mode 100644
index 0000000..145c8fa
--- /dev/null
+++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewBundle.java
@@ -0,0 +1,103 @@
+package io.dropwizard.views;
+
+import com.sun.jersey.spi.service.ServiceFinder;
+import io.dropwizard.Bundle;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+/**
+ * A {@link Bundle}, which by default, enables the rendering of FreeMarker & Mustache views by your application.
+ *
+ * <p>Other instances of {@link ViewRenderer} can be used by initializing your {@link ViewBundle} with a
+ * {@link Iterable} of the {@link ViewRenderer} instances to be used when configuring your {@link Bundle}:</p>
+ *
+ * <pre><code>
+ * new ViewBundle(ImmutableList.of(myViewRenderer))
+ * </code></pre>
+ *
+ * <p>A view combines a Freemarker or Mustache template with a set of Java objects:</p>
+ *
+ * <pre><code>
+ * public class PersonView extends View {
+ *     private final Person person;
+ *
+ *     public PersonView(Person person) {
+ *         super("profile.ftl"); // or super("profile.mustache"); for a Mustache template
+ *         this.person = person;
+ *     }
+ *
+ *     public Person getPerson() {
+ *         return person;
+ *     }
+ * }
+ * </code></pre>
+ *
+ *<p>The {@code "profile.ftl"} or {@code "profile.mustache"} is the path of the template relative to the class name. If
+ * this class was {@code com.example.application.PersonView}, Freemarker or Mustache would then look for the file
+ * {@code src/main/resources/com/example/application/profile.ftl} or {@code
+ * src/main/resources/com/example/application/profile.mustache} respectively. If the template path
+ * starts with a slash (e.g., {@code "/hello.ftl"} or {@code "/hello.mustache"}), Freemarker or Mustache will look for
+ * the file {@code src/main/resources/hello.ftl} or {@code src/main/resources/hello.mustache} respectively.
+ *
+ * <p>A resource method with a view would looks something like this:</p>
+ *
+ * <pre><code>
+ * \@GET
+ * public PersonView getPerson(\@PathParam("id") String id) {
+ *     return new PersonView(dao.find(id));
+ * }
+ * </code></pre>
+ *
+ * <p>Freemarker templates look something like this:</p>
+ *
+ * <pre>{@code
+ * <#-- @ftlvariable name="" type="com.example.application.PersonView" -->
+ * <html>
+ *     <body>
+ *         <h1>Hello, ${person.name?html}!</h1>
+ *     </body>
+ * </html>
+ * }</pre>
+ *
+ * <p>In this template, {@code ${person.name}} calls {@code getPerson().getName()}, and the
+ * {@code ?html} escapes all HTML control characters in the result. The {@code ftlvariable} comment
+ * at the top indicate to Freemarker (and your IDE) that the root object is a {@code Person},
+ * allowing for better typesafety in your templates.</p>
+ *
+ * @see <a href="http://freemarker.sourceforge.net/docs/index.html">FreeMarker Manual</a>
+ *
+ * <p>Mustache templates look something like this:</p>
+ *
+ * <pre>{@code
+ * <html>
+ *     <body>
+ *         <h1>Hello, {{person.name}}!</h1>
+ *     </body>
+ * </html>
+ * }</pre>
+ *
+ * <p>In this template, {@code {{person.name}}} calls {@code getPerson().getName()}.</p>
+ *
+ * @see <a href="http://mustache.github.io/mustache.5.html">Mustache Manual</a>
+ */
+public class ViewBundle implements Bundle {
+    private final Iterable<ViewRenderer> viewRenderers;
+
+    public ViewBundle() {
+        this(ServiceFinder.find(ViewRenderer.class));
+    }
+
+    public ViewBundle(Iterable<ViewRenderer> viewRenderers) {
+        this.viewRenderers = viewRenderers;
+    }
+
+    @Override
+    public void initialize(Bootstrap<?> bootstrap) {
+        // nothing doing
+    }
+
+    @Override
+    public void run(Environment environment) {
+        environment.jersey().register(new ViewMessageBodyWriter(environment.metrics(), viewRenderers));
+    }
+}
diff --git a/dropwizard-views/src/main/java/io/dropwizard/views/ViewMessageBodyWriter.java b/dropwizard-views/src/main/java/io/dropwizard/views/ViewMessageBodyWriter.java
new file mode 100644
index 0000000..a7cfab4
--- /dev/null
+++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewMessageBodyWriter.java
@@ -0,0 +1,101 @@
+package io.dropwizard.views;
+
+import com.codahale.metrics.MetricRegistry;
+import com.codahale.metrics.Timer;
+import com.google.common.collect.ImmutableList;
+import com.sun.jersey.spi.service.ServiceFinder;
+
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.*;
+import javax.ws.rs.ext.MessageBodyWriter;
+import javax.ws.rs.ext.Provider;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Locale;
+
+import static com.codahale.metrics.MetricRegistry.name;
+
+ at Provider
+ at Produces(MediaType.WILDCARD)
+public class ViewMessageBodyWriter implements MessageBodyWriter<View> {
+    private static final String MISSING_TEMPLATE_MSG =
+            "<html>" +
+                "<head><title>Missing Template</title></head>" +
+                "<body><h1>Missing Template</h1><p>{0}</p></body>" +
+            "</html>";
+
+    @Context
+    @SuppressWarnings("UnusedDeclaration")
+    private HttpHeaders headers;
+
+    private final Iterable<ViewRenderer> renderers;
+    private final MetricRegistry metricRegistry;
+
+    @Deprecated
+    public ViewMessageBodyWriter(MetricRegistry metricRegistry) {
+        this(metricRegistry, ServiceFinder.find(ViewRenderer.class));
+    }
+
+    public ViewMessageBodyWriter(MetricRegistry metricRegistry, Iterable<ViewRenderer> viewRenderers) {
+        this.metricRegistry = metricRegistry;
+        this.renderers = viewRenderers;
+    }
+
+    @Override
+    public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+        return View.class.isAssignableFrom(type);
+    }
+
+    @Override
+    public long getSize(View t,
+                        Class<?> type,
+                        Type genericType,
+                        Annotation[] annotations,
+                        MediaType mediaType) {
+        return -1;
+    }
+
+    @Override
+    public void writeTo(View t,
+                        Class<?> type,
+                        Type genericType,
+                        Annotation[] annotations,
+                        MediaType mediaType,
+                        MultivaluedMap<String, Object> httpHeaders,
+                        OutputStream entityStream) throws IOException, WebApplicationException {
+        final Timer.Context context = metricRegistry.timer(name(t.getClass(), "rendering")).time();
+        try {
+            for (ViewRenderer renderer : renderers) {
+                if (renderer.isRenderable(t)) {
+                    renderer.render(t, detectLocale(headers), entityStream);
+                    return;
+                }
+            }
+            throw new ViewRenderException("Unable to find a renderer for " + t.getTemplateName());
+        } catch (FileNotFoundException e) {
+            final String msg = MessageFormat.format(MISSING_TEMPLATE_MSG, e.getMessage());
+            throw new WebApplicationException(Response.serverError()
+                                                      .type(MediaType.TEXT_HTML_TYPE)
+                                                      .entity(msg)
+                                                      .build());
+        } finally {
+            context.stop();
+        }
+    }
+
+    private Locale detectLocale(HttpHeaders headers) {
+        final List<Locale> languages = headers.getAcceptableLanguages();
+        for (Locale locale : languages) {
+            if (!locale.toString().contains("*")) { // Freemarker doesn't do wildcards well
+                return locale;
+            }
+        }
+        return Locale.getDefault();
+    }
+}
diff --git a/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderException.java b/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderException.java
new file mode 100644
index 0000000..7093cc0
--- /dev/null
+++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderException.java
@@ -0,0 +1,20 @@
+package io.dropwizard.views;
+
+import java.io.IOException;
+
+/**
+ * Signals that an error occurred during the rendering of a view.
+ */
+public class ViewRenderException extends IOException {
+    private static final long serialVersionUID = -2972444466317717696L;
+
+    /**
+     * Constructs a {@link ViewRenderException} with the specified detail message.
+     *
+     * @param message The detail message (which is saved for later retrieval by the {@link
+     *                #getMessage()} method)
+     */
+    public ViewRenderException(String message) {
+        super(message);
+    }
+}
diff --git a/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderer.java b/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderer.java
new file mode 100644
index 0000000..9dd567d
--- /dev/null
+++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderer.java
@@ -0,0 +1,33 @@
+package io.dropwizard.views;
+
+import javax.ws.rs.WebApplicationException;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Locale;
+
+/**
+ * The rendering engine for a type of view.
+ */
+public interface ViewRenderer {
+    /**
+     * Returns {@code true} if the renderer can render the given {@link View}.
+     *
+     * @param view a view
+     * @return {@code true} if {@code view} can be rendered
+     */
+    boolean isRenderable(View view);
+
+    /**
+     * Renders the given {@link View} for the given {@link Locale} to the given {@link
+     * OutputStream}.
+     *
+     * @param view   a view
+     * @param locale the locale in which the view should be rendered
+     * @param output the output stream
+     * @throws IOException             if there is an error writing to {@code output}
+     * @throws WebApplicationException if there is an error rendering the template
+     */
+    void render(View view,
+                Locale locale,
+                OutputStream output) throws IOException, WebApplicationException;
+}
diff --git a/dropwizard-views/src/test/java/io/dropwizard/views/ViewBundleTest.java b/dropwizard-views/src/test/java/io/dropwizard/views/ViewBundleTest.java
new file mode 100644
index 0000000..cdbd7b9
--- /dev/null
+++ b/dropwizard-views/src/test/java/io/dropwizard/views/ViewBundleTest.java
@@ -0,0 +1,66 @@
+package io.dropwizard.views;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Locale;
+
+import javax.ws.rs.WebApplicationException;
+
+import com.google.common.collect.ImmutableList;
+import io.dropwizard.jersey.setup.JerseyEnvironment;
+import io.dropwizard.setup.Environment;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.*;
+
+public class ViewBundleTest {
+    private final JerseyEnvironment jerseyEnvironment = mock(JerseyEnvironment.class);
+    private final Environment environment = mock(Environment.class);
+
+    @Before
+    public void setUp() throws Exception {
+        when(environment.jersey()).thenReturn(jerseyEnvironment);
+    }
+
+    @Test
+    public void addsTheViewMessageBodyWriterToTheEnvironment() throws Exception {
+        new ViewBundle().run(environment);
+
+        verify(jerseyEnvironment).register(any(ViewMessageBodyWriter.class));
+    }
+
+    @Test
+    public void addsTheViewMessageBodyWriterWithSingleViewRendererToTheEnvironment() throws Exception {
+        ViewRenderer renderer = new ViewRenderer() {
+            @Override
+            public boolean isRenderable(View view) {
+                return false;
+            }
+
+            @Override
+            public void render(View view, Locale locale, OutputStream output) throws IOException, WebApplicationException {
+                //nothing to do
+            }
+        };
+
+        new ViewBundle(ImmutableList.of(renderer)).run(environment);
+
+        verify(jerseyEnvironment).register(any(ViewMessageBodyWriter.class));
+
+        ArgumentCaptor<ViewMessageBodyWriter> argumentCaptor = ArgumentCaptor.forClass(ViewMessageBodyWriter.class);
+        verify(jerseyEnvironment).register(argumentCaptor.capture());
+
+        Field renderers = ViewMessageBodyWriter.class.getDeclaredField("renderers");
+        renderers.setAccessible(true);
+        List<ViewRenderer> configuredRenderers = ImmutableList.copyOf((Iterable<ViewRenderer>) renderers.get(argumentCaptor.getValue()));
+        assertEquals(1, configuredRenderers.size());
+        assertTrue(configuredRenderers.contains(renderer));
+    }
+}
diff --git a/dropwizard-views/src/test/java/io/dropwizard/views/ViewTest.java b/dropwizard-views/src/test/java/io/dropwizard/views/ViewTest.java
new file mode 100644
index 0000000..0df52c1
--- /dev/null
+++ b/dropwizard-views/src/test/java/io/dropwizard/views/ViewTest.java
@@ -0,0 +1,15 @@
+package io.dropwizard.views;
+
+import org.junit.Test;
+
+import static org.fest.assertions.api.Assertions.assertThat;
+
+public class ViewTest {
+    private final View view = new View("/blah.tmp") {};
+
+    @Test
+    public void hasATemplate() throws Exception {
+        assertThat(view.getTemplateName())
+                .isEqualTo("/blah.tmp");
+    }
+}
diff --git a/dropwizard-views/src/test/resources/logback-test.xml b/dropwizard-views/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..a167d4b
--- /dev/null
+++ b/dropwizard-views/src/test/resources/logback-test.xml
@@ -0,0 +1,11 @@
+<configuration>
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <outputPatternAsHeader>false</outputPatternAsHeader>
+            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+    <root level="off">
+        <appender-ref ref="STDOUT"/>
+    </root>
+</configuration>
diff --git a/findbugs-exclude.xml b/findbugs-exclude.xml
new file mode 100644
index 0000000..e8d8852
--- /dev/null
+++ b/findbugs-exclude.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<FindBugsFilter>
+    <!-- we're *supposed* to manually GC there -->
+    <Match>
+        <Class name="io.dropwizard.servlets.tasks.GarbageCollectionTask"/>
+        <Method name="execute"/>
+        <Bug pattern="DM_GC"/>
+    </Match>
+
+    <!-- kind of hard to use exit codes without System.exit -->
+    <Match>
+        <Class name="io.dropwizard.Application"/>
+        <Method name="run"/>
+        <Bug pattern="DM_EXIT"/>
+    </Match>
+
+    <!-- serialization is boring -->
+    <Match>
+        <Bug code="Se"/>
+        <Class name="io.dropwizard.servlets.tasks.TaskServlet"/>
+    </Match>
+    <Match>
+        <Bug code="Se"/>
+        <Class name="io.dropwizard.servlets.assets.AssetServlet"/>
+    </Match>
+
+    <!-- pretty sure this is a bug in FindBugs -->
+    <Match>
+        <Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE"/>
+        <Class name="io.dropwizard.configuration.ConfigurationFactory"/>
+    </Match>
+</FindBugsFilter>
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..2a47679
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,480 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <prerequisites>
+        <maven>3.0.0</maven>
+    </prerequisites>
+
+    <groupId>io.dropwizard</groupId>
+    <artifactId>dropwizard-parent</artifactId>
+    <version>0.7.1</version>
+    <packaging>pom</packaging>
+    <name>Dropwizard Project</name>
+    <url>${dropwizard.url}</url>
+    <description>
+        Dropwizard is a Java framework for developing ops-friendly, high-performance, RESTful web
+        applications.
+    </description>
+
+    <modules>
+        <module>docs</module>
+        <module>dropwizard-core</module>
+        <module>dropwizard-client</module>
+        <module>dropwizard-db</module>
+        <module>dropwizard-jdbi</module>
+        <module>dropwizard-migrations</module>
+        <module>dropwizard-hibernate</module>
+        <module>dropwizard-auth</module>
+        <module>dropwizard-example</module>
+        <module>dropwizard-forms</module>
+        <module>dropwizard-views</module>
+        <module>dropwizard-views-freemarker</module>
+        <module>dropwizard-views-mustache</module>
+        <module>dropwizard-testing</module>
+        <module>dropwizard-util</module>
+        <module>dropwizard-jackson</module>
+        <module>dropwizard-validation</module>
+        <module>dropwizard-configuration</module>
+        <module>dropwizard-logging</module>
+        <module>dropwizard-metrics</module>
+        <module>dropwizard-metrics-ganglia</module>
+        <module>dropwizard-metrics-graphite</module>
+        <module>dropwizard-jersey</module>
+        <module>dropwizard-jetty</module>
+        <module>dropwizard-servlets</module>
+        <module>dropwizard-lifecycle</module>
+        <module>dropwizard-assets</module>
+        <module>dropwizard-spdy</module>
+    </modules>
+
+    <properties>
+        <dropwizard.url>http://www.dropwizard.io/${project.version}</dropwizard.url>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <metrics3.version>3.0.2</metrics3.version>
+        <jersey.version>1.18.1</jersey.version>
+        <jackson.api.version>2.3.0</jackson.api.version>
+        <jackson.version>2.3.3</jackson.version>
+        <logback.version>1.1.2</logback.version>
+        <slf4j.version>1.7.6</slf4j.version>
+        <servlet.version>3.0.0.v201112011016</servlet.version>
+        <jetty.version>9.0.7.v20131107</jetty.version>
+        <guava.version>17.0</guava.version>
+        <h2.version>1.4.178</h2.version>
+        <findbugs.skip>false</findbugs.skip>
+    </properties>
+
+    <developers>
+        <developer>
+            <name>Coda Hale</name>
+            <email>coda.hale at gmail.com</email>
+            <timezone>America/Los_Angeles</timezone>
+            <roles>
+                <role>architect</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Nick Telford</name>
+            <email>nick.telford at gmail.com</email>
+            <timezone>Europe/London</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Graham O'Regan</name>
+            <email>graham.oregan at ellisonbrookes.com</email>
+            <timezone>Europe/London</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Ryan Kennedy</name>
+            <email>rckenned at gmail.com</email>
+            <timezone>America/Los_Angeles</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Camille Fournier</name>
+            <email>camille at apache.org</email>
+            <timezone>America/New_York</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Carlo Barbara</name>
+            <email>carlo.barbara at gmail.com</email>
+            <timezone>America/New_York</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Matt Veitas</name>
+            <email>mveitas at gmail.com</email>
+            <timezone>America/New_York</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Ryan Tenney</name>
+            <email>ryan at 10e.us</email>
+            <timezone>America/New_York</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+        <developer>
+            <name>Jochen Schalanda</name>
+            <email>jochen at schalanda.name</email>
+            <timezone>Europe/Berlin</timezone>
+            <roles>
+                <role>committer</role>
+            </roles>
+        </developer>
+    </developers>
+
+    <licenses>
+        <license>
+            <name>Apache License 2.0</name>
+            <url>http://www.apache.org/licenses/LICENSE-2.0.html</url>
+            <distribution>repo</distribution>
+        </license>
+    </licenses>
+
+    <scm>
+        <connection>scm:git:git://github.com/dropwizard/dropwizard.git</connection>
+        <developerConnection>scm:git:git at github.com:dropwizard/dropwizard.git</developerConnection>
+        <url>https://github.com/dropwizard/dropwizard</url>
+        <tag>v0.7.1</tag>
+    </scm>
+
+    <issueManagement>
+        <system>GitHub</system>
+        <url>https://github.com/dropwizard/dropwizard/issues</url>
+    </issueManagement>
+
+    <ciManagement>
+        <system>Travis CI</system>
+        <url>https://travis-ci.org/dropwizard/dropwizard</url>
+    </ciManagement>
+
+    <mailingLists>
+        <mailingList>
+            <name>dropwizard-user</name>
+            <subscribe>dropwizard-user+subscribe at googlegroups.com</subscribe>
+            <unsubscribe>dropwizard-user+unsubscribe at googlegroups.com</unsubscribe>
+            <post>dropwizard-user at googlegroups.com</post>
+            <archive>https://groups.google.com/forum/#!forum/dropwizard-user</archive>
+        </mailingList>
+        <mailingList>
+            <name>dropwizard-dev</name>
+            <subscribe>dropwizard-dev+subscribe at googlegroups.com</subscribe>
+            <unsubscribe>dropwizard-dev+unsubscribe at googlegroups.com</unsubscribe>
+            <post>dropwizard-dev at googlegroups.com</post>
+            <archive>https://groups.google.com/forum/#!forum/dropwizard-dev</archive>
+        </mailingList>
+    </mailingLists>
+
+    <repositories>
+        <repository>
+            <id>sonatype-nexus-snapshots</id>
+            <name>Sonatype Nexus Snapshots</name>
+            <url>http://oss.sonatype.org/content/repositories/snapshots</url>
+            <releases>
+                <enabled>false</enabled>
+            </releases>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
+    </repositories>
+
+    <distributionManagement>
+        <snapshotRepository>
+            <id>sonatype-nexus-snapshots</id>
+            <name>Sonatype Nexus Snapshots</name>
+            <url>http://oss.sonatype.org/content/repositories/snapshots</url>
+        </snapshotRepository>
+        <repository>
+            <id>sonatype-nexus-staging</id>
+            <name>Nexus Release Repository</name>
+            <url>http://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+        </repository>
+        <site>
+            <id>dropwizard.io</id>
+            <url>${dropwizard.url}</url>
+        </site>
+    </distributionManagement>
+
+    <profiles>
+        <profile>
+            <id>release-sign-artifacts</id>
+            <activation>
+                <property>
+                    <name>performRelease</name>
+                    <value>true</value>
+                </property>
+            </activation>
+            <build>
+                <plugins>
+                    <plugin>
+                        <groupId>org.apache.maven.plugins</groupId>
+                        <artifactId>maven-gpg-plugin</artifactId>
+                        <version>1.4</version>
+                        <executions>
+                            <execution>
+                                <id>sign-artifacts</id>
+                                <phase>verify</phase>
+                                <goals>
+                                    <goal>sign</goal>
+                                </goals>
+                            </execution>
+                        </executions>
+                    </plugin>
+                </plugins>
+            </build>
+        </profile>
+        <profile>
+            <id>java8-disable-strict-javadoc-and-findbugs</id>
+            <activation>
+                <jdk>[1.8,)</jdk>
+            </activation>
+            <properties>
+                <javadoc.doclint.none>-Xdoclint:none</javadoc.doclint.none>
+                <findbugs.skip>true</findbugs.skip>
+            </properties>
+        </profile>
+    </profiles>
+
+    <dependencies>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>1.9.5</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.easytesting</groupId>
+            <artifactId>fest-assert-core</artifactId>
+            <version>2.0M10</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-annotations</artifactId>
+                <version>${jackson.api.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-api</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.hamcrest</groupId>
+                <artifactId>hamcrest-core</artifactId>
+                <version>1.3</version>
+            </dependency>
+            <dependency>
+                <groupId>org.objenesis</groupId>
+                <artifactId>objenesis</artifactId>
+                <version>2.1</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.1</version>
+                <configuration>
+                    <source>1.7</source>
+                    <target>1.7</target>
+                </configuration>
+            </plugin>
+            <!-- we can't run tests in parallel until http://bugzilla.slf4j.org/show_bug.cgi?id=176 is fixed -->
+            <!--<plugin>-->
+                <!--<groupId>org.apache.maven.plugins</groupId>-->
+                <!--<artifactId>maven-surefire-plugin</artifactId>-->
+                <!--<version>2.14.1</version>-->
+                <!--<configuration>-->
+                    <!--<parallel>classes</parallel>-->
+                    <!--</configuration>-->
+                <!--</plugin>-->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-enforcer-plugin</artifactId>
+                <version>1.3.1</version>
+                <executions>
+                    <execution>
+                        <id>enforce</id>
+                        <configuration>
+                            <rules>
+                                <DependencyConvergence />
+                                <bannedDependencies>
+                                    <excludes>
+                                        <!-- This should not exist as it will force SLF4J calls to be delegated to log4j -->
+                                        <exclude>org.slf4j:slf4j-log4j12</exclude>
+                                        <!-- This should not exist as it will force SLF4J calls to be delegated to jul -->
+                                        <exclude>org.slf4j:slf4j-jdk14</exclude>
+                                        <!-- Ensure only the slf4j binding for logback is on the classpath -->
+                                        <exclude>log4j:log4j</exclude>
+                                        <!-- As recommended from the slf4j guide, exclude commons-logging -->
+                                        <exclude>commons-logging:commons-logging</exclude>
+                                    </excludes>
+                                </bannedDependencies>
+                            </rules>
+                        </configuration>
+                        <goals>
+                            <goal>enforce</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-source-plugin</artifactId>
+                <version>2.2.1</version>
+                <executions>
+                    <execution>
+                        <id>attach-sources</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.9.1</version>
+                <configuration>
+                    <additionalparam>${javadoc.doclint.none}</additionalparam>
+                </configuration>
+                <executions>
+                    <execution>
+                        <id>attach-javadocs</id>
+                        <goals>
+                            <goal>jar</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-release-plugin</artifactId>
+                <version>2.5</version>
+                <configuration>
+                    <autoVersionSubmodules>true</autoVersionSubmodules>
+                    <mavenExecutorId>forked-path</mavenExecutorId>
+                    <tagNameFormat>v@{project.version}</tagNameFormat>
+                    <preparationGoals>clean test</preparationGoals>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>findbugs-maven-plugin</artifactId>
+                <version>2.5.3</version>
+                <configuration>
+                    <effort>Max</effort>
+                    <threshold>Default</threshold>
+                    <xmlOutput>true</xmlOutput>
+                    <excludeFilterFile>${basedir}/../findbugs-exclude.xml</excludeFilterFile>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>check</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <version>2.4</version>
+                <configuration>
+                    <archive>
+                        <manifest>
+                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+                        </manifest>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-site-plugin</artifactId>
+                <version>3.3</version>
+                <configuration>
+                    <skipDeploy>true</skipDeploy>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>com.github.github</groupId>
+                <artifactId>site-maven-plugin</artifactId>
+                <version>0.9</version>
+                <inherited>false</inherited>
+                <configuration>
+                    <!-- Make sure that each sub-module gets its own site -->
+                    <path>${project.version}</path>
+                    <message>Create site for ${project.name} ${project.version}</message>
+                    <noJekyll>true</noJekyll>
+                    <merge>true</merge>
+                    <outputDirectory>${project.build.directory}/staging</outputDirectory>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>site</goal>
+                        </goals>
+                        <phase>site-deploy</phase>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-project-info-reports-plugin</artifactId>
+                <version>2.7</version>
+                <configuration>
+                    <dependencyDetailsEnabled>true</dependencyDetailsEnabled>
+                    <dependencyLocationsEnabled>false</dependencyLocationsEnabled>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <version>2.9.1</version>
+                <reportSets>
+                    <reportSet>
+                        <id>html</id>
+                        <reports>
+                            <report>javadoc</report>
+                        </reports>
+                    </reportSet>
+                </reportSets>
+            </plugin>
+        </plugins>
+    </reporting>
+</project>

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-java/dropwizard.git



More information about the pkg-java-commits mailing list