[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 [](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