diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000000..3a6d35d23c2 --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,20 @@ +engines: + pmd: + enabled: true + channel: "beta" + checkstyle: + enabled: true + config: checkstyle.xml + channel: "beta" + +ratings: + paths: + - "**.java" + +exclude_paths: +- "dropwizard-archetypes/" +- "**.png" +- "**.ts" +- "**.p12" +- "**.jts" +- "**.keystore" diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000000..70024884362 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# Configuration file for EditorConfig: http://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.properties] +charset = latin1 + +[travis.yml] +indent_size = 2 +indent_style = space diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000000..7194c2b4519 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +*.txt text=auto +*.java text=auto +*.yml text=auto +*.xml text=auto +*.md text=auto +*.mustache text eol=lf +*.ftl text eol=lf diff --git a/.gitignore b/.gitignore index 89f5244dd77..b841e1d34c7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,22 @@ -.idea -lib_managed -src_managed -project/boot -target -src/main/java/com/yammer/streamie/data/pb -streamie.conf +# Maven +target/ +dependency-reduced-pom.xml +pom.xml.releaseBackup +pom.xml.versionsBackup +release.properties + +# IntelliJ IDEA +.idea/ +*.iml +*.ipr +*.iws + +# Eclipse +.settings/ +.classpath +.project + +nb-configuration.xml atlassian-ide-plugin.xml -project/plugins/project/build.properties + logs diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000000..c2ff14dc4c3 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,30 @@ +sudo: false +language: java +jdk: + - oraclejdk8 +after_success: + - bash .travis_after_success.sh +cache: + directories: + - $HOME/.m2 + +# Use updated jdk version instead of buggy default +addons: + apt: + packages: + - oracle-java8-installer + +# encrypted CI_DEPLOY_USERNAME and CI_DEPLOY_PASSWORD +# values for maven deploy. these have been encrypted +# with the public key from https://github.com/dropwizard/dropwizard +# and won't work in any other repository. if you want +# to run CI in your local fork you'll need to run `travis encrypt ...` +# accordingly (http://docs.travis-ci.com/user/encryption-keys/) +# +# NOTE: CI_DEPLOY_USERNAME is set to dropwizardci, the username +# we've set up with Sonatype, which only has permission to push +# to the snapshot repo. +env: + global: + - secure: "EAuz7bCKj4r438IEC2y73WVrFwXirflbXA4HhpwVmAFWNqC9LIIpkWcO5GVp773HsZvJBcJjJriP+aKRkImV8AyMgjCeEUv2dlezvbkIIz38vKyw9MWaPIyZ3uS9RCuL7OwGf5BeJ1DvHFYdMBaspZd+EmCYr7abnHdqs+Tm/W8=" + - secure: "RI0QcuKMsij3sgRm+Bjhu3X217U6UslvSzcRv13iLLwrTj73zhGi5PF/+kj8Qh1HMQw0oQRR6M8qPqGy82KcjiGbpgPgSy1rVAvkYg+Yw1k7v4l7Vgyj7TNsAM3pqHyojx2jgRjpQsgw/WXQmiahWV6OCOUzdbhEUwVzIXI+vtk=" diff --git a/.travis_after_success.sh b/.travis_after_success.sh new file mode 100755 index 00000000000..3ee63d1e73e --- /dev/null +++ b/.travis_after_success.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +if [[ "${TRAVIS_JDK_VERSION}" != "oraclejdk8" ]]; then + echo "Skipping after_success actions for JDK version \"${TRAVIS_JDK_VERSION}\"" + exit +fi + +mvn -B cobertura:cobertura coveralls:report + +if [[ -n ${TRAVIS_TAG} ]]; then + echo "Skipping deployment for tag \"${TRAVIS_TAG}\"" + exit +fi + +if [[ ${TRAVIS_BRANCH} != 'master' ]]; then + echo "Skipping deployment for branch \"${TRAVIS_BRANCH}\"" + exit +fi + +if [[ "$TRAVIS_PULL_REQUEST" = "true" ]]; then + echo "Skipping deployment for pull request" + exit +fi + +mvn -B deploy --settings maven_deploy_settings.xml -Dmaven.test.skip=true -Dfindbugs.skip=true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..9c7c809a4e7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,64 @@ +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.** + +Developer List +=== +The Google Group [dropwizard-dev](https://groups.google.com/forum/#!forum/dropwizard-dev) +is the place to discuss everything to do with the development of the framework itself, +including docs, process and community management. + + +Feel free to post questions about internals, ideas for new features or refactorings, +different strategies, requests for comment/review etc. This is the forum for everyone +who wants to actively contribute to the project itself. + +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 00000000000..190db18259d --- /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., 2014-2016 Dropwizard Team + + 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 00000000000..255f6a193ad --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +Dropwizard +Copyright 2010-2013 Coda Hale and Yammer, Inc., 2014-2016 Dropwizard Team + +This product includes software developed by Coda Hale and Yammer, Inc. diff --git a/README.md b/README.md index c040ee26511..d21d6a535f6 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,42 @@ Dropwizard ========== +[![Build Status](https://travis-ci.org/dropwizard/dropwizard.svg?branch=master)](https://travis-ci.org/dropwizard/dropwizard) +[![Coverage Status](https://img.shields.io/coveralls/dropwizard/dropwizard.svg)](https://coveralls.io/r/dropwizard/dropwizard) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.dropwizard/dropwizard-core/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.dropwizard/dropwizard-core/) +[![Javadoc](https://javadoc-emblem.rhcloud.com/doc/io.dropwizard/dropwizard-core/badge.svg)](http://www.javadoc.io/doc/io.dropwizard/dropwizard-core) +[![Code Climate](https://codeclimate.com/github/dropwizard/dropwizard/badges/gpa.svg)](https://codeclimate.com/github/dropwizard/dropwizard) -*Dropwizard is a sneaky way of making fast Java or Scala web services.* +*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'. -* [Log4j](http://logging.apache.org/log4j/1.2/) for loggin'. -* [Hibernate Validator](http://www.hibernate.org/subprojects/validator.html) for validatin'. -* [Metrics](https://github.com/codahale/metrics) for figurin' out what your service is doing in - production. -* [SnakeYAML](http://code.google.com/p/snakeyaml/) for YAML parsin' and configuratin'. +* [Jackson](https://github.com/FasterXML/jackson) for JSON parsin' and generatin'. +* [Logback](http://logback.qos.ch/) for loggin'. +* [Hibernate Validator](http://hibernate.org/validator/) for validatin'. +* [Metrics](http://metrics.dropwizard.io) for figurin' out what your application is doin' in production. +* [JDBI](http://www.jdbi.org) and [Hibernate](http://www.hibernate.org/orm/) for databasin'. +* [Liquibase](http://www.liquibase.org/) for migratin'. -[Yammer](https://www.yammer.com)'s high-performance, low-latency, Java and Scala services all use -Dropwizard. In fact, Dropwizard is really just a simple extraction of -[Yammer](https://www.yammer.com)'s glue code. +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 [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! diff --git a/RELEASES.md b/RELEASES.md new file mode 100644 index 00000000000..8a14ba5cd45 --- /dev/null +++ b/RELEASES.md @@ -0,0 +1,45 @@ +# Dropwizard Release Process + +This document describes the technical aspects of the Dropwizard release process. + +## Who is responsible for a release? + +A release can be performed by any member of the Dropwizard project with the commit rights to the repository. +The person responsible for the release MUST seek an approval for it in the `dropwizard-committers` +mailing list beforehand. + +## Prerequisites for a new maintainer + +* Register an account in the Sonatype's [Jira](https://issues.sonatype.org) +* Create a Jira ticket asking to obtain the right permissions to push artifacts belonging +to the `io.dropwizard` group. +* Write an email to @joschi or @carlo-rtr, so they can approve the request in the Jira ticket. +* Generate a gpg key with an email linked to your Github/Sonatype account +`gpg --gen-key` +* Distribute the key +`gpg --keyserver hkp://pgp.mit.edu --send-keys XXXX` # XXXX - the code of the generated key + +## Performing a release + +* Edit `docs/source/about/release-notes.rst` and set the release date; +* Edit `docs/source/about/docs-index.rst` and set the link to the release docs; +* Run `mvn release:prepare` in the master branch; +* Observe that all tests passed, there is no build errors and the corresponding git tag was created; +* Run `mvn release:perform -Dgoals=deploy`; +* Login at Sonatype's OSS Nexus `https://oss.sonatype.org`; +* Click "Staging repositories"; +* Find the `io.dropwizard` group, and click on the close button on the top bar; +* Wait while the Nexus server will perform some checks that artifacts were uploaded correctly; +* Click the refresh button; +* Select `io.dropwizard` again, and hit the release button on the top bar; +* Normally the release will be available in the Maven Central repository in 3-4 hours (this may vary depending on the +indexing process). +* Publish the documentation. Run the script `prepare_docs.sh` and verify it completed successfully. +Push the changes to the remote `gh-pages` branch. + +## Making an announcement + +After the release has been uploaded to the repository and the documentation has been updated, a release announcement +should be published in the `dropwizard-user` and `dropwizard-dev` mailing lists. There is no formal structure for +the announcement, but generally it should contain a short description of the release, the artifact coordinates in the +Maven Central, a link to documentation, a link to the release notes, and a link to the bug tracker. diff --git a/checkstyle.xml b/checkstyle.xml new file mode 100644 index 00000000000..22b49859f82 --- /dev/null +++ b/checkstyle.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 00000000000..14479fe8a5b --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gem 'octokit' diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 00000000000..77ee7cb6da2 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,18 @@ +GEM + remote: https://rubygems.org/ + specs: + addressable (2.3.6) + faraday (0.9.0) + multipart-post (>= 1.2, < 3) + multipart-post (2.0.0) + octokit (3.4.0) + sawyer (~> 0.5.3) + sawyer (0.5.5) + addressable (~> 2.3.5) + faraday (~> 0.8, < 0.10) + +PLATFORMS + ruby + +DEPENDENCIES + octokit diff --git a/docs/Guardfile b/docs/Guardfile new file mode 100644 index 00000000000..c56139109d2 --- /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 00000000000..25f30d39aa7 --- /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 ' where 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 00000000000..cd6feb5f502 Binary files /dev/null and b/docs/dropwizard-hat.eps differ diff --git a/docs/list_contributors.rb b/docs/list_contributors.rb new file mode 100755 index 00000000000..8ea14e76768 --- /dev/null +++ b/docs/list_contributors.rb @@ -0,0 +1,16 @@ +#!/usr/bin/env ruby +require 'octokit' + +Octokit.configure do |c| + # Provide an Access Token to prevent running into the hourly rate-limit + # see https://help.github.com/articles/creating-an-access-token-for-command-line-use + c.access_token = ENV['GITHUB_TOKEN'] || '' + c.auto_paginate = true +end + +contributors = Octokit.contributors('dropwizard/dropwizard') +contributors.each do |c| + user = Octokit.user(c.login) + name = if user.name.nil? then user.login else user.name end + puts "* `#{name} <#{user.html_url}>`_" +end diff --git a/docs/pom.xml b/docs/pom.xml new file mode 100644 index 00000000000..6247d9b3a2c --- /dev/null +++ b/docs/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + docs + Dropwizard Documentation + + + true + true + true + + + + + + ${basedir}/source + true + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + parse-version + initialize + + parse-version + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + process-resources + initialize + + resources + + + + @ + + ${project.build.directory}/source + false + + + + + + + + + + + kr.motd.maven + sphinx-maven-plugin + + ${project.build.directory}/source + + + + + diff --git a/docs/source/_static/dropwizard-hat.png b/docs/source/_static/dropwizard-hat.png new file mode 100644 index 00000000000..8081ccbe2e7 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 00000000000..7bc002b6c1e --- /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) %} +
+ {%- if links -%} + + {%- if links[0][0] %}{% endif -%} + {{ firstname|e }} + {%- if links[0][0] %}{% endif -%} + + + {%- for ismain, link in links[1:] -%} + , {% if ismain %}{% endif -%} + [{{ loop.index }}] + {%- if ismain %}{% endif -%} + + {%- endfor %} + {%- else %} + {{ firstname|e }} + {%- endif %} +
+{% endmacro %} + +{% extends "layout.html" %} +{% set title = _('Index') %} +{% block body %} + +

{{ _('Index') }}

+ +
+ {% for key, dummy in genindexentries -%} + {{ key }} + {% if not loop.last %}| {% endif %} + {%- endfor %} +
+ +{%- for key, entries in genindexentries %} +

{{ key }}

+ + {%- for column in entries|slice(2) if column %} + + {%- endfor %} +
+ {%- for entryname, (links, subitems) in column %} + {{ indexentries(entryname, links) }} + {%- if subitems %} +
+ {%- for subentryname, subentrylinks in subitems %} + {{ indexentries(subentryname, subentrylinks) }} + {%- endfor %} +
+ {%- endif -%} + {%- endfor %} +
+{% endfor %} + +{% endblock %} + +{% block sidebarrel %} +{% if split_index %} +

{{ _('Index') }}

+

{% for key, dummy in genindexentries -%} + {{ key }} + {% if not loop.last %}| {% endif %} + {%- endfor %}

+ +

{{ _('Full index on one page') }}

+{% endif %} + {{ super() }} +{% endblock %} diff --git a/docs/source/_themes/dropwizard/layout.html b/docs/source/_themes/dropwizard/layout.html new file mode 100644 index 00000000000..da39ec7e2d9 --- /dev/null +++ b/docs/source/_themes/dropwizard/layout.html @@ -0,0 +1,133 @@ + + +{%- 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 %} + + + {{ title|striptags|e }}{{ titlesuffix }} + + {%- for cssfile in css_files %} + + {%- endfor %} + + {%- if favicon %} + + {%- endif %} + {%- block linktags %} + + {%- if parents %} + + {%- endif %} + {%- if next %} + + {%- endif %} + {%- if prev %} + + {%- endif %} + {%- endblock %} + + + + + Fork me on GitHub + +
+
+ {%- block sidebar %} + {%- if title != "Home" %} + + {%- endif %} + {%- endblock %} +
+ {% block body %} {% endblock %} +
+
+
+
+

+ {%- if show_copyright %} + {%- if hasdoc('copyright') %} + {% trans path=pathto('copyright'), copyright=copyright|e %}© Copyright + {{ 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 Sphinx + {{ sphinx_version }}.{% endtrans %} + {%- endif %} +

+

Dropwizard v{{ release }}

+
+
+ + + + + diff --git a/docs/source/_themes/dropwizard/less/accordion.less b/docs/source/_themes/dropwizard/less/accordion.less new file mode 100644 index 00000000000..11a36b544e8 --- /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 00000000000..562826fd30c --- /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 00000000000..ea84f489987 --- /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 +@import "reset.less"; + +// Core variables and mixins +@import "variables.less"; // Modify this for custom colors, font-sizes, etc +@import "mixins.less"; + +// Grid system and page structure +@import "scaffolding.less"; +@import "grid.less"; +@import "layouts.less"; + +// Base CSS +@import "type.less"; +@import "code.less"; +@import "forms.less"; +@import "tables.less"; + +// Components: common +@import "sprites.less"; +@import "dropdowns.less"; +@import "wells.less"; +@import "component-animations.less"; +@import "close.less"; + +// Components: Buttons & Alerts +@import "buttons.less"; +@import "button-groups.less"; +@import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less + +// Components: Nav +@import "navs.less"; +@import "navbar.less"; +@import "breadcrumbs.less"; +@import "pagination.less"; +@import "pager.less"; + +// Components: Popovers +@import "modals.less"; +@import "tooltip.less"; +@import "popovers.less"; + +// Components: Misc +@import "thumbnails.less"; +@import "labels.less"; +@import "progress-bars.less"; +@import "accordion.less"; +@import "carousel.less"; +@import "hero-unit.less"; + +// Utility classes +@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 00000000000..19b8081e1b6 --- /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 00000000000..4b0523df295 --- /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 00000000000..07a2b5879e0 --- /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 00000000000..8fbd303154a --- /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 00000000000..a0e5edba1b6 --- /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 00000000000..c640537ae87 --- /dev/null +++ b/docs/source/_themes/dropwizard/less/code.less @@ -0,0 +1,44 @@ +// Code.less +// Code typography styles for the and
 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 00000000000..4f2a4fd1185
--- /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 00000000000..83f535ae861
--- /dev/null
+++ b/docs/source/_themes/dropwizard/less/dropdowns.less
@@ -0,0 +1,131 @@
+// DROPDOWN MENUS
+// --------------
+
+// Use the .menu class on any 
  • 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 00000000000..a1dd9e4c510 --- /dev/null +++ b/docs/source/_themes/dropwizard/less/dropwizard.less @@ -0,0 +1,231 @@ +@import "reset.less"; +@import "variables.less"; // Modify this for custom colors, font-sizes, etc +@import "mixins.less"; +@import "scaffolding.less"; +@import "grid.less"; +@import "layouts.less"; +@import "type.less"; +@import "code.less"; +@import "tables.less"; +@import "buttons.less"; +@import "navs.less"; +@import "navbar.less"; +@import "hero-unit.less"; +@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 00000000000..d70d532e8c4 --- /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,@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 00000000000..4acb0a44ce6 --- /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 00000000000..cba1cc46cff --- /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 00000000000..c0f42775020 --- /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 00000000000..c8d358b24a1 --- /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 00000000000..30e868e72fb --- /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)",@startColor,@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)",@startColor,@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)",@startColor,@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: -@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: -@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: -@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: -@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 00000000000..aa14675edee --- /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 00000000000..93c0400b6ef --- /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 00000000000..dfb2996f098 --- /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 00000000000..104e41cab01 --- /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 00000000000..de578075944 --- /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 00000000000..d94b921e422 --- /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 00000000000..558d99ec999 --- /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 00000000000..4fd45e28220 --- /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 00000000000..c3144e1bd74 --- /dev/null +++ b/docs/source/_themes/dropwizard/less/progress-bars.less @@ -0,0 +1,95 @@ +// PROGRESS BARS +// ------------- + + +// ANIMATIONS +// ---------- + +// Webkit +@-webkit-keyframes progress-bar-stripes { + from { background-position: 0 0; } + to { background-position: 40px 0; } +} + +// Firefox +@-moz-keyframes progress-bar-stripes { + from { background-position: 0 0; } + to { background-position: 40px 0; } +} + +// Spec +@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 00000000000..28d8eb60ca4 --- /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 00000000000..7d494a35765 --- /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 + +@import "variables.less"; // Modify this for custom colors, font-sizes, etc +@import "mixins.less"; + + +// RESPONSIVE CLASSES +// ------------------ + +// Hide from screenreaders and browsers +// Credit: HTML5 Boilerplate +.hidden { + display: none; + visibility: hidden; +} + + + +// UP TO LANDSCAPE PHONE +// --------------------- + +@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 +// -------------------------------------------------- + +@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 +// ---------------------------------- + +@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 +// ----------------- +@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 +// --------------- + +@media (min-width: 980px) { + .nav-collapse.collapse { + height: auto !important; + } +} + + + +// LARGE DESKTOP & UP +// ------------------ + +@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 00000000000..47ce53818b6 --- /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 00000000000..a56216c71c6 --- /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 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 +// . + +// For the white version of the icons, just add the .icon-white class: +// + +[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 00000000000..f98564d68e0 --- /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 00000000000..541fbd6a746 --- /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 00000000000..5111a193f03 --- /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 00000000000..7841bb0906a --- /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,@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 00000000000..d60d2203119 --- /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 00000000000..f01c232e4a8 --- /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 +@linkColor: #08c; +@linkColorHover: darken(@linkColor, 15%); + +// Grays +@black: #000; +@grayDarker: #222; +@grayDark: #333; +@gray: #555; +@grayLight: #999; +@grayLighter: #eee; +@white: #fff; + +// Accent colors +@blue: #049cdb; +@blueDark: #0064cd; +@green: #46a546; +@red: #9d261d; +@yellow: #ffc40d; +@orange: #f89406; +@pink: #c3325f; +@purple: #7a43b6; + +// Typography +@baseFontSize: 13px; +@baseFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif; +@baseLineHeight: 18px; +@textColor: @grayDark; + +// Buttons +@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 :) +@zindexDropdown: 1000; +@zindexPopover: 1010; +@zindexTooltip: 1020; +@zindexFixedNavbar: 1030; +@zindexModalBackdrop: 1040; +@zindexModal: 1050; + +// Input placeholder text color +@placeholderText: @grayLight; + +// Navbar +@navbarHeight: 40px; +@navbarBackground: @grayDarker; +@navbarBackgroundHighlight: @grayDark; + +@navbarText: @grayLight; +@navbarLinkColor: @grayLight; +@navbarLinkColorHover: @white; + +// Form states and alerts +@warningText: #c09853; +@warningBackground: #fcf8e3; +@warningBorder: darken(spin(@warningBackground, -10), 3%); + +@errorText: #b94a48; +@errorBackground: #f2dede; +@errorBorder: darken(spin(@errorBackground, -10), 3%); + +@successText: #468847; +@successBackground: #dff0d8; +@successBorder: darken(spin(@successBackground, -10), 5%); + +@infoText: #3a87ad; +@infoBackground: #d9edf7; +@infoBorder: darken(spin(@infoBackground, -10), 7%); + + + +// GRID +// -------------------------------------------------- + +// Default 940px grid +@gridColumns: 12; +@gridColumnWidth: 60px; +@gridGutterWidth: 20px; +@gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1)); + +// Fluid grid +@fluidGridColumnWidth: 6.382978723%; +@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 00000000000..244b8ca102b --- /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 00000000000..f6e7a688cb6 --- /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 00000000000..4cdc6935c0d --- /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 %} + + {{ super() }} +{% endblock %} +{% block body %} +

    {{ _('Search') }}

    +
    + +

    + {% trans %}Please activate JavaScript to enable the search + functionality.{% endtrans %} +

    +
    +

    + {% 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 %} +

    +
    + + + +
    + {% if search_performed %} +

    {{ _('Search Results') }}

    + {% if not search_results %} +

    {{ _('Your search did not match any results.') }}

    + {% endif %} + {% endif %} +
    + {% if search_results %} +
      + {% for href, caption, context in search_results %} +
    • {{ caption }} +
      {{ context|e }}
      +
    • + {% endfor %} +
    + {% endif %} +
    +{% endblock %} diff --git a/docs/source/_themes/dropwizard/static/dropwizard.css b/docs/source/_themes/dropwizard/static/dropwizard.css new file mode 100644 index 00000000000..0b3d58e6e2c --- /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 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;*margin-left:.3em;}.btn:first-child{*margin-left:0;} +.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.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0);border-color:#0044cc #0044cc #002a80;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-primary:hover,.btn-primary:active,.btn-primary.active,.btn-primary.disabled,.btn-primary[disabled]{background-color:#0044cc;} +.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.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);border-color:#f89406 #f89406 #ad6704;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-warning:hover,.btn-warning:active,.btn-warning.active,.btn-warning.disabled,.btn-warning[disabled]{background-color:#f89406;} +.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.gradient(startColorstr='#ee5f5b', endColorstr='#bd362f', GradientType=0);border-color:#bd362f #bd362f #802420;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-danger:hover,.btn-danger:active,.btn-danger.active,.btn-danger.disabled,.btn-danger[disabled]{background-color:#bd362f;} +.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.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-success:hover,.btn-success:active,.btn-success.active,.btn-success.disabled,.btn-success[disabled]{background-color:#51a351;} +.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.gradient(startColorstr='#5bc0de', endColorstr='#2f96b4', GradientType=0);border-color:#2f96b4 #2f96b4 #1f6377;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.btn-info:hover,.btn-info:active,.btn-info.active,.btn-info.disabled,.btn-info[disabled]{background-color:#2f96b4;} +.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:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);} +.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, #222222);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);border-color:#222222 #222222 #000000;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.1),0 1px 0 rgba(255, 255, 255, 0.075);}.btn-navbar:hover,.btn-navbar:active,.btn-navbar.active,.btn-navbar.disabled,.btn-navbar[disabled]{background-color:#222222;} +.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 0px rgba(255, 255, 255, 0.15);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.15);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.navbar-search .search-query :-moz-placeholder{color:#eeeeee;} +.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(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-repeat:no-repeat;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);cursor:pointer;*margin-left:.3em;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, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);padding:10px 10px 10px;font-size:16pt;}.hero-unit li a:first-child{*margin-left:0;} +.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);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.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.hero-unit li a:hover:hover,.hero-unit li a:hover:active,.hero-unit li a:hover.active,.hero-unit li a:hover.disabled,.hero-unit li a:hover[disabled]{background-color:#51a351;} +.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, #62c462, #51a351);background-image:linear-gradient(top, #62c462, #51a351);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.hero-unit li a:focus:hover,.hero-unit li a:focus:active,.hero-unit li a:focus.active,.hero-unit li a:focus.disabled,.hero-unit li a:focus[disabled]{background-color:#51a351;} +.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-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.gradient(startColorstr='#62c462', endColorstr='#51a351', GradientType=0);border-color:#51a351 #51a351 #387038;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);}.hero-unit li a:active:hover,.hero-unit li a:active:active,.hero-unit li a:active.active,.hero-unit li a:active.disabled,.hero-unit li a:active[disabled]{background-color:#51a351;} +.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 00000000000..52cf96275d8 --- /dev/null +++ b/docs/source/_themes/dropwizard/theme.conf @@ -0,0 +1,18 @@ +[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 +maven_site = https://dropwizard.github.io/dropwizard/ +mailing_list_user = https://groups.google.com/forum/#!forum/dropwizard-user +mailing_list_dev = https://groups.google.com/forum/#!forum/dropwizard-dev diff --git a/docs/source/about/contributors.rst b/docs/source/about/contributors.rst new file mode 100644 index 00000000000..f9af5260b7f --- /dev/null +++ b/docs/source/about/contributors.rst @@ -0,0 +1,284 @@ +.. _about-contributors: + +############ +Contributors +############ + +Dropwizard wouldn't exist without the hard work contributed by numerous individuals. + +Many, many thanks to: + +* `Aaron Ingram `_ +* `Adam Jordens `_ +* `Adam Jordens `_ +* `Adam Marcus `_ +* `Aidan `_ +* `akumlehn `_ +* `Alex Ausch `_ +* `Alex Butler `_ +* `Alex Heneveld `_ +* `Alice Chen `_ +* `Anders Hedström `_ +* `Andreas Petersson `_ +* `Andreas Stührk `_ +* `Andrei Savu `_ +* `Andrew Clay Shafer `_ +* `anikiej `_ +* `Antanas KonÄius `_ +* `Anthony Milbourne `_ +* `Arien Kock `_ +* `Armando Singer `_ +* `Artem Prigoda `_ +* `Arun Horne `_ +* `Athou `_ +* `Basil James Whitehouse III `_ +* `Ben Bader `_ +* `Ben Ripkens `_ +* `Ben Smith `_ +* `Benjamin Bentmann `_ +* `Bo Gotthardt `_ +* `Børge Nese `_ +* `Boyd Meier `_ +* `Bradley Schmidt `_ +* `Brandon Beck `_ +* `Brett Hoerner `_ +* `Brian McCallister `_ +* `Brian O'Neill `_ +* `Bruce Ritchie `_ +* `Burak Dede `_ +* `BusComp `_ +* `Børge Nese `_ +* `Cagatay Kavukcuoglu `_ +* `Cameron Fieber `_ +* `Camille Fournier `_ +* `Carl Lerche `_ +* `Carlo Barbara `_ +* `Cemalettin Koc `_ +* `Chad Selph `_ +* `Charlie Greenbacker `_ +* `Charlie La Mothe `_ +* `cheddar `_ +* `chena `_ +* `Chen Wang `_ +* `Chris Gray `_ +* `Chris Micali `_ +* `Chris Pimlott `_ +* `Chris Tierney `_ +* `Christoffer Eide `_ +* `Christoph Kutzinski `_ +* `Christopher Currie `_ +* `Christopher Elkins `_ +* `Christopher Gray `_ +* `Christopher Holmes `_ +* `Christopher Kingsbury `_ +* `Christoph Kutzinski `_ +* `Coda Hale `_ +* `Collin Van Dyck `_ +* `Csaba Palfi `_ +* `Dale Wijnand `_ +* `Damian Pawlowski `_ +* `Dan Everton `_ +* `Dan McWeeney `_ +* `Dang Nguyen Anh Khoa `_ +* `Daniel Temme `_ +* `Darren Yin `_ +* `David Illsley `_ +* `David Martin `_ +* `David Morgantini `_ +* `David Stendardi `_ +* `Dennis Hoersch `_ +* `Denny Abraham Cheriyan `_ +* `Deepu Mohan Puthrote `_ +* `Derek Cicerone `_ +* `Derek Stainer `_ +* `Devin Breen `_ +* `Devin Smith `_ +* `Dheerendra Rathor `_ +* `Dietrich Featherston `_ +* `Dimitris Zavaliadis `_ +* `Dmitry Minkovsky `_ +* `Dmitry Ustalov `_ +* `Dominic Tootell `_ +* `Drew Stephens `_ +* `Doug Roccato `_ +* `douzzi `_ +* `Dom Farr `_ +* `Dylan Scott `_ +* `eepstein `_ +* `eitan101 `_ +* `Ellis Pritchard `_ +* `Emeka Mosanya `_ +* `Eric Tschetter `_ +* `Evan Jones `_ +* `Evan Meagher `_ +* `Farid Zakaria `_ +* `Felix Braun `_ +* `FleaflickerLLC `_ +* `florinn `_ +* `Fredrik Sundberg `_ +* `Frode NerbrÃ¥ten `_ +* `Gabe Henkes `_ +* `Gary Dusbabek `_ +* `Glenn McAllister `_ +* `Graham O'Regan `_ +* `Greg Bowyer `_ +* `Gunnar Ahlberg `_ +* `Hal Hildebrand `_ +* `Henrik StrÃ¥th `_ +* `Hrvoje SlaviÄek `_ +* `HÃ¥kan Jonson `_ +* `Hrvoje SlaviÄek `_ +* `Ian Eure `_ +* `Ilias Bartolini `_ +* `Jacek Jackowiak `_ +* `Jake Swenson `_ +* `James Morris `_ +* `James Ward `_ +* `Jamie Furnaghan `_ +* `Jan Galinski `_ +* `Jan Olaf Krems `_ +* `Jan-Terje Sørensen `_ +* `Jared Stehler `_ +* `Jason Clawson `_ +* `Jason Dunkelberger `_ +* `Jason Toffaletti `_ +* `Javier Campanini `_ +* `Jeff Klukas `_ +* `Jerry-Carter `_ +* `Jesse Hodges `_ +* `Jilles Oldenbeuving `_ +* `Jochen Schalanda `_ +* `Joe Lauer `_ +* `Joe Schmetzer `_ +* `Johan Wirde (@jwirde) `_ +* `Jon Radon `_ +* `Jonathan Halterman `_ +* `Jonathan Ruckwood `_ +* `Jonathan Welzel `_ +* `Jon Radon `_ +* `Jordan Zimmerman `_ +* `Joshua Spiewak `_ +* `Julien `_ +* `Justin Miller `_ +* `Justin Plock `_ +* `Justin Rudd `_ +* `Kashyap Paidimarri `_ +* `Kerry Kimbrough `_ +* `Kilemensi `_ +* `Kirill Vlasov `_ +* `Konstantin Yegupov `_ +* `Kristian Klette `_ +* `Krzysztof Mejka `_ +* `kschjeld `_ +* `LeekAnarchism `_ +* `lehcim `_ +* `Lucas `_ +* `Lunfu Zhong `_ +* `mabuthraa `_ +* `maffe `_ +* `Malte S. Stretz `_ +* `Manabu Matsuzaki `_ +* `Marcin Biegan `_ +* `Marcus Höjvall `_ +* `Marius Volkhart `_ +* `Mark Reddy `_ +* `Mark Wolfe `_ +* `markez92 `_ +* `MÃ¥rten Gustafson `_ +* `Martin W. Kirst `_ +* `Matt Brown `_ +* `Matt Carrier `_ +* `Matt Hurne `_ +* `Matt Nelson `_ +* `Matt Thomson `_ +* `Matt Veitas `_ +* `Matt Whipple `_ +* `Matthew Clarke `_ +* `Max Wenzin `_ +* `Maximilien Marie `_ +* `Michael Chaten `_ +* `Michael Fairley `_ +* `Michael Kearns `_ +* `Michael McCarthy `_ +* `Michael Piefel `_ +* `Michal Rutkowski `_ +* `Mikael Amborn `_ +* `Mike Miller `_ +* `mnrasul `_ +* `Moritz Kammerer `_ +* `MÃ¥rten Gustafson `_ +* `natnan `_ +* `Nick Babcock `_ +* `Nick Telford `_ +* `Nikhil Bafna `_ +* `Nisarg Shah `_ +* `Oddmar Sandvik `_ +* `Oliver B. Fischer `_ +* `Oliver Charlesworth `_ +* `Olivier Abdesselam `_ +* `Ori Schwartz `_ +* `Otto Jongerius `_ +* `Owen Jacobson `_ +* `pandaadb `_ +* `Patrick Stegmann `_ +* `Patryk Najda `_ +* `Paul Samsotha `_ +* `Paul Tomlin `_ +* `Philip K. Warren `_ +* `Philip Potter `_ +* `Punyashloka Biswal `_ +* `Qinfeng Chen `_ +* `Quoc-Viet Nguyen `_ +* `Rachel Newstead `_ +* `rayokota `_ +* `Rémi Alvergnat `_ +* `Richard Kettelerij `_ +* `Richard Nyström `_ +* `Robert Barbey `_ +* `Rüdiger zu Dohna `_ +* `Ryan Berdeen `_ +* `Ryan Kennedy `_ +* `Saad Mufti `_ +* `Sam Perman `_ +* `Sam Quigley `_ +* `Scott Askew `_ +* `Scott D. `_ +* `Scott Horn `_ +* `Sean Scanlon `_ +* `Sebastian Hartte `_ +* `Simon Collins `_ +* `smolloy `_ +* `Sourav Mitra `_ +* `Stan Svec `_ +* `Stephen Huenneke `_ +* `Steve Agalloco `_ +* `Steve Hill `_ +* `Stevo Slavić `_ +* `Stuart Gunter `_ +* `Szymon Pacanowski `_ +* `Tatu Saloranta `_ +* `Ted Nyman `_ +* `Thiago Moretto `_ +* `Thomas Darimont `_ +* `Tim Bart `_ +* `Tom Akehurst `_ +* `Tom Crayford `_ +* `Tom Lee `_ +* `Tom Morris `_ +* `Tom Shen `_ +* `Tony Gaetani `_ +* `Tristan Burch `_ +* `Tyrone Cutajar `_ +* `Vadim Spivak `_ +* `Varun Loiwal `_ +* `Vasyl Vavrychuk `_ +* `Vidit Drolia `_ +* `Vitor Reis `_ +* `VojtÄ›ch Vondra `_ +* `vzx `_ +* `Wank Sinatra `_ +* `William Herbert `_ +* `Xavier Shay `_ +* `Xiaodong-Xie `_ +* `Yiwei Gao `_ +* `Yun Zhi Lin `_ diff --git a/docs/source/about/docs-index.rst b/docs/source/about/docs-index.rst new file mode 100644 index 00000000000..9756905847e --- /dev/null +++ b/docs/source/about/docs-index.rst @@ -0,0 +1,14 @@ +.. _docs-index: + +############## +Other Versions +############## + +- `0.9.0 `_ +- `0.8.4 `_ +- `0.8.2 `_ +- `0.8.1 `_ +- `0.8.0 `_ +- `0.7.1 `_ +- `0.6.2 `_ + diff --git a/docs/source/about/faq.rst b/docs/source/about/faq.rst new file mode 100644 index 00000000000..f6823cf6d56 --- /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 00000000000..b93b5c70c58 --- /dev/null +++ b/docs/source/about/index.rst @@ -0,0 +1,15 @@ +.. title:: About + +.. _about: + +################ +About Dropwizard +################ + +.. toctree:: + + contributors + faq + release-notes + security + todos diff --git a/docs/source/about/javadoc.rst b/docs/source/about/javadoc.rst new file mode 100644 index 00000000000..0c68391d546 --- /dev/null +++ b/docs/source/about/javadoc.rst @@ -0,0 +1,31 @@ +.. _javadoc: + +####### +Javadoc +####### + +- `dropwizard-auth <../../dropwizard-auth/apidocs/index.html>`_ +- `dropwizard-client <../../dropwizard-client/apidocs/index.html>`_ +- `dropwizard-configuration <../../dropwizard-configuration/apidocs/index.html>`_ +- `dropwizard-core <../../dropwizard-core/apidocs/index.html>`_ +- `dropwizard-db <../../dropwizard-db/apidocs/index.html>`_ +- `dropwizard-forms <../../dropwizard-forms/apidocs/index.html>`_ +- `dropwizard-hibernate <../../dropwizard-hibernate/apidocs/index.html>`_ +- `dropwizard-jackson <../../dropwizard-jackson/apidocs/index.html>`_ +- `dropwizard-jdbi <../../dropwizard-jdbi/apidocs/index.html>`_ +- `dropwizard-jersey <../../dropwizard-jersey/apidocs/index.html>`_ +- `dropwizard-jetty <../../dropwizard-jetty/apidocs/index.html>`_ +- `dropwizard-lifecycle <../../dropwizard-lifecycle/apidocs/index.html>`_ +- `dropwizard-logging <../../dropwizard-logging/apidocs/index.html>`_ +- `dropwizard-metrics <../../dropwizard-metrics/apidocs/index.html>`_ +- `dropwizard-metrics-ganglia <../../dropwizard-metrics-ganglia/apidocs/index.html>`_ +- `dropwizard-metrics-graphite <../../dropwizard-metrics-graphite/apidocs/index.html>`_ +- `dropwizard-migrations <../../dropwizard-migrations/apidocs/index.html>`_ +- `dropwizard-servlets <../../dropwizard-servlets/apidocs/index.html>`_ +- `dropwizard-spdy <../../dropwizard-spdy/apidocs/index.html>`_ +- `dropwizard-testing <../../dropwizard-testing/apidocs/index.html>`_ +- `dropwizard-util <../../dropwizard-util/apidocs/index.html>`_ +- `dropwizard-validation <../../dropwizard-validation/apidocs/index.html>`_ +- `dropwizard-views <../../dropwizard-views/apidocs/index.html>`_ +- `dropwizard-views-freemarker <../../dropwizard-views-freemarker/apidocs/index.html>`_ +- `dropwizard-views-mustache <../../dropwizard-views-mustache/apidocs/index.html>`_ \ No newline at end of file diff --git a/docs/source/about/release-notes.rst b/docs/source/about/release-notes.rst new file mode 100644 index 00000000000..7c1a6f8212b --- /dev/null +++ b/docs/source/about/release-notes.rst @@ -0,0 +1,756 @@ +.. _release-notes: + +############# +Release Notes +############# + +.. _rel-1.1.0: + +v1.1.0: Unreleased +================== + +* Remove OptionalValidatedValueUnwrapper `#1583 `_ +* Allow constraints to be applied to type `#1586 `_ +* Use LoadingCache in CachingAuthenticator `#1615 `_ +* Introduce CachingAuthorizer `#1639 `_ +* Upgraded to Jetty 9.3.11.v20160721 `#1649 `_ +* Upgraded to tomcat-jdbc 8.5.4 `#1654 `_ +* Upgraded to Objenesis 2.4 `#1654 `_ +* Upgraded to AssertJ 3.5.2 `#1654 `_ +* Upgraded to classmate 1.3.1 `#1654 `_ +* Upgraded to Mustache 0.9.3 `#1654 `_ + +.. _rel-1.0.0: + +v1.0.0: Jul 26 2016 +=================== + +* Using Java 8 as baseline +* ``dropwizard-java8`` bundle merged into mainline `#1365 `_ +* HTTP/2 and server push support `#1349 `_ +* ``dropwizard-spdy`` module is removed in favor of ``dropwizard-http2`` `#1330 `_ +* Switching to ``logback-access`` for HTTP request logging `#1415 `_ +* Support for validating return values in JAX-RS resources `#1251 `_ +* Consistent handling null entities in JAX-RS resources `#1251 `_ +* Support for validating bean members in JAX-RS resources `#1572 `_ +* Returning an HTTP 500 error for entities that can't be serialized `#1347 `_ +* Support serialisation of lazy loaded POJOs in Hibernate `#1466 `_ +* Support fallback to the ``toString`` method during deserializing enum values from JSON `#1340 `_ +* Support for setting default headers in Apache HTTP client `#1354 `_ +* Printing help once on invalid command line arguments `#1376 `_ +* Support for case insensitive and all single letter ``SizeUnit`` suffixes `#1380 `_ +* Added a development profile to the build `#1364 `_ +* All the default exception mappers in ``ResourceTestRule`` are registered by default `#1387 `_ +* Allow DB minSize and initialSize to be zero for lazy connections `#1517 `_ +* Ability to provide own ``RequestLogFactory`` `#1290 `_ +* Support for authentication by polymorphic principals `#1392 `_ +* Support for configuring Jetty's ``inheritedChannel`` option `#1410 `_ +* Support for using ``DropwizardAppRule`` at the suite level `#1411 `_ +* Support for adding multiple ``MigrationBundles`` `#1430 `_ +* Support for obtaining server context paths in the ``Application.run`` method `#1503 `_ +* Support for unlimited log files for file appender `#1549 `_ +* Support for log file names determined by logging policy `#1561 `_ +* Default Graphite reporter port changed from 8080 to 2003 `#1538 `_ +* Upgraded to Apache HTTP Client 4.5.2 +* Upgraded to argparse4j 0.7.0 +* Upgraded to Guava 19.0 +* Upgraded to H2 1.4.192 +* Upgraded to Hibernate 5.1.0 `#1429 `_ +* Upgraded to Hibernate Validator 5.2.4.Final +* Upgraded to HSQLDB 2.3.4 +* Upgraded to Jadira Usertype Core 5.0.0.GA +* Upgraded to Jackson 2.7.6 +* Upgraded to JDBI 2.73 `#1358 `_ +* Upgraded to Jersey 2.23.1 +* Upgraded to Jetty 9.3.9.v20160517 `#1330 `_ +* Upgraded to JMH 1.12 +* Upgraded to Joda-Time 2.9.4 +* Upgraded to Liquibase 3.5.1 +* Upgraded to liquibase-slf4j 2.0.0 +* Upgraded to Logback 1.1.7 +* Upgraded to Mustache 0.9.2 +* Upgraded to SLF4J 1.7.21 +* Upgraded to tomcat-jdbc 8.5.3 +* Upgraded to Objenesis 2.3 +* Upgraded to AssertJ 3.4.1 +* Upgraded to Mockito 2.0.54-beta + +.. _rel-0.9.2: + +v0.9.2: Jan 20 2016 +=================== + +* Support `@UnitOfWork` annotation outside of Jersey resources `#1361 `_ + +.. _rel-0.9.1: + +v0.9.1: Nov 3 2015 +================== + +* Add ``ConfigurationSourceProvider`` for reading resources from classpath `#1314 `_ +* Add ``@UnwrapValidatedValue`` annotation to `BaseReporterFactory.frequency` `#1308 `_, `#1309 `_ +* Fix serialization of default configuration for ``DataSourceFactory`` by deprecating ``PooledDataSourceFactory#getHealthCheckValidationQuery()`` and ``PooledDataSourceFactory#getHealthCheckValidationTimeout()`` `#1321 `_, `#1322 `_ +* Treat ``null`` values in JAX-RS resource method parameters of type ``Optional`` as absent value after conversion `#1323 `_ + +.. _rel-0.9.0: + +v0.9.0: Oct 28 2015 +=================== + +* Various documentation fixes and improvements +* New filter-based authorization & authentication `#952 `_, `#1023 `_, `#1114 `_, `#1162 `_, `#1241 `_ +* Fixed a security bug in ``CachingAuthenticator`` with caching results of failed authentication attempts `#1082 `_ +* Correct handling misconfigured context paths in ``ServerFactory`` `#785 `_ +* Logging context paths during application startup `#994 `_, `#1072 `_ +* Support for `Jersey Bean Validation `_ `#842 `_ +* Returning descriptive constraint violation messages `#1039 `_, +* Trace logging of failed constraint violations `#992 `_ +* Returning correct HTTP status codes for constraint violations `#993 `_ +* Fixed possible XSS in constraint violations `#892 `_ +* Support for including caller data in appenders `#995 `_ +* Support for defining custom logging factories (e.g. native Logback) `#996 `_ +* Support for defining the maximum log file size in ``FileAppenderFactory``. `#1000 `_ +* Support for fixed window rolling policy in ``FileAppenderFactory`` `#1218 `_ +* Support for individual logger appenders `#1092 `_ +* Support for disabling logger additivity `#1215 `_ +* Sorting endpoints in the application startup log `#1002 `_ +* Dynamic DNS resolution in the Graphite metric reporter `#1004 `_ +* Support for defining a custom ``MetricRegistry`` during bootstrap (e.g. with HdrHistogram) `#1015 `_ +* Support for defining a custom ``ObjectMapper`` during bootstrap. `#1112 `_ +* Added facility to plug-in custom DB connection pools (e.g. HikariCP) `#1030 `_ +* Support for setting a custom DB pool connection validator `#1113 `_ +* Support for enabling of removing abandoned DB pool connections `#1264 `_ +* Support for credentials in a DB data source URL `#1260 `_ +* Support for simultaneous work of several Hibernate bundles `#1276 `_ +* HTTP(S) proxy support for Dropwizard HTTP client `#657 `_ +* Support external configuration of TLS properties for Dropwizard HTTP client `#1224 `_ +* Support for not accepting GZIP-compressed responses in HTTP clients `#1270 `_ +* Support for setting a custom redirect strategy in HTTP clients `#1281 `_ +* Apache and Jersey clients are now managed by the application environment `#1061 `_ +* Support for request-scoped configuration for Jersey client `#939 `_ +* Respecting Jackson feature for deserializing enums using ``toString`` `#1104 `_ +* Support for passing explicit ``Configuration`` via test rules `#1131 `_ +* On view template error, return a generic error page instead of template not found `#1178 `_ +* In some cases an instance of Jersey HTTP client could be abruptly closed during the application lifetime `#1232 `_ +* Improved build time build by running tests in parallel `#1032 `_ +* Added JMH benchmarks `#990 `_ +* Allow customization of Hibernate ``SessionFactory`` `#1182 `_ +* Removed javax.el-2.x in favour of javax.el-3.0 +* Upgraded to argparse4j 0.6.0 +* Upgrade to AssertJ 2.2.0 +* Upgraded to JDBI 2.63.1 +* Upgraded to Apache HTTP Client 4.5.1 +* Upgraded to Dropwizard Metrics 3.1.2 +* Upgraded to Freemarker 2.3.23 +* Upgraded to H2 1.4.190 +* Upgraded to Hibernate 4.3.11.Final +* Upgraded to Jackson 2.6.3 +* Upgraded to Jadira Usertype Core 4.0.0.GA +* Upgraded to Jersey 2.22.1 +* Upgraded to Jetty 9.2.13.v20150730 +* Upgraded to Joda-Time 2.9 +* Upgraded to JSR305 annotations 3.0.1 +* Upgraded to Hibernate Validator 5.2.2.Final +* Upgraded to Jetty ALPN boot 7.1.3.v20150130 +* Upgraded to Jetty SetUID support 1.0.3 +* Upgraded to Liquibase 3.4.1 +* Upgraded to Logback 1.1.3 +* Upgraded to Metrics 3.1.2 +* Upgraded to Mockito 1.10.19 +* Upgraded to SLF4J 1.7.12 +* Upgraded to commons-lang3 3.4 +* Upgraded to tomcat-jdbc 8.0.28 + +.. _rel-0.8.5: + +v0.8.5: Nov 3 2015 +================== + +* Treat ``null`` values in JAX-RS resource method parameters of type ``Optional`` as absent value after conversion `#1323 `_ + +.. _rel-0.8.4: + +v0.8.4: Aug 26 2015 +=================== + +* Upgrade to Apache HTTP Client 4.5 +* Upgrade to Jersey 2.21 +* Fixed user-agent shadowing in Jersey HTTP Client `#1198 `_ + +.. _rel-0.8.3: + +v0.8.3: Aug 24 2015 +=================== +* Fixed an issue with closing the HTTP client connection pool after a full GC `#1160 `_ + +.. _rel-0.8.2: + +v0.8.2: Jul 6 2015 +================== + +* Support for request-scoped configuration for Jersey client `#1137 `_ +* Upgraded to Jersey 2.19 `#1143 `_ + +.. _rel-0.8.1: + +v0.8.1: Apr 7 2015 +================== + +* Fixed transaction committing lifecycle for ``@UnitOfWork`` (#850, #915) +* Fixed noisy Logback messages on startup (#902) +* Ability to use providers in TestRule, allows testing of auth & views (#513, #922) +* Custom ExceptionMapper not invoked when Hibernate rollback (#949) +* Support for setting a time bound on DBI and Hibernate health checks +* Default configuration for views +* Ensure that JerseyRequest scoped ClientConfig gets propagated to HttpUriRequest +* More example tests +* Fixed security issue where info is leaked during validation of unauthenticated resources(#768) + +.. _rel-0.8.0: + +v0.8.0: Mar 5 2015 +================== + +* Migrated ``dropwizard-spdy`` from NPN to ALPN +* Dropped support for deprecated SPDY/2 in ``dropwizard-spdy`` +* Upgrade to argparse4j 0.4.4 +* Upgrade to commons-lang3 3.3.2 +* Upgrade to Guava 18.0 +* Upgrade to H2 1.4.185 +* Upgrade to Hibernate 4.3.5.Final +* Upgrade to Hibernate Validator 5.1.3.Final +* Upgrade to Jackson 2.5.1 +* Upgrade to JDBI 2.59 +* Upgrade to Jersey 2.16 +* Upgrade to Jetty 9.2.9.v20150224 +* Upgrade to Joda-Time 2.7 +* Upgrade to Liquibase 3.3.2 +* Upgrade to Mustache 0.8.16 +* Upgrade to SLF4J 1.7.10 +* Upgrade to tomcat-jdbc 8.0.18 +* Upgrade to JSR305 annotations 3.0.0 +* Upgrade to Junit 4.12 +* Upgrade to AssertJ 1.7.1 +* Upgrade to Mockito 1.10.17 +* Support for range headers +* Ability to use Apache client configuration for Jersey client +* Warning when maximum pool size and unbounded queues are combined +* Fixed connection leak in CloseableLiquibase +* Support ScheduledExecutorService with daemon thread +* Improved DropwizardAppRule +* Better connection pool metrics +* Removed final modifier from Application#run +* Fixed gzip encoding to support Jersey 2.x +* Configuration to toggle regex [in/ex]clusion for Metrics +* Configuration to disable default exception mappers +* Configuration support for disabling chunked encoding +* Documentation fixes and upgrades + + +.. _rel-0.7.1: + +v0.7.1: Jun 18 2014 +=================== + +* 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 configurable 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 connectors 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 permanently 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 4.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 ``startsAsRoot`` 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`` 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)`` **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 `_. + 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`` 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 `_. + +.. _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/security.rst b/docs/source/about/security.rst new file mode 100644 index 00000000000..9121a96144e --- /dev/null +++ b/docs/source/about/security.rst @@ -0,0 +1,7 @@ +.. _security: + +######## +Security +######## + +No known issues exist diff --git a/docs/source/about/todos.rst b/docs/source/about/todos.rst new file mode 100644 index 00000000000..f38ac427228 --- /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 00000000000..f09c750aca1 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,302 @@ +# -*- 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., 2014-2016 Dropwizard Team' + +# 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 = '@parsedVersion.majorVersion@.@parsedVersion.minorVersion@' +# The full version, including alpha/beta/rc tags. +release = '@project.version@' + +# 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', + 'maven_site': u'https://dropwizard.github.io/dropwizard/' + release, + 'mailing_list_user': u'https://groups.google.com/forum/#!forum/dropwizard-user', + 'mailing_list_dev': u'https://groups.google.com/forum/#!forum/dropwizard-dev' +} + +# 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 +# " v 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 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 00000000000..157f688b685 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 00000000000..3d948660301 --- /dev/null +++ b/docs/source/getting-started.rst @@ -0,0 +1,822 @@ +.. _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-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.dropwizard.io/ + +.. _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 fewer 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 349`_ reference implementation, provides an easy, declarative + framework for validating user input and generating helpful and 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 straightforward 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: https://github.com/google/guava +.. _Logback: http://logback.qos.ch/ +.. _slf4j: http://www.slf4j.org/ +.. _Hibernate Validator: http://www.hibernate.org/subprojects/validator.html +.. _JSR 349: http://jcp.org/en/jsr/detail?id=349 +.. _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 Using 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. + +.. _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: https://github.com/Gant/Gant +.. _Leiningen: https://github.com/technomancy/leiningen +.. __: https://books.sonatype.com/mvnref-book/reference/ + + +You have three alternatives from here: + +1. Create a project using dropwizard-archetype_ + + mvn archetype:generate -DarchetypeGroupId=io.dropwizard.archetypes -DarchetypeArtifactId=java-simple -DarchetypeVersion=1.0.0 + +2. Look at the dropwizard-example_ + +3. Follow the tutorial below to see how you can include it in your existing project + +.. _dropwizard-archetype: https://github.com/dropwizard/dropwizard/tree/master/dropwizard-archetypes +.. _dropwizard-example: https://github.com/dropwizard/dropwizard/tree/master/dropwizard-example + +Tutorial +-------- + +First, add a ``dropwizard.version`` property to your POM with the current version of Dropwizard +(which is |release|): + +.. code-block:: xml + + + INSERT VERSION HERE + + +Add the ``dropwizard-core`` library as a dependency: + +.. _gs-pom-dependencies: + +.. code-block:: xml + + + + io.dropwizard + dropwizard-core + ${dropwizard.version} + + + +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 look 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 { + public static void main(String[] args) throws Exception { + new HelloWorldApplication().run(args); + } + + @Override + public String getName() { + return "hello-world"; + } + + @Override + public void initialize(Bootstrap 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.api; + + 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 JavaBeans 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 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 looks like this: + +.. code-block:: java + + package com.example.helloworld.resources; + + import com.example.helloworld.api.Saying; + 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; + import java.util.Optional; + + @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 name) { + final String value = String.format(template, name.orElse(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)`` 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.) + +.. note:: + + If the client sends a request to ``/hello-world?name=``, ``sayHello`` will be called with + ``Optional.of("")``. This may seem odd at first, but this follows the standards (an application + may have different behavior depending on if a parameter is empty vs nonexistent). You can swap + ``Optional`` parameter with ``NonEmptyStringParam`` if you want ``/hello-world?name=`` + to return "Hello, Stranger!" For more information on resource parameters see + :ref:`the documentation ` + +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 ```` 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 + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + + + com.example.helloworld.HelloWorldApplication + + + + + + + +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. + +.. warning:: + + Since Dropwizard is using the Java `ServiceLoader`_ functionality to register and load extensions, + the `minimizeJar`_ option of the `maven-shade-plugin` will lead to non-working application JARs. + +.. _`JCA/JCE`: http://docs.oracle.com/javase/7/docs/technotes/guides/security/crypto/CryptoSpec.html +.. _`exclusion`: http://maven.apache.org/plugins/maven-shade-plugin/examples/includes-excludes.html +.. _`minimizeJar`: https://maven.apache.org/plugins/maven-shade-plugin/shade-mojo.html#minimizeJar +.. _`ServiceLoader`: http://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.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 +```` section of your ``pom.xml`` file: + +.. code-block:: xml + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 + + + + true + + + + + +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 `:: + + 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@0.0.0.0:8080 STARTING + INFO [2011-12-03 00:38:33,238] org.eclipse.jetty.server.AbstractConnector: Started SocketConnector@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. + +However, while it's up, let's give it a whirl! +`Click here to say hello! `_ +`Click here to get even friendlier! `_ + +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 `_. + +If you click through to the `metrics resource `_, you can see all of +your application's metrics represented as a JSON object. + +The `threads resource `_ 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 `_ runs the +:ref:`health check class we wrote `. 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 `. diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 00000000000..07a8f37cc8f --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,29 @@ +.. title:: Home + +.. raw:: html + +
    + +################################################################################################### +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/javadoc.rst + about/index + about/docs-index.rst + +.. raw:: html + +
    diff --git a/docs/source/manual/auth.rst b/docs/source/manual/auth.rst new file mode 100644 index 00000000000..d82e94ebbf8 --- /dev/null +++ b/docs/source/manual/auth.rst @@ -0,0 +1,385 @@ +.. _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`` interface, which has a single method: + +.. code-block:: java + + public class ExampleAuthenticator implements Authenticator { + @Override + public Optional 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 ` 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 not to 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 + + SimpleAuthenticator simpleAuthenticator = new SimpleAuthenticator(); + CachingAuthenticator cachingAuthenticator = new CachingAuthenticator<>( + metricRegistry, simpleAuthenticator, + 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-authorizer: + +Authorizer +========== + +An authorizer is a strategy class which, given a principal and a role, decides if access is granted to the +principal. + +The authorizer implements the ``Authorizer

    `` interface, which has a single method: + +.. code-block:: java + + public class ExampleAuthorizer implements Authorizer { + @Override + public boolean authorize(User user, String role) { + return user.getName().equals("good-guy") && role.equals("ADMIN"); + } + } + +.. _man-auth-basic: + +Basic Authentication +==================== + +The ``AuthDynamicFeature`` with the ``BasicCredentialAuthFilter`` and ``RolesAllowedDynamicFeature`` +enables HTTP Basic authentication and authorization; requires an authenticator which +takes instances of ``BasicCredentials``. If you don't use authorization, then ``RolesAllowedDynamicFeature`` +is not required. + +.. code-block:: java + + @Override + public void run(ExampleConfiguration configuration, + Environment environment) { + environment.jersey().register(new AuthDynamicFeature( + new BasicCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setRealm("SUPER SECRET STUFF") + .buildAuthFilter())); + environment.jersey().register(RolesAllowedDynamicFeature.class); + //If you want to use @Auth to inject a custom Principal type into your resource + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class)); + } + +.. _man-auth-oauth2: + +OAuth2 +====== + +The ``AuthDynamicFeature`` with ``OAuthCredentialAuthFilter`` and ``RolesAllowedDynamicFeature`` +enables OAuth2 bearer-token authentication and authorization; requires an authenticator which +takes instances of ``String``. If you don't use authorization, then ``RolesAllowedDynamicFeature`` +is not required. + +.. code-block:: java + + @Override + public void run(ExampleConfiguration configuration, + Environment environment) { + environment.jersey().register(new AuthDynamicFeature( + new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleOAuthAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setPrefix("Bearer") + .buildAuthFilter())); + environment.jersey().register(RolesAllowedDynamicFeature.class); + //If you want to use @Auth to inject a custom Principal type into your resource + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class)); + } + +.. _man-auth-chained: + +Chained Factories +================= + +The ``ChainedAuthFilter`` enables usage of various authentication factories at the same time. + +.. code-block:: java + + @Override + public void run(ExampleConfiguration configuration, + Environment environment) { + AuthFilter basicCredentialAuthFilter = new BasicCredentialAuthFilter.Builder<>() + .setAuthenticator(new ExampleBasicAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setPrefix("Basic") + .buildAuthFilter(); + + AuthFilter oauthCredentialAuthFilter = new OAuthCredentialAuthFilter.Builder<>() + .setAuthenticator(new ExampleOAuthAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setPrefix("Bearer") + .buildAuthFilter(); + + List filters = Lists.newArrayList(basicCredentialAuthFilter, oauthCredentialAuthFilter); + environment.jersey().register(new AuthDynamicFeature(new ChainedAuthFilter(filters))); + environment.jersey().register(RolesAllowedDynamicFeature.class); + //If you want to use @Auth to inject a custom Principal type into your resource + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class)); + } + +For this to work properly, all chained factories must produce the same type of principal, here ``User``. + + +.. _man-auth-resources: + +Protecting Resources +==================== + +There are two ways to protect a resource. You can mark your resource method with one of the following annotations: + +* ``@PermitAll``. All authenticated users will have access to the method. +* ``@RolesAllowed``. Access will be granted to the users with the specified roles. +* ``@DenyAll``. No access will be granted to anyone. + +.. note:: + You can use ``@RolesAllowed``,``@PermitAll`` on the class level. Method annotations take precedence over the class ones. + +Alternatively, you can annotate the parameter representing your principal with ``@Auth``. Note you must register a +jersey provider to make this work. + +.. code-block:: java + + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class)); + + @RolesAllowed("ADMIN") + @GET + public SecretPlan getSecretPlan(@Auth User user) { + return dao.findPlanForUser(user); + } + +You can also access the Principal by adding a parameter to your method ``@Context SecurityContext context``. Note this +will not automatically register the servlet filter which performs authentication. You will still need to add one of +``@PermitAll``, ``@RolesAllowed``, or ``@DenyAll``. This is not the case with ``@Auth``. When that is present, the auth +filter is automatically registered to facilitate users upgrading from older versions of Dropwizard + +.. code-block:: java + + @RolesAllowed("ADMIN") + @GET + public SecretPlan getSecretPlan(@Context SecurityContext context) { + User userPrincipal = (User) context.getUserPrincipal(); + 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), you need to implement a custom filter which injects a security context +containing the principal if it exists, without performing authentication. + +Testing Protected Resources +=========================== + +Add this dependency into your ``pom.xml`` file: + +.. code-block:: xml + + + + io.dropwizard + dropwizard-testing + ${dropwizard.version} + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${jersey.version} + + + javax.servlet + javax.servlet-api + + + junit + junit + + + + + +When you build your ``ResourceTestRule``, add the ``GrizzlyWebTestContainerFactory`` line. + +.. code-block:: java + + @Rule + public ResourceTestRule rule = ResourceTestRule + .builder() + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addProvider(new AuthDynamicFeature(new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(new MyOAuthAuthenticator()) + .setAuthorizer(new MyAuthorizer()) + .setRealm("SUPER SECRET STUFF") + .setPrefix("Bearer") + .buildAuthFilter())) + .addProvider(RolesAllowedDynamicFeature.class) + .addProvider(new AuthValueFactoryProvider.Binder<>(User.class)) + .addResource(new ProtectedResource()) + .build(); + + +In this example, we are testing the oauth authentication, so we need to set the header manually. Note the use of ``resources.getJerseyTest()`` to make the test work + +.. code-block:: java + + @Test + public void testProtected() throws Exception { + final Response response = rule.getJerseyTest().target("/protected") + .request(MediaType.APPLICATION_JSON_TYPE) + .header("Authorization", "Bearer TOKEN") + .get(); + + assertThat(response.getStatus()).isEqualTo(200); + } + +Multiple Principals and Authenticators +====================================== + +In some cases you may want to use different authenticators/authentication schemes for different +resources. For example you may want Basic authentication for one resource and OAuth +for another resource, at the same time using a different `Principal` for each +authentication scheme. + +For this use case, there is the ``PolymorphicAuthDynamicFeature`` and the +``PolymorphicAuthValueFactoryProvider``. With these two components, we can use different +combinations of authentication schemes/authenticators/authorizers/principals. To use this +feature, we need to do a few things: + +* Register the ``PolymorphicAuthDynamicFeature`` with a map that maps principal types to + authentication filters. + +* Register the ``PolymorphicAuthValueFactoryProvider`` with a set of principal classes + that you will be using. + +* Annotate your resource method ``Principal`` parameters with ``@Auth``. + +As an example, the following code configures both OAuth and Basic authentication, using +a different principal for each. + +.. code-block:: java + + final AuthFilter basicFilter + = new BasicCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleAuthenticator()) + .setRealm("SUPER SECRET STUFF") + .buildAuthFilter()); + final AuthFilter oauthFilter + = new OAuthCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleOAuthAuthenticator()) + .setPrefix("Bearer") + .buildAuthFilter()); + + final PolymorphicAuthDynamicFeature feature = new PolymorphicAuthDynamicFeature<>( + ImmutableMap.of( + BasicPrincipal.class, basicFilter, + OAuthPrincipal.class, oauthFilter)); + final AbstractBinder binder = new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(BasicPrincipal.class, OAuthPrincipal.class)); + + environment.jersey().register(feature); + environment.jersey().register(binder); + +Now we are able to do something like the following + +.. code-block:: java + + @GET + public Response basicAuthResource(@Auth BasicPrincipal principal) {} + + @GET + public Response oauthResource(@Auth OAuthPrincipal principal) {} + +The first resource method will use Basic authentication while the second one will use OAuth. + +Note that with the above example, only *authentication* is configured. If you also want +*authorization*, the following steps will need to be taken. + +* Register the ``RolesAllowedDynamicFeature`` with the application. + +* Make sure you add ``Authorizers`` when you build your ``AuthFilters``. + +* Annotate the resource *method* with the authorization annotation. Unlike the note earlier in + this document that says authorization annotations are allowed on classes, with this + poly feature, currently that is not supported. The annotation MUST go on the resource *method* + +So continuing with the previous example you should add the following configurations + +.. code-block:: java + + ... = new BasicCredentialAuthFilter.Builder() + .setAuthorizer(new ExampleAuthorizer()).. // set authorizer + + ... = new OAuthCredentialAuthFilter.Builder() + .setAuthorizer(new ExampleAuthorizer()).. // set authorizer + + environment.jersey().register(RolesAllowedDynamicFeature.class); + +Now we can do + +.. code-block:: java + + @GET + @RolesAllowed({ "ADMIN" }) + public Response baseAuthResource(@Auth BasicPrincipal principal) {} + + @GET + @RolesAllowed({ "ADMIN" }) + public Response oauthResource(@Auth OAuthPrincipal principal) {} + +.. note:: + The polymorphic auth feature *SHOULD NOT* be used with any other ``AuthDynamicFeature``. Doing so may have undesired effects. + + + diff --git a/docs/source/manual/client.rst b/docs/source/manual/client.rst new file mode 100644 index 00000000000..2282cecfe78 --- /dev/null +++ b/docs/source/manual/client.rst @@ -0,0 +1,181 @@ +.. _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 +================= + +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 `, instrumented ``HttpClient`` instance, your +:ref:`configuration class ` needs an :ref:`http client configuration ` instance: + +.. code-block:: java + + public class ExampleConfiguration extends Configuration { + @Valid + @NotNull + private HttpClientConfiguration httpClient = new HttpClientConfiguration(); + + @JsonProperty("httpClient") + public HttpClientConfiguration getHttpClientConfiguration() { + return httpClient; + } + + @JsonProperty("httpClient") + public void setHttpClientConfiguration(HttpClientConfiguration httpClient) { + this.httpClient = 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.jersey().register(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 execute 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 +============= + +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/2.22.1/client.html + +To create a :ref:`managed `, instrumented ``JerseyClient`` instance, your +:ref:`configuration class ` needs an :ref:`jersey client configuration ` instance: + +.. code-block:: java + + public class ExampleConfiguration extends Configuration { + @Valid + @NotNull + private JerseyClientConfiguration jerseyClient = new JerseyClientConfiguration(); + + @JsonProperty("jerseyClient") + public JerseyClientConfiguration getJerseyClientConfiguration() { + return jerseyClient; + } + } + +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.jersey().register(new ExternalServiceResource(client)); + } + +Configuration +------------- + +The Client that Dropwizard creates deviates from the `Jersey Client Configuration` defaults. The +default, in Jersey, is for a client to never timeout reading or connecting in a request, while in +Dropwizard, the default is 500 milliseconds. + +There are a couple of ways to change this behavior. The recommended way is to modify the +:ref:`YAML configuration `. Alternatively, set the properties on +the ``JerseyClientConfiguration``, which will take effect for all built clients. On a per client +basis, the configuration can be changed by utilizing the ``property`` method and, in this case, +the `Jersey Client Properties`_ can be used. + +.. warning:: + + Do not try to change Jersey properties using `Jersey Client Properties`_ through the + + ``withProperty(String propertyName, Object propertyValue)`` + + method on the ``JerseyClientBuilder``, because by default it's configured by Dropwizard's + ``HttpClientBuilder``, so the Jersey properties are ignored. + +.. _Jersey Client Configuration: https://jersey.java.net/documentation/latest/appendix-properties.html#appendix-properties-client +.. _Jersey Client Properties: https://jersey.java.net/apidocs/2.22/jersey/org/glassfish/jersey/client/ClientProperties.html diff --git a/docs/source/manual/configuration.rst b/docs/source/manual/configuration.rst new file mode 100644 index 00000000000..8958fd545bd --- /dev/null +++ b/docs/source/manual/configuration.rst @@ -0,0 +1,1280 @@ +.. _man-configuration: + +################################## +Dropwizard Configuration Reference +################################## + +.. highlight:: text + +.. _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. +rootPath ``/*`` The URL pattern relative to ``applicationContextPath`` from which + the JAX-RS resources will be served. +registerDefaultExceptionMappers true Whether or not the default Jersey ExceptionMappers should be registered. + Set this to false if you want to register your own. +=================================== =============================================== ============================================================================= + + +.. _man-configuration-gzip: + +GZip +.... + +.. code-block:: yaml + + server: + gzip: + bufferSize: 8KiB + + ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| Name | Default | Description | ++===========================+=====================+======================================================================================================+ +| enabled | true | If true, all requests with ``gzip`` or ``deflate`` in the ``Accept-Encoding`` header will have their | +| | | response entities compressed and requests with ``gzip`` or ``deflate`` in the ``Content-Encoding`` | +| | | header will have their request entities decompressed. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| minimumEntitySize | 256 bytes | All response entities under this size are not compressed. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| bufferSize | 8KiB | The size of the buffer to use when compressing. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| excludedUserAgentPatterns | [] | The set of user agent patterns to exclude from compression. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| compressedMimeTypes | Jetty's default | The list of mime types to compress. The default is all types apart | +| | | the commonly known image, video, audio and compressed types. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| includedMethods | Jetty's default | The list list of HTTP methods to compress. The default is to compress only GET responses. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| deflateCompressionLevel | -1 | The compression level used for ZLIB deflation(compression). | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| gzipCompatibleInflation | true | If true, then ZLIB inflation(decompression) will be performed in the GZIP-compatible mode. | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ +| syncFlush | false | The flush mode. Set to true if the application wishes to stream (e.g. SSE) the data, | +| | | but this may hurt compression performance (as all pending output is flushed). | ++---------------------------+---------------------+------------------------------------------------------------------------------------------------------+ + +.. _man-configuration-requestLog: + +Request Log +........... + +.. code-block:: yaml + + server: + requestLog: + appenders: + - type: console + timeZone: UTC + + +====================== ================ =========== +Name Default Description +====================== ================ =========== +appenders console appender The set of AppenderFactory appenders to which requests will be logged. + See :ref:`logging ` for more info. +====================== ================ =========== + +.. _man-configuration-server-push: + +Server Push +........... + +Server push technology allows a server to send additional resources to a client along with the requested resource. +It works only for HTTP/2 connections. + +.. code-block:: yaml + + server: + serverPush: + enabled: true + associatePeriod: '4 seconds' + maxAssociations: 16 + refererHosts: ['dropwizard.io', 'dropwizard.github.io'] + refererPorts: [8444, 8445] + + ++-----------------+------------+------------------------------------------------------------------------------------------------------+ +| Name | Default | Description | ++=================+============+======================================================================================================+ +| enabled | false | If true, the filter will organize resources as primary resources (those referenced by the | +| | | ``Referer`` header) and secondary resources (those that have the ``Referer`` header). Secondary | +| | | resources that have been requested within a time window from the request of the primary resource | +| | | will be associated with the it. The next time a client will request the primary resource, the | +| | | server will send to the client the secondary resources along with the primary in a single response. | ++-----------------+------------+------------------------------------------------------------------------------------------------------+ +| associatePeriod | 4 seconds | The time window within which a request for a secondary resource will be associated to a | +| | | primary resource.. | ++-----------------+------------+------------------------------------------------------------------------------------------------------+ +| maxAssociations | 16 | The maximum number of secondary resources that may be associated to a primary resource. | ++-----------------+------------+------------------------------------------------------------------------------------------------------+ +| refererHosts | All hosts | The list of referrer hosts for which the server push technology is supported. | ++-----------------+------------+------------------------------------------------------------------------------------------------------+ +| refererPorts | All ports | The list of referrer ports for which the server push technology is supported | ++-----------------+------------+------------------------------------------------------------------------------------------------------+ + + +.. _man-configuration-simple: + +Simple +------ + +Extends the attributes that are available to :ref:`all servers ` + +.. 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 ` + +.. code-block:: yaml + + server: + adminMinThreads: 1 + adminMaxThreads: 64 + adminContextPath: / + applicationContextPath: / + 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 ` 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 ` 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. +adminContextPath / The context path of the admin servlets, including metrics and tasks. +applicationContextPath / The context path of the application servlets, including Jersey. +======================== ======================= ===================================================================== + +.. _`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 + inheritChannel: false + 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. +inheritChannel false Whether this connector uses a channel inherited from the JVM. + Use it with `Server::Starter`_, to launch an instance of Jetty on demand. +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 + `ForwardedRequestCustomizer`_ for details. +======================== ================== ====================================================================================== + +.. _`java.net.Socket#setSoTimeout(int)`: http://docs.oracle.com/javase/7/docs/api/java/net/Socket.html#setSoTimeout(int) +.. _`ForwardedRequestCustomizer`: http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/server/ForwardedRequestCustomizer.html + +.. _`Server::Starter`: https://github.com/kazuho/p5-Server-Starter + +.. _man-configuration-https: + +HTTPS +----- + +Extends the attributes that are available to the :ref:`HTTP connector ` + +.. 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: + crlPath: /path/to/file + enableCRLDP: false + enableOCSP: false + maxCertPathLength: (unlimited) + ocspResponderUrl: (none) + jceProvider: (none) + validateCerts: true + validatePeers: true + supportedProtocols: [SSLv3] + excludedProtocols: (none) + supportedCipherSuites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256] + excludedCipherSuites: (none) + 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. +excludedProtocols (none) A list of protocols (e.g., ``SSLv3``, ``TLSv1``) which are excluded. These + 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 ``excludedCipherSuites``, 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/nabla-c0d3/sslyze + +.. _man-configuration-http2: + +HTTP/2 over TLS +--------------- + +HTTP/2 is a new protocol, intended as a successor of HTTP/1.1. It adds several important features +like binary structure, stream multiplexing over a single connection, header compression, and server push. +At the same time it remains semantically compatible with HTTP/1.1, which should make the upgrade process more +seamless. Checkout HTTP/2 FAQ__ for the further information. + +.. __: https://http2.github.io/faq/ + +For an encrypted connection HTTP/2 uses ALPN protocol. It's a TLS extension, that allows a client to negotiate +a protocol to use after the handshake is complete. If either side does not support ALPN, then the protocol will +be ignored, and an HTTP/1.1 connection over TLS will be used instead. + +For this connector to work with ALPN protocol you need to provide alpn-boot library to JVM's bootpath. +The correct library version depends on a JVM version. Consult Jetty ALPN guide__ for the reference. + +.. __: http://www.eclipse.org/jetty/documentation/current/alpn-chapter.html + +Note that your JVM also must provide ``TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256`` cipher. The specification states__ +that HTTP/2 deployments must support it to avoid handshake failures. It's the single supported cipher in HTTP/2 +connector by default. + +.. __: http://http2.github.io/http2-spec/index.html#rfc.section.9.2.2 + +This connector extends the attributes that are available to the :ref:`HTTPS connector ` + +.. code-block:: yaml + + server: + applicationConnectors: + - type: h2 + port: 8445 + maxConcurrentStreams: 1024 + initialStreamRecvWindow: 65535 + keyStorePath: /path/to/file # required + keyStorePassword: changeit + trustStorePath: /path/to/file # required + trustStorePassword: changeit + + +======================== ======== =================================================================================== +Name Default Description +======================== ======== =================================================================================== +maxConcurrentStreams 1024 The maximum number of concurrently open streams allowed on a single HTTP/2 + connection. Larger values increase parallelism, but cost a memory commitment. +initialStreamRecvWindow 65535 The initial flow control window size for a new stream. Larger values may allow + greater throughput, but also risk head of line blocking if TCP/IP flow control is + triggered. +======================== ======== =================================================================================== + +.. _man-configuration-http2c: + +HTTP/2 Plain Text +----------------- + +HTTP/2 promotes using encryption, but doesn't require it. However, most browsers stated that they will +not support HTTP/2 without encryption. Currently no browser supports HTTP/2 unencrypted. + +The connector should only be used in closed secured networks or during development. It expects from clients +an HTTP/1.1 OPTIONS request with ``Upgrade : h2c`` header to indicate a wish to upgrade to HTTP/2, or a request with +the HTTP/2 connection preface. If the client doesn't support HTTP/2, a plain HTTP/1.1 connections will be used instead. + +This connector extends the attributes that are available to the :ref:`HTTP connector ` + +.. code-block:: yaml + + server: + applicationConnectors: + - type: h2c + port: 8446 + maxConcurrentStreams: 1024 + initialStreamRecvWindow: 65535 + + +======================== ======== =================================================================================== +Name Default Description +======================== ======== =================================================================================== +maxConcurrentStreams 1024 The maximum number of concurrently open streams allowed on a single HTTP/2 + connection. Larger values increase parallelism, but cost a memory commitment. +initialStreamRecvWindow 65535 The initial flow control window size for a new stream. Larger values may allow + greater throughput, but also risk head of line blocking if TCP/IP flow control is + triggered. +======================== ======== =================================================================================== + + +.. _man-configuration-logging: + +Logging +======= + +.. code-block:: yaml + + logging: + level: INFO + loggers: + "io.dropwizard": INFO + "org.hibernate.SQL": + level: DEBUG + additive: false + appenders: + - type: file + currentLogFilename: /var/log/myapplication-sql.log + archivedLogFilenamePattern: /var/log/myapplication-sql-%d.log.gz + archivedFileCount: 5 + appenders: + - type: console + + +====================== =========== =========== +Name Default Description +====================== =========== =========== +level Level.INFO Logback logging level. +additive true Logback additive setting. +loggers (none) Individual logger configuration (both forms are acceptable). +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 + filterFactories: + - type: URI + + +====================== =========== =========== +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. +filterFactories (none) The list of filters to apply to the appender, in order, after + the thresold. +====================== =========== =========== + +.. _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 + filterFactories: + - type: URI + + +============================ =========== ================================================================================================== +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. + If ``maxFileSize`` is specified, rollover is size-based, and the pattern must contain ``%i`` for + an integer index of the archived file. + Otherwise rollover is date-based, and the pattern must contain ``%d``, which is replaced with the + date in ``yyyy-MM-dd`` form. + If the pattern ends with ``.gz`` or ``.zip``, files will be compressed as they are archived. +archivedFileCount 5 The number of archived files to keep. Must be greater than or equal to ``0``. Zero is a + special value signifying to keep infinite logs (use with caution) +maxFileSize (unlimited) The maximum size of the currently active file before a rollover is triggered. The value can be + expressed in bytes, kilobytes, megabytes, gigabytes, and terabytes by appending B, K, MB, GB, or + TB to the numeric value. Examples include 100MB, 1GB, 1TB. Sizes can also be spelled out, such + as 100 megabytes, 1 gigabyte, 1 terabyte. +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. +filterFactories (none) The list of filters to apply to the appender, in order, after + the thresold. +============================ =========== ================================================================================================== + + +.. _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 + filterFactories: + - type: URI + + +============================ =========== ================================================================================================== +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) +filterFactories (none) The list of filters to apply to the appender, in order, after + the thresold. +============================ =========== ================================================================================================== + + +.. _man-configuration-logging-filter-factories: + +FilterFactories +--------------- + +.. code-block:: yaml + + logging: + level: INFO + appenders: + - type: console + filterFactories: + - type: URI + + +====================== =========== ================ +Name Default Description +====================== =========== ================ +type REQUIRED The filter type. +====================== =========== ================ + +.. _man-configuration-metrics: + +Metrics +======= + +The metrics configuration has two fields; frequency and reporters. + +.. code-block:: yaml + + metrics: + frequency: 1 minute + reporters: + - type: + + +====================== =========== =========== +Name Default Description +====================== =========== =========== +frequency 1 minute 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: + durationUnit: milliseconds + rateUnit: seconds + excludes: (none) + includes: (all) + useRegexFilters: false + frequency: 1 minute + + +====================== ============= =========== +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. +useRegexFilters false Indicates whether the values of the 'includes' and 'excludes' fields should be treated as regular expressions or not. +frequency (none) The frequency to report metrics. Overrides the default. +====================== ============= =========== + +The inclusion and exclusion rules are defined as: + +* If **includes** is empty, then all metrics are included; +* If **includes** is not empty, only metrics from this list are included; +* If **excludes** is empty, no metrics are excluded; +* If **excludes** is not empty, then exclusion rules take precedence over inclusion rules. Thus if a name matches the exclusion rules it will not be included in reports even if it also matches the inclusion rules. + + +.. _man-configuration-metrics-formatted: + +Formatted Reporters +................... + +These options are available only to "formatted" reporters and extend the options available to :ref:`all reporters ` + +.. code-block:: yaml + + metrics: + reporters: + - type: + locale: + + +====================== =============== =========== +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 ` + +.. 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 ` + +.. 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 ` + +.. note:: + + You will need to add ``dropwizard-metrics-ganglia`` to your POM. + +.. 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 announce 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 ` + +.. note:: + + You will need to add ``dropwizard-metrics-graphite`` to your POM. + +.. code-block:: yaml + + metrics: + reporters: + - type: graphite + host: localhost + port: 8080 + 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 ` + +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: + + +====================== =============== ==================================================================================================== +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: () + + +============================= ====================================== ============================================================================= +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. +connectionRequestTimeout 500 milliseconds The maximum time to wait for a connection to be returned from the connection pool. +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. +validateAfterInactivityPeriod 0 milliseconds The maximum time before a persistent connection is checked to remain active. + If set to 0, no inactivity check will be performed. +============================= ====================================== ============================================================================= + + +.. _man-configuration-clients-http-proxy: + +Proxy +..... + +.. code-block:: yaml + + httpClient: + proxy: + host: 192.168.52.11 + port: 8080 + scheme : http + auth: + username: secret + password: stuff + nonProxyHosts: + - localhost + - '192.168.52.*' + - '*.example.com' + + +============= ================= ====================================================================== +Name Default Description +============= ================= ====================================================================== +host REQUIRED The proxy server host name or ip address. +port (scheme default) The proxy server port. + If the port is not set then the scheme default port is used. +scheme http The proxy server URI scheme. HTTP and HTTPS schemas are permitted. + By default HTTP scheme is used. +auth (none) The proxy server BASIC authentication credentials. + If they are not set then no credentials will be passed to the server. +username REQUIRED The username used to connect to the server. +password REQUIRED The password used to connect to the server. + +nonProxyHosts (none) List of patterns of hosts that should be reached without proxy. + The patterns may contain symbol '*' as a wildcard. + If a host matches one of the patterns it will be reached through a direct connection. +============= ================= ====================================================================== + + +.. _man-configuration-clients-http-tls: + +TLS +..... + +.. code-block:: yaml + + httpClient: + tls: + protocol: TLSv1.2 + verifyHostname: true + keyStorePath: /path/to/file + keyStorePassword: changeit + keyStoreType: JKS + trustStorePath: /path/to/file + trustStorePassword: changeit + trustStoreType: JKS + trustSelfSignedCertificates: false + supportedProtocols: TLSv1.1,TLSv1.2 + supportedCipherSuites: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 + + +=========================== ================= ============================================================================================================================ +Name Default Description +=========================== ================= ============================================================================================================================ +protocol TLSv1.2 The default protocol the client will attempt to use during the SSL Handshake. + See + `here `_ for more information. +verifyHostname true Whether to verify the hostname of the server against the hostname presented in the server certificate. +keyStorePath (none) The path to the Java key store which contains the client certificate and private key. +keyStorePassword (none) The password used to access the key store. +keyStoreType JKS The type of key store (usually ``JKS``, ``PKCS12``, ``JCEKS``, ``Windows-MY``, or ``Windows-ROOT``). +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``). +trustSelfSignedCertificates false Whether the client will trust certificates of servers that are self-signed. +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. +=========================== ================= ============================================================================================================================ + + +.. _man-configuration-clients-jersey: + +JerseyClient +------------ + +Extends the attributes that are available to :ref:`http clients ` + +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 + workQueueSize: 8 + gzipEnabled: true + gzipEnabledForRequests: true + chunkedEncodingEnabled: 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. If asynchronous requests made by jersey client while serving requests, the number must be set according to the `maxThread` setting of the :ref:`server `. Otherwise some requests made to dropwizard on heavy load may fail due to congestion on the jersey client's thread pool. +workQueueSize 8 The size of the work queue of the pool used for asynchronous requests. + Additional threads will be spawn only if the queue is reached its maximum size. +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. +chunkedEncodingEnabled true Enables the use of chunked encoding for requests. +======================= ================== =================================================================================================== + + +.. _man-configuration-database: + +Database +======== + +.. code-block:: yaml + + database: + driverClass : org.postgresql.Driver + url: 'jdbc:postgresql://db.example.com/db-prod' + user: pg-user + password: iAMs00perSecrEET + + +============================ ===================== =============================================================== +Name Default Description +============================ ===================== =============================================================== +driverClass REQUIRED The full name of the JDBC driver class. + +url REQUIRED The URL of the server. + +user none The username used to connect to the server. + +password none The password used to connect to the server. + +removeAbandoned false Remove abandoned connections if they exceed + removeAbandonedTimeout. If set to true a connection is + considered abandoned and eligible for removal if it has been in + use longer than the removeAbandonedTimeout and the condition + for abandonWhenPercentageFull is met. + +removeAbandonedTimeout 60 seconds The time before a database connection can be considered + abandoned. + +abandonWhenPercentageFull 0 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 abandonWhenPercentageFull. + The value should be between 0-100. + +alternateUsernamesAllowed false Set to true if the call 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. + +commitOnReturn false Set to true if you want the connection pool to commit any + pending transaction when a connection is returned. + +rollbackOnReturn false Set to true if you want the connection pool to rollback any + pending transaction when a connection is returned. + + +autoCommitByDefault JDBC driver's default The default auto-commit state of the connections. + +readOnlyByDefault JDBC driver's default The default read-only state of the connections. + +properties none Any additional JDBC driver parameters. + +defaultCatalog none The default catalog to use for the connections. + +defaultTransactionIsolation JDBC driver's default The default transaction isolation to use for the connections. + Can be one of none, default, read-uncommitted, read-committed, + repeatable-read, or serializable. + +useFairQueue true If true, calls to getConnection are handled in a FIFO manner. + +initialSize 10 The initial size of the connection pool. + +minSize 10 The minimum size of the connection pool. + +maxSize 100 The maximum size of the connection pool. + +initializationQuery none A custom query to be run when a connection is first created. + +logAbandonedConnections false If true, logs stack traces of abandoned connections. + +logValidationErrors false If true, logs errors when connections fail validation. + +maxConnectionAge none If set, connections which have been open for longer than + maxConnectionAge are closed when returned. + +maxWaitForConnection 30 seconds If a request for a connection is blocked for longer than this + period, an exception will be thrown. + +minIdleTime 1 minute The minimum amount of time an connection must sit idle in the + pool before it is eligible for eviction. + +validationQuery SELECT 1 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.( FireBird will throw exception unless validationQuery set to **select 1 from rdb$database**) + +validationQueryTimeout none The timeout before a connection validation queries fail. + +checkConnectionWhileIdle true Set to true if query validation should take place while the + connection is idle. + +checkConnectionOnBorrow false 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. + +checkConnectionOnConnect false 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. + +checkConnectionOnReturn false 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. + +autoCommentsEnabled true Whether or not ORMs should automatically add comments. + +evictionInterval 5 seconds The amount of time to sleep between runs of the idle + connection validation, abandoned cleaner and idle pool + resizing. + +validationInterval 30 seconds To avoid excess validation, only run validation once every + interval. + +validatorClassName none Name of a class of a custom validator implementation, which + will be used for validating connections. +============================ ===================== =============================================================== + +.. _man-configuration-polymorphic: + +Polymorphic configuration +========================= + +.. rubric:: The ``dropwizard-configuration`` module provides you with a polymorphic configuration + mechanism, meaning that a particular section of your configuration file can be implemented + using one or more configuration classes. + +To use this capability for your own configuration classes, create a top-level configuration interface or class that +implements ``Discoverable`` and add the name of that class to ``META-INF/services/io.dropwizard.jackson.Discoverable``. +Make sure to use `Jackson polymorphic deserialization`_ annotations appropriately. + +.. _Jackson polymorphic deserialization: http://wiki.fasterxml.com/JacksonPolymorphicDeserialization + +.. code-block:: java + + @JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type") + interface WidgetFactory extends Discoverable { + Widget createWidget(); + } + +Then create subtypes of the top-level type corresponding to each alternative, and add their names to +``META-INF/services/WidgetFactory``. + +.. code-block:: java + + @JsonTypeName("hammer") + public class HammerFactory implements WidgetFactory { + @JsonProperty + private int weight = 10; + + @Override + public Hammer createWidget() { + return new Hammer(weight); + } + } + + @JsonTypeName("chisel") + public class ChiselFactory implements WidgetFactory { + @JsonProperty + private float radius = 1; + + @Override + public Chisel createWidget() { + return new Chisel(radius); + } + } + +Now you can use ``WidgetFactory`` objects in your application's configuration. + +.. code-block:: java + + public class MyConfiguration extends Configuration { + @JsonProperty + @NotNull + @Valid + private List widgets; + } + +.. code-block:: yaml + + widgets: + - type: hammer + weight: 20 + - type: chisel + radius: 0.4 diff --git a/docs/source/manual/core.rst b/docs/source/manual/core.rst new file mode 100644 index 00000000000..deee1a4861d --- /dev/null +++ b/docs/source/manual/core.rst @@ -0,0 +1,1563 @@ +.. _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 ` 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 ` implementation for your application + * ``core``: Domain implementation + * ``jdbi``: :ref:`Database ` access classes + * ``health``: :ref:`man-core-healthchecks` + * ``resources``: :ref:`man-core-resources` + * ``MyApplication``: The :ref:`application ` class + * ``MyApplicationConfiguration``: :ref:`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`__ and :ref:`configuration refererence `. + +.. __: 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``. + +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. + +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 ExampleConfiguration 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 +:ref:`man-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-environment-variables: + +Environment variables +--------------------- + +The ``dropwizard-configuration`` module also provides the capabilities to substitute configuration settings with the +value of environment variables using a ``SubstitutingSourceProvider`` and ``EnvironmentVariableSubstitutor``. + +.. code-block:: java + + public class MyApplication extends Application { + // [...] + @Override + public void initialize(Bootstrap bootstrap) { + // Enable variable substitution with environment variables + bootstrap.setConfigurationSourceProvider( + new SubstitutingSourceProvider(bootstrap.getConfigurationSourceProvider(), + new EnvironmentVariableSubstitutor(false) + ) + ); + + } + + // [...] + } + +The configuration settings which should be substituted need to be explicitly written in the configuration file and +follow the substitution rules of StrSubstitutor_ from the Apache Commons Lang library. + +.. code-block:: yaml + + mySetting: ${DW_MY_SETTING} + defaultSetting: ${DW_DEFAULT_SETTING:-default value} + +In general ``SubstitutingSourceProvider`` isn't restricted to substitute environment variables but can be used to replace +variables in the configuration source with arbitrary values by passing a custom ``StrSubstitutor`` implementation. + +.. _StrSubstitutor: https://commons.apache.org/proper/commons-lang/javadocs/api-release/org/apache/commons/lang3/text/StrSubstitutor.html + +.. _man-core-ssl: + +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, and `Jetty's documentation`_ can get you started). There is a +test keystore you can use in the `Dropwizard example project`__. + +.. _`Jetty's documentation`: http://www.eclipse.org/jetty/documentation/current/configuring-ssl.html +.. __: 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 + +By default, only secure TLSv1.2 cipher suites are allowed. Older versions of cURL, Java 6 and 7, and +other clients may be unable to communicate with the allowed cipher suites, but this was a conscious +decision that sacrifices interoperability for security. Dropwizard allows a workaround by specifying +a customized list of cipher suites. The following list of excluded cipher suites will allow for +TLSv1 and TLSv1.1 clients to negotiate a connection similar to pre-Dropwizard 1.0. + +.. code-block:: yaml + + server: + applicationConnectors: + - type: https + port: 8443 + excludedCipherSuites: + - SSL_RSA_WITH_DES_CBC_SHA + - SSL_DHE_RSA_WITH_DES_CBC_SHA + - SSL_DHE_DSS_WITH_DES_CBC_SHA + - SSL_RSA_EXPORT_WITH_RC4_40_MD5 + - SSL_RSA_EXPORT_WITH_DES40_CBC_SHA + - SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA + - SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA + +.. _man-core-bootstrapping: + +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. + + +.. _man-core-environments: + +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://basho.com/products/ + +.. 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{ + @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`` from the ``dropwizard-assets`` module provides a simple way +to serve static assets from your application's ``src/main/resources/assets`` directory as files +available from ``/assets/*`` (or any other path) in your application. + +Configured Bundles +------------------ + +Some bundles require configuration parameters. These bundles implement ``ConfiguredBundle`` and will +require your application's ``Configuration`` subclass to implement a specific interface. + + +For example: given the configured bundle ``MyConfiguredBundle`` and the interface ``MyConfiguredBundleConfig`` below. +Your application's ``Configuration`` subclass would need to implement ``MyConfiguredBundleConfig``. + +.. code-block:: java + + public class MyConfiguredBundle implements ConfiguredBundle{ + + @Override + public void run(MyConfiguredBundleConfig applicationConfig, Environment environment) { + applicationConfig.getBundleSpecificConfig(); + } + + @Override + public void initialize(Bootstrap bootstrap) { + + } + } + + public interface MyConfiguredBundleConfig{ + + String getBundleSpecificConfig(); + + } + + +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: + rootPath: /api/ + +.. note:: + + If you use the :ref:`man-configuration-simple` server configuration, then ``rootPath`` is calculated relatively from + ``applicationContextPath``. So, your API will be accessible from the path ``/application/api/`` + + +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 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 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. + +Below is an example on how to add a command and have Dropwizard recognize it. + +.. code-block:: java + + public class MyCommand extends Command { + public MyCommand() { + // The name of our command is "hello" and the description printed is + // "Prints a greeting" + super("hello", "Prints a greeting"); + } + + @Override + public void configure(Subparser subparser) { + // Add a command line option + subparser.addArgument("-u", "--user") + .dest("user") + .type(String.class) + .required(true) + .help("The user of the program"); + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + System.out.println("Hello " + namespace.getString("user")); + } + } + +Dropwizard recognizes our command once we add it in the ``initialize`` stage of our application. + +.. code-block:: java + + public class MyApplication extends Application{ + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addCommand(new MyCommand()); + } + } + +To invoke the new functionality, run the following: + +.. code-block:: text + + java -jar hello dropwizard + +.. _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. By default, +Dropwizard will treat the last 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. + +A ``ConfiguredCommand`` can have additional command line options specified, while keeping the last +argument the path to the YAML configuration. + +.. code-block:: java + + @Override + public void configure(Subparser subparser) { + super.configure(subparser); + + // Add a command line option + subparser.addArgument("-u", "--user") + .dest("user") + .type(String.class) + .required(true) + .help("The user of the program"); + } + +For more advanced customization of the command line (for example, having the configuration file +location specified by ``-c``), adapt the ConfiguredCommand_ class as needed. + +.. _ConfiguredCommand: https://github.com/dropwizard/dropwizard/blob/master/dropwizard-core/src/main/java/io/dropwizard/cli/ConfiguredCommand.java + +.. _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.); and the ``log-level`` task, which configures the level +of any number of loggers at runtime (akin to Logback's ``JmxConfigurator``). 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 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. The task will receive any query parameters as arguments. For example:: + + $ curl -X POST http://dw.example.com:8081/tasks/gc + Running GC... + Done! + +You can also extend ``PostBodyTask`` to create a task which uses the body of the post request. Here's an example: + +.. code-block:: java + + public class EchoTask extends PostBodyTask { + public EchoTask() { + super("echo"); + } + + @Override + public void execute(ImmutableMultimap parameters, String postBody, PrintWriter output) throws Exception { + output.write(postBody); + output.flush(); + } + } + +.. _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, override the levels of other loggers in your YAML configuration file, +and even specify appenders for them. The latter form of configuration is preferable, but the former is also +acceptable. + +.. 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 + + # Enables the SQL query log and redirect it to a separate file + "org.hibernate.SQL": + level: DEBUG + # This line stops org.hibernate.SQL (or anything under it) from using the root logger + additive: false + appenders: + - type: file + currentLogFilename: ./logs/example-sql.log + archivedLogFilenamePattern: ./logs/example-sql-%d.log.gz + archivedFileCount: 5 +.. _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-logging-http-config: + +Logging Configuration via HTTP +------------------------------ + +Active log levels can be changed during the runtime of a Dropwizard application via HTTP using +the ``LogConfigurationTask``. For instance, to configure the log level for a +single ``Logger``: + +.. code-block:: shell + + curl -X POST -d "logger=com.example.helloworld&level=INFO" http://localhost:8081/tasks/log-level + +.. _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(isA(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 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, + @NotNull @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 +be added to the :ref:`list of allowed methods `. 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 ` 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. + +* ``@Timed`` measures the duration of requests to a resource +* ``@Metered`` measures the rate at which the resource is accessed +* ``@ExceptionMetered`` measures how often exceptions occur processing the resource + +.. _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`` parameter takes all the ``name`` values from a + posted form and passes them to the method as a set of strings. +* A ``*Param``--annotated ``NonEmptyStringParam`` will interpret empty strings as absent strings, + which is useful in cases where the endpoint treats empty strings and absent strings as + interchangeable. + +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 `, the ``add`` method provides a good example of +this: + +.. code-block:: java + :emphasize-lines: 3 + + @POST + public Response add(@PathParam("user") LongParam userId, + @NotNull @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-validation-validations-constraining-entities`. + +If the deserialized ``Notification`` isn't valid, Dropwizard returns a ``422 Unprocessable Entity`` +response to the client. + +.. note:: + + If a request entity parameter is just annotated with ``@Valid``, it is still allowed to be + ``null``, so to ensure that the object is present and validated ``@NotNull @Valid`` is a + powerful combination. + +.. _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 ` much easier. + +.. _man-core-resource-error-handling: + +Error Handling +-------------- + +Almost as important as an application's happy path (receiving expected input and returning expected +output) is an application behavior when something goes wrong. + +If your resource class unintentionally throws an exception, Dropwizard will log that exception under +the ``ERROR`` level (including stack traces) and return a terse, safe ``application/json`` ``500 +Internal Server Error`` response. The response will contain an ID that can be grepped out the server +logs for additional information. + +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, as that will make resource endpoints more self describing and easier to test. + +The least instrusive way to map error conditions to a response is to throw a ``WebApplicationException``: + +.. code-block:: java + + @GET + @Path("/{collection}") + public Saying reduceCols(@PathParam("collection") String collection) { + if (!collectionMap.containsKey(collection)) { + final String msg = String.format("Collection %s does not exist", collection); + throw new WebApplicationException(msg, Status.NOT_FOUND) + } + + // ... + } + +In this example a ``GET`` request to ``/foobar`` will return + +.. code-block:: json + + {"code":404,"message":"Collection foobar does not exist"} + +One can also take exceptions that your resource may throw and map them to appropriate responses. For instance, +an endpoint may throw ``IllegalArugmentException`` and it may be worthy enough of a response to warrant a +custom metric to track how often the event occurs. Here's an example of such an ``ExceptionMapper`` + +.. code-block:: java + + public class IllegalArgumentExceptionMapper implements ExceptionMapper { + private final Meter exceptions; + public IllegalArgumentExceptionMapper(MetricRegistry metrics) { + exceptions = metrics.meter(name(getClass(), "exceptions")); + } + + @Override + public Response toResponse(IllegalArgumentException e) { + exceptions.mark(); + return Response.status(Status.BAD_REQUEST) + .header("X-YOU-SILLY", "true") + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(new ErrorMessage(Status.BAD_REQUEST.getStatusCode(), + "You passed an illegal argument!")) + .build(); + } + } + +and then registering the exception mapper: + +.. code-block:: java + + @Override + public void run(final MyConfiguration conf, final Environment env) { + env.jersey().register(new IllegalArgumentExceptionMapper(env.metrics())); + env.jersey().register(new Resource()); + } + +Overriding Default Exception Mappers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you want more control, you can disable the exception mappers Dropwizard provides by default. This is done +by setting ``server.registerDefaultExceptionMappers`` to ``false``. Since this disables all default exception +mappers make sure to re-enable exception mappers that are wanted. The default exception mappers are: + +- ``LoggingExceptionMapper`` +- ``JerseyViolationExceptionMapper`` +- ``JsonProcessingExceptionMapper`` +- ``EarlyEofExceptionMapper`` + +.. _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 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`` and a +``FunkyDeserializer`` class which implements ``JsonDeserializer``. + +.. _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-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 `. + +.. _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`` and +``MessageBodyWriter`` 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 +has a rich api for `filters and interceptors`_ that can be used directly in Dropwizard. +You can stop the request from reaching your resources by throwing a ``WebApplicationException``. Alternatively, +you can use filters to modify inbound requests or outbound responses. + +.. _filters and interceptors: http://jersey.java.net/documentation/latest/filters-and-interceptors.html + +.. code-block:: java + + @Provider + public class DateNotSpecifiedFilter implements ContainerRequestFilter { + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + String dateHeader = requestContext.getHeaderString(HttpHeaders.DATE); + + if (dateHeader == null) { + Exception cause = new IllegalArgumentException("Date Header was not specified"); + throw new WebApplicationException(cause, Response.Status.BAD_REQUEST); + } + } + } + +This example filter checks the request for the "Date" header, and denies the request if was missing. Otherwise, +the request is passed through. + +Filters can be dynamically bound to resource methods using `DynamicFeature`_: + +.. _DynamicFeature: http://jax-rs-spec.java.net/nonav/2.0-rev-a/apidocs/index.html + +.. code-block:: java + + @Provider + public class DateRequiredFeature implements DynamicFeature { + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + if (resourceInfo.getResourceMethod().getAnnotation(DateRequired.class) != null) { + context.register(DateNotSpecifiedFilter.class); + } + } + } + +The DynamicFeature is invoked by the Jersey runtime when the application is started. In this example, the feature checks +for methods that are annotated with ``@DateRequired`` and registers the ``DateNotSpecified`` filter on those methods only. + +You typically register the feature in your Application class, like so: + +.. code-block:: java + + environment.jersey().register(DateRequiredFeature.class); + + +.. _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 omitted 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("DateNotSpecifiedServletFilter", new DateNotSpecifiedServletFilter()) + .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 00000000000..c5d788744b2 --- /dev/null +++ b/docs/source/manual/example.rst @@ -0,0 +1,37 @@ +.. _man-example: + +################################ +Dropwizard Example, Step by Step +################################ + +.. highlight:: text + +.. rubric:: The ``dropwizard-example`` module provides you with a working Dropwizard Example Application. + +* Preconditions + + * Make sure you have Maven_ installed + * Make sure ``JAVA_HOME`` points at JDK 8 + * Make sure you have ``curl`` + +.. _Maven: https://maven.apache.org/ + +* Preparations to start the Dropwizard Example Application + + * Open a terminal / cmd + * Navigate to the project folder of the Dropwizard Example Application + * ``mvn clean install`` + * ``java -jar target/dropwizard-example-1.0.0.jar db migrate example.yml`` + * The statement above ran the liquibase migration in ``/src/main/resources/migrations.xml``, creating the table schema + +* Starting the Dropwizard Example Application + + * You can now start the Dropwizard Example Application by running ``java -jar target/dropwizard-example-1.0.0.jar server example.yml`` + * Alternatively, you can run the Dropwizard Example Application in your IDE: ``com.example.helloworld.HelloWorldApplication server example.yml`` + +* Working with the Dropwizard Example Application + + * Insert a new person: ``curl -H "Content-Type: application/json" -d '{"fullName":"John Doe", "jobTitle" : "Chief Wizard" }' http://localhost:8080/people`` + * Retrieve that person: ``curl http://localhost:8080/people/1`` + * View that person in a freemarker template: curl or open in a browser ``http://localhost:8080/people/1/view_freemarker`` + * View that person in a mustache template: curl or open in a browser ``http://localhost:8080/people/1/view_mustache`` diff --git a/docs/source/manual/forms.rst b/docs/source/manual/forms.rst new file mode 100644 index 00000000000..46744de9c68 --- /dev/null +++ b/docs/source/manual/forms.rst @@ -0,0 +1,33 @@ +.. _man-forms: + +################ +Dropwizard Forms +################ + +.. highlight:: text + +.. rubric:: The ``dropwizard-forms`` module provides you with a support for multi-part forms + via Jersey_. + +.. _Jersey: https://jersey.java.net/ + +Adding The Bundle +================= + +Then, in your application's ``initialize`` method, add a new ``MultiPartBundle`` subclass: + +.. code-block:: java + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new MultiPartBundle()); + } + +More Information +================ + +For additional and more detailed documentation about the Jersey multi-part support, please refer to the +documentation in the `Jersey User Guide`_ and Javadoc_. + +.. _Jersey User Guide: https://jersey.java.net/documentation/latest/media.html#multipart +.. _Javadoc: https://jersey.java.net/apidocs/latest/jersey/org/glassfish/jersey/media/multipart/package-summary.html diff --git a/docs/source/manual/hibernate.rst b/docs/source/manual/hibernate.rst new file mode 100644 index 00000000000..4a839ae0b9e --- /dev/null +++ b/docs/source/manual/hibernate.rst @@ -0,0 +1,184 @@ +.. _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 `, instrumented ``SessionFactory`` instance, your +:ref:`configuration class ` needs a ``DataSourceFactory`` instance: + +.. code-block:: java + + public class ExampleConfiguration extends Configuration { + @Valid + @NotNull + private DataSourceFactory database = new DataSourceFactory(); + + @JsonProperty("database") + 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 hibernate = new HibernateBundle(Person.class) { + @Override + public DataSourceFactory getDataSourceFactory(ExampleConfiguration configuration) { + return configuration.getDataSourceFactory(); + } + }; + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(hibernate); + } + + @Override + public void run(ExampleConfiguration config, Environment environment) { + final UserDAO dao = new UserDAO(hibernate.getSessionFactory()); + environment.jersey().register(new UserResource(dao)); + } + +This will create a new :ref:`managed ` connection pool to the database, a +:ref:`health check ` 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 { + 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 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 + @Path("/{id}") + @Timed + @UnitOfWork + public Person findPerson(@PathParam("id") LongParam id) { + return dao.findById(id.get()); + } + +This will automatically open a session, begin a transaction, call ``findById``, 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). + +Transactional Resource Methods Outside Jersey Resources +---------------------------------------------------- + +Currently creating transactions with the `@UnitOfWork` annotation works out-of-box only for resources +managed by Jersey. If you want to use it outside Jersey resources, e.g. in authenticators, you should +instantiate your class with ``UnitOfWorkAwareProxyFactory``. + +.. code-block:: java + + SessionDao dao = new SessionDao(hibernateBundle.getSessionFactory()); + ExampleAuthenticator exampleAuthenticator = new UnitOfWorkAwareProxyFactory(hibernateBundle) + .create(ExampleAuthenticator.class, SessionDao.class, dao); + +It will create a proxy of your class, which will open a Hibernate session with a transaction around +methods with the ``@UnitOfWork`` annotation. + +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 00000000000..45de226b1c4 --- /dev/null +++ b/docs/source/manual/index.rst @@ -0,0 +1,28 @@ +.. _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 + forms + validation + views + scala + testing + example + configuration + internals + diff --git a/docs/source/manual/internals.rst b/docs/source/manual/internals.rst new file mode 100644 index 00000000000..6daa841d619 --- /dev/null +++ b/docs/source/manual/internals.rst @@ -0,0 +1,81 @@ +.. _man-internals: + +#################### +Dropwizard Internals +#################### + +You already read through the whole Dropwizard documentation? +Congrats! Then you are ready to have a look into some nitty-gritty details of Dropwizard. + +Startup Sequence +================ + +Below you find the startup sequence of a Dropwizard Application: + +#. Application.run(args) + + #. new Bootstrap + #. bootstrap.addCommand(new ServerCommand) + #. bootstrap.addCommand(new CheckCommand) + #. initialize(bootstrap) (implemented by your Application) + + #. bootstrap.addBundle(bundle) + + #. bundle.initialize(bootstrap) + + #. bootstrap.addCommand(cmd) + + #. cmd.initialize() + + #. new Cli(bootstrap and other params) + + #. for each cmd in bootstrap.getCommands() + + #. configure parser w/ cmd + + #. cli.run() + + #. is help flag on cmdline? if so, print usage + #. parse cmdline args, determine subcommand (rest of these notes are specific to ServerCommand) + #. command.run(bootstrap, namespace) (implementation in ConfiguredCommand) + + #. parse configuration + #. setup logging + + #. command.run(bootstrap, namespace, cfg) (implementation in EnvironmentCommand) + + #. create Environment + #. bootstrap.run(cfg, env) + + #. for each Bundle: bundle.run() + #. for each ConfiguredBundle: bundle.run() + + #. application.run(cfg, env) (implemented by your Application) + + #. command.run(env, namespace, cfg) (implemented by ServerCommand) + + #. starts Jetty + + +On Bundles +========== + +Running bundles happens in FIFO order (ConfiguredBundles are always run after Bundles). + +Jetty Lifecycle +=============== +If you have a component of your app that needs to know when Jetty is going to start, +you can implement Managed as described in the dropwizard docs. + +If you have a component that needs to be signaled that Jetty has started +(this happens after all Managed objects' start() methods are called), +you can register with the env's lifecycle like: + +.. code-block:: java + + env.lifecycle().addServerLifecycleListener(new ServerLifecycleListener() { + @Override + public void serverStarted(Server server) { + /// ... do things here .... + } + }); \ No newline at end of file diff --git a/docs/source/manual/jdbi.rst b/docs/source/manual/jdbi.rst new file mode 100644 index 00000000000..f9ba863c3e0 --- /dev/null +++ b/docs/source/manual/jdbi.rst @@ -0,0 +1,159 @@ +.. _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 `, instrumented ``DBI`` instance, your +:ref:`configuration class ` needs a ``DataSourceFactory`` instance: + +.. code-block:: java + + public class ExampleConfiguration extends Configuration { + @Valid + @NotNull + private DataSourceFactory database = new DataSourceFactory(); + + @JsonProperty("database") + public void setDataSourceFactory(DataSourceFactory factory) { + this.database = factory; + } + + @JsonProperty("database") + 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) { + 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 ` connection pool to the database, a +:ref:`health check ` for connectivity to the database, and a new ``DBI`` +instance for you to use. + +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 timeout before a connection validation queries fail + validationQueryTimeout: 3s + + # 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 `, 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. + +Library Support +=============== + +``dropwizard-jdbi`` supports a number of popular libraries data types that can be automatically +serialized into the appropriate SQL type. Here's a list of what integration ``dropwizard-jdbi`` +provides: + +* Guava: support for ``Optional`` arguments and ``ImmutableList`` and ``ImmutableSet`` query results. +* Joda Time: support for ``DateTime`` arguments and ``DateTime`` fields in query results +* Java 8: support for ``Optional`` and kin (``OptionalInt``, etc.) arguments and java.time_ arguments. + +.. _java.time: https://docs.oracle.com/javase/8/docs/api/java/time/package-summary.html diff --git a/docs/source/manual/migrations.rst b/docs/source/manual/migrations.rst new file mode 100644 index 00000000000..0dfea0b0b96 --- /dev/null +++ b/docs/source/manual/migrations.rst @@ -0,0 +1,319 @@ +.. _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 ` needs a +``DataSourceFactory`` instance: + +.. code-block:: java + + public class ExampleConfiguration extends Configuration { + @Valid + @NotNull + private DataSourceFactory database = new DataSourceFactory(); + + @JsonProperty("database") + 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 bootstrap) { + bootstrap.addBundle(new MigrationsBundle() { + @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. You can change the name of the migrations +file used by overriding the ``getMigrationsFileName()`` method in ``MigrationsBundle``. + +For example, to create a new ``people`` table, you might create an initial ``migrations.xml`` like +this: + +.. code-block:: xml + + + + + + + + + + + + + + + + + + +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 changeset 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 changesets 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 changesets, use the ``--count`` flag. + +Rolling Back Your Schema +======================== + +To roll back changesets which have already been applied, run the ``db rollback`` command. You will +need to specify either a **tag**, a **date**, or a **number of changesets** 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 changesets 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 changesets *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 changesets. + +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 Changeset +==================================== + +To mark a pending changeset 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 changeset as applied. You can also use the ``--all`` flag to mark +all pending changesets as applied. + +Support For Adding Multiple Migration Bundles +============================================= + +Assuming migrations need to be done for two different databases, you would need to have two different data source factories: + +.. code-block:: java + + public class ExampleConfiguration extends Configuration { + @Valid + @NotNull + private DataSourceFactory database1 = new DataSourceFactory(); + + @Valid + @NotNull + private DataSourceFactory database2 = new DataSourceFactory(); + + @JsonProperty("database1") + public DataSourceFactory getDb1DataSourceFactory() { + return database1; + } + + @JsonProperty("database2") + public DataSourceFactory getDb2DataSourceFactory() { + return database2; + } + } + +Now multiple migration bundles can be added with unique names like so: + +.. code-block:: java + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new MigrationsBundle() { + @Override + public DataSourceFactory getDataSourceFactory(ExampleConfiguration configuration) { + return configuration.getDb1DataSourceFactory(); + } + + @Override + public String name() { + return "db1"; + } + }); + + bootstrap.addBundle(new MigrationsBundle() { + @Override + public DataSourceFactory getDataSourceFactory(ExampleConfiguration configuration) { + return configuration.getDb2DataSourceFactory(); + } + + @Override + public String name() { + return "db2"; + } + }); + } + +To migrate your schema: + +.. code-block:: text + + java -jar hello-world.jar db1 migrate helloworld.yml + +and + +.. code-block:: text + + java -jar hello-world.jar db2 migrate helloworld.yml + +.. note:: + + Whenever a name is added to a migration bundle, it becomes the command that needs to be run at the command line. + eg: To check the state of your database, use the ``status`` command: + +.. code-block:: text + + java -jar hello-world.jar db1 status helloworld.yml + +or + +.. code-block:: text + + java -jar hello-world.jar db2 status helloworld.yml + +By default the migration bundle uses the "db" command. By overriding you can customize it to provide any name you want +and have multiple migration bundles. Wherever the "db" command was being used, this custom name can be used. + +There will also be a need to provide different change log migration files as well. This can be done as + +.. code-block:: text + + java -jar hello-world.jar db1 migrate helloworld.yml --migrations + +.. code-block:: text + + java -jar hello-world.jar db2 migrate helloworld.yml --migrations + +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 --help``. diff --git a/docs/source/manual/scala.rst b/docs/source/manual/scala.rst new file mode 100644 index 00000000000..df5f161f695 --- /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 `_. diff --git a/docs/source/manual/testing.rst b/docs/source/manual/testing.rst new file mode 100644 index 00000000000..8f9ae950583 --- /dev/null +++ b/docs/source/manual/testing.rst @@ -0,0 +1,459 @@ +.. _manual-testing: + +################## +Testing Dropwizard +################## + +.. highlight:: text + +.. rubric:: The ``dropwizard-testing`` module provides you with some handy classes for testing + your :ref:`representation classes ` + and :ref:`resource classes `. 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 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 { + private String name; + private String email; + + private Person() { + // Jackson deserialization + } + + public Person(String name, String email) { + this.name = name; + this.email = email; + } + + @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; + } + + // 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@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.assertj.core.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@example.com"); + + final String expected = MAPPER.writeValueAsString( + MAPPER.readValue(fixture("fixtures/person.json"), Person.class)); + + assertThat(MAPPER.writeValueAsString(person)).isEqualTo(expected); + } + } + +This test uses `AssertJ assertions`_ 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 on a normalized JSON +string representation, so formatting doesn't affect the results.) + +.. _AssertJ assertions: http://assertj.org/assertj-core-conditions.html +.. _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.assertj.core.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@example.com"); + assertThat(MAPPER.readValue(fixture("fixtures/person.json"), Person.class)) + .isEqualTo(person); + } + } + + +This test uses `AssertJ assertions`_ 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.assertj.core.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@example.com"); + + @Before + public void setup() { + when(dao.fetchPerson(eq("blah"))).thenReturn(person); + } + + @After + public void tearDown(){ + // 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().target("/person/blah").request().get(Person.class)) + .isEqualTo(person); + verify(dao).fetchPerson("blah"); + } + } + +Instantiate 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 `, 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? + +Default Exception Mappers +------------------------- + +By default, a ``ResourceTestRule`` will register all the default exception mappers (this behavior is new in 1.0). If +``registerDefaultExceptionMappers`` in the configuration yaml is planned to be set to ``false``, +``ResourceTestRule.Builder#setRegisterDefaultExceptionMappers(boolean)`` will also need to be set to ``false``. Then, +all custom exception mappers will need to be registered on the builder, similarly to how they are registered in an +``Application`` class. + +Test Containers +--------------- + +Note that the in-memory Jersey test container does not support all features, such as the ``@Context`` injection used by +``BasicAuthFactory`` and ``OAuthFactory``. A different `test container`__ can be used via +``ResourceTestRule.Builder#setTestContainerFactory(TestContainerFactory)``. + +For example, if you want to use the `Grizzly`_ HTTP server (which supports ``@Context`` injections) you need to add the +dependency for the Jersey Test Framework providers to your Maven POM and set ``GrizzlyWebTestContainerFactory`` as +``TestContainerFactory`` in your test classes. + +.. code-block:: xml + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${jersey.version} + test + + + javax.servlet + javax.servlet-api + + + junit + junit + + + + + +.. code-block:: java + + public class ResourceTestWithGrizzly { + @ClassRule + public static final ResourceTestRule RULE = ResourceTestRule.builder() + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addResource(new ExampleResource()) + .build(); + + @Test + public void testResource() { + assertThat(RULE.getJerseyTest().target("/example").request() + .get(String.class)) + .isEqualTo("example"); + } + } + +.. __: https://jersey.java.net/documentation/latest/test-framework.html +.. _Grizzly: https://grizzly.java.net/ + +.. _man-testing-clients: + +Testing Client Implementations +============================== + +To avoid circular dependencies in your projects or to speed up test runs, you can test your HTTP client code +by writing a JAX-RS resource as test double and let the ``DropwizardClientRule`` start and stop a simple Dropwizard +application containing your test doubles. + +.. _man-testing-clients-example: + +.. code-block:: java + + public class CustomClientTest { + @Path("/ping") + public static class PingResource { + @GET + public String ping() { + return "pong"; + } + } + + @ClassRule + public static final DropwizardClientRule dropwizard = new DropwizardClientRule(new PingResource()); + + @Test + public void shouldPing() throws IOException { + final URL url = new URL(dropwizard.baseUri() + "/ping"); + final String response = new BufferedReader(new InputStreamReader(url.openStream())).readLine(); + assertEquals("pong", response); + } + } + +.. hint:: + + Of course you would use your HTTP client in the ``@Test`` method and not ``java.net.URL#openStream()``. + +The ``DropwizardClientRule`` takes care of: + +* Creating a simple default configuration. +* Creating a simplistic application. +* Adding a dummy health check to the application to suppress the startup warning. +* Adding your JAX-RS resources (test doubles) to the Dropwizard application. +* Choosing a free random port number (important for running tests in parallel). +* Starting the Dropwizard application containing the test doubles. +* Stopping the Dropwizard application containing the test doubles. + + +Integration Testing +=================== + +It can be useful to start up your entire application and hit it with real HTTP requests during testing. +The ``dropwizard-testing`` module offers helper classes for your easily doing so. +The optional ``dropwizard-client`` module offers more helpers, e.g. a custom JerseyClientBuilder, +which is aware of your application's environment. + +JUnit +----- +Adding ``DropwizardAppRule`` to your JUnit test class 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 RULE = + new DropwizardAppRule(MyApp.class, ResourceHelpers.resourceFilePath("my-app-config.yaml")); + + @Test + public void loginHandlerRedirectsAfterPost() { + Client client = new JerseyClientBuilder(RULE.getEnvironment()).build("test client"); + + Response response = client.target( + String.format("http://localhost:%d/login", RULE.getLocalPort())) + .request() + .post(Entity.json(loginForm())); + + assertThat(response.getStatus()).isEqualTo(302); + } + } + +Non-JUnit +--------- +By creating a DropwizardTestSupport instance in your test you can manually start and stop the app in your tests, you do this by calling its ``before`` and ``after`` methods. ``DropwizardTestSupport`` 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 { + + public static final DropwizardTestSupport SUPPORT = + new DropwizardTestSupport(MyApp.class, + ResourceHelpers.resourceFilePath("my-app-config.yaml"), + ConfigOverride.config("server.applicationConnectors[0].port", "0") // Optional, if not using a separate testing-specific configuration file, use a randomly selected port + ); + + @BeforeClass + public void beforeClass() { + SUPPORT.before(); + } + + @AfterClass + public void afterClass() { + SUPPORT.after(); + } + + @Test + public void loginHandlerRedirectsAfterPost() { + Client client = new JerseyClientBuilder(SUPPORT.getEnvironment()).build("test client"); + + Response response = client.target( + String.format("http://localhost:%d/login", SUPPORT.getLocalPort())) + .request() + .post(Entity.json(loginForm())); + + assertThat(response.getStatus()).isEqualTo(302); + } + } + +.. _man-testing-commands: + +Testing Commands +================ + +:ref:`Commands ` can and should be tested, as it's important to ensure arguments +are interpreted correctly, and the output is as expected. + +Below is a test for a command that adds the arguments as numbers and outputs the summation to the +console. The test ensures that the result printed to the screen is correct by capturing standard out +before the command is ran. + +.. code-block:: java + + public class CommandTest { + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private final InputStream originalIn = System.in; + + private final ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); + private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + private Cli cli; + + @Before + public void setUp() throws Exception { + // Setup necessary mock + final JarLocation location = mock(JarLocation.class); + when(location.getVersion()).thenReturn(Optional.of("1.0.0")); + + // Add commands you want to test + final Bootstrap bootstrap = new Bootstrap<>(new MyApplication()); + bootstrap.addCommand(new MyAddCommand()); + + // Redirect stdout and stderr to our byte streams + System.setOut(new PrintStream(stdOut)); + System.setErr(new PrintStream(stdErr)); + + // Build what'll run the command and interpret arguments + cli = new Cli(location, bootstrap, stdOut, stdErr); + } + + @After + public void teardown() { + System.setOut(originalOut); + System.setErr(originalErr); + System.setIn(originalIn); + } + + @Test + public void myAddCanAddThreeNumbersCorrectly() { + final boolean success = cli.run("add", "2", "3", "6"); + + SoftAssertions softly = new SoftAssertions(); + softly.assertThat(success).as("Exit success").isTrue(); + + // Assert that 2 + 3 + 6 outputs 11 + softly.assertThat(stdOut.toString()).as("stdout").isEqualTo("11"); + softly.assertThat(stdErr.toString()).as("stderr").isEmpty(); + softly.assertAll(); + } + } diff --git a/docs/source/manual/validation.rst b/docs/source/manual/validation.rst new file mode 100644 index 00000000000..5997c8d6f23 --- /dev/null +++ b/docs/source/manual/validation.rst @@ -0,0 +1,416 @@ +.. _man-validation: + +##################### +Dropwizard Validation +##################### + +.. highlight:: text + +.. rubric:: Dropwizard comes with a host of validation tools out of the box to allow endpoints to return meaningful error messages when constraints are violated. `Hibernate Validator`_ is packaged with Dropwizard, so what can be done in Hibernate Validator, can be done with Dropwizard. + +.. _Hibernate Validator: http://hibernate.org/validator/ + +.. _man-validation-validations: + +Validations +=========== + +Almost anything can be validated on resource endpoints. To give a quick example, the following +endpoint doesn't allow a null or empty ``name`` query parameter. + +.. code-block:: java + + @GET + public String find(@QueryParam("name") @NotEmpty String arg) { + // ... + } + +If a client sends an empty or nonexistent name query param, Dropwizard will respond with a ``400 Bad Request`` +code with the error: ``query param name may not be empty``. + +Additionally, annotations such as ``HeaderParam``, ``CookieParam``, ``FormParam``, etc, can be +constrained with violations giving descriptive errors and 400 status codes. + +.. _man-validation-validations-constraining-entities: + +Constraining Entities +********************* + +If we're accepting client-provided ``Person``, we probably want to ensure that the ``name`` field of +the object isn't ``null`` or blank in the request. 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 Person replace(@NotNull @Valid Person person) { + // ... + } + +If the name field is missing, Dropwizard will return a ``422 Unprocessable Entity`` response +detailing the validation errors: ``name may not be empty`` + +.. note:: + + You don't need ``@Valid`` when the type you are validating can be validated directly (``int``, + ``String``, ``Integer``). If a class has fields that need validating, then instances of the + class must be marked ``@Valid``. For more information, see the Hibernate Validator documentation + on `Object graphs`_ and `Cascaded validation`_. + +.. _Object graphs: http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/chapter-bean-constraints.html#section-object-graph-validation + +.. _Cascaded validation: http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/chapter-method-constraints.html#_cascaded_validation + +Since our entity is also annotated with ``@NotNull``, Dropwizard will also guard against ``null`` +input with a response stating that the body must not be null. + +.. _man-validation-validations-optional-constraints: + +``Optional`` Constraints +*************************** + +If an entity, field, or parameter is not required, it can be wrapped in an ``Optional``, but the +inner value can still be constrained with the ``@UnwrapValidatedValue`` annotation. If the +``Optional`` is absent, then the constraints are not applied. + +.. note:: + + Be careful when using constraints with ``*Param`` annotations on ``Optional`` parameters + as there is a subtle, but important distinction between null and empty. If a client requests + ``bar?q=``, ``q`` will evaluate to ``Optional.of("")``. If you want ``q`` to evaluate to + ``Optional.absent()`` in this situation, change the type to ``NonEmptyStringParam`` + +.. note:: + + Param types such as ``IntParam`` and ``NonEmptyStringParam`` can also be constrained. + +There is a caveat regarding ``@UnwrapValidatedValue`` and ``*Param`` types, as there still are some +cumbersome situations when constraints need to be applied to the container and the value. + +.. code-block:: java + + @POST + // The @NotNull is supposed to mean that the parameter is required but the Max(3) is supposed to + // apply to the contained integer. Currently, this code will fail saying that Max can't + // be applied on an IntParam + public List createNum(@QueryParam("num") @UnwrapValidatedValue(false) + @NotNull @Max(3) IntParam num) { + // ... + } + + @GET + // Similarly, the underlying validation framework can't unwrap nested types (an integer wrapped + // in an IntParam wrapped in an Optional), regardless if the @UnwrapValidatedValue is used + public Person retrieve(@QueryParam("num") @Max(3) Optional num) { + // ... + } + +To work around these limitations, if the parameter is required check for it in the endpoint and +throw an exception, else use ``@DefaultValue`` or move the ``Optional`` into the endpoint. + +.. code-block:: java + + @POST + // Workaround to handle required int params and validations + public List createNum(@QueryParam("num") @Max(3) IntParam num) { + if (num == null) { + throw new WebApplicationException("query param num must not be null", 400); + } + // ... + } + + @GET + // Workaround to handle optional int params and validations with DefaultValue + public Person retrieve(@QueryParam("num") @DefaultValue("0") @Max(3) IntParam num) { + // ... + } + + @GET + // Workaround to handle optional int params and validations with Optional + public Person retrieve2(@QueryParam("num") @Max(3) IntParam num) { + Optional.fromNullable(num); + // ... + } + +.. _man-validation-validations-return-value-validations: + +Return Value Validations +************************ + +It's reasonable to want to make guarantees to clients regarding the server response. For example, +you may want to assert that no response will ever be ``null``, and if an endpoint creates a +``Person`` that the person is valid. + +.. code-block:: java + + @POST + @NotNull + @Valid + public Person create() { + return new Person(null); + } + +In this instance, instead of returning someone with a null name, Dropwizard will return an ``HTTP +500 Internal Server Error`` with the error ``server response name may not be empty``, so the client +knows the server failed through no fault of their own. + +Analogous to an empty request body, an empty entity annotated with ``@NotNull`` will return ``server +response may not be null`` + +.. _man-validation-limitations: + +Limitations +=========== + +Jersey allows for ``BeanParam`` to have setters with ``*Param`` annotations. While nice for simple +transformations it does obstruct validation, so clients won't receive as instructive of error +messages. The following example shows the behavior: + +.. code-block:: java + + @Path("/root") + @Produces(MediaType.APPLICATION_JSON) + public class Resource { + + @GET + @Path("params") + public String getBean(@Valid @BeanParam MyBeanParams params) { + return params.getField(); + } + + public static class MyBeanParams { + @NotEmpty + private String field; + + public String getField() { + return field; + } + + @QueryParam("foo") + public void setField(String field) { + this.field = Strings.nullToEmpty(field).trim(); + } + } + } + +A client submitting the query parameter ``foo`` as blank will receive the following error message: + +.. code-block:: json + + {"errors":["getBean.arg0.field may not be empty"]} + +Workarounds include: + +* Name ``BeanParam`` fields the same as the ``*Param`` annotation values +* Supply validation message on annotation: ``@NotEmpty(message = "query param foo must not be empty")`` +* Perform transformations and validations on ``*Param`` inside endpoint + +The same kind of limitation applies for :ref:`Configuration ` objects: + +.. code-block:: java + + public class MyConfiguration extends Configuration { + @NotNull + @JsonProperty("foo") + private String baz; + } + +Even though the property's name is ``foo``, the error when property is null will be: + +.. code-block:: plain + + * baz may not be null + + +Annotations +=========== + +In addition to the `annotations defined in Hibernate Validator`_, Dropwizard contains another set of annotations, +which are briefly shown below. + +.. _annotations defined in Hibernate Validator: http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/chapter-bean-constraints.html#section-builtin-constraints + +.. code-block:: java + + public class Person { + @NotEmpty + private final String name; + + @NotEmpty + @OneOf(value = {"m", "f"}, ignoreCase = true, ignoreWhitespace = true) + // @OneOf forces a value to value within certain values. + private final String gender; + + @Max(10) + @Min(0) + // The integer contained, if present, can attain a min value of 0 and a max of 10. + private final Optional animals; + + @JsonCreator + public Person(@JsonProperty("name") String name) { + this.name = name; + } + + @JsonProperty("name") + public String getName() { + return name; + } + + // Method that must return true for the object to be valid + @ValidationMethod(message="name may not be Coda") + @JsonIgnore + public boolean isNotCoda() { + return !"Coda".equals(name); + } + } + +The reason why Dropwizard defines ``@ValidationMethod`` is that more complex validations (for +example, cross-field comparisons) are often hard to do using declarative annotations. Adding +``@ValidationMethod`` to any ``boolean``-returning method which begins with ``is`` is a short and +simple workaround: + +.. note:: + + Due to the rather daft JavaBeans conventions, when using ``@ValidationMethod``, the method must + begin with ``is`` (e.g., ``#isValidPortRange()``. This is a limitation of Hibernate Validator, + not Dropwizard. + +.. _man-validation-annotations-validated: + +Validating Grouped Constraints with ``@Validated`` +************************************************** + +The ``@Validated`` annotation allows for `validation groups`_ to be specifically set, instead of the +default group. This is useful when different endpoints share the same entity but may have different +requirements. + +.. _validation groups: https://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html/chapter-groups.html + +Going back to our favorite ``Person`` class. Let's say we initially coded it such that ``name`` has +to be non-empty, but realized that business requirements needs the max length to be no more than 5. +Instead of blowing away our current version of our API and creating angry clients, we can accept +both versions of the API but at different endpoints. + +.. code-block:: java + + public interface Version1Checks { } + + public interface Version2Checks { } + + public class Person { + @NotEmpty(groups = Version1Checks.class) + @Length(max = 5, groups = Version2Checks.class) + private String name; + + @JsonCreator + public Person(@JsonProperty("name") String name) { + this.name = name; + } + + @JsonProperty + public String getName() { + return name; + } + } + + @Path("/person") + @Produces(MediaType.APPLICATION_JSON) + public class PersonResource { + @POST + @Path("/v1") + public void createPersonV1(@Valid @Validated(Version1Checks.class) Person person) { + } + + @POST + @Path("/v2") + public void createPersonV2(@Valid @Validated({Version1Checks.class, Version2Checks.class}) Person person) { + } + } + +Now, when clients hit ``/person/v1`` the ``Person`` entity will be checked by all the constraints +that are a part of the ``Version1Checks`` group. If ``/person/v2`` is hit, then all the validations +are performed. + +.. note:: + + Since interfaces can inherit other interfaces, ``Version2Checks`` can extend ``Version1Checks`` + and wherever ``@Validated(Version2Checks.class)`` is used, version 1 constraints are checked + too. + +.. _man-validation-testing: + +Testing +======= + +It is critical to test the constraints so that you can ensure the assumptions about the data hold +and see what kinds of error messages clients will receive for bad input. The recommended way for +testing annotations is through :ref:`Testing Resources `, as Dropwizard does +a bit of magic behind the scenes when a constraint violation occurs to set the response's status +code and ensure that the error messages are user friendly. + +.. code-block:: java + + @Test + public void personNeedsAName() { + // Tests what happens when a person with a null name is sent to + // the endpoint. + final Response post = resources.client() + .target("/person/v1").request() + .post(Entity.json(new Person(null))); + + // Clients will receive a 422 on bad request entity + assertThat(post.getStatus()).isEqualTo(422); + + // Check to make sure that errors are correct and human readable + ValidationErrorMessage msg = post.readEntity(ValidationErrorMessage.class); + assertThat(msg.getErrors()) + .containsOnly("name may not be empty"); + } + +.. _man-validation-extending: + +Extending +========= + +While Dropwizard provides good defaults for error messages, one size may not fit all and so there +are a series of extension points. To register your own +``ExceptionMapper`` you'll need to first set +``registerDefaultExceptionMappers`` to false in the configuration file or in code before registering +your exception mapper with jersey. Then, optionally, register other default exception mappers: + +* ``LoggingExceptionMapper`` +* ``JsonProcessingExceptionMapper`` +* ``EarlyEofExceptionMapper`` + +If you need to validate entities outside of resource endpoints, the validator can be accessed in the +``Environment`` when the application is first ran. + +.. code-block:: java + + Validator validator = environment.getValidator(); + Set errors = validator.validate(/* instance of class */) + +The method used to determine what status code to return based on violations is +``ConstraintViolations.determineStatus`` + +The method used to determine the human friendly error message due to a constraint violation is +``ConstraintMessage.getMessage``. diff --git a/docs/source/manual/views.rst b/docs/source/manual/views.rst new file mode 100644 index 00000000000..1d3b5e28ebb --- /dev/null +++ b/docs/source/manual/views.rst @@ -0,0 +1,128 @@ +.. _manual-views: + +################ +Dropwizard Views +################ + +.. highlight:: text + +.. rubric:: The ``dropwizard-views-mustache`` & ``dropwizard-views-freemarker`` modules provide 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 `, add the ``ViewBundle`` in the ``initialize`` method of your Application class: + +.. code-block:: java + + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new ViewBundle()); + } + +You can pass configuration through to view renderers by overriding ``getViewConfiguration``: + +.. code-block:: java + + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(new ViewBundle() { + @Override + public Map> getViewConfiguration(MyConfiguration config) { + return config.getViewRendererConfiguration(); + } + }); + } + +The returned map should have, for each extension (such as ``.ftl``), a ``Map`` describing how to configure the renderer. Specific keys and their meanings can be found in the FreeMarker and Mustache documentation: + +.. code-block:: yaml + + views: + .ftl: + strict_syntax: yes + +Then, in your :ref:`resource method `, 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 Freemarker_ 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" --> + + + +

    Hello, ${person.name?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 + +.. _man-views-template-errors: + +Template Errors +=============== + +If there is an error with the template (eg. the template file is not found or there is a compilation +error with the template), the user will receive a ``500 Internal Sever Error`` with a generic HTML +message. The exact error will logged under error mode. diff --git a/dropwizard-archetypes/README.md b/dropwizard-archetypes/README.md new file mode 100644 index 00000000000..0bcf25b1a33 --- /dev/null +++ b/dropwizard-archetypes/README.md @@ -0,0 +1,10 @@ +# Dropwizard Archetypes + +How to create project using dropwizard archetype (interactive mode) +--- + +``` +mvn archetype:generate -DarchetypeGroupId=io.dropwizard.archetypes -DarchetypeArtifactId=java-simple -DarchetypeVersion=[REPLACE ME WITH A VALID DROPWIZARD VERSION] +``` + +(when asked for ``$name`` during project creation via maven, make sure to use a camel case word such as ``HelloWorld`` as it is used to generate Configuration and Application classess such as ``HelloWorldConfiguration.java`` and ``HelloWorldApplication.java``. Furthermore, do not include any blank space for the same reason!) diff --git a/dropwizard-archetypes/java-simple/pom.xml b/dropwizard-archetypes/java-simple/pom.xml new file mode 100644 index 00000000000..4a93a933812 --- /dev/null +++ b/dropwizard-archetypes/java-simple/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + 3.0.0 + + + + io.dropwizard.archetypes + dropwizard-archetypes + 1.0.1-SNAPSHOT + + + java-simple + maven-archetype + + Dropwizard Archetype for Simple Java Services + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + + + + + + maven-invoker-plugin + + + + + + + src/main/resources + true + + archetype-resources/pom.xml + + + + src/main/resources + false + + archetype-resources/pom.xml + + + + + + diff --git a/dropwizard-archetypes/java-simple/src/main/resources/META-INF/maven/archetype-metadata.xml b/dropwizard-archetypes/java-simple/src/main/resources/META-INF/maven/archetype-metadata.xml new file mode 100644 index 00000000000..f458c1dbd6b --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/META-INF/maven/archetype-metadata.xml @@ -0,0 +1,120 @@ + + + + + + null + + + true + + + + + + + src/main/java + + **/* + + + + + + src/main/java/__packageInPathFormat__/api + + + + + src/main/java/__packageInPathFormat__/cli + + + + + src/main/java/__packageInPathFormat__/client + + + + + src/main/java/__packageInPathFormat__/core + + + + + src/main/java/__packageInPathFormat__/db + + + + + src/main/java/__packageInPathFormat__/health + + + + + src/main/java/__packageInPathFormat__/resources + + + + + src/main/resources/assets + + + + + src/main/resources + + banner.txt + + + + + + src/test/java/__packageInPathFormat__/api + + + + + src/test/java/__packageInPathFormat__/client + + + + + src/test/java/__packageInPathFormat__/core + + + + + src/test/java/__packageInPathFormat__/db + + + + + src/test/java/__packageInPathFormat__/resources + + + + + src/test/resources/fixtures + + + + + + + config.yml + + + + + + + + README.md + + + + + diff --git a/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/README.md b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/README.md new file mode 100644 index 00000000000..be4def7ee31 --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/README.md @@ -0,0 +1,13 @@ +# ${name} + +How to start the ${name} application +--- + +1. Run `mvn clean install` to build your application +1. Start application with `java -jar target/${artifactId}-${version}.jar server config.yml` +1. To check that your application is running enter url `http://localhost:8080` + +Health Check +--- + +To see your applications health enter url `http://localhost:8081/healthcheck` diff --git a/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/config.yml b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/config.yml new file mode 100644 index 00000000000..f7392a24aa8 --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/config.yml @@ -0,0 +1,4 @@ +logging: + level: INFO + loggers: + ${groupId}: DEBUG diff --git a/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/pom.xml b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/pom.xml new file mode 100644 index 00000000000..024c1f0c239 --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/pom.xml @@ -0,0 +1,147 @@ + + + + 4.0.0 + + 3.0.0 + + + \${groupId} + \${artifactId} + \${version} + jar + + \${name} +#if( $description != "null" ) + \${description} +#end + + + UTF-8 + UTF-8 + ${project.version} + \${package}.\${name}Application + + + + + + io.dropwizard + dropwizard-bom + \${dropwizard.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + + + +#if( $shaded == "true" ) + + maven-shade-plugin + 2.4.1 + + true + + + + \${mainClass} + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + package + + shade + + + + +#end + + maven-jar-plugin + 2.6 + + + + true + \${mainClass} + + + + + + maven-compiler-plugin + 3.3 + + 1.8 + 1.8 + + + + maven-source-plugin + 2.4 + + + attach-sources + + jar + + + + + + maven-javadoc-plugin + 2.10.3 + + + attach-javadocs + + jar + + + + + + + + + + + maven-project-info-reports-plugin + 2.8.1 + + false + false + + + + maven-javadoc-plugin + 2.10.3 + + + + diff --git a/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/java/__name__Application.java b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/java/__name__Application.java new file mode 100644 index 00000000000..98c3df0b6ff --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/java/__name__Application.java @@ -0,0 +1,29 @@ +package ${package}; + +import io.dropwizard.Application; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +public class ${name}Application extends Application<${name}Configuration> { + + public static void main(final String[] args) throws Exception { + new ${name}Application().run(args); + } + + @Override + public String getName() { + return "${name}"; + } + + @Override + public void initialize(final Bootstrap<${name}Configuration> bootstrap) { + // TODO: application initialization + } + + @Override + public void run(final ${name}Configuration configuration, + final Environment environment) { + // TODO: implement application + } + +} diff --git a/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/java/__name__Configuration.java b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/java/__name__Configuration.java new file mode 100644 index 00000000000..6b6f0dbad0a --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/java/__name__Configuration.java @@ -0,0 +1,10 @@ +package ${package}; + +import io.dropwizard.Configuration; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.*; +import javax.validation.constraints.*; + +public class ${name}Configuration extends Configuration { + // TODO: implement service configuration +} diff --git a/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/resources/banner.txt b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/resources/banner.txt new file mode 100644 index 00000000000..58a68f28f39 --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/main/resources/archetype-resources/src/main/resources/banner.txt @@ -0,0 +1,6 @@ +================================================================================ + + ${name} + +================================================================================ + diff --git a/dropwizard-archetypes/java-simple/src/test/resources/projects/ordinary/test.properties b/dropwizard-archetypes/java-simple/src/test/resources/projects/ordinary/test.properties new file mode 100644 index 00000000000..3089e589f7e --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/test/resources/projects/ordinary/test.properties @@ -0,0 +1,7 @@ +groupId=com.example +artifactId=test-project +version=1.0.0-SNAPSHOT +name=FooBar +package=com.example.generated +description=An integration test project +shaded=false diff --git a/dropwizard-archetypes/java-simple/src/test/resources/projects/shaded/test.properties b/dropwizard-archetypes/java-simple/src/test/resources/projects/shaded/test.properties new file mode 100644 index 00000000000..16c7e55eb60 --- /dev/null +++ b/dropwizard-archetypes/java-simple/src/test/resources/projects/shaded/test.properties @@ -0,0 +1,7 @@ +groupId=com.example +artifactId=test-project +version=1.0.0-SNAPSHOT +name=FooBar +package=com.example.generated +description=An integration test project +shaded=true diff --git a/dropwizard-archetypes/pom.xml b/dropwizard-archetypes/pom.xml new file mode 100644 index 00000000000..5a7a665750f --- /dev/null +++ b/dropwizard-archetypes/pom.xml @@ -0,0 +1,176 @@ + + + + 4.0.0 + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + + ${project.basedir}/src/test/resources/projects + ${project.build.directory}/it + ${project.build.itDirectory}/projects + + + io.dropwizard.archetypes + dropwizard-archetypes + pom + + Dropwizard Archetypes + + A collection of Maven Archetypes for bootstrapping development of a new Dropwizard Service. + + + + java-simple + + + + + + org.apache.maven.archetype + archetype-packaging + 2.4 + + + + + + maven-archetype-plugin + 2.4 + + + maven-resources-plugin + 2.7 + + \ + + + + + + + maven-clean-plugin + 2.6.1 + + + prepare-integration-test + + clean + + pre-integration-test + + true + + + ${project.build.itOutputDirectory} + + **/* + + + + + + + + + maven-invoker-plugin + 2.0.0 + + + ${project.parent.basedir}/src/test/resources/settings.xml + + ${project.build.itDirectory}/repo + + true + + + + + generate-project + + install + run + + + + org.apache.maven.plugins:maven-archetype-plugin:generate + + + * + + ${project.build.itOutputDirectory} + ${project.build.itSourceDirectory} + + ${project.artifactId} + ${project.groupId} + ${project.version} + local + false + + + + + + verify + + run + + + + verify + + + */*/pom.xml + + ${project.build.itOutputDirectory} + + + + + + + + + + maven-resources-plugin + 2.7 + + + package-tools + + resources + + package + false + + + + + + + src/main/resources + true + + dropwizard-create + + + + + + + + dev + + true + true + + + + diff --git a/dropwizard-archetypes/src/main/resources/dropwizard-create b/dropwizard-archetypes/src/main/resources/dropwizard-create new file mode 100755 index 00000000000..f080a8deb84 --- /dev/null +++ b/dropwizard-archetypes/src/main/resources/dropwizard-create @@ -0,0 +1,180 @@ +#!/usr/bin/env bash +# +# Bootstrap a new Dropwizard project +# + +# defaults +DW_VERSION="${project.version}" +SHADED="true" +PACKAGE= +NAME= +DESCRIPTION="null" +GROUP= +ARTIFACT= +VERSION="0.1.0-SNAPSHOT" +MAVEN=`which mvn` + +# display usage message +function usage() +{ + cat << EOF +Usage: dropwizard-create [OPTIONS] PACKAGE NAME [DESCRIPTION] + +Creates a new Dropwizard project, NAME, under the specified PACKAGE with an +optional DESCRIPTION. + +If the project is new, it will be generated in a directory corresponding to +the artifact ID (or NAME, if no explicit artifact ID was given). + +When executed inside a multi-module Maven project, a new project will be +created as a sub-module of the current project. + +When executed inside an existing project, the current project will be +transformed to a Dropwizard project. + + --artifact=ARTIFACTID Apply the specified Maven artifact label. + Formatted as follows: + + [GROUPID]:[ARTIFACTID]:[VERSION] + + If a component is omitted, the default will be used: + + GROUPID = PACKAGE + ARTIFACTID = NAME + VERSION = 0.1.0-SNAPSHOT + + Examples: + + ::1.0.0-SNAPSHOT + + :example-project: + + com.acme::1.0-SNAPSHOT + + --version Display this programs' version information and exit. + + --version=VERSION Use the specified Dropwizard version. + Defaults to the version this program came from. + + --maven=/path/to/mvn Explicitly provide path to Maven binary. + + --help Print this usage message. + +EOF +} + +function warn() +{ + echo "WARNING: $1" + + if [ $2 ]; then + echo "Press ENTER to continue anyway, or Ctrl+C to stop..." + read + fi +} + +# display an error message, usage information then exit +function error() +{ + if [ $# -gt 0 ]; then + echo "ERROR: $1" + else + echo "ERROR: An unknwon error occurred." + fi + + usage + + exit 1 +} + +# parse options +while [ $# -gt 0 ] +do + case "$1" in + "--shaded="*) + SHADED="${1:9}" + shift;; + "--artifact="*":"*":"*) + IFS=":" read -ra TAG <<< "${1:11}" + GROUP=${TAG[0]} + ARTIFACT=${TAG[1]} + VERSION=${TAG[2]} + shift;; + "--artifact="*) + error "Invalid artifact tag format, must be: [GROUPID]:[ARTIFACTID]:[VERSION]";; + "--version="[0-9]"."[0-9]"."[0-9]) + DW_VERSION="${1:10}" + shift;; + "--version="[0-9]"."[0-9]"."[0-9]"-SNAPSHOT") + DW_VERSION="${1:10}" + warn "Selected Dropwizard version is a SNAPSHOT. It is recommended that you use a stable release." true + shift;; + "--version="*) + error "Invalid version number for Dropwizard version: ${1:10}";; + "--help" | "-h") + usage + exit 0;; + "--maven="*) + MAVEN="${1:8}" + shift;; + "--"*) + error "Unknown option $1";; + "-"*) + error "Unknown option $1";; + *) + if [ $# -lt 2 ]; then + error "You must specify both the package and name for the project." + fi + + PACKAGE=$1 + NAME=$2 + + if [ $# -gt 2 ]; then + DESCRIPTION=$3 + shift + fi + + shift 2 + + esac +done + +# ensure we have a path to Maven +if [ -z $MAVEN ]; then + error "Unable to locate Maven, please provide path with --maven=/path/to/mvn" +fi + +# ensure maven's binary is executable +if [ ! -x $MAVEN ]; then + error "Unable to execute Maven, please ensure $MAVEN is executable by $USER" +fi + +# verify package/name +if [ -z $PACKAGE ]; then + error "You must specify a package for the project." +elif [ -z $NAME ]; then + error "You must specify a name for the project." +fi + +# assign defaults for groupId, artifactId and target +if [ -z $GROUP ]; then + GROUP="$PACKAGE" +fi + +if [ -z $ARTIFACT ]; then + ARTIFACT=`echo "$NAME" | tr A-Z a-z` +fi + +# capitalize first character of NAME +NAME="`echo ${NAME:0:1} | tr a-z A-Z`${NAME:1}" + +PACKAGING_TYPE= +if [ "true" == $SHADED ]; then + PACKAGING_TYPE="shaded " +fi +echo "Creating ${PACKAGING_TYPE}project $NAME with root package $PACKAGE using Dropwizard $DW_VERSION and ID $GROUP:$ARTIFACT:$VERSION ..." +echo + +# create from archetype +$MAVEN "archetype:generate" "-DinteractiveMode=false" "-DarchetypeGroupId=io.dropwizard.archetypes" "-DarchetypeArtifactId=java-simple" "-DarchetypeVersion=$DW_VERSION" "-DgroupId=$GROUP" "-DartifactId=$ARTIFACT" "-Dpackage=$PACKAGE" "-Dname=$NAME" "-Dversion=$VERSION" "-Dshaded=$SHADED" "-Ddescription=$DESCRIPTION" + diff --git a/dropwizard-archetypes/src/test/resources/settings.xml b/dropwizard-archetypes/src/test/resources/settings.xml new file mode 100644 index 00000000000..74e1173feae --- /dev/null +++ b/dropwizard-archetypes/src/test/resources/settings.xml @@ -0,0 +1,36 @@ + + + + + it-repo + + true + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + local.central + @localRepositoryUrl@ + + true + + + true + + + + + + + diff --git a/dropwizard-assets/pom.xml b/dropwizard-assets/pom.xml new file mode 100644 index 00000000000..135f05ab389 --- /dev/null +++ b/dropwizard-assets/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-assets + Dropwizard Asset Bundle + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + io.dropwizard + dropwizard-servlets + + + 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 00000000000..8f712b86219 --- /dev/null +++ b/dropwizard-assets/src/main/java/io/dropwizard/assets/AssetsBundle.java @@ -0,0 +1,128 @@ +package io.dropwizard.assets; + +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 java.nio.charset.StandardCharsets; + +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 + '*'); + } + + public String getResourcePath() { + return resourcePath; + } + + public String getUriPath() { + return uriPath; + } + + public String getIndexFile() { + return indexFile; + } + + protected AssetServlet createServlet() { + return new AssetServlet(resourcePath, uriPath, indexFile, StandardCharsets.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 00000000000..2bfc200d875 --- /dev/null +++ b/dropwizard-assets/src/test/java/io/dropwizard/assets/AssetsBundleTest.java @@ -0,0 +1,156 @@ +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.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 servletCaptor = ArgumentCaptor.forClass(AssetServlet.class); + final ArgumentCaptor 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/src/test/resources/empty.yml b/dropwizard-assets/src/test/resources/assets/git-turd.txt similarity index 100% rename from dropwizard/src/test/resources/empty.yml rename to dropwizard-assets/src/test/resources/assets/git-turd.txt 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 00000000000..e69de29bb2d diff --git a/dropwizard-auth/pom.xml b/dropwizard-auth/pom.xml new file mode 100644 index 00000000000..a07cf003e9e --- /dev/null +++ b/dropwizard-auth/pom.xml @@ -0,0 +1,42 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-auth + Dropwizard Authentication + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + 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 00000000000..6fcd350fc11 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/Auth.java @@ -0,0 +1,12 @@ +package io.dropwizard.auth; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +public @interface Auth { +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthDynamicFeature.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthDynamicFeature.java new file mode 100644 index 00000000000..e3f285ffee3 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthDynamicFeature.java @@ -0,0 +1,56 @@ +package io.dropwizard.auth; + +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; +import org.glassfish.jersey.server.model.AnnotatedMethod; + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.FeatureContext; +import java.lang.annotation.Annotation; + +/** + * A {@link DynamicFeature} that registers the provided auth filter + * to resource methods annotated with the {@link RolesAllowed}, {@link PermitAll} + * and {@link DenyAll} annotations. + *

    In conjunction with {@link RolesAllowedDynamicFeature} it enables + * authorization AND authentication of requests on the annotated methods.

    + *

    If authorization is not a concern, then {@link RolesAllowedDynamicFeature} + * could be omitted. But to enable authentication, the {@link PermitAll} annotation + * should be placed on the corresponding resource methods.

    + */ +public class AuthDynamicFeature implements DynamicFeature { + + private final ContainerRequestFilter authFilter; + + public AuthDynamicFeature(ContainerRequestFilter authFilter) { + this.authFilter = authFilter; + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + final AnnotatedMethod am = new AnnotatedMethod(resourceInfo.getResourceMethod()); + final Annotation[][] parameterAnnotations = am.getParameterAnnotations(); + //@DenyAll shouldn't be attached to classes + final boolean annotationOnClass = (resourceInfo.getResourceClass().getAnnotation(RolesAllowed.class) != null) || + (resourceInfo.getResourceClass().getAnnotation(PermitAll.class) != null); + final boolean annotationOnMethod = am.isAnnotationPresent(RolesAllowed.class) || am.isAnnotationPresent(DenyAll.class) || + am.isAnnotationPresent(PermitAll.class); + + if (annotationOnClass || annotationOnMethod) { + context.register(authFilter); + } else { + for (Annotation[] annotations : parameterAnnotations) { + for (Annotation annotation : annotations) { + if (annotation instanceof Auth) { + context.register(authFilter); + return; + } + } + } + } + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthFilter.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthFilter.java new file mode 100644 index 00000000000..2d56bf8f90a --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthFilter.java @@ -0,0 +1,171 @@ +package io.dropwizard.auth; + +import com.google.common.base.Preconditions; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Priority; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.SecurityContext; +import java.security.Principal; +import java.util.Optional; + +@Priority(Priorities.AUTHENTICATION) +public abstract class AuthFilter implements ContainerRequestFilter { + + protected final Logger logger = LoggerFactory.getLogger(getClass()); + + protected String prefix; + protected String realm; + protected Authenticator authenticator; + protected Authorizer

    authorizer; + protected UnauthorizedHandler unauthorizedHandler; + + /** + * Abstract builder for auth filters. + * + * @param the type of credentials that the filter accepts + * @param

    the type of the principal that the filter accepts + */ + public abstract static class AuthFilterBuilder> { + + private String realm = "realm"; + private String prefix = "Basic"; + private Authenticator authenticator; + private Authorizer

    authorizer = new PermitAllAuthorizer<>(); + private UnauthorizedHandler unauthorizedHandler = new DefaultUnauthorizedHandler(); + + /** + * Sets the given realm + * + * @param realm a realm + * @return the current builder + */ + public AuthFilterBuilder setRealm(String realm) { + this.realm = realm; + return this; + } + + /** + * Sets the given prefix + * + * @param prefix a prefix + * @return the current builder + */ + public AuthFilterBuilder setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + /** + * Sets the given authorizer + * + * @param authorizer an {@link Authorizer} + * @return the current builder + */ + public AuthFilterBuilder setAuthorizer(Authorizer

    authorizer) { + this.authorizer = authorizer; + return this; + } + + /** + * Sets the given authenticator + * + * @param authenticator an {@link Authenticator} + * @return the current builder + */ + public AuthFilterBuilder setAuthenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + /** + * Sets the given unauthorized handler + * + * @param unauthorizedHandler an {@link UnauthorizedHandler} + * @return the current builder + */ + public AuthFilterBuilder setUnauthorizedHandler(UnauthorizedHandler unauthorizedHandler) { + this.unauthorizedHandler = unauthorizedHandler; + return this; + } + + /** + * Builds an instance of the filter with a provided authenticator, + * an authorizer, a prefix, and a realm. + * + * @return a new instance of the filter + */ + public T buildAuthFilter() { + Preconditions.checkArgument(realm != null, "Realm is not set"); + Preconditions.checkArgument(prefix != null, "Prefix is not set"); + Preconditions.checkArgument(authenticator != null, "Authenticator is not set"); + Preconditions.checkArgument(authorizer != null, "Authorizer is not set"); + Preconditions.checkArgument(unauthorizedHandler != null, "Unauthorized handler is not set"); + + final T authFilter = newInstance(); + authFilter.authorizer = authorizer; + authFilter.authenticator = authenticator; + authFilter.prefix = prefix; + authFilter.realm = realm; + authFilter.unauthorizedHandler = unauthorizedHandler; + return authFilter; + } + + protected abstract T newInstance(); + } + + /** + * Authenticates a request with user credentials and setup the security context. + * + * @param requestContext the context of the request + * @param credentials the user credentials + * @param scheme the authentication scheme; one of {@code BASIC_AUTH, FORM_AUTH, CLIENT_CERT_AUTH, DIGEST_AUTH}. + * See {@link SecurityContext} + * @return {@code true}, if the request is authenticated, otherwise {@code false} + */ + protected boolean authenticate(ContainerRequestContext requestContext, C credentials, String scheme) { + try { + if (credentials == null) { + return false; + } + + final Optional

    principal = authenticator.authenticate(credentials); + if (!principal.isPresent()) { + return false; + } + + final SecurityContext securityContext = requestContext.getSecurityContext(); + final boolean secure = securityContext != null && securityContext.isSecure(); + + requestContext.setSecurityContext(new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return principal.get(); + } + + @Override + public boolean isUserInRole(String role) { + return authorizer.authorize(principal.get(), role); + } + + @Override + public boolean isSecure() { + return secure; + } + + @Override + public String getAuthenticationScheme() { + return scheme; + } + }); + return true; + } catch (AuthenticationException e) { + logger.warn("Error authenticating credentials", e); + throw new InternalServerErrorException(); + } + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthValueFactoryProvider.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthValueFactoryProvider.java new file mode 100644 index 00000000000..e1d46f7cb7f --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthValueFactoryProvider.java @@ -0,0 +1,104 @@ +package io.dropwizard.auth; + +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.security.Principal; + +/** + * Value factory provider supporting {@link Principal} injection + * by the {@link Auth} annotation. + * + * @param the type of the principal + */ +@Singleton +public class AuthValueFactoryProvider extends AbstractValueFactoryProvider { + + /** + * Class of the provided {@link Principal} + */ + private final Class principalClass; + + /** + * {@link Principal} value factory provider injection constructor. + * + * @param mpep multivalued parameter extractor provider + * @param injector injector instance + * @param principalClassProvider provider of the principal class + */ + @Inject + public AuthValueFactoryProvider(MultivaluedParameterExtractorProvider mpep, + ServiceLocator injector, PrincipalClassProvider principalClassProvider) { + super(mpep, injector, Parameter.Source.UNKNOWN); + this.principalClass = principalClassProvider.clazz; + } + + /** + * Return a factory for the provided parameter. We only expect objects of + * the type {@link T} being annotated with {@link Auth} annotation. + * + * @param parameter parameter that was annotated for being injected + * @return the factory if annotated parameter matched type + */ + @Override + public AbstractContainerRequestValueFactory createValueFactory(Parameter parameter) { + if (!parameter.isAnnotationPresent(Auth.class) || !principalClass.equals(parameter.getRawType())) { + return null; + } else { + return new PrincipalContainerRequestValueFactory(); + } + } + + @Singleton + static class AuthInjectionResolver extends ParamInjectionResolver { + + /** + * Create new {@link Auth} annotation injection resolver. + */ + AuthInjectionResolver() { + super(AuthValueFactoryProvider.class); + } + } + + @Singleton + static class PrincipalClassProvider { + + private final Class clazz; + + PrincipalClassProvider(Class clazz) { + this.clazz = clazz; + } + } + + /** + * Injection binder for {@link AuthValueFactoryProvider} and {@link AuthInjectionResolver}. + * + * @param the type of the principal + */ + public static class Binder extends AbstractBinder { + + private final Class principalClass; + + public Binder(Class principalClass) { + this.principalClass = principalClass; + } + + @Override + protected void configure() { + bind(new PrincipalClassProvider<>(principalClass)).to(PrincipalClassProvider.class); + bind(AuthValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(AuthInjectionResolver.class).to(new TypeLiteral>() { + }).in(Singleton.class); + } + } +} 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 00000000000..012031440c3 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/AuthenticationException.java @@ -0,0 +1,22 @@ +package io.dropwizard.auth; + +/** + * An exception thrown to indicate that an {@link Authenticator} is unable to check the + * validity of the given credentials. + *

    DO NOT USE THIS TO INDICATE THAT THE CREDENTIALS ARE INVALID.

    + */ +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 00000000000..c9588ec22a6 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/Authenticator.java @@ -0,0 +1,27 @@ +package io.dropwizard.auth; + +import java.security.Principal; +import java.util.Optional; + +/** + * An interface for classes which authenticate user-provided credentials and return principal + * objects. + * + * @param the type of credentials the authenticator can authenticate + * @param

    the type of principals the authenticator returns + */ +public interface Authenticator { + /** + * Given a set of user-provided credentials, return an optional principal. + * + * If the credentials are valid and map to a principal, returns an {@link Optional#of(Object)}. + * + * If the credentials are invalid, returns an {@link Optional#empty()}. + * + * @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

    authenticate(C credentials) throws AuthenticationException; +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/Authorizer.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/Authorizer.java new file mode 100644 index 00000000000..4feafb42ace --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/Authorizer.java @@ -0,0 +1,20 @@ +package io.dropwizard.auth; + +import java.security.Principal; + +/** + * An interface for classes which authorize principal objects. + * + * @param

    the type of principals + */ +public interface Authorizer

    { + + /** + * Decides if access is granted for the given principal in the given role. + * + * @param principal a {@link Principal} object, representing a user + * @param role a user role + * @return {@code true}, if the access is granted, {@code false otherwise} + */ + boolean authorize(P principal, String role); +} 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 00000000000..646697d5553 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthenticator.java @@ -0,0 +1,151 @@ +package io.dropwizard.auth; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.google.common.base.Predicate; +import com.google.common.base.Throwables; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.CacheStats; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.Sets; +import com.google.common.util.concurrent.UncheckedExecutionException; + +import java.security.Principal; +import java.util.Optional; +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 the type of credentials the authenticator can authenticate + * @param

    the type of principals the authenticator returns + */ +public class CachingAuthenticator implements Authenticator { + private final LoadingCache> 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(final MetricRegistry metricRegistry, + final Authenticator authenticator, + final 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(final MetricRegistry metricRegistry, + final Authenticator authenticator, + final CacheBuilder builder) { + this.cacheMisses = metricRegistry.meter(name(authenticator.getClass(), "cache-misses")); + this.gets = metricRegistry.timer(name(authenticator.getClass(), "gets")); + this.cache = builder.recordStats().build(new CacheLoader>() { + @Override + public Optional

    load(C key) throws Exception { + cacheMisses.mark(); + final Optional

    optPrincipal = authenticator.authenticate(key); + if (!optPrincipal.isPresent()) { + // Prevent caching of unknown credentials + throw new InvalidCredentialsException(); + } + return optPrincipal; + } + }); + } + + @Override + public Optional

    authenticate(C credentials) throws AuthenticationException { + final Timer.Context context = gets.time(); + try { + return cache.get(credentials); + } catch (ExecutionException e) { + final Throwable cause = e.getCause(); + if (cause instanceof InvalidCredentialsException) { + return Optional.empty(); + } + // Attempt to re-throw as-is + Throwables.propagateIfPossible(cause, AuthenticationException.class); + throw new AuthenticationException(cause); + } catch (UncheckedExecutionException e) { + throw Throwables.propagate(e.getCause()); + } 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 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 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(); + } + + /** + * Exception thrown by {@link CacheLoader#load(Object)} when the authenticator returns {@link Optional#empty()}. + * This is used to prevent caching of invalid credentials. + */ + private static class InvalidCredentialsException extends Exception { + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthorizer.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthorizer.java new file mode 100644 index 00000000000..754bd57b877 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/CachingAuthorizer.java @@ -0,0 +1,170 @@ +package io.dropwizard.auth; + +import com.codahale.metrics.Meter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.cache.CacheStats; +import com.google.common.collect.Iterables; +import com.google.common.collect.Sets; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import java.security.Principal; +import java.util.function.Predicate; + +import static com.codahale.metrics.MetricRegistry.name; + +/** + * An {@link Authorizer} decorator which uses a Guava {@link Cache} to + * temporarily cache principals' role associations. + *

    + * Cache entries include both inclusion and exclusion of a principal + * within a given role. + * + * @param

    the type of principals on which the authorizer operates + */ +public class CachingAuthorizer

    implements Authorizer

    { + private final Authorizer

    underlying; + private final Meter cacheMisses; + private final Timer getsTimer; + + // A cache which maps (principal, role) tuples to boolean + // authorization states. + // + // A cached value of `true` indicates that the key's principal is + // authorized to assume the given role. False values indicate the + // principal is not authorized to assume the role. + // + // `null` cache values are interpreted as cache misses, and will + // thus result in read through to the underlying `Authorizer`. + private final Cache, Boolean> cache; + + /** + * Creates a new cached authorizer. + * + * @param metricRegistry the application's registry of metrics + * @param authorizer the underlying authorizer + * @param cacheSpec {@link CacheBuilderSpec} + */ + public CachingAuthorizer( + final MetricRegistry metricRegistry, + final Authorizer

    authorizer, + final CacheBuilderSpec cacheSpec + ) { + this(metricRegistry, authorizer, CacheBuilder.from(cacheSpec)); + } + + /** + * Creates a new cached authorizer. + * + * @param metricRegistry the application's registry of metrics + * @param authorizer the underlying authorizer + * @param builder a {@link CacheBuilder} + */ + public CachingAuthorizer( + final MetricRegistry metricRegistry, + final Authorizer

    authorizer, + final CacheBuilder builder + ) { + this.underlying = authorizer; + this.cacheMisses = metricRegistry.meter(name(authorizer.getClass(), "cache-misses")); + this.getsTimer = metricRegistry.timer(name(authorizer.getClass(), "gets")); + this.cache = builder.recordStats().build(); + } + + @Override + public boolean authorize(P principal, String role) { + final Timer.Context context = getsTimer.time(); + + try { + final ImmutablePair cacheKey = ImmutablePair.of(principal, role); + Boolean isAuthorized = cache.getIfPresent(cacheKey); + + if (isAuthorized == null) { + cacheMisses.mark(); + isAuthorized = underlying.authorize(principal, role); + cache.put(cacheKey, isAuthorized); + } + + return isAuthorized; + } finally { + context.stop(); + } + } + + /** + * Discards any cached role associations for the given principal and role. + * + * @param principal + * @param role + */ + public void invalidate(P principal, String role) { + cache.invalidate(ImmutablePair.of(principal, role)); + } + + /** + * Discards any cached role associations for the given principal. + * + * @param principal + */ + public void invalidate(P principal) { + final Predicate> predicate = + cacheKey -> cacheKey.getLeft().equals(principal); + + cache.invalidateAll(Sets.filter(cache.asMap().keySet(), predicate::test)); + } + + /** + * Discards any cached role associations for the given collection + * of principals. + * + * @param principals a list of principals + */ + public void invalidateAll(Iterable

    principals) { + final Predicate> predicate = + cacheKey -> Iterables.contains(principals, cacheKey.getLeft()); + + cache.invalidateAll(Sets.filter(cache.asMap().keySet(), predicate::test)); + } + + /** + * Discards any cached role associations for principals satisfying + * the given predicate. + * + * @param predicate a predicate to filter credentials + */ + public void invalidateAll(Predicate predicate) { + final Predicate> nestedPredicate = + cacheKey -> predicate.test(cacheKey.getLeft()); + + cache.invalidateAll(Sets.filter(cache.asMap().keySet(), nestedPredicate::test)); + } + + /** + * Discards all cached role associations. + */ + public void invalidateAll() { + cache.invalidateAll(); + } + + /** + * Returns the number of principals for which there are cached + * role associations. + * + * @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/DefaultUnauthorizedHandler.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/DefaultUnauthorizedHandler.java new file mode 100644 index 00000000000..a820ded4fcb --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/DefaultUnauthorizedHandler.java @@ -0,0 +1,18 @@ +package io.dropwizard.auth; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +public class DefaultUnauthorizedHandler implements UnauthorizedHandler { + private static final String CHALLENGE_FORMAT = "%s realm=\"%s\""; + + @Override + public Response buildResponse(String prefix, String realm) { + return Response.status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.WWW_AUTHENTICATE, String.format(CHALLENGE_FORMAT, prefix, realm)) + .type(MediaType.TEXT_PLAIN_TYPE) + .entity("Credentials are required to access this resource.") + .build(); + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/PermitAllAuthorizer.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/PermitAllAuthorizer.java new file mode 100644 index 00000000000..6bc8522ebbd --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/PermitAllAuthorizer.java @@ -0,0 +1,16 @@ +package io.dropwizard.auth; + +import java.security.Principal; + +/** + * An {@link Authorizer} that grants access for any principal in any role. + * + * @param

    the type of the principal + */ +public class PermitAllAuthorizer

    implements Authorizer

    { + + @Override + public boolean authorize(P principal, String role) { + return true; + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/PolymorphicAuthDynamicFeature.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/PolymorphicAuthDynamicFeature.java new file mode 100644 index 00000000000..b1e6cb832fa --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/PolymorphicAuthDynamicFeature.java @@ -0,0 +1,43 @@ +package io.dropwizard.auth; + +import com.google.common.collect.ImmutableMap; +import org.glassfish.jersey.server.model.AnnotatedMethod; + +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.FeatureContext; +import java.lang.annotation.Annotation; +import java.security.Principal; + +/** + * A {@link DynamicFeature} that registers the provided auth filters + * to resource methods annotated with the {@link Auth} according to + * the type of the annotated method parameter. + */ +public class PolymorphicAuthDynamicFeature implements DynamicFeature { + + private final ImmutableMap, ContainerRequestFilter> authFilterMap; + + public PolymorphicAuthDynamicFeature( + ImmutableMap, ContainerRequestFilter> authFilterMap + ) { + this.authFilterMap = authFilterMap; + } + + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + final AnnotatedMethod am = new AnnotatedMethod(resourceInfo.getResourceMethod()); + final Annotation[][] parameterAnnotations = am.getParameterAnnotations(); + final Class[] parameterTypes = am.getParameterTypes(); + + for (int i = 0; i < parameterAnnotations.length; i++) { + for (final Annotation annotation : parameterAnnotations[i]) { + if (annotation instanceof Auth && authFilterMap.containsKey(parameterTypes[i])) { + context.register(authFilterMap.get(parameterTypes[i])); + return; + } + } + } + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/PolymorphicAuthValueFactoryProvider.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/PolymorphicAuthValueFactoryProvider.java new file mode 100644 index 00000000000..384284bf92d --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/PolymorphicAuthValueFactoryProvider.java @@ -0,0 +1,110 @@ +package io.dropwizard.auth; + +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +import javax.inject.Inject; +import javax.inject.Singleton; +import java.security.Principal; +import java.util.Set; + +/** + * Value factory provider supporting injection of a hierarchy of + * {@link Principal} subclasses by the {@link Auth} annotation. + * + * @param the type acting as the superclass from which injected + * principals inherit + */ +@Singleton +public class PolymorphicAuthValueFactoryProvider extends AbstractValueFactoryProvider { + /** + * Set of provided {@link Principal} subclasses. + */ + protected final Set> principalClassSet; + + + /** + * {@link Principal} value factory provider injection constructor. + * + * @param mpep multivalued parameter extractor provider + * @param injector injector instance + * @param principalClassSetProvider provider(s) of the principal class + */ + @Inject + public PolymorphicAuthValueFactoryProvider( + MultivaluedParameterExtractorProvider mpep, + ServiceLocator injector, + PrincipalClassSetProvider principalClassSetProvider + ) { + super(mpep, injector, Parameter.Source.UNKNOWN); + this.principalClassSet = principalClassSetProvider.clazzSet; + } + + /** + * Return a factory for the provided parameter. We only expect objects of + * the type {@link T} being annotated with {@link Auth} annotation. + * + * @param parameter parameter that was annotated for being injected + * @return the factory if annotated parameter matched type + */ + @Override + public AbstractContainerRequestValueFactory createValueFactory(Parameter parameter) { + if (!parameter.isAnnotationPresent(Auth.class) || !principalClassSet.contains(parameter.getRawType())) { + return null; + } else { + return new PrincipalContainerRequestValueFactory(); + } + } + + @Singleton + static class AuthInjectionResolver extends ParamInjectionResolver { + + /** + * Create new {@link Auth} annotation injection resolver. + */ + public AuthInjectionResolver() { + super(PolymorphicAuthValueFactoryProvider.class); + } + } + + @Singleton + protected static class PrincipalClassSetProvider { + + private final Set> clazzSet; + + public PrincipalClassSetProvider(Set> clazzSet) { + this.clazzSet = clazzSet; + } + } + + /** + * Injection binder for {@link PolymorphicAuthValueFactoryProvider} and + * {@link AuthInjectionResolver}. + * + * @param the type of the principal + */ + public static class Binder extends AbstractBinder { + + private final Set> principalClassSet; + + public Binder(Set> principalClassSet) { + this.principalClassSet = principalClassSet; + } + + @Override + protected void configure() { + bind(new PrincipalClassSetProvider<>(principalClassSet)).to(PrincipalClassSetProvider.class); + bind(PolymorphicAuthValueFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(AuthInjectionResolver.class).to(new TypeLiteral>() { + }).in(Singleton.class); + } + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/PrincipalContainerRequestValueFactory.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/PrincipalContainerRequestValueFactory.java new file mode 100644 index 00000000000..c5f3d223aee --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/PrincipalContainerRequestValueFactory.java @@ -0,0 +1,25 @@ +package io.dropwizard.auth; + +import org.glassfish.jersey.server.ContainerRequest; +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +import java.security.Principal; + +/** + * A value factory which extracts the {@link Principal} from the + * current {@link ContainerRequest} instance. + */ +class PrincipalContainerRequestValueFactory extends AbstractContainerRequestValueFactory { + /** + * @return {@link Principal} stored on the request, or {@code null} + * if no object was found. + */ + @Override + public Principal provide() { + final Principal principal = getContainerRequest().getSecurityContext().getUserPrincipal(); + if (principal == null) { + throw new IllegalStateException("Cannot inject a custom principal into unauthenticated request"); + } + return principal; + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/PrincipalImpl.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/PrincipalImpl.java new file mode 100644 index 00000000000..5b1f3beddb1 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/PrincipalImpl.java @@ -0,0 +1,42 @@ +package io.dropwizard.auth; + +import com.google.common.base.MoreObjects; + +import java.security.Principal; +import java.util.Objects; + +public class PrincipalImpl implements Principal { + private final String name; + + public PrincipalImpl(String name) { + this.name = name; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + final PrincipalImpl principal = (PrincipalImpl) o; + return Objects.equals(this.name, principal.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(name); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("name", name).toString(); + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/UnauthorizedHandler.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/UnauthorizedHandler.java new file mode 100644 index 00000000000..a6c8f276bef --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/UnauthorizedHandler.java @@ -0,0 +1,7 @@ +package io.dropwizard.auth; + +import javax.ws.rs.core.Response; + +public interface UnauthorizedHandler { + Response buildResponse(String prefix, String realm); +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicCredentialAuthFilter.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicCredentialAuthFilter.java new file mode 100644 index 00000000000..83e98cae465 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/basic/BasicCredentialAuthFilter.java @@ -0,0 +1,89 @@ +package io.dropwizard.auth.basic; + +import com.google.common.io.BaseEncoding; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.Authenticator; + +import javax.annotation.Nullable; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.Principal; + +@Priority(Priorities.AUTHENTICATION) +public class BasicCredentialAuthFilter

    extends AuthFilter { + + private BasicCredentialAuthFilter() { + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + final BasicCredentials credentials = + getCredentials(requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + if (!authenticate(requestContext, credentials, SecurityContext.BASIC_AUTH)) { + throw new WebApplicationException(unauthorizedHandler.buildResponse(prefix, realm)); + } + } + + /** + * Parses a Base64-encoded value of the `Authorization` header + * in the form of `Basic dXNlcm5hbWU6cGFzc3dvcmQ=`. + * + * @param header the value of the `Authorization` header + * @return a username and a password as {@link BasicCredentials} + */ + @Nullable + private BasicCredentials getCredentials(String header) { + if (header == null) { + return null; + } + + final int space = header.indexOf(' '); + if (space <= 0) { + return null; + } + + final String method = header.substring(0, space); + if (!prefix.equalsIgnoreCase(method)) { + return null; + } + + final String decoded; + try { + decoded = new String(BaseEncoding.base64().decode(header.substring(space + 1)), StandardCharsets.UTF_8); + } catch (IllegalArgumentException e) { + logger.warn("Error decoding credentials", e); + return null; + } + + // Decoded credentials is 'username:password' + final int i = decoded.indexOf(':'); + if (i <= 0) { + return null; + } + + final String username = decoded.substring(0, i); + final String password = decoded.substring(i + 1); + return new BasicCredentials(username, password); + } + + /** + * Builder for {@link BasicCredentialAuthFilter}. + *

    An {@link Authenticator} must be provided during the building process.

    + * + * @param

    the principal + */ + public static class Builder

    extends + AuthFilterBuilder> { + + @Override + protected BasicCredentialAuthFilter

    newInstance() { + return new BasicCredentialAuthFilter<>(); + } + } +} 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 00000000000..e0b287a5e1c --- /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.MoreObjects; + +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +/** + * 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 = requireNonNull(username); + this.password = requireNonNull(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 int hashCode() { + return Objects.hash(username, password); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final BasicCredentials other = (BasicCredentials) obj; + return Objects.equals(this.username, other.username) && Objects.equals(this.password, other.password); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("username", username) + .add("password", "**********") + .toString(); + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/chained/ChainedAuthFilter.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/chained/ChainedAuthFilter.java new file mode 100644 index 00000000000..e7bba74116f --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/chained/ChainedAuthFilter.java @@ -0,0 +1,57 @@ +package io.dropwizard.auth.chained; + +import io.dropwizard.auth.AuthFilter; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.security.Principal; +import java.util.List; + +/** + * Chains together authFilters, short circuits when the first filter + * successfully authenticates + * + * N.B. AuthFilters can be chained together as long as they produce the same type + * of Principal. This is not enforced by the type system at compile time, using + * inconsistent principals will lead to runtime errors + * + * There is no requirement for the filters that are chained to use the same type for credentials. + * The reason is that the ChainedFilter delegates to a filter which encapsulates + * the authenticator and credential type + * + * + * @param the type of Credentials to be authenticated + * @param

    the type of the Principal + */ +@Priority(Priorities.AUTHENTICATION) +public class ChainedAuthFilter extends AuthFilter { + private final List handlers; + + public ChainedAuthFilter(List handlers) { + this.handlers = handlers; + } + + @Override + public void filter(ContainerRequestContext containerRequestContext) throws IOException { + WebApplicationException firstException = null; + for (AuthFilter authFilter : handlers) { + final SecurityContext securityContext = containerRequestContext.getSecurityContext(); + try { + authFilter.filter(containerRequestContext); + if (securityContext != containerRequestContext.getSecurityContext()) { + return; + } + } catch (WebApplicationException e) { + if (firstException == null) { + firstException = e; + } + } + } + + throw firstException; + } +} diff --git a/dropwizard-auth/src/main/java/io/dropwizard/auth/oauth/OAuthCredentialAuthFilter.java b/dropwizard-auth/src/main/java/io/dropwizard/auth/oauth/OAuthCredentialAuthFilter.java new file mode 100644 index 00000000000..080b3532704 --- /dev/null +++ b/dropwizard-auth/src/main/java/io/dropwizard/auth/oauth/OAuthCredentialAuthFilter.java @@ -0,0 +1,82 @@ +package io.dropwizard.auth.oauth; + +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.Authenticator; + +import javax.annotation.Nullable; +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.SecurityContext; +import java.io.IOException; +import java.security.Principal; + +@Priority(Priorities.AUTHENTICATION) +public class OAuthCredentialAuthFilter

    extends AuthFilter { + + /** + * Query parameter used to pass Bearer token + * + * @see The OAuth 2.0 Authorization Framework: Bearer Token Usage + */ + public static final String OAUTH_ACCESS_TOKEN_PARAM = "access_token"; + + private OAuthCredentialAuthFilter() { + } + + @Override + public void filter(final ContainerRequestContext requestContext) throws IOException { + String credentials = getCredentials(requestContext.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)); + + // If Authorization header is not used, check query parameter where token can be passed as well + if (credentials == null) { + credentials = requestContext.getUriInfo().getQueryParameters().getFirst(OAUTH_ACCESS_TOKEN_PARAM); + } + + if (!authenticate(requestContext, credentials, SecurityContext.BASIC_AUTH)) { + throw new WebApplicationException(unauthorizedHandler.buildResponse(prefix, realm)); + } + } + + /** + * Parses a value of the `Authorization` header in the form of `Bearer a892bf3e284da9bb40648ab10`. + * + * @param header the value of the `Authorization` header + * @return a token + */ + @Nullable + private String getCredentials(String header) { + if (header == null) { + return null; + } + + final int space = header.indexOf(' '); + if (space <= 0) { + return null; + } + + final String method = header.substring(0, space); + if (!prefix.equalsIgnoreCase(method)) { + return null; + } + + return header.substring(space + 1); + } + + /** + * Builder for {@link OAuthCredentialAuthFilter}. + *

    An {@link Authenticator} must be provided during the building process.

    + * + * @param

    the type of the principal + */ + public static class Builder

    + extends AuthFilterBuilder> { + + @Override + protected OAuthCredentialAuthFilter

    newInstance() { + return new OAuthCredentialAuthFilter<>(); + } + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/AbstractAuthResourceConfig.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/AbstractAuthResourceConfig.java new file mode 100644 index 00000000000..758794f12af --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/AbstractAuthResourceConfig.java @@ -0,0 +1,48 @@ +package io.dropwizard.auth; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; + +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import java.security.Principal; + +public abstract class AbstractAuthResourceConfig extends DropwizardResourceConfig { + + public AbstractAuthResourceConfig() { + super(true, new MetricRegistry()); + register(getAuthDynamicFeature(getAuthFilter())); + register(getAuthBinder()); + register(RolesAllowedDynamicFeature.class); + } + + /** + * @return The type of injected principal instance. + */ + protected Class getPrincipalClass() { + return Principal.class; + } + + /** + * @return The binder to use for injecting request authentication. + */ + protected AbstractBinder getAuthBinder() { + return new AuthValueFactoryProvider.Binder<>(getPrincipalClass()); + } + + /** + * @return The {@link DynamicFeature} used to register a request + * authentication provider. + */ + protected DynamicFeature getAuthDynamicFeature(ContainerRequestFilter authFilter) { + return new AuthDynamicFeature(authFilter); + } + + /** + * @return The {@link ContainerRequestFilter} to use for request + * authentication. + */ + protected abstract ContainerRequestFilter getAuthFilter(); +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthBaseTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthBaseTest.java new file mode 100644 index 00000000000..d450f844048 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthBaseTest.java @@ -0,0 +1,174 @@ +package io.dropwizard.auth; + +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public abstract class AuthBaseTest extends JerseyTest { + protected static final String ADMIN_ROLE = "ADMIN"; + protected static final String ADMIN_USER = "good-guy"; + protected static final String ORDINARY_USER = "ordinary-guy"; + protected static final String BADGUY_USER = "bad-guy"; + protected static final String CUSTOM_PREFIX = "Custom"; + protected static final String BEARER_PREFIX = "Bearer"; + protected static final String BASIC_PREFIX = "Basic"; + protected static final String ORDINARY_USER_ENCODED_TOKEN = "b3JkaW5hcnktZ3V5OnNlY3JldA=="; + protected static final String GOOD_USER_ENCODED_TOKEN = "Z29vZC1ndXk6c2VjcmV0"; + protected static final String BAD_USER_ENCODED_TOKEN = "YmFkLWd1eTpzZWNyZXQ="; + + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected TestContainerFactory getTestContainerFactory() + throws TestContainerException { + return new GrizzlyWebTestContainerFactory(); + } + + protected abstract DropwizardResourceConfig getDropwizardResourceConfig(); + protected abstract Class getDropwizardResourceConfigClass(); + protected abstract String getPrefix(); + protected abstract String getOrdinaryGuyValidToken(); + protected abstract String getGoodGuyValidToken(); + protected abstract String getBadGuyToken(); + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return ServletDeploymentContext.builder(getDropwizardResourceConfig()) + .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, getDropwizardResourceConfigClass().getName()) + .build(); + } + + @Test + public void respondsToMissingCredentialsWith401() throws Exception { + try { + target("/test/admin").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE)) + .containsOnly(getPrefix() + " realm=\"realm\""); + } + } + + @Test + public void resourceWithoutAuth200() { + assertThat(target("/test/noauth").request() + .get(String.class)) + .isEqualTo("hello"); + } + + @Test + public void resourceWithAuthenticationWithoutAuthorizationWithCorrectCredentials200() { + assertThat(target("/test/profile").request() + .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getOrdinaryGuyValidToken()) + .get(String.class)) + .isEqualTo("'" + ORDINARY_USER + "' has user privileges"); + } + + @Test + public void resourceWithAuthenticationWithoutAuthorizationNoCredentials401() { + try { + target("/test/profile").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE)) + .containsOnly(getPrefix() + " realm=\"realm\""); + } + } + + @Test + public void resourceWithAuthorizationPrincipalIsNotAuthorized403() { + try { + target("/test/admin").request() + .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getOrdinaryGuyValidToken()) + .get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(403); + } + } + + + @Test + public void resourceWithDenyAllAndNoAuth401() { + try { + target("/test/denied").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + } + } + + @Test + public void resourceWithDenyAllAndAuth403() { + try { + target("/test/denied").request() + .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getGoodGuyValidToken()) + .get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(403); + } + } + + @Test + public void transformsCredentialsToPrincipals() throws Exception { + assertThat(target("/test/admin").request() + .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getGoodGuyValidToken()) + .get(String.class)) + .isEqualTo("'" + ADMIN_USER + "' has admin privileges"); + } + + @Test + public void transformsCredentialsToPrincipalsWhenAuthAnnotationExistsWithoutMethodAnnotation() throws Exception { + assertThat(target("/test/implicit-permitall").request() + .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getGoodGuyValidToken()) + .get(String.class)) + .isEqualTo("'" + ADMIN_USER + "' has user privileges"); + } + + + @Test + public void respondsToNonBasicCredentialsWith401() throws Exception { + try { + target("/test/admin").request() + .header(HttpHeaders.AUTHORIZATION, "Derp irrelevant") + .get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE)) + .containsOnly(getPrefix() + " realm=\"realm\""); + } + } + + @Test + public void respondsToExceptionsWith500() throws Exception { + try { + target("/test/admin").request() + .header(HttpHeaders.AUTHORIZATION, getPrefix() + " " + getBadGuyToken()) + .get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(500); + } + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthFilterTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthFilterTest.java new file mode 100644 index 00000000000..a1ecf2f61b6 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthFilterTest.java @@ -0,0 +1,188 @@ +package io.dropwizard.auth; + +import io.dropwizard.auth.principal.NullPrincipal; +import org.junit.Test; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.core.Cookie; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Request; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.SecurityContext; +import javax.ws.rs.core.UriInfo; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.security.Principal; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; + +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AuthFilterTest { + + @Test + public void isSecureShouldStayTheSame() throws Exception { + ContainerRequestContext requestContext = new FakeSecureRequestContext(); + + new SimpleAuthFilter().filter(requestContext); + + assertTrue(requestContext.getSecurityContext().isSecure()); + } + + static class SimpleAuthFilter extends AuthFilter { + + SimpleAuthFilter() { + authenticator = credentials -> Optional.of(new NullPrincipal()); + authorizer = new PermitAllAuthorizer<>(); + } + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + authenticate(requestContext, "some-password", "SOME_SCHEME"); + } + } + + static class FakeSecureRequestContext implements ContainerRequestContext { + + private SecurityContext securityContext; + + FakeSecureRequestContext() { + securityContext = mock(SecurityContext.class); + when(securityContext.isSecure()).thenReturn(true); + } + + @Override + public SecurityContext getSecurityContext() { + return securityContext; + } + + @Override + public void setSecurityContext(SecurityContext context) { + this.securityContext = context; + } + + @Override + public Object getProperty(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Collection getPropertyNames() { + throw new UnsupportedOperationException(); + } + + @Override + public void setProperty(String name, Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public void removeProperty(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public UriInfo getUriInfo() { + throw new UnsupportedOperationException(); + } + + @Override + public void setRequestUri(URI requestUri) { + throw new UnsupportedOperationException(); + } + + @Override + public void setRequestUri(URI baseUri, URI requestUri) { + throw new UnsupportedOperationException(); + } + + @Override + public Request getRequest() { + throw new UnsupportedOperationException(); + } + + @Override + public String getMethod() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMethod(String method) { + throw new UnsupportedOperationException(); + } + + @Override + public MultivaluedMap getHeaders() { + throw new UnsupportedOperationException(); + } + + @Override + public String getHeaderString(String name) { + throw new UnsupportedOperationException(); + } + + @Override + public Date getDate() { + throw new UnsupportedOperationException(); + } + + @Override + public Locale getLanguage() { + throw new UnsupportedOperationException(); + } + + @Override + public int getLength() { + throw new UnsupportedOperationException(); + } + + @Override + public MediaType getMediaType() { + throw new UnsupportedOperationException(); + } + + @Override + public List getAcceptableMediaTypes() { + throw new UnsupportedOperationException(); + } + + @Override + public List getAcceptableLanguages() { + throw new UnsupportedOperationException(); + } + + @Override + public Map getCookies() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasEntity() { + throw new UnsupportedOperationException(); + } + + @Override + public InputStream getEntityStream() { + throw new UnsupportedOperationException(); + } + + @Override + public void setEntityStream(InputStream input) { + throw new UnsupportedOperationException(); + } + + + @Override + public void abortWith(Response response) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthResource.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthResource.java new file mode 100644 index 00000000000..53965cfeece --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/AuthResource.java @@ -0,0 +1,49 @@ +package io.dropwizard.auth; + + +import javax.annotation.security.DenyAll; +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.security.Principal; + +@Path("/test/") +@Produces(MediaType.TEXT_PLAIN) +public class AuthResource { + + @RolesAllowed({"ADMIN"}) + @GET + @Path("admin") + public String show(@Auth Principal principal) { + return "'" + principal.getName() + "' has admin privileges"; + } + + @PermitAll + @GET + @Path("profile") + public String showForEveryUser(@Auth Principal principal) { + return "'" + principal.getName() + "' has user privileges"; + } + + @GET + @Path("implicit-permitall") + public String implicitPermitAllAuthorization(@Auth Principal principal) { + return "'" + principal.getName() + "' has user privileges"; + } + + @GET + @Path("noauth") + public String hello() { + return "hello"; + } + + @DenyAll + @GET + @Path("denied") + public String denied() { + return "denied"; + } +} 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 00000000000..ac7fbd8b3ba --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthenticatorTest.java @@ -0,0 +1,129 @@ +package io.dropwizard.auth; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.collect.ImmutableSet; +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.InOrder; + +import java.security.Principal; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CachingAuthenticatorTest { + @SuppressWarnings("unchecked") + private final Authenticator underlying = mock(Authenticator.class); + private final CachingAuthenticator cached = + new CachingAuthenticator<>(new MetricRegistry(), underlying, CacheBuilderSpec.parse("maximumSize=1")); + @Rule + public ExpectedException expected = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + when(underlying.authenticate(anyString())).thenReturn(Optional.of(new PrincipalImpl("principal"))); + } + + @Test + public void cachesTheFirstReturnedPrincipal() throws Exception { + assertThat(cached.authenticate("credentials")).isEqualTo(Optional.of(new PrincipalImpl("principal"))); + assertThat(cached.authenticate("credentials")).isEqualTo(Optional.of(new PrincipalImpl("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 { + cached.authenticate("credentials"); + cached.invalidateAll("credentials"::equals); + 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"); + assertThat(cached.stats().loadCount()).isEqualTo(1); + assertThat(cached.size()).isEqualTo(1); + } + + @Test + public void shouldNotCacheAbsentPrincipals() throws Exception { + when(underlying.authenticate(anyString())).thenReturn(Optional.empty()); + assertThat(cached.authenticate("credentials")).isEqualTo(Optional.empty()); + verify(underlying).authenticate("credentials"); + assertThat(cached.size()).isEqualTo(0); + } + + @Test + public void shouldPropagateAuthenticationException() throws AuthenticationException { + final AuthenticationException e = new AuthenticationException("Auth failed"); + when(underlying.authenticate(anyString())).thenThrow(e); + expected.expect(CoreMatchers.sameInstance(e)); + cached.authenticate("credentials"); + } + + @Test + public void shouldPropagateRuntimeException() throws AuthenticationException { + final RuntimeException e = new NullPointerException(); + when(underlying.authenticate(anyString())).thenThrow(e); + expected.expect(CoreMatchers.sameInstance(e)); + cached.authenticate("credentials"); + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthorizerTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthorizerTest.java new file mode 100644 index 00000000000..ee830651565 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/CachingAuthorizerTest.java @@ -0,0 +1,126 @@ +package io.dropwizard.auth; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.collect.ImmutableSet; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import java.security.Principal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyObject; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CachingAuthorizerTest { + @SuppressWarnings("unchecked") + private final Authorizer underlying = mock(Authorizer.class); + private final CachingAuthorizer cached = new CachingAuthorizer<>( + new MetricRegistry(), + underlying, + CacheBuilderSpec.parse("maximumSize=1") + ); + + private final Principal principal = new PrincipalImpl("principal"); + private final Principal principal2 = new PrincipalImpl("principal2"); + private final Principal principal3 = new PrincipalImpl("principal3"); + private final String role = "popular_kids"; + + @Before + public void setUp() throws Exception { + when(underlying.authorize(anyObject(), anyString())).thenReturn(true); + } + + @Test + public void cachesTheFirstReturnedPrincipal() throws Exception { + assertThat(cached.authorize(principal, role)).isTrue(); + assertThat(cached.authorize(principal, role)).isTrue(); + + verify(underlying, times(1)).authorize(principal, role); + } + + @Test + public void respectsTheCacheConfiguration() throws Exception { + cached.authorize(principal, role); + cached.authorize(principal2, role); + cached.authorize(principal, role); + + final InOrder inOrder = inOrder(underlying); + inOrder.verify(underlying, times(1)).authorize(principal, role); + inOrder.verify(underlying, times(1)).authorize(principal2, role); + inOrder.verify(underlying, times(1)).authorize(principal, role); + } + + @Test + public void invalidatesPrincipalAndRole() throws Exception { + cached.authorize(principal, role); + cached.invalidate(principal, role); + cached.authorize(principal, role); + + verify(underlying, times(2)).authorize(principal, role); + } + + @Test + public void invalidatesSinglePrincipal() throws Exception { + cached.authorize(principal, role); + cached.invalidate(principal); + cached.authorize(principal, role); + + verify(underlying, times(2)).authorize(principal, role); + } + + @Test + public void invalidatesSetsofPrincipals() throws Exception { + cached.authorize(principal, role); + cached.authorize(principal2, role); + cached.invalidateAll(ImmutableSet.of(principal, principal2)); + cached.authorize(principal, role); + cached.authorize(principal2, role); + + verify(underlying, times(2)).authorize(principal, role); + verify(underlying, times(2)).authorize(principal2, role); + } + + @Test + public void invalidatesPrincipalsMatchingGivenPredicate() throws Exception { + cached.authorize(principal, role); + cached.invalidateAll(principal::equals); + cached.authorize(principal, role); + + verify(underlying, times(2)).authorize(principal, role); + } + + @Test + public void invalidatesAllPrincipals() throws Exception { + cached.authorize(principal, role); + cached.authorize(principal2, role); + cached.invalidateAll(); + cached.authorize(principal, role); + cached.authorize(principal2, role); + + verify(underlying, times(2)).authorize(principal, role); + verify(underlying, times(2)).authorize(principal2, role); + } + + @Test + public void calculatesTheSizeOfTheCache() throws Exception { + assertThat(cached.size()).isEqualTo(0); + cached.authorize(principal, role); + assertThat(cached.size()).isEqualTo(1); + cached.invalidateAll(); + assertThat(cached.size()).isEqualTo(0); + } + + @Test + public void calculatesCacheStats() throws Exception { + cached.authorize(principal, role); + assertThat(cached.stats().loadCount()).isEqualTo(0); + assertThat(cached.size()).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 00000000000..a8d94eea486 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicAuthProviderTest.java @@ -0,0 +1,56 @@ +package io.dropwizard.auth.basic; + +import com.google.common.collect.ImmutableList; +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.AuthBaseTest; +import io.dropwizard.auth.AuthResource; +import io.dropwizard.auth.util.AuthUtil; +import io.dropwizard.jersey.DropwizardResourceConfig; + +import javax.ws.rs.container.ContainerRequestFilter; +import java.security.Principal; + +public class BasicAuthProviderTest extends AuthBaseTest { + public static class BasicAuthTestResourceConfig extends AbstractAuthResourceConfig { + public BasicAuthTestResourceConfig() { + register(AuthResource.class); + } + + @Override protected ContainerRequestFilter getAuthFilter() { + BasicCredentialAuthFilter.Builder builder = new BasicCredentialAuthFilter.Builder<>(); + builder.setAuthorizer(AuthUtil.getTestAuthorizer(ADMIN_USER, ADMIN_ROLE)); + builder.setAuthenticator(AuthUtil.getBasicAuthenticator(ImmutableList.of(ADMIN_USER, ORDINARY_USER))); + return builder.buildAuthFilter(); + } + } + + @Override + protected DropwizardResourceConfig getDropwizardResourceConfig() { + return new BasicAuthTestResourceConfig(); + } + + @Override + protected Class getDropwizardResourceConfigClass() { + return BasicAuthTestResourceConfig.class; + } + + @Override + protected String getPrefix() { + return BASIC_PREFIX; + } + + @Override + protected String getOrdinaryGuyValidToken() { + return ORDINARY_USER_ENCODED_TOKEN; + } + + @Override + protected String getGoodGuyValidToken() { + return GOOD_USER_ENCODED_TOKEN; + } + + @Override + protected String getBadGuyToken() { + return BAD_USER_ENCODED_TOKEN; + } +} 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 00000000000..9fd3a488ed1 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCredentialsTest.java @@ -0,0 +1,42 @@ +package io.dropwizard.auth.basic; + +import org.junit.Test; + +import static org.assertj.core.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=**********}"); + } +} \ No newline at end of file diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCustomAuthProviderTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCustomAuthProviderTest.java new file mode 100644 index 00000000000..80f1f0b385a --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/basic/BasicCustomAuthProviderTest.java @@ -0,0 +1,58 @@ +package io.dropwizard.auth.basic; + +import com.google.common.collect.ImmutableList; +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.AuthBaseTest; +import io.dropwizard.auth.AuthResource; +import io.dropwizard.auth.util.AuthUtil; +import io.dropwizard.jersey.DropwizardResourceConfig; + +import javax.ws.rs.container.ContainerRequestFilter; +import java.security.Principal; + +public class BasicCustomAuthProviderTest extends AuthBaseTest { + + public static class BasicAuthTestResourceConfig extends AbstractAuthResourceConfig { + public BasicAuthTestResourceConfig() { + register(AuthResource.class); + } + + @Override protected ContainerRequestFilter getAuthFilter() { + BasicCredentialAuthFilter.Builder builder = new BasicCredentialAuthFilter.Builder<>(); + builder.setAuthorizer(AuthUtil.getTestAuthorizer(ADMIN_USER, ADMIN_ROLE)); + builder.setAuthenticator(AuthUtil.getBasicAuthenticator(ImmutableList.of(ADMIN_USER, ORDINARY_USER))); + builder.setPrefix(CUSTOM_PREFIX); + return builder.buildAuthFilter(); + } + } + + @Override + protected DropwizardResourceConfig getDropwizardResourceConfig() { + return new BasicAuthTestResourceConfig(); + } + + @Override + protected Class getDropwizardResourceConfigClass() { + return BasicAuthTestResourceConfig.class; + } + + @Override + protected String getPrefix() { + return CUSTOM_PREFIX; + } + + @Override + protected String getOrdinaryGuyValidToken() { + return ORDINARY_USER_ENCODED_TOKEN; + } + + @Override + protected String getGoodGuyValidToken() { + return GOOD_USER_ENCODED_TOKEN; + } + + @Override + protected String getBadGuyToken() { + return BAD_USER_ENCODED_TOKEN; + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/chained/ChainedAuthProviderTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/chained/ChainedAuthProviderTest.java new file mode 100644 index 00000000000..ae93e793c27 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/chained/ChainedAuthProviderTest.java @@ -0,0 +1,94 @@ +package io.dropwizard.auth.chained; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import io.dropwizard.auth.AuthBaseTest; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.AuthResource; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.Authorizer; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.auth.basic.BasicCredentials; +import io.dropwizard.auth.oauth.OAuthCredentialAuthFilter; +import io.dropwizard.auth.util.AuthUtil; +import io.dropwizard.jersey.DropwizardResourceConfig; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; +import org.junit.Test; + +import javax.ws.rs.core.HttpHeaders; +import java.security.Principal; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ChainedAuthProviderTest extends AuthBaseTest { + private static final String BEARER_USER = "A12B3C4D"; + public static class ChainedAuthTestResourceConfig extends DropwizardResourceConfig { + @SuppressWarnings("unchecked") + public ChainedAuthTestResourceConfig() { + super(true, new MetricRegistry()); + + final Authorizer authorizer = AuthUtil.getTestAuthorizer(ADMIN_USER, ADMIN_ROLE); + final AuthFilter basicAuthFilter = new BasicCredentialAuthFilter.Builder<>() + .setAuthenticator(AuthUtil.getBasicAuthenticator(ImmutableList.of(ADMIN_USER, ORDINARY_USER))) + .setAuthorizer(authorizer) + .buildAuthFilter(); + + final AuthFilter oAuthFilter = new OAuthCredentialAuthFilter.Builder<>() + .setAuthenticator(AuthUtil.getSingleUserOAuthAuthenticator(BEARER_USER, ADMIN_USER)) + .setPrefix(BEARER_PREFIX) + .setAuthorizer(authorizer) + .buildAuthFilter(); + + register(new AuthValueFactoryProvider.Binder(Principal.class)); + register(new AuthDynamicFeature(new ChainedAuthFilter<>(buildHandlerList(basicAuthFilter, oAuthFilter)))); + register(RolesAllowedDynamicFeature.class); + register(AuthResource.class); + } + + @SuppressWarnings("unchecked") + public List buildHandlerList(AuthFilter basicAuthFilter, + AuthFilter oAuthFilter) { + return ImmutableList.of(basicAuthFilter, oAuthFilter); + } + } + + @Test + public void transformsBearerCredentialsToPrincipals() throws Exception { + assertThat(target("/test/admin").request() + .header(HttpHeaders.AUTHORIZATION, BEARER_PREFIX + " " + BEARER_USER) + .get(String.class)) + .isEqualTo("'" + ADMIN_USER + "' has admin privileges"); + } + + @Override + protected DropwizardResourceConfig getDropwizardResourceConfig() { + return new ChainedAuthTestResourceConfig(); + } + + @Override + protected Class getDropwizardResourceConfigClass() { + return ChainedAuthTestResourceConfig.class; + } + + @Override + protected String getPrefix() { + return BASIC_PREFIX; + } + + @Override + protected String getOrdinaryGuyValidToken() { + return ORDINARY_USER_ENCODED_TOKEN; + } + + @Override + protected String getGoodGuyValidToken() { + return GOOD_USER_ENCODED_TOKEN; + } + + @Override + protected String getBadGuyToken() { + return BAD_USER_ENCODED_TOKEN; + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthCustomProviderTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthCustomProviderTest.java new file mode 100644 index 00000000000..6ebdd671a2b --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthCustomProviderTest.java @@ -0,0 +1,55 @@ +package io.dropwizard.auth.oauth; + +import com.google.common.collect.ImmutableList; +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.AuthBaseTest; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.AuthResource; +import io.dropwizard.auth.util.AuthUtil; +import io.dropwizard.jersey.DropwizardResourceConfig; + +public class OAuthCustomProviderTest extends AuthBaseTest { + public static class OAuthTestResourceConfig extends AbstractAuthResourceConfig { + public OAuthTestResourceConfig() { + register(AuthResource.class); + } + + @Override protected AuthFilter getAuthFilter() { + return new OAuthCredentialAuthFilter.Builder<>() + .setAuthenticator(AuthUtil.getMultiplyUsersOAuthAuthenticator(ImmutableList.of(ADMIN_USER, ORDINARY_USER))) + .setAuthorizer(AuthUtil.getTestAuthorizer(ADMIN_USER, ADMIN_ROLE)) + .setPrefix(CUSTOM_PREFIX) + .buildAuthFilter(); + } + } + + @Override + protected DropwizardResourceConfig getDropwizardResourceConfig() { + return new OAuthProviderTest.OAuthTestResourceConfig(); + } + + @Override + protected Class getDropwizardResourceConfigClass() { + return OAuthTestResourceConfig.class; + } + + @Override + protected String getPrefix() { + return CUSTOM_PREFIX; + } + + @Override + protected String getOrdinaryGuyValidToken() { + return ORDINARY_USER; + } + + @Override + protected String getGoodGuyValidToken() { + return ADMIN_USER; + } + + @Override + protected String getBadGuyToken() { + return BADGUY_USER; + } +} 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 00000000000..e487e2c43e1 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/oauth/OAuthProviderTest.java @@ -0,0 +1,69 @@ +package io.dropwizard.auth.oauth; + +import com.google.common.collect.ImmutableList; + +import org.junit.Test; + +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.AuthBaseTest; +import io.dropwizard.auth.AuthFilter; +import io.dropwizard.auth.AuthResource; +import io.dropwizard.auth.util.AuthUtil; +import io.dropwizard.jersey.DropwizardResourceConfig; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OAuthProviderTest extends AuthBaseTest { + public static class OAuthTestResourceConfig extends AbstractAuthResourceConfig { + public OAuthTestResourceConfig() { + register(AuthResource.class); + } + + @Override protected AuthFilter getAuthFilter() { + return new OAuthCredentialAuthFilter.Builder<>() + .setAuthenticator(AuthUtil.getMultiplyUsersOAuthAuthenticator(ImmutableList.of(ADMIN_USER, ORDINARY_USER))) + .setAuthorizer(AuthUtil.getTestAuthorizer(ADMIN_USER, ADMIN_ROLE)) + .setPrefix(BEARER_PREFIX) + .buildAuthFilter(); + } + } + + @Test + public void checksQueryStringAccessTokenIfAuthorizationHeaderMissing() { + assertThat(target("/test/profile") + .queryParam(OAuthCredentialAuthFilter.OAUTH_ACCESS_TOKEN_PARAM, getOrdinaryGuyValidToken()) + .request() + .get(String.class)) + .isEqualTo("'" + ORDINARY_USER + "' has user privileges"); + } + + @Override + protected DropwizardResourceConfig getDropwizardResourceConfig() { + return new OAuthTestResourceConfig(); + } + + @Override + protected Class getDropwizardResourceConfigClass() { + return OAuthTestResourceConfig.class; + } + + @Override + protected String getPrefix() { + return BEARER_PREFIX; + } + + @Override + protected String getOrdinaryGuyValidToken() { + return "ordinary-guy"; + } + + @Override + protected String getGoodGuyValidToken() { + return "good-guy"; + } + + @Override + protected String getBadGuyToken() { + return "bad-guy"; + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/JsonPrincipal.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/JsonPrincipal.java new file mode 100644 index 00000000000..d8f6ef4ebaa --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/JsonPrincipal.java @@ -0,0 +1,16 @@ +package io.dropwizard.auth.principal; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.auth.PrincipalImpl; + +/** + * Principal instance supporting JSON deserialization. + */ +public class JsonPrincipal extends PrincipalImpl { + + @JsonCreator + public JsonPrincipal(@JsonProperty("name") String name) { + super(name); + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPolymorphicPrincipalEntityResource.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPolymorphicPrincipalEntityResource.java new file mode 100644 index 00000000000..74f9f33f253 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPolymorphicPrincipalEntityResource.java @@ -0,0 +1,69 @@ +package io.dropwizard.auth.principal; + +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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Contains resource methods which don't authenticate but use + * multi-principal injection and thus might be affected by + * authentication logic. + */ +@Path("/no-auth-test") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.TEXT_PLAIN) +public class NoAuthPolymorphicPrincipalEntityResource { + + /** + * Principal instance must be injected even when no authentication is required. + */ + @POST + @Path("json-principal-entity") + public String principalEntityWithoutAuth(JsonPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + /** + * Principal instance must be injected even when no authentication is required. + */ + @POST + @Path("null-principal-entity") + public String principalEntityWithoutAuth(NullPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + /** + * Annotated principal instance must be injected even when no authentication is required. + */ + @POST + @Path("annotated-json-principal-entity") + public String annotatedPrincipalEntityWithoutAuth(@DummyAnnotation JsonPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + /** + * Annotated principal instance must be injected even when no authentication is required. + */ + @POST + @Path("annotated-null-principal-entity") + public String annotatedPrincipalEntityWithoutAuth(@DummyAnnotation NullPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.PARAMETER }) + public @interface DummyAnnotation { + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPolymorphicPrincipalEntityTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPolymorphicPrincipalEntityTest.java new file mode 100644 index 00000000000..4e84ff390d3 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPolymorphicPrincipalEntityTest.java @@ -0,0 +1,120 @@ +package io.dropwizard.auth.principal; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.PolymorphicAuthDynamicFeature; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import java.security.Principal; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Testing that principal entity is not affected by authentication logic and can be injected as any other entity. + */ +public class NoAuthPolymorphicPrincipalEntityTest extends JerseyTest { + + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return ServletDeploymentContext + .builder(new NoAuthPolymorphicPrincipalInjectedResourceConfig()) + .initParam( + ServletProperties.JAXRS_APPLICATION_CLASS, + NoAuthPolymorphicPrincipalInjectedResourceConfig.class.getName()) + .build(); + } + + public static class NoAuthPolymorphicPrincipalInjectedResourceConfig extends AbstractAuthResourceConfig { + + public NoAuthPolymorphicPrincipalInjectedResourceConfig() { + register(NoAuthPolymorphicPrincipalEntityResource.class); + packages("io.dropwizard.jersey.jackson"); + } + + @Override protected Class getPrincipalClass() { + throw new AssertionError("Authentication must not be performed"); + } + + @Override protected ContainerRequestFilter getAuthFilter() { + return requestContext -> { + throw new AssertionError("Authentication must not be performed"); + }; + } + + @Override protected AbstractBinder getAuthBinder() { + return new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(JsonPrincipal.class, NullPrincipal.class)); + } + + @Override protected DynamicFeature getAuthDynamicFeature(ContainerRequestFilter authFilter) { + return new PolymorphicAuthDynamicFeature(ImmutableMap.of( + JsonPrincipal.class, getAuthFilter(), + NullPrincipal.class, getAuthFilter() + )); + } + } + + @Test + public void jsonPrincipalEntityResourceWithoutAuth200() { + String principalName = "Astar Seran"; + assertThat(target("/no-auth-test/json-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Anything here") + .post(Entity.entity(new JsonPrincipal(principalName), MediaType.APPLICATION_JSON)) + .readEntity(String.class)) + .isEqualTo(principalName); + } + + @Test + public void nullPrincipalEntityResourceWithoutAuth200() { + assertThat(target("/no-auth-test/null-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Anything here") + .post(Entity.entity(new NullPrincipal(), MediaType.APPLICATION_JSON)) + .readEntity(String.class)) + .isEqualTo("null"); + } + + /** + * When parameter is annotated then Jersey classifies such + * parameter as {@link org.glassfish.jersey.server.model.Parameter.Source#UNKNOWN} + * instead of {@link org.glassfish.jersey.server.model.Parameter.Source#ENTITY} + * which is used for unannotated parameters. ValueFactoryProvider resolution + * logic is different for these two sources therefore must be tested separately. + */ + @Test + public void annotatedJsonPrincipalEntityResourceWithoutAuth200() { + String principalName = "Astar Seran"; + assertThat(target("/no-auth-test/annotated-json-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Anything here") + .post(Entity.entity(new JsonPrincipal(principalName), MediaType.APPLICATION_JSON)) + .readEntity(String.class)) + .isEqualTo(principalName); + } + + @Test + public void annotatedNullPrincipalEntityResourceWithoutAuth200() { + assertThat(target("/no-auth-test/annotated-null-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Anything here") + .post(Entity.entity(new NullPrincipal(), MediaType.APPLICATION_JSON)) + .readEntity(String.class)) + .isEqualTo("null"); + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPrincipalEntityResource.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPrincipalEntityResource.java new file mode 100644 index 00000000000..ce0738ec8fd --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPrincipalEntityResource.java @@ -0,0 +1,47 @@ +package io.dropwizard.auth.principal; + +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.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Contains resource methods which don't authenticate but use principal instance injection and thus might be affected by authentication logic. + */ +@Path("/no-auth-test") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.TEXT_PLAIN) +public class NoAuthPrincipalEntityResource { + + /** + * Principal instance must be injected even when no authentication is required. + */ + @POST + @Path("principal-entity") + public String principalEntityWithoutAuth(JsonPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + /** + * Annotated principal instance must be injected even when no authentication is required. + */ + @POST + @Path("annotated-principal-entity") + public String annotatedPrincipalEntityWithoutAuth(@DummyAnnotation JsonPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.PARAMETER }) + public @interface DummyAnnotation { + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPrincipalEntityTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPrincipalEntityTest.java new file mode 100644 index 00000000000..4eecfc1e6ad --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NoAuthPrincipalEntityTest.java @@ -0,0 +1,94 @@ +package io.dropwizard.auth.principal; + +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import java.security.Principal; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Testing that principal entity is not affected by authentication logic and can be injected as any other entity. + */ +public class NoAuthPrincipalEntityTest extends JerseyTest { + + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return ServletDeploymentContext + .builder(new NoAuthPrincipalInjectedResourceConfig()) + .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, NoAuthPrincipalInjectedResourceConfig.class.getName()) + .build(); + } + + public static class NoAuthPrincipalInjectedResourceConfig extends AbstractAuthResourceConfig { + + public NoAuthPrincipalInjectedResourceConfig() { + register(NoAuthPrincipalEntityResource.class); + packages("io.dropwizard.jersey.jackson"); + } + + @Override protected Class getPrincipalClass() { + return JsonPrincipal.class; + } + + @Override protected ContainerRequestFilter getAuthFilter() { + return requestContext -> { + throw new AssertionError("Authentication must not be performed"); + }; + } + + @Override protected AbstractBinder getAuthBinder() { + return new AuthValueFactoryProvider.Binder<>(getPrincipalClass()); + } + + @Override protected DynamicFeature getAuthDynamicFeature(ContainerRequestFilter authFilter) { + return new AuthDynamicFeature(authFilter); + } + } + + @Test + public void principalEntityResourceWithoutAuth200() { + String principalName = "Astar Seran"; + assertThat(target("/no-auth-test/principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Anything here") + .post(Entity.entity(new JsonPrincipal(principalName), MediaType.APPLICATION_JSON)) + .readEntity(String.class)) + .isEqualTo(principalName); + } + + /** + * When parameter is annotated then Jersey classifies such parameter as + * {@link org.glassfish.jersey.server.model.Parameter.Source#UNKNOWN} instead of + * {@link org.glassfish.jersey.server.model.Parameter.Source#ENTITY} which + * is used for unannotated parameters. ValueFactoryProvider resolution logic is + * different for these two sources therefore must be tested separately. + */ + @Test + public void annotatedPrincipalEntityResourceWithoutAuth200() { + String principalName = "Astar Seran"; + assertThat(target("/no-auth-test/annotated-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Anything here") + .post(Entity.entity(new JsonPrincipal(principalName), MediaType.APPLICATION_JSON)) + .readEntity(String.class)) + .isEqualTo(principalName); + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NullPrincipal.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NullPrincipal.java new file mode 100644 index 00000000000..e966f0cf644 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/NullPrincipal.java @@ -0,0 +1,12 @@ +package io.dropwizard.auth.principal; + +import io.dropwizard.auth.PrincipalImpl; + +/** + * An empty principal. + */ +public class NullPrincipal extends PrincipalImpl { + public NullPrincipal() { + super("null"); + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/PolymorphicPrincipalEntityResource.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/PolymorphicPrincipalEntityResource.java new file mode 100644 index 00000000000..199c7da9a40 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/PolymorphicPrincipalEntityResource.java @@ -0,0 +1,34 @@ +package io.dropwizard.auth.principal; + +import io.dropwizard.auth.Auth; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Contains resource methods which are authenticated with + * multi-principal injection. + */ +@Path("/auth-test") +@Consumes(MediaType.APPLICATION_JSON) +@Produces(MediaType.TEXT_PLAIN) +public class PolymorphicPrincipalEntityResource { + @GET + @Path("json-principal-entity") + public String principalEntityWithoutAuth(@Auth JsonPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } + + @GET + @Path("null-principal-entity") + public String principalEntityWithoutAuth(@Auth NullPrincipal principal) { + assertThat(principal).isNotNull(); + return principal.getName(); + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/PolymorphicPrincipalEntityTest.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/PolymorphicPrincipalEntityTest.java new file mode 100644 index 00000000000..65bd7bb53be --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/principal/PolymorphicPrincipalEntityTest.java @@ -0,0 +1,143 @@ +package io.dropwizard.auth.principal; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.AbstractAuthResourceConfig; +import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.PolymorphicAuthDynamicFeature; +import io.dropwizard.auth.PolymorphicAuthValueFactoryProvider; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.auth.basic.BasicCredentials; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.core.HttpHeaders; +import java.security.Principal; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +/** + * Testing that polymorphic principal entity injection works. + */ +public class PolymorphicPrincipalEntityTest extends JerseyTest { + private static final String JSON_USERNAME = "good-guy"; + private static final String NULL_USERNAME = "bad-guy"; + private static final String JSON_USERNAME_ENCODED_TOKEN = "Z29vZC1ndXk6c2VjcmV0"; + private static final String NULL_USERNAME_ENCODED_TOKEN = "YmFkLWd1eTpzZWNyZXQ="; + + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return ServletDeploymentContext + .builder(new PolymorphicPrincipalInjectedResourceConfig()) + .initParam( + ServletProperties.JAXRS_APPLICATION_CLASS, + PolymorphicPrincipalInjectedResourceConfig.class.getName()) + .build(); + } + + public static class PolymorphicPrincipalInjectedResourceConfig extends AbstractAuthResourceConfig { + + public PolymorphicPrincipalInjectedResourceConfig() { + register(PolymorphicPrincipalEntityResource.class); + packages("io.dropwizard.jersey.jackson"); + } + + @Override protected Class getPrincipalClass() { + throw new AssertionError("getPrincipalClass must not be invoked"); + } + + @Override protected ContainerRequestFilter getAuthFilter() { + return requestContext -> { + throw new AssertionError("getAuthFilter result must not be invoked"); + }; + } + + @Override protected AbstractBinder getAuthBinder() { + return new PolymorphicAuthValueFactoryProvider.Binder<>( + ImmutableSet.of(JsonPrincipal.class, NullPrincipal.class)); + } + + @Override protected DynamicFeature getAuthDynamicFeature(ContainerRequestFilter authFilter) { + final Authenticator jsonAuthenticator = credentials -> { + if (credentials.getUsername().equals(JSON_USERNAME)) { + return Optional.of(new JsonPrincipal(credentials.getUsername())); + } else { + return Optional.empty(); + } + }; + + final Authenticator nullAuthenticator = credentials -> { + if (credentials.getUsername().equals(NULL_USERNAME)) { + return Optional.of(new NullPrincipal()); + } else { + return Optional.empty(); + } + }; + + final BasicCredentialAuthFilter jsonAuthFilter = new BasicCredentialAuthFilter.Builder() + .setAuthenticator(jsonAuthenticator) + .buildAuthFilter(); + + final BasicCredentialAuthFilter nullAuthFilter = new BasicCredentialAuthFilter.Builder() + .setAuthenticator(nullAuthenticator) + .buildAuthFilter(); + + return new PolymorphicAuthDynamicFeature(ImmutableMap.of( + JsonPrincipal.class, jsonAuthFilter, + NullPrincipal.class, nullAuthFilter + )); + } + } + + @Test + public void jsonPrincipalEntityResourceAuth200() { + assertThat(target("/auth-test/json-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Basic " + JSON_USERNAME_ENCODED_TOKEN) + .get(String.class)) + .isEqualTo(JSON_USERNAME); + } + + @Test + public void jsonPrincipalEntityResourceNoAuth401() { + try { + target("/auth-test/json-principal-entity").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + } + } + + @Test + public void nullPrincipalEntityResourceAuth200() { + assertThat(target("/auth-test/null-principal-entity").request() + .header(HttpHeaders.AUTHORIZATION, "Basic " + NULL_USERNAME_ENCODED_TOKEN) + .get(String.class)) + .isEqualTo("null"); + } + + @Test + public void nullPrincipalEntityResourceNoAuth401() { + try { + target("/auth-test/null-principal-entity").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + } + } +} diff --git a/dropwizard-auth/src/test/java/io/dropwizard/auth/util/AuthUtil.java b/dropwizard-auth/src/test/java/io/dropwizard/auth/util/AuthUtil.java new file mode 100644 index 00000000000..3e83d4e7172 --- /dev/null +++ b/dropwizard-auth/src/test/java/io/dropwizard/auth/util/AuthUtil.java @@ -0,0 +1,58 @@ +package io.dropwizard.auth.util; + +import io.dropwizard.auth.AuthenticationException; +import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.Authorizer; +import io.dropwizard.auth.PrincipalImpl; +import io.dropwizard.auth.basic.BasicCredentials; + +import java.security.Principal; +import java.util.List; +import java.util.Optional; + +public class AuthUtil { + + public static Authenticator getBasicAuthenticator(final List validUsers) { + return credentials -> { + if (validUsers.contains(credentials.getUsername()) && "secret".equals(credentials.getPassword())) { + return Optional.of(new PrincipalImpl(credentials.getUsername())); + } + if ("bad-guy".equals(credentials.getUsername())) { + throw new AuthenticationException("CRAP"); + } + return Optional.empty(); + }; + } + + public static Authenticator getSingleUserOAuthAuthenticator(final String presented, + final String returned) { + return user -> { + if (presented.equals(user)) { + return Optional.of(new PrincipalImpl(returned)); + } + if ("bad-guy".equals(user)) { + throw new AuthenticationException("CRAP"); + } + return Optional.empty(); + }; + } + + public static Authenticator getMultiplyUsersOAuthAuthenticator(final List validUsers) { + return credentials -> { + if (validUsers.contains(credentials)) { + return Optional.of(new PrincipalImpl(credentials)); + } + if ("bad-guy".equals(credentials)) { + throw new AuthenticationException("CRAP"); + } + return Optional.empty(); + }; + } + + public static Authorizer getTestAuthorizer(final String validUser, + final String validRole) { + return (principal, role) -> principal != null + && validUser.equals(principal.getName()) + && validRole.equals(role); + } +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-auth/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-benchmarks/pom.xml b/dropwizard-benchmarks/pom.xml new file mode 100644 index 00000000000..0e8c4098b8c --- /dev/null +++ b/dropwizard-benchmarks/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + dropwizard-parent + io.dropwizard + 1.0.1-SNAPSHOT + + + + + false + 1.12 + + true + true + true + true + true + + + dropwizard-benchmarks + Dropwizard Benchmarks + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + io.dropwizard + dropwizard-util + + + io.dropwizard + dropwizard-jersey + + + diff --git a/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/jersey/ConstraintViolationBenchmark.java b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/jersey/ConstraintViolationBenchmark.java new file mode 100644 index 00000000000..a728afa2d32 --- /dev/null +++ b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/jersey/ConstraintViolationBenchmark.java @@ -0,0 +1,98 @@ +package io.dropwizard.benchmarks.jersey; + +import io.dropwizard.jersey.validation.ConstraintMessage; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.server.model.Invocable; +import org.hibernate.validator.constraints.NotEmpty; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import javax.validation.ConstraintViolation; +import javax.validation.Valid; +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; +import javax.ws.rs.HeaderParam; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.apache.commons.lang3.reflect.MethodUtils.getAccessibleMethod; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class ConstraintViolationBenchmark { + + static { + BootstrapLogging.bootstrap(); + } + + public static class Resource { + public String paramFunc(@HeaderParam("cheese") @NotEmpty String secretSauce) { + return secretSauce; + } + + public String objectFunc(@Valid Foo foo) { + return foo.toString(); + } + } + + public static class Foo { + @NotEmpty + private String bar; + } + + private ConstraintViolation paramViolation; + private ConstraintViolation objViolation; + + final Invocable invocable = Invocable.create(request -> null); + + @Setup + public void prepare() { + final Validator validator = Validators.newValidator(); + final ExecutableValidator execValidator = validator.forExecutables(); + + final Set> paramViolations = + execValidator.validateParameters( + new Resource(), + getAccessibleMethod(ConstraintViolationBenchmark.Resource.class, "paramFunc", String.class), + new Object[]{""} // the parameter value + ); + paramViolation = paramViolations.iterator().next(); + + final Set> objViolations = + execValidator.validateParameters( + new Resource(), + getAccessibleMethod(ConstraintViolationBenchmark.Resource.class, "objectFunc", Foo.class), + new Object[]{new Foo()} // the parameter value + ); + objViolation = objViolations.iterator().next(); + } + + @Benchmark + public String paramViolation() { + return ConstraintMessage.getMessage(paramViolation, invocable); + } + + @Benchmark + public String objViolation() { + return ConstraintMessage.getMessage(objViolation, invocable); + } + + public static void main(String[] args) throws Exception { + new Runner(new OptionsBuilder() + .include(ConstraintViolationBenchmark.class.getSimpleName()) + .forks(1) + .warmupIterations(5) + .measurementIterations(5) + .build()) + .run(); + } +} diff --git a/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/jersey/DropwizardResourceConfigBenchmark.java b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/jersey/DropwizardResourceConfigBenchmark.java new file mode 100644 index 00000000000..b1837662d71 --- /dev/null +++ b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/jersey/DropwizardResourceConfigBenchmark.java @@ -0,0 +1,152 @@ +package io.dropwizard.benchmarks.jersey; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import io.dropwizard.jersey.DropwizardResourceConfig; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import java.util.List; +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@State(Scope.Benchmark) +public class DropwizardResourceConfigBenchmark { + + private DropwizardResourceConfig dropwizardResourceConfig = + new DropwizardResourceConfig(true, new MetricRegistry()); + + @Setup + public void setUp() { + dropwizardResourceConfig.register(DistributionResource.class); + dropwizardResourceConfig.register(AssetResource.class); + dropwizardResourceConfig.register(ClustersResource.class); + } + + @Benchmark + public String getEndpointsInfo() { + return dropwizardResourceConfig.getEndpointsInfo(); + } + + public static void main(String[] args) throws Exception { + new Runner(new OptionsBuilder() + .include(DropwizardResourceConfigBenchmark.class.getSimpleName()) + .forks(1) + .warmupIterations(10) + .measurementIterations(5) + .build()) + .run(); + } + + // Jersey resources (test data) + + @Path("assets") + public static class AssetResource { + + @POST + public String insert(String asset) { + return "id"; + } + + @GET + @Path("{id}") + public String get(@PathParam("id") String id) { + return "asset_by_id"; + } + + @GET + @Path("{id}/details") + public String getDetails(@PathParam("id") String id) { + return "asset_details"; + } + + @GET + public List getAll() { + return ImmutableList.of("first_asset", "second_asset"); + } + + @DELETE + @Path("{id}") + public void delete(@PathParam("id") String id) { + } + + @PUT + @Path("{id}") + public void update(@PathParam("id") String id, String asset) { + } + } + + @Path("distributions") + public static class DistributionResource { + + @POST + @Path("{assetId}/clusters/{code}/start") + public void start(@PathParam("assetId") String assetId, + @PathParam("code") String code) { + } + + @POST + @Path("{assetId}/clusters/{code}/complete") + public void complete(@PathParam("assetId") String assetId, + @PathParam("code") String code) { + } + + @POST + @Path("{assetId}/clusters/{code}/abort") + public void abort(@PathParam("assetId") String assetId, + @PathParam("code") String code) { + } + + @POST + @Path("{assetId}/clusters/{code}/delete") + public void delete(@PathParam("assetId") String assetId, + @PathParam("code") String code) { + } + + @GET + @Path("{assetId}/clusters/{code}") + public String getStatus(@PathParam("assetId") String assetId, + @PathParam("code") String code) { + return "distributed"; + } + } + + @Path("clusters") + public static class ClustersResource { + + @POST + public String insert(String cluster) { + return "code"; + } + + @GET + @Path("{code}") + public String get(@PathParam("code") String code) { + return "cluster_by_code"; + } + + @GET + public List getAll() { + return ImmutableList.of("first_cluster", "second_cluster", "third_cluster"); + } + + @DELETE + @Path("{code}") + public void delete(@PathParam("code") String code) { + } + } +} diff --git a/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/util/DurationBenchmark.java b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/util/DurationBenchmark.java new file mode 100644 index 00000000000..0e3728a1daa --- /dev/null +++ b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/util/DurationBenchmark.java @@ -0,0 +1,39 @@ +package io.dropwizard.benchmarks.util; + +import io.dropwizard.util.Duration; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class DurationBenchmark { + + /** + * Don't trust the IDE, it's advisedly non-final to avoid constant folding + */ + private String duration = "12h"; + + @Benchmark + public Duration parseDuration() { + return Duration.parse(duration); + } + + public static void main(String[] args) throws Exception { + new Runner(new OptionsBuilder() + .include(DurationBenchmark.class.getSimpleName()) + .forks(1) + .warmupIterations(5) + .measurementIterations(5) + .build()) + .run(); + } +} diff --git a/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/util/SizeBenchmark.java b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/util/SizeBenchmark.java new file mode 100644 index 00000000000..56b01a83087 --- /dev/null +++ b/dropwizard-benchmarks/src/main/java/io/dropwizard/benchmarks/util/SizeBenchmark.java @@ -0,0 +1,39 @@ +package io.dropwizard.benchmarks.util; + +import io.dropwizard.util.Size; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@State(Scope.Benchmark) +public class SizeBenchmark { + + /** + * Don't trust the IDE, it's advisedly non-final to avoid constant folding + */ + private String size = "256KiB"; + + @Benchmark + public Size parseSize() { + return Size.parse(size); + } + + public static void main(String[] args) throws Exception { + new Runner(new OptionsBuilder() + .include(SizeBenchmark.class.getSimpleName()) + .forks(1) + .warmupIterations(5) + .measurementIterations(5) + .build()) + .run(); + } +} diff --git a/dropwizard-bom/pom.xml b/dropwizard-bom/pom.xml new file mode 100644 index 00000000000..18d170cafe1 --- /dev/null +++ b/dropwizard-bom/pom.xml @@ -0,0 +1,834 @@ + + + 4.0.0 + + 3.0.1 + + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-bom + pom + Dropwizard BOM + + Bill of materials to make sure a consistent set of versions is used for Dropwizard modules. + + + + UTF-8 + UTF-8 + ${project.version} + 19.0 + 2.23.1 + 2.7.6 + 2.7.6 + 9.3.11.v20160721 + 3.0.0.v201112011016 + 3.1.2 + 1.7.21 + 1.1.7 + 1.4.192 + + + + + + org.objenesis + objenesis + 2.4 + + + org.apache.commons + commons-lang3 + 3.4 + + + com.google.guava + guava + ${guava.version} + + + net.sourceforge.argparse4j + argparse4j + 0.7.0 + + + com.google.code.findbugs + jsr305 + 3.0.1 + + + joda-time + joda-time + 2.9.4 + + + org.hibernate + hibernate-validator + 5.2.4.Final + + + org.glassfish + javax.el + 3.0.0 + + + javax.servlet + javax.servlet-api + 3.1.0 + + + org.apache.httpcomponents + httpclient + 4.5.2 + + + commons-logging + commons-logging + + + + + org.apache.tomcat + tomcat-jdbc + 8.5.4 + + + com.h2database + h2 + ${h2.version} + + + org.jadira.usertype + usertype.core + 5.0.0.GA + + + org.hibernate + hibernate-entitymanager + + + org.slf4j + slf4j-api + + + org.joda + joda-money + + + + + org.hibernate + hibernate-core + 5.1.0.Final + + + org.jboss.logging + jboss-logging + + + + + org.javassist + javassist + 3.20.0-GA + + + com.fasterxml + classmate + 1.3.1 + + + org.hsqldb + hsqldb + 2.3.4 + + + org.liquibase + liquibase-core + 3.5.1 + + + org.yaml + snakeyaml + + + + + com.mattbertolini + liquibase-slf4j + 2.0.0 + + + org.slf4j + slf4j-api + + + org.liquibase + liquibase-core + + + + + net.jcip + jcip-annotations + 1.0 + + + com.github.spullara.mustache.java + compiler + 0.9.3 + + + com.google.guava + guava + + + + + org.freemarker + freemarker + 2.3.23 + + + org.jdbi + jdbi + 2.73 + + + + + org.eclipse.jetty + jetty-server + ${jetty.version} + + + org.eclipse.jetty + jetty-util + ${jetty.version} + + + org.eclipse.jetty + jetty-webapp + ${jetty.version} + + + org.eclipse.jetty + jetty-continuation + ${jetty.version} + + + org.eclipse.jetty + jetty-servlet + ${jetty.version} + + + org.eclipse.jetty + jetty-servlet + tests + ${jetty.version} + + + org.eclipse.jetty + jetty-servlets + ${jetty.version} + + + org.eclipse.jetty + jetty-http + ${jetty.version} + + + org.eclipse.jetty + jetty-http + tests + ${jetty.version} + + + org.eclipse.jetty + jetty-alpn-server + ${jetty.version} + + + org.eclipse.jetty.http2 + http2-server + ${jetty.version} + + + org.eclipse.jetty.http2 + http2-client + ${jetty.version} + + + org.eclipse.jetty + jetty-client + ${jetty.version} + + + org.eclipse.jetty.http2 + http2-http-client-transport + ${jetty.version} + + + org.eclipse.jetty.toolchain.setuid + jetty-setuid-java + 1.0.3 + + + org.eclipse.jetty + jetty-util + + + org.eclipse.jetty + jetty-server + + + + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.api.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk7 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + ${jackson.version} + + + com.google.guava + guava + + + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + + + + + com.fasterxml.jackson.module + jackson-module-afterburner + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + ${jackson.version} + + + joda-time + joda-time + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + ${jackson.version} + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + + + + + org.glassfish.jersey + jersey-bom + ${jersey.version} + pom + import + + + org.glassfish.jersey.core + jersey-server + ${jersey.version} + + + com.google.guava + guava + + + org.slf4j + slf4j-api + + + + + org.glassfish.jersey.connectors + jersey-apache-connector + ${jersey.version} + + + org.apache.httpcomponents + httpclient + + + org.slf4j + slf4j-api + + + + + org.glassfish.jersey.ext + jersey-bean-validation + ${jersey.version} + + + org.hibernate + hibernate-validator + + + javax.el + javax.el-api + + + org.glassfish.web + javax.el + + + + + + + io.dropwizard.metrics + metrics-annotation + ${metrics3.version} + + + io.dropwizard.metrics + metrics-core + ${metrics3.version} + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-jvm + ${metrics3.version} + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-servlets + ${metrics3.version} + + + com.fasterxml.jackson.core + jackson-databind + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-healthchecks + ${metrics3.version} + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-logback + ${metrics3.version} + + + ch.qos.logback + logback-classic + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-jersey2 + ${metrics3.version} + + + org.glassfish.jersey.core + jersey-server + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-jetty9 + ${metrics3.version} + + + org.eclipse.jetty + jetty-server + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-httpclient + ${metrics3.version} + + + commons-logging + commons-logging + + + org.slf4j + slf4j-api + + + org.apache.httpcomponents + httpclient + + + + + io.dropwizard.metrics + metrics-jdbi + ${metrics3.version} + + + org.jdbi + jdbi + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-ganglia + ${metrics3.version} + + + org.slf4j + slf4j-api + + + + + io.dropwizard.metrics + metrics-graphite + ${metrics3.version} + + + org.slf4j + slf4j-api + + + + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + org.slf4j + jul-to-slf4j + ${slf4j.version} + + + org.slf4j + log4j-over-slf4j + ${slf4j.version} + + + org.slf4j + jcl-over-slf4j + ${slf4j.version} + + + ch.qos.logback + logback-access + ${logback.version} + + + ch.qos.logback + logback-core + ${logback.version} + + + ch.qos.logback + logback-classic + ${logback.version} + + + org.slf4j + slf4j-api + + + + + + + junit + junit + 4.12 + + + org.hamcrest + hamcrest-core + 1.3 + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + ${jersey.version} + + + javax.servlet + javax.servlet-api + + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + ${jersey.version} + + + javax.servlet + javax.servlet-api + + + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + ${jersey.version} + + + javax.servlet + javax.servlet-api + + + + + + + io.dropwizard + dropwizard-assets + ${project.version} + + + io.dropwizard + dropwizard-auth + ${project.version} + + + io.dropwizard + dropwizard-client + ${project.version} + + + io.dropwizard + dropwizard-configuration + ${project.version} + + + io.dropwizard + dropwizard-core + ${project.version} + + + io.dropwizard + dropwizard-db + ${project.version} + + + io.dropwizard + dropwizard-forms + ${project.version} + + + io.dropwizard + dropwizard-hibernate + ${project.version} + + + io.dropwizard + dropwizard-jackson + ${project.version} + + + io.dropwizard + dropwizard-jdbi + ${project.version} + + + io.dropwizard + dropwizard-jersey + ${project.version} + + + io.dropwizard + dropwizard-jetty + ${project.version} + + + io.dropwizard + dropwizard-lifecycle + ${project.version} + + + io.dropwizard + dropwizard-logging + ${project.version} + + + io.dropwizard + dropwizard-metrics + ${project.version} + + + io.dropwizard + dropwizard-metrics-ganglia + ${project.version} + + + io.dropwizard + dropwizard-metrics-graphite + ${project.version} + + + io.dropwizard + dropwizard-migrations + ${project.version} + + + io.dropwizard + dropwizard-request-logging + ${project.version} + + + io.dropwizard + dropwizard-servlets + ${project.version} + + + io.dropwizard + dropwizard-testing + ${project.version} + + + io.dropwizard + dropwizard-util + ${project.version} + + + io.dropwizard + dropwizard-validation + ${project.version} + + + io.dropwizard + dropwizard-views + ${project.version} + + + io.dropwizard + dropwizard-views-freemarker + ${project.version} + + + io.dropwizard + dropwizard-views-mustache + ${project.version} + + + io.dropwizard + dropwizard-http2 + ${project.version} + + + + diff --git a/dropwizard-client/pom.xml b/dropwizard-client/pom.xml new file mode 100644 index 00000000000..8a06d6d38ff --- /dev/null +++ b/dropwizard-client/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-client + Dropwizard HTTP Client + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + org.glassfish.jersey.core + jersey-client + + + org.apache.httpcomponents + httpclient + + + io.dropwizard.metrics + metrics-httpclient + + + org.glassfish.jersey.connectors + jersey-apache-connector + + + io.dropwizard + dropwizard-testing + test + + + diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/ConfiguredCloseableHttpClient.java b/dropwizard-client/src/main/java/io/dropwizard/client/ConfiguredCloseableHttpClient.java new file mode 100644 index 00000000000..82263216477 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/ConfiguredCloseableHttpClient.java @@ -0,0 +1,22 @@ +package io.dropwizard.client; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; + +public class ConfiguredCloseableHttpClient { + private final CloseableHttpClient closeableHttpClient; + private final RequestConfig defaultRequestConfig; + + /* package */ ConfiguredCloseableHttpClient(CloseableHttpClient closeableHttpClient, RequestConfig defaultRequestConfig) { + this.closeableHttpClient = closeableHttpClient; + this.defaultRequestConfig = defaultRequestConfig; + } + + public RequestConfig getDefaultRequestConfig() { + return defaultRequestConfig; + } + + public CloseableHttpClient getClient() { + return closeableHttpClient; + } +} diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardApacheConnector.java b/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardApacheConnector.java new file mode 100644 index 00000000000..e75b513e793 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardApacheConnector.java @@ -0,0 +1,349 @@ +package io.dropwizard.client; + +import com.google.common.collect.Lists; +import com.google.common.util.concurrent.MoreExecutors; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.StatusLine; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.entity.AbstractHttpEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.VersionInfo; +import org.glassfish.jersey.apache.connector.LocalizationMessages; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.spi.AsyncConnectorCallback; +import org.glassfish.jersey.client.spi.Connector; +import org.glassfish.jersey.message.internal.Statuses; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.core.Response; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Future; + +import static com.google.common.base.MoreObjects.firstNonNull; + +/** + * Dropwizard Apache Connector. + *

    + * It's a custom version of Jersey's {@link org.glassfish.jersey.client.spi.Connector} + * that uses Apache's {@link org.apache.http.client.HttpClient} + * as an HTTP transport implementation. + *

    + *

    + * It uses a pre-configured HTTP client by {@link io.dropwizard.client.HttpClientBuilder} + * rather then creates a client from the Jersey configuration. + *

    + *

    + * This approach affords to use the extended configuration of + * the Apache HttpClient in Dropwizard with a fluent interface + * of JerseyClient. + *

    + */ +public class DropwizardApacheConnector implements Connector { + + private static final String APACHE_HTTP_CLIENT_VERSION = VersionInfo + .loadVersionInfo("org.apache.http.client", DropwizardApacheConnector.class.getClassLoader()) + .getRelease(); + + /** + * Actual HTTP client + */ + private final CloseableHttpClient client; + /** + * Default HttpUriRequestConfig + */ + private final RequestConfig defaultRequestConfig; + + /** + * Should a chunked encoding be used in POST requests + */ + private final boolean chunkedEncodingEnabled; + + public DropwizardApacheConnector(CloseableHttpClient client, RequestConfig defaultRequestConfig, + boolean chunkedEncodingEnabled) { + this.client = client; + this.defaultRequestConfig = defaultRequestConfig; + this.chunkedEncodingEnabled = chunkedEncodingEnabled; + } + + /** + * {@inheritDoc} + */ + @Override + public ClientResponse apply(ClientRequest jerseyRequest) { + try { + final HttpUriRequest apacheRequest = buildApacheRequest(jerseyRequest); + final CloseableHttpResponse apacheResponse = client.execute(apacheRequest); + + final StatusLine statusLine = apacheResponse.getStatusLine(); + final Response.StatusType status = Statuses.from(statusLine.getStatusCode(), + firstNonNull(statusLine.getReasonPhrase(), "")); + + final ClientResponse jerseyResponse = new ClientResponse(status, jerseyRequest); + for (Header header : apacheResponse.getAllHeaders()) { + final List headerValues = jerseyResponse.getHeaders().get(header.getName()); + if (headerValues == null) { + jerseyResponse.getHeaders().put(header.getName(), Lists.newArrayList(header.getValue())); + } else { + headerValues.add(header.getValue()); + } + } + + final HttpEntity httpEntity = apacheResponse.getEntity(); + jerseyResponse.setEntityStream(httpEntity != null ? httpEntity.getContent() : + new ByteArrayInputStream(new byte[0])); + + return jerseyResponse; + } catch (Exception e) { + throw new ProcessingException(e); + } + } + + /** + * Build a new Apache's {@link org.apache.http.client.methods.HttpUriRequest} + * from Jersey's {@link org.glassfish.jersey.client.ClientRequest} + *

    + * Convert a method, URI, body, headers and override a user-agent if necessary + *

    + * + * @param jerseyRequest representation of an HTTP request in Jersey + * @return a new {@link org.apache.http.client.methods.HttpUriRequest} + */ + private HttpUriRequest buildApacheRequest(ClientRequest jerseyRequest) { + final RequestBuilder builder = RequestBuilder + .create(jerseyRequest.getMethod()) + .setUri(jerseyRequest.getUri()) + .setEntity(getHttpEntity(jerseyRequest)); + for (String headerName : jerseyRequest.getHeaders().keySet()) { + builder.addHeader(headerName, jerseyRequest.getHeaderString(headerName)); + } + + final Optional requestConfig = addJerseyRequestConfig(jerseyRequest); + requestConfig.ifPresent(builder::setConfig); + + return builder.build(); + } + + private Optional addJerseyRequestConfig(ClientRequest clientRequest) { + final Integer timeout = clientRequest.resolveProperty(ClientProperties.READ_TIMEOUT, Integer.class); + final Integer connectTimeout = clientRequest.resolveProperty(ClientProperties.CONNECT_TIMEOUT, Integer.class); + final Boolean followRedirects = clientRequest.resolveProperty(ClientProperties.FOLLOW_REDIRECTS, Boolean.class); + + if (timeout != null || connectTimeout != null || followRedirects != null) { + final RequestConfig.Builder requestConfig = RequestConfig.copy(defaultRequestConfig); + + if (timeout != null) { + requestConfig.setSocketTimeout(timeout); + } + + if (connectTimeout != null) { + requestConfig.setConnectTimeout(connectTimeout); + } + + if (followRedirects != null) { + requestConfig.setRedirectsEnabled(followRedirects); + } + + return Optional.of(requestConfig.build()); + } + + return Optional.empty(); + } + + /** + * Get an Apache's {@link org.apache.http.HttpEntity} + * from Jersey's {@link org.glassfish.jersey.client.ClientRequest} + *

    + * Create a custom HTTP entity, because Jersey doesn't provide + * a request stream or a byte buffer. + *

    + * + * @param jerseyRequest representation of an HTTP request in Jersey + * @return a correct {@link org.apache.http.HttpEntity} implementation + */ + private HttpEntity getHttpEntity(ClientRequest jerseyRequest) { + if (jerseyRequest.getEntity() == null) { + return null; + } + + return chunkedEncodingEnabled ? new JerseyRequestHttpEntity(jerseyRequest) : + new BufferedJerseyRequestHttpEntity(jerseyRequest); + } + + /** + * {@inheritDoc} + */ + @Override + public Future apply(final ClientRequest request, final AsyncConnectorCallback callback) { + // Simulate an asynchronous execution + return MoreExecutors.newDirectExecutorService().submit(() -> { + try { + callback.response(apply(request)); + } catch (Exception e) { + callback.failure(e); + } + }); + } + + /** + * {@inheritDoc} + */ + @Override + public String getName() { + return "Apache-HttpClient/" + APACHE_HTTP_CLIENT_VERSION; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() { + // Should not close the client here, because it's managed by the Dropwizard environment + } + + /** + * A custom {@link org.apache.http.entity.AbstractHttpEntity} that uses + * a Jersey request as a content source. It's chunked because we don't + * know the content length beforehand. + */ + private static class JerseyRequestHttpEntity extends AbstractHttpEntity { + + private ClientRequest clientRequest; + + private JerseyRequestHttpEntity(ClientRequest clientRequest) { + this.clientRequest = clientRequest; + setChunked(true); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRepeatable() { + return false; + } + + /** + * {@inheritDoc} + */ + @Override + public long getContentLength() { + return -1; + } + + /** + * {@inheritDoc} + *

    + * This method isn't supported at will throw an {@link java.lang.UnsupportedOperationException} + * if invoked. + *

    + */ + @Override + public InputStream getContent() throws IOException { + // Shouldn't be called + throw new UnsupportedOperationException("Reading from the entity is not supported"); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeTo(final OutputStream outputStream) throws IOException { + clientRequest.setStreamProvider(contentLength -> outputStream); + clientRequest.writeEntity(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isStreaming() { + return false; + } + + } + + /** + * A custom {@link org.apache.http.entity.AbstractHttpEntity} that uses + * a Jersey request as a content source. + *

    + * In contrast to {@link io.dropwizard.client.DropwizardApacheConnector.JerseyRequestHttpEntity} + * its contents are buffered on initialization. + *

    + */ + private static class BufferedJerseyRequestHttpEntity extends AbstractHttpEntity { + + private static final int BUFFER_INITIAL_SIZE = 512; + private byte[] buffer; + + private BufferedJerseyRequestHttpEntity(ClientRequest clientRequest) { + final ByteArrayOutputStream stream = new ByteArrayOutputStream(BUFFER_INITIAL_SIZE); + clientRequest.setStreamProvider(contentLength -> stream); + try { + clientRequest.writeEntity(); + } catch (IOException e) { + throw new ProcessingException(LocalizationMessages.ERROR_BUFFERING_ENTITY(), e); + } + buffer = stream.toByteArray(); + setChunked(false); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isRepeatable() { + return true; + } + + /** + * {@inheritDoc} + */ + @Override + public long getContentLength() { + return buffer.length; + } + + /** + * {@inheritDoc} + *

    + * This method isn't supported at will throw an {@link java.lang.UnsupportedOperationException} + * if invoked. + *

    + */ + @Override + public InputStream getContent() throws IOException { + // Shouldn't be called + throw new UnsupportedOperationException("Reading from the entity is not supported"); + } + + /** + * {@inheritDoc} + */ + @Override + public void writeTo(OutputStream outstream) throws IOException { + outstream.write(buffer); + outstream.flush(); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean isStreaming() { + return false; + } + } +} + diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardExecutorProvider.java b/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardExecutorProvider.java new file mode 100644 index 00000000000..7d1193585ea --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardExecutorProvider.java @@ -0,0 +1,70 @@ +package io.dropwizard.client; + +import com.google.common.util.concurrent.ForwardingExecutorService; +import io.dropwizard.util.Duration; +import org.glassfish.jersey.client.ClientAsyncExecutor; +import org.glassfish.jersey.spi.ExecutorServiceProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutorService; + +/** + * An {@link ExecutorServiceProvider} implementation for use within + * Dropwizard. + * + * With {DropwizardExecutorProvider.DisposableExecutorService}, one + * can signal that an {ExecutorService} is to be gracefully shut down + * upon its disposal by the Jersey runtime. It is used as a means of + * signaling to {@link DropwizardExecutorProvider} that the executor + * is not shared. + */ +@ClientAsyncExecutor +class DropwizardExecutorProvider implements ExecutorServiceProvider { + /** + * An {@link ExecutorService} decorator used as a marker by + * {@link DropwizardExecutorProvider#dispose} to induce service + * shutdown. + */ + static class DisposableExecutorService extends ForwardingExecutorService { + private final ExecutorService delegate; + + public DisposableExecutorService(ExecutorService delegate) { + this.delegate = delegate; + } + + @Override + protected ExecutorService delegate() { + return delegate; + } + } + + private static final Logger LOGGER = LoggerFactory.getLogger(DropwizardExecutorProvider.class); + + private final ExecutorService executor; + private final Duration shutdownGracePeriod; + + DropwizardExecutorProvider(ExecutorService executor, Duration shutdownGracePeriod) { + this.executor = executor; + this.shutdownGracePeriod = shutdownGracePeriod; + } + + @Override + public ExecutorService getExecutorService() { + return this.executor; + } + + @Override + public void dispose(ExecutorService executorService) { + if (executorService instanceof DisposableExecutorService) { + executorService.shutdown(); + + try { + executorService.awaitTermination( + shutdownGracePeriod.getQuantity(), shutdownGracePeriod.getUnit()); + } catch (InterruptedException err) { + LOGGER.warn("Interrupted while waiting for ExecutorService shutdown", err); + } + } + } +} diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardSSLConnectionSocketFactory.java b/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardSSLConnectionSocketFactory.java new file mode 100644 index 00000000000..b50b3324796 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/DropwizardSSLConnectionSocketFactory.java @@ -0,0 +1,104 @@ +package io.dropwizard.client; + +import io.dropwizard.client.ssl.TlsConfiguration; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLInitializationException; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.ssl.SSLContextBuilder; +import org.apache.http.ssl.TrustStrategy; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.List; + +public class DropwizardSSLConnectionSocketFactory { + + private final TlsConfiguration configuration; + private final HostnameVerifier verifier; + + public DropwizardSSLConnectionSocketFactory(TlsConfiguration configuration) { + this(configuration, null); + } + + public DropwizardSSLConnectionSocketFactory(TlsConfiguration configuration, HostnameVerifier verifier) { + this.configuration = configuration; + this.verifier = verifier; + } + + public SSLConnectionSocketFactory getSocketFactory() throws SSLInitializationException { + return new SSLConnectionSocketFactory(buildSslContext(), getSupportedProtocols(), getSupportedCiphers(), + chooseHostnameVerifier()); + } + + private String[] getSupportedCiphers() { + final List supportedCiphers = configuration.getSupportedCiphers(); + if (supportedCiphers == null) { + return null; + } + return supportedCiphers.toArray(new String[supportedCiphers.size()]); + } + + private String[] getSupportedProtocols() { + final List supportedProtocols = configuration.getSupportedProtocols(); + if (supportedProtocols == null) { + return null; + } + return supportedProtocols.toArray(new String[supportedProtocols.size()]); + } + + private HostnameVerifier chooseHostnameVerifier() { + if (configuration.isVerifyHostname()) { + return verifier != null ? verifier : SSLConnectionSocketFactory.getDefaultHostnameVerifier(); + } else { + return new NoopHostnameVerifier(); + } + } + + private SSLContext buildSslContext() throws SSLInitializationException { + final SSLContext sslContext; + try { + final SSLContextBuilder sslContextBuilder = new SSLContextBuilder(); + sslContextBuilder.useProtocol(configuration.getProtocol()); + loadKeyMaterial(sslContextBuilder); + loadTrustMaterial(sslContextBuilder); + sslContext = sslContextBuilder.build(); + } catch (Exception e) { + throw new SSLInitializationException(e.getMessage(), e); + } + return sslContext; + } + + private void loadKeyMaterial(SSLContextBuilder sslContextBuilder) throws Exception { + if (configuration.getKeyStorePath() != null) { + final KeyStore keystore = loadKeyStore(configuration.getKeyStoreType(), configuration.getKeyStorePath(), + configuration.getKeyStorePassword()); + sslContextBuilder.loadKeyMaterial(keystore, configuration.getKeyStorePassword().toCharArray()); + } + } + + private void loadTrustMaterial(SSLContextBuilder sslContextBuilder) throws Exception { + KeyStore trustStore = null; + if (configuration.getTrustStorePath() != null) { + trustStore = loadKeyStore(configuration.getTrustStoreType(), configuration.getTrustStorePath(), + configuration.getTrustStorePassword()); + } + TrustStrategy trustStrategy = null; + if (configuration.isTrustSelfSignedCertificates()) { + trustStrategy = new TrustSelfSignedStrategy(); + } + sslContextBuilder.loadTrustMaterial(trustStore, trustStrategy); + } + + private static KeyStore loadKeyStore(String type, File path, String password) throws Exception { + final KeyStore keyStore = KeyStore.getInstance(type); + try (InputStream inputStream = new FileInputStream(path)) { + keyStore.load(inputStream, password.toCharArray()); + } + return keyStore; + } +} 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 00000000000..0aba8e98515 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientBuilder.java @@ -0,0 +1,440 @@ +package io.dropwizard.client; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.httpclient.HttpClientMetricNameStrategies; +import com.codahale.metrics.httpclient.HttpClientMetricNameStrategy; +import com.codahale.metrics.httpclient.InstrumentedHttpClientConnectionManager; +import com.codahale.metrics.httpclient.InstrumentedHttpRequestExecutor; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.client.proxy.AuthConfiguration; +import io.dropwizard.client.proxy.NonProxyListProxyRoutePlanner; +import io.dropwizard.client.proxy.ProxyConfiguration; +import io.dropwizard.client.ssl.TlsConfiguration; +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.setup.Environment; +import io.dropwizard.util.Duration; +import javax.net.ssl.HostnameVerifier; +import org.apache.http.ConnectionReuseStrategy; +import org.apache.http.Header; +import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.RedirectStrategy; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.DnsResolver; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.DefaultConnectionReuseStrategy; +import org.apache.http.impl.NoConnectionReuseStrategy; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.conn.SystemDefaultDnsResolver; +import org.apache.http.protocol.HttpContext; + +import java.util.List; + +/** + * A convenience class for building {@link HttpClient} instances. + *

    + * Among other things, + *

      + *
    • Disables stale connection checks by default
    • + *
    • Disables Nagle's algorithm
    • + *
    • Disables cookie management by default
    • + *
    + *

    + */ +public class HttpClientBuilder { + private static final HttpRequestRetryHandler NO_RETRIES = (exception, executionCount, context) -> false; + + private final MetricRegistry metricRegistry; + private String environmentName; + private Environment environment; + private HttpClientConfiguration configuration = new HttpClientConfiguration(); + private DnsResolver resolver = new SystemDefaultDnsResolver(); + private HostnameVerifier verifier; + private HttpRequestRetryHandler httpRequestRetryHandler; + private Registry registry; + + private CredentialsProvider credentialsProvider = null; + private HttpClientMetricNameStrategy metricNameStrategy = HttpClientMetricNameStrategies.METHOD_ONLY; + private HttpRoutePlanner routePlanner = null; + private RedirectStrategy redirectStrategy; + private boolean disableContentCompression; + private List defaultHeaders; + + public HttpClientBuilder(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + } + + public HttpClientBuilder(Environment environment) { + this(environment.metrics()); + name(environment.getName()); + this.environment = environment; + } + + /** + * 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; + } + + /** + * Use the give (@link HostnameVerifier} instance. + * + * @param verifier a {@link HostnameVerifier} instance + * @return {@code this} + */ + public HttpClientBuilder using(HostnameVerifier verifier) { + this.verifier = verifier; + 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 Registry} instance. + * + * @param registry + * @return {@code this} + */ + public HttpClientBuilder using(Registry registry) { + this.registry = registry; + return this; + } + + /** + * Use the given {@link HttpRoutePlanner} instance. + * + * @param routePlanner a {@link HttpRoutePlanner} instance + * @return {@code this} + */ + public HttpClientBuilder using(HttpRoutePlanner routePlanner) { + this.routePlanner = routePlanner; + 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; + } + + /** + * Use the given {@link HttpClientMetricNameStrategy} instance. + * + * @param metricNameStrategy a {@link HttpClientMetricNameStrategy} instance + * @return {@code this} + */ + public HttpClientBuilder using(HttpClientMetricNameStrategy metricNameStrategy) { + this.metricNameStrategy = metricNameStrategy; + return this; + } + + /** + * Use the given {@link org.apache.http.client.RedirectStrategy} instance. + * + * @param redirectStrategy a {@link org.apache.http.client.RedirectStrategy} instance + * @return {@code this} + */ + public HttpClientBuilder using(RedirectStrategy redirectStrategy) { + this.redirectStrategy = redirectStrategy; + return this; + } + + /** + * Use the given default headers for each HTTP request + * + * @param defaultHeaders HTTP headers + * @return {@code} this + */ + public HttpClientBuilder using(List defaultHeaders) { + this.defaultHeaders = defaultHeaders; + return this; + } + + /** + * Disable support of decompression of responses + * + * @param disableContentCompression {@code true}, if disabled + * @return {@code this} + */ + public HttpClientBuilder disableContentCompression(boolean disableContentCompression) { + this.disableContentCompression = disableContentCompression; + return this; + } + + /** + * Builds the {@link HttpClient}. + * + * @param name + * @return an {@link CloseableHttpClient} + */ + public CloseableHttpClient build(String name) { + final CloseableHttpClient client = buildWithDefaultRequestConfiguration(name).getClient(); + // If the environment is present, we tie the client with the server lifecycle + if (environment != null) { + environment.lifecycle().manage(new Managed() { + @Override + public void start() throws Exception { + } + + @Override + public void stop() throws Exception { + client.close(); + } + }); + } + return client; + } + + /** + * For internal use only, used in {@link io.dropwizard.client.JerseyClientBuilder} + * to create an instance of {@link io.dropwizard.client.DropwizardApacheConnector} + * + * @param name + * @return an {@link io.dropwizard.client.ConfiguredCloseableHttpClient} + */ + ConfiguredCloseableHttpClient buildWithDefaultRequestConfiguration(String name) { + return createClient(org.apache.http.impl.client.HttpClientBuilder.create(), + createConnectionManager(createConfiguredRegistry(), name), name); + } + + /** + * Configures an Apache {@link org.apache.http.impl.client.HttpClientBuilder HttpClientBuilder}. + * + * Intended for use by subclasses to inject HttpClientBuilder + * configuration. The default implementation is an identity + * function. + */ + protected org.apache.http.impl.client.HttpClientBuilder customizeBuilder( + org.apache.http.impl.client.HttpClientBuilder builder + ) { + return builder; + } + + /** + * Map the parameters in {@link HttpClientConfiguration} to configuration on a + * {@link org.apache.http.impl.client.HttpClientBuilder} instance + * + * @param builder + * @param manager + * @param name + * @return the configured {@link CloseableHttpClient} + */ + protected ConfiguredCloseableHttpClient createClient( + final org.apache.http.impl.client.HttpClientBuilder builder, + final InstrumentedHttpClientConnectionManager manager, + final String name) { + final String cookiePolicy = configuration.isCookiesEnabled() ? CookieSpecs.DEFAULT : CookieSpecs.IGNORE_COOKIES; + final Integer timeout = (int) configuration.getTimeout().toMilliseconds(); + final Integer connectionTimeout = (int) configuration.getConnectionTimeout().toMilliseconds(); + final Integer connectionRequestTimeout = (int) configuration.getConnectionRequestTimeout().toMilliseconds(); + final long keepAlive = configuration.getKeepAlive().toMilliseconds(); + final ConnectionReuseStrategy reuseStrategy = keepAlive == 0 + ? new NoConnectionReuseStrategy() + : new DefaultConnectionReuseStrategy(); + final HttpRequestRetryHandler retryHandler = configuration.getRetries() == 0 + ? NO_RETRIES + : (httpRequestRetryHandler == null ? new DefaultHttpRequestRetryHandler(configuration.getRetries(), + false) : httpRequestRetryHandler); + + final RequestConfig requestConfig + = RequestConfig.custom().setCookieSpec(cookiePolicy) + .setSocketTimeout(timeout) + .setConnectTimeout(connectionTimeout) + .setConnectionRequestTimeout(connectionRequestTimeout) + .build(); + final SocketConfig socketConfig = SocketConfig.custom() + .setTcpNoDelay(true) + .setSoTimeout(timeout) + .build(); + + customizeBuilder(builder) + .setRequestExecutor(new InstrumentedHttpRequestExecutor(metricRegistry, metricNameStrategy, name)) + .setConnectionManager(manager) + .setDefaultRequestConfig(requestConfig) + .setDefaultSocketConfig(socketConfig) + .setConnectionReuseStrategy(reuseStrategy) + .setRetryHandler(retryHandler) + .setUserAgent(createUserAgent(name)); + + if (keepAlive != 0) { + // 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 + builder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy() { + @Override + public long getKeepAliveDuration(HttpResponse response, HttpContext context) { + final long duration = super.getKeepAliveDuration(response, context); + return (duration == -1) ? keepAlive : duration; + } + }); + } + + // create a tunnel through a proxy host if it's specified in the config + final ProxyConfiguration proxy = configuration.getProxyConfiguration(); + if (proxy != null) { + final HttpHost httpHost = new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getScheme()); + builder.setRoutePlanner(new NonProxyListProxyRoutePlanner(httpHost, proxy.getNonProxyHosts())); + // if the proxy host requires authentication then add the host credentials to the credentials provider + final AuthConfiguration auth = proxy.getAuth(); + if (auth != null) { + if (credentialsProvider == null) { + credentialsProvider = new BasicCredentialsProvider(); + } + credentialsProvider.setCredentials(new AuthScope(httpHost), + new UsernamePasswordCredentials(auth.getUsername(), auth.getPassword())); + } + } + + if (credentialsProvider != null) { + builder.setDefaultCredentialsProvider(credentialsProvider); + } + + if (routePlanner != null) { + builder.setRoutePlanner(routePlanner); + } + + if (disableContentCompression) { + builder.disableContentCompression(); + } + + if (redirectStrategy != null) { + builder.setRedirectStrategy(redirectStrategy); + } + + if (defaultHeaders != null) { + builder.setDefaultHeaders(defaultHeaders); + } + + if (verifier != null) { + builder.setSSLHostnameVerifier(verifier); + } + + return new ConfiguredCloseableHttpClient(builder.build(), requestConfig); + } + + /** + * 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().orElse(defaultUserAgent); + } + + + /** + * Create a InstrumentedHttpClientConnectionManager based on the + * HttpClientConfiguration. It sets the maximum connections per route and + * the maximum total connections that the connection manager can create + * + * @param registry + * @param name + * @return a InstrumentedHttpClientConnectionManger instance + */ + protected InstrumentedHttpClientConnectionManager createConnectionManager(Registry registry, + String name) { + final Duration ttl = configuration.getTimeToLive(); + final InstrumentedHttpClientConnectionManager manager = new InstrumentedHttpClientConnectionManager( + metricRegistry, + registry, + null, null, + resolver, + ttl.getQuantity(), + ttl.getUnit(), + name); + return configureConnectionManager(manager); + } + + @VisibleForTesting + Registry createConfiguredRegistry() { + if (registry != null) { + return registry; + } + + TlsConfiguration tlsConfiguration = configuration.getTlsConfiguration(); + if (tlsConfiguration == null && verifier != null) { + tlsConfiguration = new TlsConfiguration(); + } + + final SSLConnectionSocketFactory sslConnectionSocketFactory; + if (tlsConfiguration == null) { + sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); + } else { + sslConnectionSocketFactory = new DropwizardSSLConnectionSocketFactory(tlsConfiguration, + verifier).getSocketFactory(); + } + + return RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", sslConnectionSocketFactory) + .build(); + } + + + @VisibleForTesting + protected InstrumentedHttpClientConnectionManager configureConnectionManager( + InstrumentedHttpClientConnectionManager connectionManager) { + connectionManager.setDefaultMaxPerRoute(configuration.getMaxConnectionsPerRoute()); + connectionManager.setMaxTotal(configuration.getMaxConnections()); + connectionManager.setValidateAfterInactivity((int) configuration.getValidateAfterInactivityPeriod().toMilliseconds()); + return connectionManager; + } +} 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 00000000000..68f2887c8b7 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/HttpClientConfiguration.java @@ -0,0 +1,194 @@ +package io.dropwizard.client; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.client.proxy.ProxyConfiguration; +import io.dropwizard.client.ssl.TlsConfiguration; +import io.dropwizard.util.Duration; +import org.hibernate.validator.valuehandling.UnwrapValidatedValue; + +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.Optional; + +/** + * The configuration class used by {@link HttpClientBuilder}. + * + * @see Http Client Configuration + */ +public class HttpClientConfiguration { + @NotNull + private Duration timeout = Duration.milliseconds(500); + + @NotNull + private Duration connectionTimeout = Duration.milliseconds(500); + + @NotNull + private Duration connectionRequestTimeout = 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 + @UnwrapValidatedValue(false) + private Optional userAgent = Optional.empty(); + + @Valid + @Nullable + private ProxyConfiguration proxyConfiguration; + + @NotNull + private Duration validateAfterInactivityPeriod = Duration.microseconds(0); + + public Duration getKeepAlive() { + return keepAlive; + } + + @Valid + @Nullable + private TlsConfiguration tlsConfiguration; + + @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 Duration getConnectionRequestTimeout() { + return connectionRequestTimeout; + } + + @JsonProperty + public void setConnectionRequestTimeout(Duration connectionRequestTimeout) { + this.connectionRequestTimeout = connectionRequestTimeout; + } + + @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 getUserAgent() { + return userAgent; + } + + @JsonProperty + public void setUserAgent(Optional userAgent) { + this.userAgent = userAgent; + } + + @JsonProperty("proxy") + public ProxyConfiguration getProxyConfiguration() { + return proxyConfiguration; + } + + @JsonProperty("proxy") + public void setProxyConfiguration(ProxyConfiguration proxyConfiguration) { + this.proxyConfiguration = proxyConfiguration; + } + + @JsonProperty + public Duration getValidateAfterInactivityPeriod() { + return validateAfterInactivityPeriod; + } + + @JsonProperty + public void setValidateAfterInactivityPeriod(Duration validateAfterInactivityPeriod) { + this.validateAfterInactivityPeriod = validateAfterInactivityPeriod; + } + + @JsonProperty("tls") + public TlsConfiguration getTlsConfiguration() { + return tlsConfiguration; + } + + @JsonProperty("tls") + public void setTlsConfiguration(TlsConfiguration tlsConfiguration) { + this.tlsConfiguration = tlsConfiguration; + } +} 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 00000000000..09cb1b630ce --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientBuilder.java @@ -0,0 +1,414 @@ +package io.dropwizard.client; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.httpclient.HttpClientMetricNameStrategy; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.jersey.gzip.ConfiguredGZipEncoder; +import io.dropwizard.jersey.gzip.GZipDecoder; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.jersey.validation.HibernateValidationFeature; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.setup.Environment; +import io.dropwizard.util.Duration; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.config.Registry; +import org.apache.http.conn.DnsResolver; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.client.spi.ConnectorProvider; + +import javax.net.ssl.HostnameVerifier; +import javax.validation.Validator; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Configuration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; + +import static java.util.Objects.requireNonNull; + + +/** + * A convenience class for building {@link Client} instances. + *

    + * Among other things, + *

      + *
    • Backed by Apache HttpClient
    • + *
    • Disables stale connection checks
    • + *
    • Disables Nagle's algorithm
    • + *
    • Disables cookie management by default
    • + *
    • Compress requests and decompress responses using GZIP
    • + *
    • Supports parsing and generating JSON data using Jackson
    • + *
    + *

    + * + * @see HttpClientBuilder + */ +public class JerseyClientBuilder { + + private final List singletons = new ArrayList<>(); + private final List> providers = new ArrayList<>(); + private final Map properties = new LinkedHashMap<>(); + private JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + + private HttpClientBuilder apacheHttpClientBuilder; + private Validator validator = Validators.newValidator(); + private Environment environment; + private ObjectMapper objectMapper; + private ExecutorService executorService; + private ConnectorProvider connectorProvider; + private Duration shutdownGracePeriod = Duration.seconds(5); + + public JerseyClientBuilder(Environment environment) { + this.apacheHttpClientBuilder = new HttpClientBuilder(environment); + this.environment = environment; + } + + public JerseyClientBuilder(MetricRegistry metricRegistry) { + this.apacheHttpClientBuilder = new HttpClientBuilder(metricRegistry); + } + + @VisibleForTesting + public void setApacheHttpClientBuilder(HttpClientBuilder apacheHttpClientBuilder) { + this.apacheHttpClientBuilder = apacheHttpClientBuilder; + } + + /** + * Adds the given object as a Jersey provider. + * + * @param provider a Jersey provider + * @return {@code this} + */ + public JerseyClientBuilder withProvider(Object provider) { + singletons.add(requireNonNull(provider)); + return this; + } + + /** + * Adds the given class as a Jersey provider.

    N.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(requireNonNull(klass)); + return this; + } + + /** + * Sets the state of the given Jersey property. + *

    + *

    WARNING: The default connector ignores Jersey properties. + * Use {@link JerseyClientConfiguration} instead. + * + * @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; + } + + /** + * Sets the shutdown grace period. + * + * @param shutdownGracePeriod a period of time to await shutdown of the + * configured {ExecutorService}. + * @return {@code this} + */ + public JerseyClientBuilder withShutdownGracePeriod(Duration shutdownGracePeriod) { + this.shutdownGracePeriod = shutdownGracePeriod; + return this; + } + + /** + * Uses the given {@link JerseyClientConfiguration}. + * + * @param configuration a configuration object + * @return {@code this} + */ + public JerseyClientBuilder using(JerseyClientConfiguration configuration) { + this.configuration = configuration; + apacheHttpClientBuilder.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 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; + } + + /** + * Uses the given {@link ExecutorService}. + * + * @param executorService a thread pool + * @return {@code this} + * @see #using(io.dropwizard.setup.Environment) + */ + public JerseyClientBuilder using(ExecutorService executorService) { + this.executorService = executorService; + return this; + } + + /** + * Uses the given {@link ObjectMapper}. + * + * @param objectMapper an object mapper + * @return {@code this} + * @see #using(io.dropwizard.setup.Environment) + */ + public JerseyClientBuilder using(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + return this; + } + + /** + * Use the given {@link ConnectorProvider} instance. + *

    WARNING: Use it with a caution. Most of features will not + * work in a custom connection provider. + * + * @param connectorProvider a {@link ConnectorProvider} instance + * @return {@code this} + */ + public JerseyClientBuilder using(ConnectorProvider connectorProvider) { + this.connectorProvider = connectorProvider; + 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) { + apacheHttpClientBuilder.using(httpRequestRetryHandler); + return this; + } + + /** + * Use the given {@link DnsResolver} instance. + * + * @param resolver a {@link DnsResolver} instance + * @return {@code this} + */ + public JerseyClientBuilder using(DnsResolver resolver) { + apacheHttpClientBuilder.using(resolver); + return this; + } + + /** + * Use the given {@link HostnameVerifier} instance. + * + * Note that if {@link io.dropwizard.client.ssl.TlsConfiguration#isVerifyHostname()} + * returns false, all host name verification is bypassed, including + * host name verification performed by a verifier specified + * through this interface. + * + * @param verifier a {@link HostnameVerifier} instance + * @return {@code this} + */ + public JerseyClientBuilder using(HostnameVerifier verifier) { + apacheHttpClientBuilder.using(verifier); + return this; + } + + /** + * Use the given {@link Registry} instance of connection socket factories. + * + * @param registry a {@link Registry} instance of connection socket factories + * @return {@code this} + */ + public JerseyClientBuilder using(Registry registry) { + apacheHttpClientBuilder.using(registry); + return this; + } + + /** + * Use the given {@link HttpClientMetricNameStrategy} instance. + * + * @param metricNameStrategy a {@link HttpClientMetricNameStrategy} instance + * @return {@code this} + */ + public JerseyClientBuilder using(HttpClientMetricNameStrategy metricNameStrategy) { + apacheHttpClientBuilder.using(metricNameStrategy); + return this; + } + + /** + * 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 JerseyClientBuilder name(String environmentName) { + apacheHttpClientBuilder.name(environmentName); + return this; + } + + /** + * Use the given {@link HttpRoutePlanner} instance. + * + * @param routePlanner a {@link HttpRoutePlanner} instance + * @return {@code this} + */ + public JerseyClientBuilder using(HttpRoutePlanner routePlanner) { + apacheHttpClientBuilder.using(routePlanner); + return this; + } + + /** + * Use the given {@link CredentialsProvider} instance. + * + * @param credentialsProvider a {@link CredentialsProvider} instance + * @return {@code this} + */ + public JerseyClientBuilder using(CredentialsProvider credentialsProvider) { + apacheHttpClientBuilder.using(credentialsProvider); + 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 (executorService == null) { + // Create an ExecutorService based on the provided + // configuration. The DisposableExecutorService decorator + // is used to ensure that the service is shut down if the + // Jersey client disposes of it. + executorService = new DropwizardExecutorProvider.DisposableExecutorService( + environment.lifecycle() + .executorService("jersey-client-" + name + "-%d") + .minThreads(configuration.getMinThreads()) + .maxThreads(configuration.getMaxThreads()) + .workQueue(new ArrayBlockingQueue<>(configuration.getWorkQueueSize())) + .build() + ); + } + + if (objectMapper == null) { + objectMapper = environment.getObjectMapper(); + } + + if (environment != null) { + validator = environment.getValidator(); + } + + return build(name, executorService, objectMapper, validator); + } + + private Client build(String name, ExecutorService threadPool, + ObjectMapper objectMapper, + Validator validator) { + if (!configuration.isGzipEnabled()) { + apacheHttpClientBuilder.disableContentCompression(true); + } + + final Client client = ClientBuilder.newClient(buildConfig(name, threadPool, objectMapper, validator)); + client.register(new JerseyIgnoreRequestUserAgentHeaderFilter()); + + // Tie the client to server lifecycle + if (environment != null) { + environment.lifecycle().manage(new Managed() { + @Override + public void start() throws Exception { + } + + @Override + public void stop() throws Exception { + client.close(); + } + }); + } + if (configuration.isGzipEnabled()) { + client.register(new GZipDecoder()); + client.register(new ConfiguredGZipEncoder(configuration.isGzipEnabledForRequests())); + } + + return client; + } + + private Configuration buildConfig(final String name, final ExecutorService threadPool, + final ObjectMapper objectMapper, + final Validator validator) { + final ClientConfig config = new ClientConfig(); + + for (Object singleton : this.singletons) { + config.register(singleton); + } + + for (Class provider : this.providers) { + config.register(provider); + } + + config.register(new JacksonMessageBodyProvider(objectMapper)); + config.register(new HibernateValidationFeature(validator)); + + for (Map.Entry property : this.properties.entrySet()) { + config.property(property.getKey(), property.getValue()); + } + + config.register(new DropwizardExecutorProvider(threadPool, shutdownGracePeriod)); + if (connectorProvider == null) { + final ConfiguredCloseableHttpClient apacheHttpClient = + apacheHttpClientBuilder.buildWithDefaultRequestConfiguration(name); + connectorProvider = (client, runtimeConfig) -> new DropwizardApacheConnector( + apacheHttpClient.getClient(), + apacheHttpClient.getDefaultRequestConfig(), + configuration.isChunkedEncodingEnabled()); + } + config.connectorProvider(connectorProvider); + + 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 00000000000..2e798cf9977 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyClientConfiguration.java @@ -0,0 +1,107 @@ +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; + +/** + * The configuration class used by {@link JerseyClientBuilder}. Extends + * {@link HttpClientConfiguration}. + * + * @see HttpClientConfiguration + * @see Jersey Client Configuration + */ +public class JerseyClientConfiguration extends HttpClientConfiguration { + @Min(1) + @Max(16 * 1024) + private int minThreads = 1; + + @Min(1) + @Max(16 * 1024) + private int maxThreads = 128; + + @Min(1) + @Max(16 * 1024) + private int workQueueSize = 8; + + private boolean gzipEnabled = true; + + private boolean gzipEnabledForRequests = true; + + private boolean chunkedEncodingEnabled = 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; + } + + @JsonProperty + public boolean isChunkedEncodingEnabled() { + return chunkedEncodingEnabled; + } + + @JsonProperty + public void setChunkedEncodingEnabled(final boolean chunkedEncodingEnabled) { + this.chunkedEncodingEnabled = chunkedEncodingEnabled; + } + + @JsonProperty + public int getWorkQueueSize() { + return workQueueSize; + } + + @JsonProperty + public void setWorkQueueSize(int workQueueSize) { + this.workQueueSize = workQueueSize; + } + + @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/main/java/io/dropwizard/client/JerseyIgnoreRequestUserAgentHeaderFilter.java b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyIgnoreRequestUserAgentHeaderFilter.java new file mode 100644 index 00000000000..8f061ac3a6e --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/JerseyIgnoreRequestUserAgentHeaderFilter.java @@ -0,0 +1,20 @@ +package io.dropwizard.client; + +import org.glassfish.jersey.client.ClientRequest; + +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +/** + * Prevents Jersey from modification Request's User-Agent header with default value, + * to escape the value conflict with Dropwizard + */ +@Provider +public class JerseyIgnoreRequestUserAgentHeaderFilter implements ClientRequestFilter { + @Override + public void filter(ClientRequestContext requestContext) throws IOException { + ((ClientRequest) requestContext).ignoreUserAgent(true); + } +} diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/proxy/AuthConfiguration.java b/dropwizard-client/src/main/java/io/dropwizard/client/proxy/AuthConfiguration.java new file mode 100644 index 00000000000..021679cd952 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/proxy/AuthConfiguration.java @@ -0,0 +1,63 @@ +package io.dropwizard.client.proxy; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.NotEmpty; + +/** + * Represents a configuration of credentials (username / password) + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code username}REQUIREDThe username used to connect to the server.
    {@code password}REQUIREDThe password used to connect to the server.
    + */ +public class AuthConfiguration { + + @NotEmpty + private String username; + + @NotEmpty + private String password; + + public AuthConfiguration() { + } + + public AuthConfiguration(String username, String password) { + this.username = username; + this.password = password; + } + + @JsonProperty + public String getUsername() { + return username; + } + + @JsonProperty + public void setUsername(String username) { + this.username = username; + } + + @JsonProperty + public String getPassword() { + return password; + } + + @JsonProperty + public void setPassword(String password) { + this.password = password; + } +} diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/proxy/NonProxyListProxyRoutePlanner.java b/dropwizard-client/src/main/java/io/dropwizard/client/proxy/NonProxyListProxyRoutePlanner.java new file mode 100644 index 00000000000..f2f1f6b57fa --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/proxy/NonProxyListProxyRoutePlanner.java @@ -0,0 +1,65 @@ +package io.dropwizard.client.proxy; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.conn.SchemePortResolver; +import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.protocol.HttpContext; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Implementation of {@link org.apache.http.conn.routing.HttpRoutePlanner} + * that routes requests through proxy and takes into account list of hosts that should not be proxied + */ +public class NonProxyListProxyRoutePlanner extends DefaultProxyRoutePlanner { + + private static final Pattern WILDCARD = Pattern.compile("\\*"); + private static final String REGEX_WILDCARD = ".*"; + + private List nonProxyHostPatterns; + + public NonProxyListProxyRoutePlanner(HttpHost proxy, @Nullable List nonProxyHosts) { + super(proxy, null); + nonProxyHostPatterns = getNonProxyHostPatterns(nonProxyHosts); + } + + public NonProxyListProxyRoutePlanner(HttpHost proxy, SchemePortResolver schemePortResolver, + @Nullable List nonProxyHosts) { + super(proxy, schemePortResolver); + this.nonProxyHostPatterns = getNonProxyHostPatterns(nonProxyHosts); + } + + private List getNonProxyHostPatterns(@Nullable List nonProxyHosts) { + if (nonProxyHosts == null) { + return ImmutableList.of(); + } + + final ImmutableList.Builder patterns = ImmutableList.builder(); + for (String nonProxyHost : nonProxyHosts) { + // Replaces a wildcard to a regular expression + patterns.add(Pattern.compile(WILDCARD.matcher(nonProxyHost).replaceAll(REGEX_WILDCARD))); + } + return patterns.build(); + } + + @VisibleForTesting + protected List getNonProxyHostPatterns() { + return nonProxyHostPatterns; + } + + @Override + protected HttpHost determineProxy(HttpHost target, HttpRequest request, HttpContext context) throws HttpException { + for (Pattern nonProxyHostPattern : nonProxyHostPatterns) { + if (nonProxyHostPattern.matcher(target.getHostName()).matches()) { + return null; + } + } + return super.determineProxy(target, request, context); + } +} diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/proxy/ProxyConfiguration.java b/dropwizard-client/src/main/java/io/dropwizard/client/proxy/ProxyConfiguration.java new file mode 100644 index 00000000000..d9fdeebcc35 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/proxy/ProxyConfiguration.java @@ -0,0 +1,140 @@ +package io.dropwizard.client.proxy; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.dropwizard.validation.OneOf; +import io.dropwizard.validation.PortRange; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.annotation.Nullable; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Configuration of access to a remote host through a proxy server + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code host}REQUIREDThe proxy server host name or ip address.
    {@code port}scheme defaultThe proxy server port. If the port is not set then the scheme default port is used.
    {@code scheme}httpThe proxy server URI scheme. HTTP and HTTPS schemas are permitted. By default HTTP scheme is used.
    {@code auth}(none) + * The proxy server {@link io.dropwizard.client.proxy.AuthConfiguration} BASIC authentication credentials. + * If they are not set then no credentials will be passed to the server. + *
    {@code nonProxyHosts}(none) + * List of patterns of hosts that should be reached without proxy. + * The patterns may contain symbol '*' as a wildcard. + * If a host matches one of the patterns it will be reached through a direct connection. + *
    + */ +public class ProxyConfiguration { + + @NotEmpty + private String host; + + @PortRange(min = -1) + private Integer port = -1; + + @OneOf(value = {"http", "https"}, ignoreCase = true) + private String scheme = "http"; + + @Valid + @Nullable + private AuthConfiguration auth; + + @Nullable + private List nonProxyHosts; + + public ProxyConfiguration() { + } + + public ProxyConfiguration(@NotNull String host) { + this.host = host; + } + + public ProxyConfiguration(@NotNull String host, int port) { + this(host); + this.port = port; + } + + public ProxyConfiguration(@NotNull String host, int port, String scheme, AuthConfiguration auth) { + this(host, port); + this.scheme = scheme; + this.auth = auth; + } + + @JsonProperty + public String getHost() { + return host; + } + + @JsonProperty + public void setHost(String host) { + this.host = host; + } + + @JsonProperty + public Integer getPort() { + return port; + } + + @JsonProperty + public void setPort(Integer port) { + this.port = port; + } + + @JsonProperty + public String getScheme() { + return scheme; + } + + @JsonProperty + public void setScheme(String scheme) { + this.scheme = scheme; + } + + @JsonProperty + public List getNonProxyHosts() { + return nonProxyHosts; + } + + @JsonProperty + public void setNonProxyHosts(List nonProxyHosts) { + this.nonProxyHosts = nonProxyHosts; + } + + public AuthConfiguration getAuth() { + return auth; + } + + public void setAuth(AuthConfiguration auth) { + this.auth = auth; + } +} diff --git a/dropwizard-client/src/main/java/io/dropwizard/client/ssl/TlsConfiguration.java b/dropwizard-client/src/main/java/io/dropwizard/client/ssl/TlsConfiguration.java new file mode 100644 index 00000000000..576f85418f1 --- /dev/null +++ b/dropwizard-client/src/main/java/io/dropwizard/client/ssl/TlsConfiguration.java @@ -0,0 +1,162 @@ +package io.dropwizard.client.ssl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import io.dropwizard.validation.ValidationMethod; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.annotation.Nullable; +import java.io.File; +import java.util.List; + +public class TlsConfiguration { + + @NotEmpty + private String protocol = "TLSv1.2"; + + private File keyStorePath; + + private String keyStorePassword; + + @NotEmpty + private String keyStoreType = "JKS"; + + private File trustStorePath; + + private String trustStorePassword; + + @NotEmpty + private String trustStoreType = "JKS"; + + private boolean trustSelfSignedCertificates = false; + + private boolean verifyHostname = true; + + @Nullable + private List supportedProtocols = null; + + @Nullable + private List supportedCiphers = null; + + @JsonProperty + public void setTrustSelfSignedCertificates(boolean trustSelfSignedCertificates) { + this.trustSelfSignedCertificates = trustSelfSignedCertificates; + } + + @JsonProperty + public boolean isTrustSelfSignedCertificates() { + return trustSelfSignedCertificates; + } + + @JsonProperty + public File getKeyStorePath() { + return keyStorePath; + } + + @JsonProperty + public void setKeyStorePath(File 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 getTrustStoreType() { + return trustStoreType; + } + + @JsonProperty + public void setTrustStoreType(String trustStoreType) { + this.trustStoreType = trustStoreType; + } + + + @JsonProperty + public File getTrustStorePath() { + return trustStorePath; + } + + @JsonProperty + public void setTrustStorePath(File trustStorePath) { + this.trustStorePath = trustStorePath; + } + + @JsonProperty + public String getTrustStorePassword() { + return trustStorePassword; + } + + @JsonProperty + public void setTrustStorePassword(String trustStorePassword) { + this.trustStorePassword = trustStorePassword; + } + + @JsonProperty + public boolean isVerifyHostname() { + return verifyHostname; + } + + @JsonProperty + public void setVerifyHostname(boolean verifyHostname) { + this.verifyHostname = verifyHostname; + } + + @JsonProperty + public String getProtocol() { + return protocol; + } + + @JsonProperty + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + @Nullable + @JsonProperty + public List getSupportedCiphers() { + return supportedCiphers; + } + + @JsonProperty + public void setSupportedCiphers(@Nullable List supportedCiphers) { + this.supportedCiphers = supportedCiphers; + } + + @Nullable + @JsonProperty + public List getSupportedProtocols() { + return supportedProtocols; + } + + @JsonProperty + public void setSupportedProtocols(@Nullable List supportedProtocols) { + this.supportedProtocols = supportedProtocols; + } + + @ValidationMethod(message = "keyStorePassword should not be null or empty if keyStorePath not null") + public boolean isValidKeyStorePassword() { + return keyStorePath == null || keyStoreType.startsWith("Windows-") || !Strings.isNullOrEmpty(keyStorePassword); + } + + @ValidationMethod(message = "trustStorePassword should not be null or empty if trustStorePath not null") + public boolean isValidTrustStorePassword() { + return trustStorePath == null || trustStoreType.startsWith("Windows-") || !Strings.isNullOrEmpty(trustStorePassword); + } +} \ No newline at end of file diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/ConfiguredCloseableHttpClientTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/ConfiguredCloseableHttpClientTest.java new file mode 100644 index 00000000000..cac272d5066 --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/ConfiguredCloseableHttpClientTest.java @@ -0,0 +1,35 @@ +package io.dropwizard.client; + +import org.apache.http.client.config.RequestConfig; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class ConfiguredCloseableHttpClientTest { + public ConfiguredCloseableHttpClient configuredClient; + @Mock + private CloseableHttpClient closeableHttpClientMock; + @Mock + private RequestConfig defaultRequestConfigMock; + + @Before + public void setUp() { + configuredClient = new ConfiguredCloseableHttpClient(closeableHttpClientMock, defaultRequestConfigMock); + } + + @Test + public void getDefaultRequestConfig_returns_config_provided_at_construction() { + assertThat(configuredClient.getDefaultRequestConfig()).isEqualTo(defaultRequestConfigMock); + } + + @Test + public void getClient_returns_config_provided_at_construction() { + assertThat(configuredClient.getClient()).isEqualTo(closeableHttpClientMock); + } +} \ No newline at end of file diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/DropwizardApacheConnectorTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/DropwizardApacheConnectorTest.java new file mode 100644 index 00000000000..9a64cb9a5b9 --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/DropwizardApacheConnectorTest.java @@ -0,0 +1,260 @@ +package io.dropwizard.client; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.health.HealthCheck; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.dropwizard.util.Duration; +import org.apache.http.Header; +import org.apache.http.HttpStatus; +import org.apache.http.ProtocolVersion; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.conn.ConnectTimeoutException; +import org.apache.http.conn.HttpHostConnectException; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.assertj.core.api.AbstractLongAssert; +import org.eclipse.jetty.util.component.LifeCycle; +import org.glassfish.jersey.client.ClientProperties; +import org.glassfish.jersey.client.ClientRequest; +import org.glassfish.jersey.client.ClientResponse; +import org.glassfish.jersey.client.JerseyClient; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.mockito.Matchers; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.Response; +import java.net.NoRouteToHostException; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DropwizardApacheConnectorTest { + + private static final int SLEEP_TIME_IN_MILLIS = 1000; + private static final int DEFAULT_CONNECT_TIMEOUT_IN_MILLIS = 500; + private static final int ERROR_MARGIN_IN_MILLIS = 300; + private static final int INCREASE_IN_MILLIS = 100; + private static final URI NON_ROUTABLE_ADDRESS = URI.create("http://10.255.255.1"); + + @ClassRule + public static final DropwizardAppRule APP_RULE = new DropwizardAppRule<>( + TestApplication.class, + ResourceHelpers.resourceFilePath("yaml/dropwizardApacheConnectorTest.yml")); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final URI testUri = URI.create("http://localhost:" + APP_RULE.getLocalPort()); + + private JerseyClient client; + private Environment environment; + + @Before + public void setup() throws Exception { + JerseyClientConfiguration clientConfiguration = new JerseyClientConfiguration(); + clientConfiguration.setConnectionTimeout(Duration.milliseconds(SLEEP_TIME_IN_MILLIS / 2)); + clientConfiguration.setTimeout(Duration.milliseconds(DEFAULT_CONNECT_TIMEOUT_IN_MILLIS)); + + environment = new Environment("test-dropwizard-apache-connector", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), + getClass().getClassLoader()); + client = (JerseyClient) new JerseyClientBuilder(environment) + .using(clientConfiguration) + .build("test"); + for (LifeCycle lifeCycle : environment.lifecycle().getManagedObjects()) { + lifeCycle.start(); + } + } + + @After + public void tearDown() throws Exception { + for (LifeCycle lifeCycle : environment.lifecycle().getManagedObjects()) { + lifeCycle.stop(); + } + assertThat(client.isClosed()).isTrue(); + } + + @Test + public void when_no_read_timeout_override_then_client_request_times_out() { + thrown.expect(ProcessingException.class); + thrown.expectCause(any(SocketTimeoutException.class)); + + client.target(testUri + "/long_running") + .request() + .get(); + } + + @Test + public void when_read_timeout_override_created_then_client_requests_completes_successfully() { + client.target(testUri + "/long_running") + .property(ClientProperties.READ_TIMEOUT, SLEEP_TIME_IN_MILLIS * 2) + .request() + .get(); + } + + /** + *

    In first assertion we prove, that a request takes no longer than: + * request_time < connect_timeout + error_margin (1)

    + *

    + *

    In the second we show that if we set connect_timeout to + * set_connect_timeout + increase + error_margin then + * request_time > connect_timeout + increase + error_margin (2)

    + *

    + *

    Now, (1) and (2) can hold at the same time if then connect_timeout update was successful.

    + */ + @Test + public void connect_timeout_override_changes_how_long_it_takes_for_a_connection_to_timeout() { + // before override + WebTarget target = client.target(NON_ROUTABLE_ADDRESS); + + //This can't be tested without a real connection + try { + target.request().get(Response.class); + } catch (ProcessingException e) { + if (e.getCause() instanceof HttpHostConnectException) { + return; + } + } + + assertThatConnectionTimeoutFor(target).isLessThan(DEFAULT_CONNECT_TIMEOUT_IN_MILLIS + ERROR_MARGIN_IN_MILLIS); + + // after override + final int newTimeout = DEFAULT_CONNECT_TIMEOUT_IN_MILLIS + INCREASE_IN_MILLIS + ERROR_MARGIN_IN_MILLIS; + final WebTarget newTarget = target.property(ClientProperties.CONNECT_TIMEOUT, newTimeout); + assertThatConnectionTimeoutFor(newTarget).isGreaterThan(newTimeout); + } + + @Test + public void when_no_override_then_redirected_request_successfully_redirected() { + assertThat(client.target(testUri + "/redirect") + .request() + .get(String.class) + ).isEqualTo("redirected"); + } + + @Test + public void when_configuration_overridden_to_disallow_redirects_temporary_redirect_status_returned() { + assertThat(client.target(testUri + "/redirect") + .property(ClientProperties.FOLLOW_REDIRECTS, false) + .request() + .get(Response.class) + .getStatus() + ).isEqualTo(HttpStatus.SC_TEMPORARY_REDIRECT); + } + + @Test + public void when_jersey_client_runtime_is_garbage_collected_apache_client_is_not_closed() { + for (int j = 0; j < 5; j++) { + System.gc(); // We actually want GC here + final String response = client.target(testUri + "/long_running") + .property(ClientProperties.READ_TIMEOUT, SLEEP_TIME_IN_MILLIS * 2) + .request() + .get(String.class); + assertThat(response).isEqualTo("success"); + } + } + + @Test + public void multiple_headers_with_the_same_name_are_processed_successfully() throws Exception { + + final CloseableHttpClient client = mock(CloseableHttpClient.class); + final DropwizardApacheConnector dropwizardApacheConnector = new DropwizardApacheConnector(client, null, false); + final Header[] apacheHeaders = { + new BasicHeader("Set-Cookie", "test1"), + new BasicHeader("Set-Cookie", "test2") + }; + + final CloseableHttpResponse apacheResponse = mock(CloseableHttpResponse.class); + when(apacheResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK")); + when(apacheResponse.getAllHeaders()).thenReturn(apacheHeaders); + when(client.execute(Matchers.any())).thenReturn(apacheResponse); + + final ClientRequest jerseyRequest = mock(ClientRequest.class); + when(jerseyRequest.getUri()).thenReturn(URI.create("http://localhost")); + when(jerseyRequest.getMethod()).thenReturn("GET"); + when(jerseyRequest.getHeaders()).thenReturn(new MultivaluedHashMap<>()); + + final ClientResponse jerseyResponse = dropwizardApacheConnector.apply(jerseyRequest); + + assertThat(jerseyResponse.getStatus()).isEqualTo(apacheResponse.getStatusLine().getStatusCode()); + + } + + @Path("/") + public static class TestResource { + + @GET + @Path("/long_running") + public String getWithSleep() throws InterruptedException { + TimeUnit.MILLISECONDS.sleep(SLEEP_TIME_IN_MILLIS); + return "success"; + } + + @GET + @Path("redirect") + public Response getWithRedirect() { + return Response.temporaryRedirect(URI.create("/redirected")).build(); + } + + @GET + @Path("redirected") + public String redirectedGet() { + return "redirected"; + } + + } + + public static class TestApplication extends Application { + public static void main(String[] args) throws Exception { + new TestApplication().run(args); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(TestResource.class); + environment.healthChecks().register("dummy", new HealthCheck() { + @Override + protected Result check() throws Exception { + return Result.healthy(); + } + }); + } + } + + private static AbstractLongAssert assertThatConnectionTimeoutFor(WebTarget webTarget) { + final long startTime = System.nanoTime(); + try { + webTarget.request().get(Response.class); + } catch (ProcessingException e) { + final long endTime = System.nanoTime(); + assertThat(e).isNotNull(); + //noinspection ConstantConditions + assertThat(e.getCause()).isNotNull(); + assertThat(e.getCause()).isInstanceOfAny(ConnectTimeoutException.class, NoRouteToHostException.class); + return assertThat(TimeUnit.MILLISECONDS.convert(endTime - startTime, TimeUnit.NANOSECONDS)); + } + throw new AssertionError("ProcessingException expected but not thrown"); + } + +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/DropwizardExecutorProviderTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/DropwizardExecutorProviderTest.java new file mode 100644 index 00000000000..f019330220c --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/DropwizardExecutorProviderTest.java @@ -0,0 +1,43 @@ +package io.dropwizard.client; + +import io.dropwizard.util.Duration; +import org.glassfish.jersey.spi.ExecutorServiceProvider; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class DropwizardExecutorProviderTest { + private static final Duration SHUTDOWN_TIME = Duration.seconds(5); + + @Test + public void doesntShutDownNonDisposableExecutorService() { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + final ExecutorServiceProvider provider = + new DropwizardExecutorProvider(executor, SHUTDOWN_TIME); + + assertThat(executor.isShutdown()).isFalse(); + provider.dispose(executor); + assertThat(executor.isShutdown()).isFalse(); + executor.shutdown(); + } + + @Test + public void shutsDownDisposableExecutorService() { + final ExecutorService executor = Executors.newSingleThreadExecutor(); + final ExecutorService disposableExecutor = + new DropwizardExecutorProvider.DisposableExecutorService(executor); + + final ExecutorServiceProvider provider = + new DropwizardExecutorProvider(disposableExecutor, SHUTDOWN_TIME); + + assertThat(executor.isShutdown()).isFalse(); + provider.dispose(disposableExecutor); + assertThat(executor.isShutdown()).isTrue(); + } +} 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 00000000000..d1a9f841ebb --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/HttpClientBuilderTest.java @@ -0,0 +1,662 @@ +package io.dropwizard.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.validateMockitoUsage; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.util.List; +import java.util.Optional; + +import javax.net.ssl.HostnameVerifier; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.http.Header; +import org.apache.http.HeaderIterator; +import org.apache.http.HttpHeaders; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolException; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.RedirectStrategy; +import org.apache.http.client.config.CookieSpecs; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.config.SocketConfig; +import org.apache.http.conn.DnsResolver; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.DefaultConnectionReuseStrategy; +import org.apache.http.impl.NoConnectionReuseStrategy; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy; +import org.apache.http.impl.conn.DefaultRoutePlanner; +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; +import org.apache.http.impl.conn.SystemDefaultDnsResolver; +import org.apache.http.impl.conn.SystemDefaultRoutePlanner; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicListHeaderIterator; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HTTP; +import org.apache.http.protocol.HttpContext; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.httpclient.HttpClientMetricNameStrategies; +import com.codahale.metrics.httpclient.InstrumentedHttpClientConnectionManager; +import com.codahale.metrics.httpclient.InstrumentedHttpRequestExecutor; +import com.google.common.collect.ImmutableList; + +import io.dropwizard.client.proxy.AuthConfiguration; +import io.dropwizard.client.proxy.ProxyConfiguration; +import io.dropwizard.client.ssl.TlsConfiguration; +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.setup.Environment; +import io.dropwizard.util.Duration; + +public class HttpClientBuilderTest { + static class CustomBuilder extends HttpClientBuilder { + public boolean customized; + + public CustomBuilder(MetricRegistry metricRegistry) { + super(metricRegistry); + customized = false; + } + + @Override + protected org.apache.http.impl.client.HttpClientBuilder customizeBuilder( + org.apache.http.impl.client.HttpClientBuilder builder + ) { + customized = true; + return builder; + } + } + + private final Class httpClientBuilderClass; + private final Class httpClientClass; + private final Registry registry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", SSLConnectionSocketFactory.getSocketFactory()) + .build(); + private HttpClientConfiguration configuration; + private HttpClientBuilder builder; + private InstrumentedHttpClientConnectionManager connectionManager; + private org.apache.http.impl.client.HttpClientBuilder apacheBuilder; + + public HttpClientBuilderTest() throws ClassNotFoundException { + this.httpClientBuilderClass = Class.forName("org.apache.http.impl.client.HttpClientBuilder"); + this.httpClientClass = Class.forName("org.apache.http.impl.client.InternalHttpClient"); + } + + @Before + public void setUp() { + final MetricRegistry metricRegistry = new MetricRegistry(); + configuration = new HttpClientConfiguration(); + builder = new HttpClientBuilder(metricRegistry); + connectionManager = spy(new InstrumentedHttpClientConnectionManager(metricRegistry, registry)); + apacheBuilder = org.apache.http.impl.client.HttpClientBuilder.create(); + initMocks(this); + } + + @After + public void validate() { + validateMockitoUsage(); + } + + @Test + public void setsTheMaximumConnectionPoolSize() throws Exception { + configuration.setMaxConnections(412); + final ConfiguredCloseableHttpClient client = builder.using(configuration) + .createClient(apacheBuilder, builder.configureConnectionManager(connectionManager), "test"); + + assertThat(client).isNotNull(); + assertThat(spyHttpClientBuilderField("connManager", apacheBuilder)).isSameAs(connectionManager); + verify(connectionManager).setMaxTotal(412); + } + + + @Test + public void setsTheMaximumRoutePoolSize() throws Exception { + configuration.setMaxConnectionsPerRoute(413); + final ConfiguredCloseableHttpClient client = builder.using(configuration) + .createClient(apacheBuilder, builder.configureConnectionManager(connectionManager), "test"); + + assertThat(client).isNotNull(); + assertThat(spyHttpClientBuilderField("connManager", apacheBuilder)).isSameAs(connectionManager); + verify(connectionManager).setDefaultMaxPerRoute(413); + } + + @Test + public void setsTheUserAgent() throws Exception { + configuration.setUserAgent(Optional.of("qwerty")); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(spyHttpClientBuilderField("userAgent", apacheBuilder)).isEqualTo("qwerty"); + } + + @Test + public void canUseACustomDnsResolver() throws Exception { + final DnsResolver resolver = mock(DnsResolver.class); + final InstrumentedHttpClientConnectionManager manager = + builder.using(resolver).createConnectionManager(registry, "test"); + + // Yes, this is gross. Thanks, Apache! + final Field connectionOperatorField = + FieldUtils.getField(PoolingHttpClientConnectionManager.class, "connectionOperator", true); + final Object connectOperator = connectionOperatorField.get(manager); + final Field dnsResolverField = FieldUtils.getField(connectOperator.getClass(), "dnsResolver", true); + assertThat(dnsResolverField.get(connectOperator)).isEqualTo(resolver); + } + + + @Test + public void usesASystemDnsResolverByDefault() throws Exception { + final InstrumentedHttpClientConnectionManager manager = builder.createConnectionManager(registry, "test"); + + // Yes, this is gross. Thanks, Apache! + final Field connectionOperatorField = + FieldUtils.getField(PoolingHttpClientConnectionManager.class, "connectionOperator", true); + final Object connectOperator = connectionOperatorField.get(manager); + final Field dnsResolverField = FieldUtils.getField(connectOperator.getClass(), "dnsResolver", true); + assertThat(dnsResolverField.get(connectOperator)).isInstanceOf(SystemDefaultDnsResolver.class); + } + + @Test + public void canUseACustomHostnameVerifierWhenTlsConfigurationNotSpecified() throws Exception { + final HostnameVerifier customVerifier = (s, sslSession) -> false; + + final Registry configuredRegistry; + configuredRegistry = builder.using(customVerifier).createConfiguredRegistry(); + assertThat(configuredRegistry).isNotNull(); + + final SSLConnectionSocketFactory socketFactory = + (SSLConnectionSocketFactory) configuredRegistry.lookup("https"); + assertThat(socketFactory).isNotNull(); + + final Field hostnameVerifierField = + FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true); + assertThat(hostnameVerifierField.get(socketFactory)).isSameAs(customVerifier); + } + + @Test + public void canUseACustomHostnameVerifierWhenTlsConfigurationSpecified() throws Exception { + final TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setVerifyHostname(true); + configuration.setTlsConfiguration(tlsConfiguration); + + final HostnameVerifier customVerifier = (s, sslSession) -> false; + + final Registry configuredRegistry; + configuredRegistry = builder.using(configuration).using(customVerifier).createConfiguredRegistry(); + assertThat(configuredRegistry).isNotNull(); + + final SSLConnectionSocketFactory socketFactory = + (SSLConnectionSocketFactory) configuredRegistry.lookup("https"); + assertThat(socketFactory).isNotNull(); + + final Field hostnameVerifierField = + FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true); + assertThat(hostnameVerifierField.get(socketFactory)).isSameAs(customVerifier); + } + + @Test + public void canUseASystemHostnameVerifierByDefaultWhenTlsConfigurationNotSpecified() throws Exception { + final Registry configuredRegistry; + configuredRegistry = builder.createConfiguredRegistry(); + assertThat(configuredRegistry).isNotNull(); + + final SSLConnectionSocketFactory socketFactory = + (SSLConnectionSocketFactory) configuredRegistry.lookup("https"); + assertThat(socketFactory).isNotNull(); + + final Field hostnameVerifierField = + FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true); + assertThat(hostnameVerifierField.get(socketFactory)).isInstanceOf(HostnameVerifier.class); + } + + @Test + public void canUseASystemHostnameVerifierByDefaultWhenTlsConfigurationSpecified() throws Exception { + final TlsConfiguration tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setVerifyHostname(true); + configuration.setTlsConfiguration(tlsConfiguration); + + final Registry configuredRegistry; + configuredRegistry = builder.using(configuration).createConfiguredRegistry(); + assertThat(configuredRegistry).isNotNull(); + + final SSLConnectionSocketFactory socketFactory = + (SSLConnectionSocketFactory) configuredRegistry.lookup("https"); + assertThat(socketFactory).isNotNull(); + + final Field hostnameVerifierField = + FieldUtils.getField(SSLConnectionSocketFactory.class, "hostnameVerifier", true); + assertThat(hostnameVerifierField.get(socketFactory)).isInstanceOf(HostnameVerifier.class); + } + + @Test + public void createClientCanPassCustomVerifierToApacheBuilder() throws Exception { + final HostnameVerifier customVerifier = (s, sslSession) -> false; + + assertThat(builder.using(customVerifier).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + final Field hostnameVerifierField = + FieldUtils.getField(org.apache.http.impl.client.HttpClientBuilder.class, "hostnameVerifier", true); + assertThat(hostnameVerifierField.get(apacheBuilder)).isSameAs(customVerifier); + } + + @Test + public void doesNotReuseConnectionsIfKeepAliveIsZero() throws Exception { + configuration.setKeepAlive(Duration.seconds(0)); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(spyHttpClientBuilderField("reuseStrategy", apacheBuilder)) + .isInstanceOf(NoConnectionReuseStrategy.class); + } + + + @Test + public void reusesConnectionsIfKeepAliveIsNonZero() throws Exception { + configuration.setKeepAlive(Duration.seconds(1)); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(spyHttpClientBuilderField("reuseStrategy", apacheBuilder)) + .isInstanceOf(DefaultConnectionReuseStrategy.class); + } + + @Test + public void usesKeepAliveForPersistentConnections() throws Exception { + configuration.setKeepAlive(Duration.seconds(1)); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + final DefaultConnectionKeepAliveStrategy strategy = + (DefaultConnectionKeepAliveStrategy) spyHttpClientBuilderField("keepAliveStrategy", apacheBuilder); + final HttpContext context = mock(HttpContext.class); + final HttpResponse response = mock(HttpResponse.class); + when(response.headerIterator(HTTP.CONN_KEEP_ALIVE)).thenReturn(mock(HeaderIterator.class)); + + assertThat(strategy.getKeepAliveDuration(response, context)).isEqualTo(1000); + } + + @Test + public void usesDefaultForNonPersistentConnections() throws Exception { + configuration.setKeepAlive(Duration.seconds(1)); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + final Field field = FieldUtils.getField(httpClientBuilderClass, "keepAliveStrategy", true); + final DefaultConnectionKeepAliveStrategy strategy = (DefaultConnectionKeepAliveStrategy) field.get(apacheBuilder); + final HttpContext context = mock(HttpContext.class); + final HttpResponse response = mock(HttpResponse.class); + final HeaderIterator iterator = new BasicListHeaderIterator( + ImmutableList.of(new BasicHeader(HttpHeaders.CONNECTION, "timeout=50")), + HttpHeaders.CONNECTION + ); + when(response.headerIterator(HTTP.CONN_KEEP_ALIVE)).thenReturn(iterator); + + assertThat(strategy.getKeepAliveDuration(response, context)).isEqualTo(50000); + } + + @Test + public void ignoresCookiesByDefault() throws Exception { + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(((RequestConfig) spyHttpClientBuilderField("defaultRequestConfig", apacheBuilder)).getCookieSpec()) + .isEqualTo(CookieSpecs.IGNORE_COOKIES); + } + + @Test + public void usesBestMatchCookiePolicyIfCookiesAreEnabled() throws Exception { + configuration.setCookiesEnabled(true); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(((RequestConfig) spyHttpClientBuilderField("defaultRequestConfig", apacheBuilder)).getCookieSpec()) + .isEqualTo(CookieSpecs.DEFAULT); + } + + @Test + public void setsTheSocketTimeout() throws Exception { + configuration.setTimeout(Duration.milliseconds(500)); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(((RequestConfig) spyHttpClientBuilderField("defaultRequestConfig", apacheBuilder)).getSocketTimeout()) + .isEqualTo(500); + } + + @Test + public void setsTheConnectTimeout() throws Exception { + configuration.setConnectionTimeout(Duration.milliseconds(500)); + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(((RequestConfig) spyHttpClientBuilderField("defaultRequestConfig", apacheBuilder)).getConnectTimeout()) + .isEqualTo(500); + } + + @Test + public void setsTheConnectionRequestTimeout() throws Exception { + configuration.setConnectionRequestTimeout(Duration.milliseconds(123)); + + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + assertThat(((RequestConfig) spyHttpClientBuilderField("defaultRequestConfig", apacheBuilder)).getConnectionRequestTimeout()) + .isEqualTo(123); + } + + @Test + public void disablesNaglesAlgorithm() throws Exception { + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(((SocketConfig) spyHttpClientBuilderField("defaultSocketConfig", apacheBuilder)).isTcpNoDelay()).isTrue(); + } + + @Test + public void disablesStaleConnectionCheck() throws Exception { + assertThat(builder.using(configuration).createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + // It is fine to use the isStaleConnectionCheckEnabled deprecated API, as we are ensuring + // that the builder creates a client that does not check for stale connections on each + // request, which adds significant overhead. + assertThat(((RequestConfig) spyHttpClientBuilderField("defaultRequestConfig", apacheBuilder)) + .isStaleConnectionCheckEnabled()).isFalse(); + } + + @Test + public void usesTheDefaultRoutePlanner() throws Exception { + final CloseableHttpClient httpClient = builder.using(configuration) + .createClient(apacheBuilder, connectionManager, "test").getClient(); + + assertThat(httpClient).isNotNull(); + assertThat(spyHttpClientBuilderField("routePlanner", apacheBuilder)).isNull(); + assertThat(spyHttpClientField("routePlanner", httpClient)).isInstanceOf(DefaultRoutePlanner.class); + } + + @Test + public void usesACustomRoutePlanner() throws Exception { + final HttpRoutePlanner routePlanner = new SystemDefaultRoutePlanner(new ProxySelector() { + @Override + public List select(URI uri) { + return ImmutableList.of(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.52.1", 8080))); + } + + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + + } + }); + final CloseableHttpClient httpClient = builder.using(configuration).using(routePlanner) + .createClient(apacheBuilder, connectionManager, "test").getClient(); + + assertThat(httpClient).isNotNull(); + assertThat(spyHttpClientBuilderField("routePlanner", apacheBuilder)).isSameAs(routePlanner); + assertThat(spyHttpClientField("routePlanner", httpClient)).isSameAs(routePlanner); + } + + @Test + public void usesACustomHttpRequestRetryHandler() throws Exception { + final HttpRequestRetryHandler customHandler = (exception, executionCount, context) -> false; + + configuration.setRetries(1); + assertThat(builder.using(configuration).using(customHandler) + .createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(spyHttpClientBuilderField("retryHandler", apacheBuilder)).isSameAs(customHandler); + } + + @Test + public void usesCredentialsProvider() throws Exception { + final CredentialsProvider credentialsProvider = new CredentialsProvider() { + @Override + public void setCredentials(AuthScope authscope, Credentials credentials) { + } + + @Override + public Credentials getCredentials(AuthScope authscope) { + return null; + } + + @Override + public void clear() { + } + }; + + assertThat(builder.using(configuration).using(credentialsProvider) + .createClient(apacheBuilder, connectionManager, "test")).isNotNull(); + + assertThat(spyHttpClientBuilderField("credentialsProvider", apacheBuilder)).isSameAs(credentialsProvider); + } + + @Test + public void usesProxy() throws Exception { + HttpClientConfiguration config = new HttpClientConfiguration(); + ProxyConfiguration proxy = new ProxyConfiguration("192.168.52.11", 8080); + config.setProxyConfiguration(proxy); + + checkProxy(config, new HttpHost("dropwizard.io", 80), new HttpHost("192.168.52.11", 8080)); + } + + @Test + public void usesProxyWithoutPort() throws Exception { + HttpClientConfiguration config = new HttpClientConfiguration(); + ProxyConfiguration proxy = new ProxyConfiguration("192.168.52.11"); + config.setProxyConfiguration(proxy); + + checkProxy(config, new HttpHost("dropwizard.io", 80), new HttpHost("192.168.52.11")); + } + + @Test + public void usesProxyWithAuth() throws Exception { + HttpClientConfiguration config = new HttpClientConfiguration(); + AuthConfiguration auth = new AuthConfiguration("secret", "stuff"); + ProxyConfiguration proxy = new ProxyConfiguration("192.168.52.11", 8080, "http", auth); + config.setProxyConfiguration(proxy); + + CloseableHttpClient httpClient = checkProxy(config, new HttpHost("dropwizard.io", 80), + new HttpHost("192.168.52.11", 8080, "http")); + CredentialsProvider credentialsProvider = (CredentialsProvider) + FieldUtils.getField(httpClient.getClass(), "credentialsProvider", true) + .get(httpClient); + + assertThat(credentialsProvider.getCredentials(new AuthScope("192.168.52.11", 8080))) + .isEqualTo(new UsernamePasswordCredentials("secret", "stuff")); + } + + @Test + public void usesProxyWithNonProxyHosts() throws Exception { + HttpClientConfiguration config = new HttpClientConfiguration(); + ProxyConfiguration proxy = new ProxyConfiguration("192.168.52.11", 8080); + proxy.setNonProxyHosts(ImmutableList.of("*.example.com")); + config.setProxyConfiguration(proxy); + + checkProxy(config, new HttpHost("host.example.com", 80), null); + } + + @Test + public void usesProxyWithNonProxyHostsAndTargetDoesNotMatch() throws Exception { + HttpClientConfiguration config = new HttpClientConfiguration(); + ProxyConfiguration proxy = new ProxyConfiguration("192.168.52.11"); + proxy.setNonProxyHosts(ImmutableList.of("*.example.com")); + config.setProxyConfiguration(proxy); + + checkProxy(config, new HttpHost("dropwizard.io", 80), new HttpHost("192.168.52.11")); + } + + @Test + public void usesNoProxy() throws Exception { + checkProxy(new HttpClientConfiguration(), new HttpHost("dropwizard.io", 80), null); + } + + private CloseableHttpClient checkProxy(HttpClientConfiguration config, HttpHost target, HttpHost expectedProxy) + throws Exception { + CloseableHttpClient httpClient = builder.using(config).build("test"); + HttpRoutePlanner routePlanner = (HttpRoutePlanner) + FieldUtils.getField(httpClient.getClass(), "routePlanner", true).get(httpClient); + + HttpRoute route = routePlanner.determineRoute(target, new HttpGet(target.toURI()), + new BasicHttpContext()); + assertThat(route.getProxyHost()).isEqualTo(expectedProxy); + assertThat(route.getTargetHost()).isEqualTo(target); + assertThat(route.getHopCount()).isEqualTo(expectedProxy != null ? 2 : 1); + + return httpClient; + } + + @Test + public void setValidateAfterInactivityPeriodFromConfiguration() throws Exception { + int validateAfterInactivityPeriod = 50000; + configuration.setValidateAfterInactivityPeriod(Duration.milliseconds(validateAfterInactivityPeriod)); + final ConfiguredCloseableHttpClient client = builder.using(configuration) + .createClient(apacheBuilder, builder.configureConnectionManager(connectionManager), "test"); + + assertThat(client).isNotNull(); + assertThat(spyHttpClientBuilderField("connManager", apacheBuilder)).isSameAs(connectionManager); + verify(connectionManager).setValidateAfterInactivity(validateAfterInactivityPeriod); + } + + @Test + public void usesACustomHttpClientMetricNameStrategy() throws Exception { + assertThat(builder.using(HttpClientMetricNameStrategies.HOST_AND_METHOD) + .createClient(apacheBuilder, connectionManager, "test")) + .isNotNull(); + assertThat(FieldUtils.getField(InstrumentedHttpRequestExecutor.class, + "metricNameStrategy", true) + .get(spyHttpClientBuilderField("requestExec", apacheBuilder))) + .isSameAs(HttpClientMetricNameStrategies.HOST_AND_METHOD); + } + + @Test + public void usesMethodOnlyHttpClientMetricNameStrategyByDefault() throws Exception { + assertThat(builder.createClient(apacheBuilder, connectionManager, "test")) + .isNotNull(); + assertThat(FieldUtils.getField(InstrumentedHttpRequestExecutor.class, + "metricNameStrategy", true) + .get(spyHttpClientBuilderField("requestExec", apacheBuilder))) + .isSameAs(HttpClientMetricNameStrategies.METHOD_ONLY); + } + + @Test + public void exposedConfigIsTheSameAsInternalToTheWrappedHttpClient() throws Exception { + ConfiguredCloseableHttpClient client = builder.createClient(apacheBuilder, connectionManager, "test"); + assertThat(client).isNotNull(); + + assertThat(spyHttpClientField("defaultConfig", client.getClient())).isEqualTo(client.getDefaultRequestConfig()); + } + + @Test + public void disablesContentCompression() throws Exception { + ConfiguredCloseableHttpClient client = builder + .disableContentCompression(true) + .createClient(apacheBuilder, connectionManager, "test"); + assertThat(client).isNotNull(); + + final Boolean contentCompressionDisabled = (Boolean) FieldUtils + .getField(httpClientBuilderClass, "contentCompressionDisabled", true) + .get(apacheBuilder); + assertThat(contentCompressionDisabled).isTrue(); + } + + @Test + public void managedByEnvironment() throws Exception { + final Environment environment = mock(Environment.class); + when(environment.getName()).thenReturn("test-env"); + when(environment.metrics()).thenReturn(new MetricRegistry()); + + final LifecycleEnvironment lifecycle = mock(LifecycleEnvironment.class); + when(environment.lifecycle()).thenReturn(lifecycle); + + final CloseableHttpClient httpClient = mock(CloseableHttpClient.class); + HttpClientBuilder httpClientBuilder = spy(new HttpClientBuilder(environment)); + when(httpClientBuilder.buildWithDefaultRequestConfiguration("test-apache-client")) + .thenReturn(new ConfiguredCloseableHttpClient(httpClient, RequestConfig.DEFAULT)); + assertThat(httpClientBuilder.build("test-apache-client")).isSameAs(httpClient); + + // Verify that we registered the managed object + final ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Managed.class); + verify(lifecycle).manage(argumentCaptor.capture()); + + // Verify that the managed object actually stops the HTTP client + final Managed managed = argumentCaptor.getValue(); + managed.stop(); + verify(httpClient).close(); + } + + @Test + public void usesACustomRedirectStrategy() throws Exception { + RedirectStrategy neverFollowRedirectStrategy = new RedirectStrategy() { + @Override + public boolean isRedirected(HttpRequest httpRequest, + HttpResponse httpResponse, + HttpContext httpContext) throws ProtocolException { + return false; + } + + @Override + public HttpUriRequest getRedirect(HttpRequest httpRequest, + HttpResponse httpResponse, + HttpContext httpContext) throws ProtocolException { + return null; + } + }; + ConfiguredCloseableHttpClient client = builder.using(neverFollowRedirectStrategy) + .createClient(apacheBuilder, connectionManager, "test"); + assertThat(client).isNotNull(); + assertThat(spyHttpClientBuilderField("redirectStrategy", apacheBuilder)).isSameAs(neverFollowRedirectStrategy); + } + + @Test + public void usesDefaultHeaders() throws Exception { + final ConfiguredCloseableHttpClient client = + builder.using(ImmutableList.of(new BasicHeader(HttpHeaders.ACCEPT_LANGUAGE, "de"))) + .createClient(apacheBuilder, connectionManager, "test"); + assertThat(client).isNotNull(); + + @SuppressWarnings("unchecked") + List defaultHeaders = (List) FieldUtils + .getField(httpClientBuilderClass, "defaultHeaders", true) + .get(apacheBuilder); + + assertThat(defaultHeaders).hasSize(1); + final Header header = defaultHeaders.get(0); + assertThat(header.getName()).isEqualTo(HttpHeaders.ACCEPT_LANGUAGE); + assertThat(header.getValue()).isEqualTo("de"); + } + + @Test + public void allowsCustomBuilderConfiguration() throws Exception { + CustomBuilder builder = new CustomBuilder(new MetricRegistry()); + assertThat(builder.customized).isFalse(); + ConfiguredCloseableHttpClient client = builder.createClient(apacheBuilder, connectionManager, "test"); + assertThat(builder.customized).isTrue(); + } + + private Object spyHttpClientBuilderField(final String fieldName, final Object obj) throws Exception { + final Field field = FieldUtils.getField(httpClientBuilderClass, fieldName, true); + return field.get(obj); + } + + private Object spyHttpClientField(final String fieldName, final Object obj) throws Exception { + final Field field = FieldUtils.getField(httpClientClass, fieldName, true); + return field.get(obj); + } +} 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 00000000000..07f02aab76f --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientBuilderTest.java @@ -0,0 +1,345 @@ +package io.dropwizard.client; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.httpclient.HttpClientMetricNameStrategies; +import com.codahale.metrics.httpclient.HttpClientMetricNameStrategy; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import io.dropwizard.jersey.gzip.ConfiguredGZipEncoder; +import io.dropwizard.jersey.gzip.GZipDecoder; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.lifecycle.setup.ExecutorServiceBuilder; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.setup.Environment; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.config.Registry; +import org.apache.http.config.RegistryBuilder; +import org.apache.http.conn.DnsResolver; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.socket.ConnectionSocketFactory; +import org.apache.http.conn.socket.PlainConnectionSocketFactory; +import org.apache.http.conn.ssl.NoopHostnameVerifier; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; +import org.apache.http.impl.client.SystemDefaultCredentialsProvider; +import org.apache.http.impl.conn.SystemDefaultDnsResolver; +import org.apache.http.impl.conn.SystemDefaultRoutePlanner; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import javax.validation.Validator; +import javax.ws.rs.Consumes; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Client; +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.net.InetSocketAddress; +import java.net.Proxy; +import java.net.ProxySelector; +import java.net.SocketAddress; +import java.net.URI; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class JerseyClientBuilderTest { + 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 = Executors.newSingleThreadExecutor(); + private final ObjectMapper objectMapper = mock(ObjectMapper.class); + private final Validator validator = Validators.newValidator(); + private final HttpClientBuilder apacheHttpClientBuilder = mock(HttpClientBuilder.class); + + @Before + public void setUp() throws Exception { + when(environment.lifecycle()).thenReturn(lifecycleEnvironment); + when(environment.getObjectMapper()).thenReturn(objectMapper); + when(environment.getValidator()).thenReturn(validator); + builder.setApacheHttpClientBuilder(apacheHttpClientBuilder); + } + + @After + public void tearDown() throws Exception { + executorService.shutdown(); + } + + @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 throwsAnExceptionWithoutAnEnvironmentAndOnlyObjectMapper() throws Exception { + try { + builder.using(objectMapper).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 throwsAnExceptionWithoutAnEnvironmentAndOnlyAThreadPool() throws Exception { + try { + builder.using(executorService).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 includesJerseyProperties() throws Exception { + final Client client = builder.withProperty("poop", true) + .using(executorService, objectMapper) + .build("test"); + + assertThat(client.getConfiguration().getProperty("poop")).isEqualTo(Boolean.TRUE); + } + + @Test + public void includesJerseyProviderSingletons() throws Exception { + final FakeMessageBodyReader provider = new FakeMessageBodyReader(); + final Client client = builder.withProvider(provider) + .using(executorService, objectMapper) + .build("test"); + + assertThat(client.getConfiguration().isRegistered(provider)).isTrue(); + } + + @Test + public void includesJerseyProviderClasses() throws Exception { + @SuppressWarnings("unused") + final Client client = builder.withProvider(FakeMessageBodyReader.class) + .using(executorService, objectMapper) + .build("test"); + + assertThat(client.getConfiguration().isRegistered(FakeMessageBodyReader.class)).isTrue(); + } + + @Test + public void usesTheObjectMapperForJson() throws Exception { + final Client client = builder.using(executorService, objectMapper).build("test"); + assertThat(client.getConfiguration().isRegistered(JacksonMessageBodyProvider.class)).isTrue(); + } + + @Test + public void usesTheGivenThreadPool() throws Exception { + final Client client = builder.using(executorService, objectMapper).build("test"); + for (Object o : client.getConfiguration().getInstances()) { + if (o instanceof DropwizardExecutorProvider) { + final DropwizardExecutorProvider provider = (DropwizardExecutorProvider) o; + assertThat(provider.getExecutorService()).isSameAs(executorService); + } + } + + } + + @Test + public void usesTheGivenThreadPoolAndEnvironmentsObjectMapper() throws Exception { + final Client client = builder.using(environment).using(executorService).build("test"); + for (Object o : client.getConfiguration().getInstances()) { + if (o instanceof DropwizardExecutorProvider) { + final DropwizardExecutorProvider provider = (DropwizardExecutorProvider) o; + assertThat(provider.getExecutorService()).isSameAs(executorService); + } + } + + } + + @Test + public void addBidirectionalGzipSupportIfEnabled() throws Exception { + final JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setGzipEnabled(true); + + final Client client = builder.using(configuration) + .using(executorService, objectMapper).build("test"); + assertThat(Iterables.filter(client.getConfiguration().getInstances(), GZipDecoder.class) + .iterator().hasNext()).isTrue(); + assertThat(Iterables.filter(client.getConfiguration().getInstances(), ConfiguredGZipEncoder.class) + .iterator().hasNext()).isTrue(); + verify(apacheHttpClientBuilder, never()).disableContentCompression(true); + } + + @Test + public void disablesGzipSupportIfDisabled() throws Exception { + final JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setGzipEnabled(false); + + final Client client = builder.using(configuration) + .using(executorService, objectMapper).build("test"); + + assertThat(Iterables.filter(client.getConfiguration().getInstances(), GZipDecoder.class) + .iterator().hasNext()).isFalse(); + assertThat(Iterables.filter(client.getConfiguration().getInstances(), ConfiguredGZipEncoder.class) + .iterator().hasNext()).isFalse(); + verify(apacheHttpClientBuilder).disableContentCompression(true); + } + + @Test + public void usesAnObjectMapperFromTheEnvironment() throws Exception { + final Client client = builder.using(environment).build("test"); + + assertThat(client.getConfiguration().isRegistered(JacksonMessageBodyProvider.class)).isTrue(); + } + + @Test + @SuppressWarnings({"unchecked", "rawtypes"}) + public void usesAnExecutorServiceFromTheEnvironment() throws Exception { + final JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setMinThreads(7); + configuration.setMaxThreads(532); + configuration.setWorkQueueSize(16); + + final ExecutorServiceBuilder executorServiceBuilderMock = mock(ExecutorServiceBuilder.class); + when(lifecycleEnvironment.executorService("jersey-client-test-%d")).thenReturn(executorServiceBuilderMock); + + when(executorServiceBuilderMock.minThreads(7)).thenReturn(executorServiceBuilderMock); + when(executorServiceBuilderMock.maxThreads(532)).thenReturn(executorServiceBuilderMock); + + final ArgumentCaptor arrayBlockingQueueCaptor = + ArgumentCaptor.forClass(ArrayBlockingQueue.class); + when(executorServiceBuilderMock.workQueue(arrayBlockingQueueCaptor.capture())) + .thenReturn(executorServiceBuilderMock); + when(executorServiceBuilderMock.build()).thenReturn(mock(ExecutorService.class)); + + builder.using(configuration).using(environment).build("test"); + + assertThat(arrayBlockingQueueCaptor.getValue().remainingCapacity()).isEqualTo(16); + } + + @Test + public void usesACustomHttpClientMetricNameStrategy() { + final HttpClientMetricNameStrategy customStrategy = HttpClientMetricNameStrategies.HOST_AND_METHOD; + builder.using(customStrategy); + verify(apacheHttpClientBuilder).using(customStrategy); + } + + @Test + public void usesACustomHttpRequestRetryHandler() { + final DefaultHttpRequestRetryHandler customRetryHandler = new DefaultHttpRequestRetryHandler(2, true); + builder.using(customRetryHandler); + verify(apacheHttpClientBuilder).using(customRetryHandler); + } + + @Test + public void usesACustomDnsResolver() { + final DnsResolver customDnsResolver = new SystemDefaultDnsResolver(); + builder.using(customDnsResolver); + verify(apacheHttpClientBuilder).using(customDnsResolver); + } + + @Test + public void usesACustomHostnameVerifier() { + final HostnameVerifier customHostnameVerifier = new NoopHostnameVerifier(); + builder.using(customHostnameVerifier); + verify(apacheHttpClientBuilder).using(customHostnameVerifier); + } + + @Test + public void usesACustomConnectionFactoryRegistry() throws Exception { + final SSLContext ctx = SSLContext.getInstance(SSLConnectionSocketFactory.TLS); + ctx.init(null, new TrustManager[]{ + new X509TrustManager() { + + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + }, null); + final Registry customRegistry = RegistryBuilder.create() + .register("http", PlainConnectionSocketFactory.getSocketFactory()) + .register("https", new SSLConnectionSocketFactory(ctx, new NoopHostnameVerifier())) + .build(); + builder.using(customRegistry); + verify(apacheHttpClientBuilder).using(customRegistry); + } + + @Test + public void usesACustomEnvironmentName() { + final String userAgent = "Dropwizard Jersey Client"; + builder.name(userAgent); + verify(apacheHttpClientBuilder).name(userAgent); + } + + @Test + public void usesACustomHttpRoutePlanner() { + final HttpRoutePlanner customHttpRoutePlanner = new SystemDefaultRoutePlanner(new ProxySelector() { + @Override + public List select(URI uri) { + return ImmutableList.of(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.53.12", 8080))); + } + + @Override + public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + + } + }); + builder.using(customHttpRoutePlanner); + verify(apacheHttpClientBuilder).using(customHttpRoutePlanner); + } + + @Test + public void usesACustomCredentialsProvider() { + CredentialsProvider customCredentialsProvider = new SystemDefaultCredentialsProvider(); + builder.using(customCredentialsProvider); + verify(apacheHttpClientBuilder).using(customCredentialsProvider); + } + + @Provider + @Consumes(MediaType.APPLICATION_SVG_XML) + public static class FakeMessageBodyReader implements MessageBodyReader { + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return JerseyClientBuilderTest.class.isAssignableFrom(type); + } + + @Override + public JerseyClientBuilderTest readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { + return null; + } + } +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientConfigurationTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientConfigurationTest.java new file mode 100644 index 00000000000..7adf1ff0623 --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientConfigurationTest.java @@ -0,0 +1,27 @@ +package io.dropwizard.client; + +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.Validators; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JerseyClientConfigurationTest { + + @Test + public void testBasicJerseyClient() throws Exception { + final JerseyClientConfiguration configuration = new YamlConfigurationFactory<>(JerseyClientConfiguration.class, + Validators.newValidator(), Jackson.newObjectMapper(), "dw") + .build(new File(Resources.getResource("yaml/jersey-client.yml").toURI())); + assertThat(configuration.getMinThreads()).isEqualTo(8); + assertThat(configuration.getMaxThreads()).isEqualTo(64); + assertThat(configuration.getWorkQueueSize()).isEqualTo(16); + assertThat(configuration.isGzipEnabled()).isFalse(); + assertThat(configuration.isGzipEnabledForRequests()).isFalse(); + assertThat(configuration.isChunkedEncodingEnabled()).isFalse(); + } +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientIntegrationTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientIntegrationTest.java new file mode 100644 index 00000000000..775e24b411c --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyClientIntegrationTest.java @@ -0,0 +1,344 @@ +package io.dropwizard.client; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.CharStreams; +import com.sun.net.httpserver.Headers; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.dropwizard.jackson.Jackson; +import org.glassfish.jersey.logging.LoggingFeature; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static javax.ws.rs.core.MediaType.TEXT_PLAIN; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration test of {@link org.glassfish.jersey.client.JerseyClient} + * with {@link io.dropwizard.client.DropwizardApacheConnector} + */ +public class JerseyClientIntegrationTest { + + private static final String TRANSFER_ENCODING = "Transfer-Encoding"; + private static final String CHUNKED = "chunked"; + private static final String GZIP = "gzip"; + private static final ObjectMapper JSON_MAPPER = Jackson.newObjectMapper(); + private static final String GZIP_DEFLATE = "gzip,deflate"; + private static final String JSON_TOKEN = JSON_MAPPER.createObjectNode() + .put("id", 214) + .put("token", "a23f78bc31cc5de821ad9412e") + .toString(); + + private HttpServer httpServer; + + @Before + public void setup() throws Exception { + httpServer = HttpServer.create(new InetSocketAddress(0), 0); + } + + @After + public void tearDown() throws Exception { + httpServer.stop(0); + } + + @Test + public void testChunkedGzipPost() throws Exception { + httpServer.createContext("/register", httpExchange -> { + try { + Headers requestHeaders = httpExchange.getRequestHeaders(); + assertThat(requestHeaders.get(TRANSFER_ENCODING)).containsExactly(CHUNKED); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_LENGTH)).isNull(); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_ENCODING)).containsExactly(GZIP); + assertThat(requestHeaders.get(HttpHeaders.ACCEPT_ENCODING)).containsExactly(GZIP_DEFLATE); + checkBody(httpExchange, true); + postResponse(httpExchange); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + postRequest(new JerseyClientConfiguration()); + } + + @Test + public void testBufferedGzipPost() { + httpServer.createContext("/register", httpExchange -> { + try { + Headers requestHeaders = httpExchange.getRequestHeaders(); + + assertThat(requestHeaders.get(HttpHeaders.CONTENT_LENGTH)).containsExactly("58"); + assertThat(requestHeaders.get(TRANSFER_ENCODING)).isNull(); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_ENCODING)).containsExactly(GZIP); + assertThat(requestHeaders.get(HttpHeaders.ACCEPT_ENCODING)); + + checkBody(httpExchange, true); + postResponse(httpExchange); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setChunkedEncodingEnabled(false); + postRequest(configuration); + } + + @Test + public void testChunkedPost() throws Exception { + httpServer.createContext("/register", httpExchange -> { + try { + Headers requestHeaders = httpExchange.getRequestHeaders(); + assertThat(requestHeaders.get(TRANSFER_ENCODING)).containsExactly(CHUNKED); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_LENGTH)).isNull(); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_ENCODING)).isNull(); + assertThat(requestHeaders.get(HttpHeaders.ACCEPT_ENCODING)).containsExactly(GZIP_DEFLATE); + + checkBody(httpExchange, false); + postResponse(httpExchange); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setGzipEnabledForRequests(false); + postRequest(configuration); + } + + @Test + public void testChunkedPostWithoutGzip() throws Exception { + httpServer.createContext("/register", httpExchange -> { + try { + Headers requestHeaders = httpExchange.getRequestHeaders(); + assertThat(requestHeaders.get(TRANSFER_ENCODING)).containsExactly(CHUNKED); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_LENGTH)).isNull(); + assertThat(requestHeaders.get(HttpHeaders.CONTENT_ENCODING)).isNull(); + assertThat(requestHeaders.get(HttpHeaders.ACCEPT_ENCODING)).isNull(); + + checkBody(httpExchange, false); + + httpExchange.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + httpExchange.sendResponseHeaders(200, 0); + httpExchange.getResponseBody().write(JSON_TOKEN.getBytes(StandardCharsets.UTF_8)); + httpExchange.getResponseBody().close(); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setGzipEnabled(false); + configuration.setGzipEnabledForRequests(false); + postRequest(configuration); + } + + private void postRequest(JerseyClientConfiguration configuration) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Client jersey = new JerseyClientBuilder(new MetricRegistry()) + .using(executor, JSON_MAPPER) + .using(configuration) + .build("jersey-test"); + Response response = jersey.target("http://127.0.0.1:" + httpServer.getAddress().getPort() + "/register") + .request() + .buildPost(Entity.entity(new Person("john@doe.me", "John Doe"), APPLICATION_JSON)) + .invoke(); + + assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE)).isEqualTo(APPLICATION_JSON); + assertThat(response.getHeaderString(TRANSFER_ENCODING)).isEqualTo(CHUNKED); + + Credentials credentials = response.readEntity(Credentials.class); + assertThat(credentials.id).isEqualTo(214); + assertThat(credentials.token).isEqualTo("a23f78bc31cc5de821ad9412e"); + + executor.shutdown(); + jersey.close(); + } + + private void postResponse(HttpExchange httpExchange) throws IOException { + httpExchange.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + httpExchange.getResponseHeaders().add(HttpHeaders.CONTENT_ENCODING, GZIP); + httpExchange.sendResponseHeaders(200, 0); + GZIPOutputStream gzipStream = new GZIPOutputStream(httpExchange.getResponseBody()); + gzipStream.write(JSON_TOKEN.getBytes(StandardCharsets.UTF_8)); + gzipStream.close(); + } + + + private void checkBody(HttpExchange httpExchange, boolean gzip) throws IOException { + assertThat(httpExchange.getRequestHeaders().get(HttpHeaders.CONTENT_TYPE)) + .containsExactly(APPLICATION_JSON); + + InputStream requestBody = gzip ? new GZIPInputStream(httpExchange.getRequestBody()) : + httpExchange.getRequestBody(); + String body = CharStreams.toString(new InputStreamReader(requestBody, StandardCharsets.UTF_8)); + assertThat(JSON_MAPPER.readTree(body)).isEqualTo(JSON_MAPPER.createObjectNode() + .put("email", "john@doe.me") + .put("name", "John Doe")); + } + + + @Test + public void testGet() { + httpServer.createContext("/player", httpExchange -> { + try { + assertThat(httpExchange.getRequestURI().getQuery()).isEqualTo("id=21"); + + httpExchange.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON); + httpExchange.sendResponseHeaders(200, 0); + httpExchange.getResponseBody().write(JSON_MAPPER.createObjectNode() + .put("email", "john@doe.me") + .put("name", "John Doe") + .toString().getBytes(StandardCharsets.UTF_8)); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Client jersey = new JerseyClientBuilder(new MetricRegistry()) + .using(executor, JSON_MAPPER) + .using(new JerseyClientConfiguration()) + .build("jersey-test"); + Response response = jersey.target("http://127.0.0.1:" + httpServer.getAddress().getPort() + "/player?id=21") + .request() + .buildGet() + .invoke(); + + assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE)).isEqualTo(APPLICATION_JSON); + assertThat(response.getHeaderString(TRANSFER_ENCODING)).isEqualTo(CHUNKED); + + Person person = response.readEntity(Person.class); + assertThat(person.email).isEqualTo("john@doe.me"); + assertThat(person.name).isEqualTo("John Doe"); + + executor.shutdown(); + jersey.close(); + } + + @Test + public void testSetUserAgent() { + httpServer.createContext("/test", httpExchange -> { + try { + assertThat(httpExchange.getRequestHeaders().get(HttpHeaders.USER_AGENT)) + .containsExactly("Custom user-agent"); + httpExchange.sendResponseHeaders(200, 0); + httpExchange.getResponseBody().write("Hello World!".getBytes(StandardCharsets.UTF_8)); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + JerseyClientConfiguration configuration = new JerseyClientConfiguration(); + configuration.setUserAgent(Optional.of("Custom user-agent")); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Client jersey = new JerseyClientBuilder(new MetricRegistry()) + .using(executor, JSON_MAPPER) + .using(configuration) + .build("jersey-test"); + String text = jersey.target("http://127.0.0.1:" + httpServer.getAddress().getPort() + "/test") + .request() + .buildGet() + .invoke() + .readEntity(String.class); + assertThat(text).isEqualTo("Hello World!"); + + executor.shutdown(); + jersey.close(); + } + + /** + * Test for ConnectorProvider idempotency + */ + @Test + public void testFilterOnAWebTarget() { + httpServer.createContext("/test", httpExchange -> { + try { + httpExchange.getResponseHeaders().add(HttpHeaders.CONTENT_TYPE, TEXT_PLAIN); + httpExchange.sendResponseHeaders(200, 0); + httpExchange.getResponseBody().write("Hello World!".getBytes(StandardCharsets.UTF_8)); + } finally { + httpExchange.close(); + } + }); + httpServer.start(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Client jersey = new JerseyClientBuilder(new MetricRegistry()) + .using(executor, JSON_MAPPER) + .build("test-jersey-client"); + String uri = "http://127.0.0.1:" + httpServer.getAddress().getPort() + "/test"; + + WebTarget target = jersey.target(uri); + target.register(new LoggingFeature()); + String firstResponse = target.request() + .buildGet() + .invoke() + .readEntity(String.class); + assertThat(firstResponse).isEqualTo("Hello World!"); + + String secondResponse = jersey.target(uri) + .request() + .buildGet() + .invoke() + .readEntity(String.class); + assertThat(secondResponse).isEqualTo("Hello World!"); + + executor.shutdown(); + jersey.close(); + } + + static class Person { + + @JsonProperty("email") + final String email; + + @JsonProperty("name") + final String name; + + Person(@JsonProperty("email") String email, @JsonProperty("name") String name) { + this.email = email; + this.name = name; + } + } + + static class Credentials { + + @JsonProperty("id") + final long id; + + @JsonProperty("token") + final String token; + + Credentials(@JsonProperty("id") long id, @JsonProperty("token") String token) { + this.id = id; + this.token = token; + } + } +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/JerseyIgnoreRequestUserAgentHeaderFilterTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyIgnoreRequestUserAgentHeaderFilterTest.java new file mode 100644 index 00000000000..fc6a40dad20 --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/JerseyIgnoreRequestUserAgentHeaderFilterTest.java @@ -0,0 +1,102 @@ +package io.dropwizard.client; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.io.Resources; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.setup.Environment; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.dropwizard.util.Duration; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import java.net.URI; +import java.util.Optional; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JerseyIgnoreRequestUserAgentHeaderFilterTest { + @ClassRule + public static final DropwizardAppRule APP_RULE = + new DropwizardAppRule<>(TestApplication.class, Resources.getResource("yaml/jerseyIgnoreRequestUserAgentHeaderFilterTest.yml").getPath()); + + private final URI testUri = URI.create("http://localhost:" + APP_RULE.getLocalPort()); + private JerseyClientBuilder clientBuilder; + private JerseyClientConfiguration clientConfiguration; + + @Before + public void setup() { + clientConfiguration = new JerseyClientConfiguration(); + clientConfiguration.setConnectionTimeout(Duration.milliseconds(1000L)); + clientConfiguration.setTimeout(Duration.milliseconds(2500L)); + clientBuilder = new JerseyClientBuilder(new MetricRegistry()) + .using(clientConfiguration) + .using(Executors.newSingleThreadExecutor(), Jackson.newObjectMapper()); + } + + @Test + public void clientIsSetRequestIsNotSet() { + clientConfiguration.setUserAgent(Optional.of("ClientUserAgentHeaderValue")); + assertThat( + clientBuilder.using(clientConfiguration). + build("ClientName").target(testUri + "/user_agent") + .request() + .get(String.class) + ).isEqualTo("ClientUserAgentHeaderValue"); + } + + @Test + public void clientIsNotSetRequestIsSet() { + assertThat( + clientBuilder.build("ClientName").target(testUri + "/user_agent") + .request().header("User-Agent", "RequestUserAgentHeaderValue") + .get(String.class) + ).isEqualTo("RequestUserAgentHeaderValue"); + } + + @Test + public void clientIsNotSetRequestIsNotSet() { + assertThat(false); + assertThat( + clientBuilder.build("ClientName").target(testUri + "/user_agent") + .request() + .get(String.class) + ).isEqualTo("ClientName"); + } + + @Test + public void clientIsSetRequestIsSet() { + clientConfiguration.setUserAgent(Optional.of("ClientUserAgentHeaderValue")); + assertThat( + clientBuilder.build("ClientName").target(testUri + "/user_agent") + .request().header("User-Agent", "RequestUserAgentHeaderValue") + .get(String.class) + ).isEqualTo("RequestUserAgentHeaderValue"); + } + + @Path("/") + public static class TestResource { + @GET + @Path("user_agent") + public String getReturnUserAgentHeader(@HeaderParam("User-Agent") String userAgentHeader) { + return userAgentHeader; + } + } + + public static class TestApplication extends Application { + public static void main(String[] args) throws Exception { + new TestApplication().run(args); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(TestResource.class); + } + } +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/proxy/HttpClientConfigurationTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/proxy/HttpClientConfigurationTest.java new file mode 100644 index 00000000000..c55d336353c --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/proxy/HttpClientConfigurationTest.java @@ -0,0 +1,121 @@ +package io.dropwizard.client.proxy; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import io.dropwizard.client.HttpClientConfiguration; +import io.dropwizard.configuration.ConfigurationParsingException; +import io.dropwizard.configuration.ConfigurationValidationException; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.Validators; +import org.junit.Test; + +import java.io.File; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class HttpClientConfigurationTest { + + private final ObjectMapper objectMapper = Jackson.newObjectMapper(); + private HttpClientConfiguration configuration; + + private void load(String configLocation) throws Exception { + configuration = new YamlConfigurationFactory<>(HttpClientConfiguration.class, + Validators.newValidator(), + objectMapper, "dw") + .build(new File(Resources.getResource(configLocation).toURI())); + } + + @Test + public void testNoProxy() throws Exception { + load("./yaml/no_proxy.yml"); + assertThat(configuration.getProxyConfiguration()).isNull(); + } + + @Test + public void testFullConfig() throws Exception { + load("yaml/proxy.yml"); + + ProxyConfiguration proxy = configuration.getProxyConfiguration(); + assertThat(proxy).isNotNull(); + + assertThat(proxy.getHost()).isEqualTo("192.168.52.11"); + assertThat(proxy.getPort()).isEqualTo(8080); + assertThat(proxy.getScheme()).isEqualTo("https"); + + AuthConfiguration auth = proxy.getAuth(); + assertThat(auth).isNotNull(); + assertThat(auth.getUsername()).isEqualTo("secret"); + assertThat(auth.getPassword()).isEqualTo("stuff"); + + List nonProxyHosts = proxy.getNonProxyHosts(); + assertThat(nonProxyHosts).contains("localhost", "192.168.52.*", "*.example.com"); + } + + @Test + public void testNoScheme() throws Exception { + load("./yaml/no_scheme.yml"); + + ProxyConfiguration proxy = configuration.getProxyConfiguration(); + assertThat(proxy).isNotNull(); + assertThat(proxy.getHost()).isEqualTo("192.168.52.11"); + assertThat(proxy.getPort()).isEqualTo(8080); + assertThat(proxy.getScheme()).isEqualTo("http"); + } + + @Test + public void testNoAuth() throws Exception { + load("./yaml/no_auth.yml"); + + ProxyConfiguration proxy = configuration.getProxyConfiguration(); + assertThat(proxy).isNotNull(); + assertThat(proxy.getHost()).isNotNull(); + assertThat(proxy.getAuth()).isNull(); + } + + @Test + public void testNoPort() throws Exception { + load("./yaml/no_port.yml"); + + ProxyConfiguration proxy = configuration.getProxyConfiguration(); + assertThat(proxy).isNotNull(); + assertThat(proxy.getHost()).isNotNull(); + assertThat(proxy.getPort()).isEqualTo(-1); + } + + @Test + public void testNoNonProxy() throws Exception { + load("./yaml/no_port.yml"); + + ProxyConfiguration proxy = configuration.getProxyConfiguration(); + assertThat(proxy.getNonProxyHosts()).isNull(); + } + + @Test(expected = ConfigurationValidationException.class) + public void testNoHost() throws Exception { + load("yaml/bad_host.yml"); + } + + @Test(expected = ConfigurationValidationException.class) + public void testBadPort() throws Exception { + load("./yaml/bad_port.yml"); + } + + @Test(expected = ConfigurationParsingException.class) + public void testBadScheme() throws Exception { + load("./yaml/bad_scheme.yml"); + } + + @Test(expected = ConfigurationValidationException.class) + public void testBadAuthUsername() throws Exception { + load("./yaml/bad_auth_username.yml"); + } + + @Test(expected = ConfigurationValidationException.class) + public void testBadPassword() throws Exception { + load("./yaml/bad_auth_password.yml"); + } + +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/proxy/NonProxyListProxyRoutePlannerTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/proxy/NonProxyListProxyRoutePlannerTest.java new file mode 100644 index 00000000000..2bdfb6dda11 --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/proxy/NonProxyListProxyRoutePlannerTest.java @@ -0,0 +1,45 @@ +package io.dropwizard.client.proxy; + +import com.google.common.collect.ImmutableList; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.protocol.HttpContext; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class NonProxyListProxyRoutePlannerTest { + + private HttpHost proxy = new HttpHost("192.168.52.15"); + private NonProxyListProxyRoutePlanner routePlanner = new NonProxyListProxyRoutePlanner(proxy, + ImmutableList.of("localhost", "*.example.com", "192.168.52.*")); + private HttpRequest httpRequest = mock(HttpRequest.class); + private HttpContext httpContext = mock(HttpContext.class); + + @Test + public void testProxyListIsNotSet() { + assertThat(new NonProxyListProxyRoutePlanner(proxy, null).getNonProxyHostPatterns()).isEmpty(); + } + + @Test + public void testHostNotInBlackList() throws Exception { + assertThat(routePlanner.determineProxy(new HttpHost("dropwizard.io"), httpRequest, httpContext)) + .isEqualTo(proxy); + } + + @Test + public void testPlainHostIsMatched() throws Exception { + assertThat(routePlanner.determineProxy(new HttpHost("localhost"), httpRequest, httpContext)).isNull(); + } + + @Test + public void testHostWithStartWildcardIsMatched() throws Exception { + assertThat(routePlanner.determineProxy(new HttpHost("test.example.com"), httpRequest, httpContext)).isNull(); + } + + @Test + public void testHostWithEndWildcardIsMatched() throws Exception { + assertThat(routePlanner.determineProxy(new HttpHost("192.168.52.94"), httpRequest, httpContext)).isNull(); + } +} diff --git a/dropwizard-client/src/test/java/io/dropwizard/client/ssl/DropwizardSSLConnectionSocketFactoryTest.java b/dropwizard-client/src/test/java/io/dropwizard/client/ssl/DropwizardSSLConnectionSocketFactoryTest.java new file mode 100644 index 00000000000..c6865263d07 --- /dev/null +++ b/dropwizard-client/src/test/java/io/dropwizard/client/ssl/DropwizardSSLConnectionSocketFactoryTest.java @@ -0,0 +1,227 @@ +package io.dropwizard.client.ssl; + +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.client.DropwizardSSLConnectionSocketFactory; +import io.dropwizard.client.JerseyClientBuilder; +import io.dropwizard.client.JerseyClientConfiguration; +import io.dropwizard.setup.Environment; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.dropwizard.util.Duration; +import org.glassfish.jersey.client.ClientResponse; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Client; +import javax.ws.rs.core.Response; +import java.io.File; +import java.lang.reflect.Field; +import java.net.SocketException; +import java.util.Optional; + +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.http.conn.ssl.NoopHostnameVerifier; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +public class DropwizardSSLConnectionSocketFactoryTest { + private TlsConfiguration tlsConfiguration; + private JerseyClientConfiguration jerseyClientConfiguration; + + @Path("/") + public static class TestResource { + @GET + public Response respondOk() { + return Response.ok().build(); + } + } + + public static class TlsTestApplication extends Application { + public static void main(String[] args) throws Exception { + new TlsTestApplication().run(args); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(TestResource.class); + } + } + + @ClassRule + public static final DropwizardAppRule TLS_APP_RULE = new DropwizardAppRule<>(TlsTestApplication.class, + ResourceHelpers.resourceFilePath("yaml/ssl_connection_socket_factory_test.yml"), + Optional.of("tls"), + ConfigOverride.config("tls", "server.applicationConnectors[0].keyStorePath", ResourceHelpers.resourceFilePath("stores/server/keycert.p12")), + ConfigOverride.config("tls", "server.applicationConnectors[1].keyStorePath", ResourceHelpers.resourceFilePath("stores/server/self_sign_keycert.p12")), + ConfigOverride.config("tls", "server.applicationConnectors[2].keyStorePath", ResourceHelpers.resourceFilePath("stores/server/keycert.p12")), + ConfigOverride.config("tls", "server.applicationConnectors[2].trustStorePath", ResourceHelpers.resourceFilePath("stores/server/ca_truststore.ts")), + ConfigOverride.config("tls", "server.applicationConnectors[2].wantClientAuth", "true"), + ConfigOverride.config("tls", "server.applicationConnectors[2].needClientAuth", "true"), + ConfigOverride.config("tls", "server.applicationConnectors[2].validatePeers", "false"), + ConfigOverride.config("tls", "server.applicationConnectors[2].trustStorePassword", "password"), + ConfigOverride.config("tls", "server.applicationConnectors[3].keyStorePath", ResourceHelpers.resourceFilePath("stores/server/bad_host_keycert.p12")), + ConfigOverride.config("tls", "server.applicationConnectors[4].keyStorePath", ResourceHelpers.resourceFilePath("stores/server/keycert.p12")), + ConfigOverride.config("tls", "server.applicationConnectors[4].supportedProtocols", "SSLv1,SSLv2,SSLv3")); + + @Before + public void setUp() throws Exception { + tlsConfiguration = new TlsConfiguration(); + tlsConfiguration.setTrustStorePath(new File(ResourceHelpers.resourceFilePath("stores/server/ca_truststore.ts"))); + tlsConfiguration.setTrustStorePassword("password"); + jerseyClientConfiguration = new JerseyClientConfiguration(); + jerseyClientConfiguration.setTlsConfiguration(tlsConfiguration); + jerseyClientConfiguration.setConnectionTimeout(Duration.milliseconds(2000)); + jerseyClientConfiguration.setTimeout(Duration.milliseconds(5000)); + } + + @Test + public void configOnlyConstructorShouldSetNullCustomVerifier() throws Exception { + final DropwizardSSLConnectionSocketFactory socketFactory; + socketFactory = new DropwizardSSLConnectionSocketFactory(tlsConfiguration); + + final Field verifierField = + FieldUtils.getField(DropwizardSSLConnectionSocketFactory.class, "verifier", true); + assertThat(verifierField.get(socketFactory)).isNull(); + } + + @Test + public void shouldReturn200IfServerCertInTruststore() throws Exception { + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("tls_working_client"); + final Response response = client.target(String.format("https://localhost:%d", TLS_APP_RULE.getLocalPort())).request().get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void shouldErrorIfServerCertNotFoundInTruststore() throws Exception { + tlsConfiguration.setTrustStorePath(new File(ResourceHelpers.resourceFilePath("stores/server/other_cert_truststore.ts"))); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("tls_broken_client"); + try { + client.target(String.format("https://localhost:%d", TLS_APP_RULE.getLocalPort())).request().get(); + fail("expected ProcessingException"); + } catch (ProcessingException e) { + assertThat(e.getCause()).isInstanceOf(SSLHandshakeException.class); + } + } + + @Test + public void shouldNotErrorIfServerCertSelfSignedAndSelfSignedCertsAllowed() throws Exception { + tlsConfiguration.setTrustSelfSignedCertificates(true); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("self_sign_permitted"); + final Response response = client.target(String.format("https://localhost:%d", TLS_APP_RULE.getTestSupport().getPort(1))).request().get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void shouldErrorIfServerCertSelfSignedAndSelfSignedCertsNotAllowed() throws Exception { + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("self_sign_failure"); + try { + client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(1))).request().get(ClientResponse.class); + fail("expected ProcessingException"); + } catch (ProcessingException e) { + assertThat(e.getCause()).isInstanceOf(SSLHandshakeException.class); + } + } + + @Test + public void shouldReturn200IfAbleToClientAuth() throws Exception { + tlsConfiguration.setKeyStorePath(new File(ResourceHelpers.resourceFilePath("stores/client/keycert.p12"))); + tlsConfiguration.setKeyStorePassword("password"); + tlsConfiguration.setKeyStoreType("PKCS12"); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("client_auth_working"); + final Response response = client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(2))).request().get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void shouldErrorIfClientAuthFails() throws Exception { + tlsConfiguration.setKeyStorePath(new File(ResourceHelpers.resourceFilePath("stores/server/self_sign_keycert.p12"))); + tlsConfiguration.setKeyStorePassword("password"); + tlsConfiguration.setKeyStoreType("PKCS12"); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("client_auth_broken"); + try { + client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(2))).request().get(); + fail("expected ProcessingException"); + } catch (ProcessingException e) { + assertThat(e.getCause()).isInstanceOfAny(SocketException.class, SSLHandshakeException.class); + } + } + + @Test + public void shouldErrorIfHostnameVerificationOnAndServerHostnameDoesntMatch() throws Exception { + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("bad_host_broken"); + try { + client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(3))).request().get(); + fail("Expected ProcessingException"); + } catch (ProcessingException e) { + assertThat(e.getCause()).isExactlyInstanceOf(SSLPeerUnverifiedException.class); + assertThat(e.getCause().getMessage()).isEqualTo("Host name 'localhost' does not match the certificate subject provided by the peer (O=server, CN=badhost)"); + } + } + + @Test + public void shouldErrorIfHostnameVerificationOnAndServerHostnameMatchesAndFailVerifierSpecified() throws Exception { + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).using(new FailVerifier()).build("bad_host_broken_fail_verifier"); + try { + client.target(String.format("https://localhost:%d", TLS_APP_RULE.getLocalPort())).request().get(); + fail("Expected ProcessingException"); + } catch (ProcessingException e) { + assertThat(e.getCause()).isExactlyInstanceOf(SSLPeerUnverifiedException.class); + assertThat(e.getCause().getMessage()).isEqualTo("Host name 'localhost' does not match the certificate subject provided by the peer (O=server, CN=localhost)"); + } + } + + @Test + public void shouldBeOkIfHostnameVerificationOnAndServerHostnameDoesntMatchAndNoopVerifierSpecified() throws Exception { + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).using(new NoopHostnameVerifier()).build("bad_host_noop_verifier_working"); + final Response response = client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(3))).request().get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void shouldBeOkIfHostnameVerificationOffAndServerHostnameDoesntMatch() throws Exception { + tlsConfiguration.setVerifyHostname(false); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("bad_host_working"); + final Response response = client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(3))).request().get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void shouldBeOkIfHostnameVerificationOffAndServerHostnameMatchesAndFailVerfierSpecified() throws Exception { + tlsConfiguration.setVerifyHostname(false); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).using(new FailVerifier()).build("bad_host_fail_verifier_working"); + final Response response = client.target(String.format("https://localhost:%d", TLS_APP_RULE.getLocalPort())).request().get(); + assertThat(response.getStatus()).isEqualTo(200); + } + + @Test + public void shouldRejectNonSupportedProtocols() throws Exception { + tlsConfiguration.setSupportedProtocols(asList("TLSv1.2")); + final Client client = new JerseyClientBuilder(TLS_APP_RULE.getEnvironment()).using(jerseyClientConfiguration).build("reject_non_supported"); + try { + client.target(String.format("https://localhost:%d", TLS_APP_RULE.getPort(4))).request().get(); + fail("expected ProcessingException"); + } catch (ProcessingException e) { + assertThat(e).hasRootCauseInstanceOf(SSLException.class); + } + } + + private static class FailVerifier implements HostnameVerifier { + @Override + public boolean verify(String arg0, SSLSession arg1) { + return false; + } + } +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-client/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-client/src/test/resources/stores/client/keycert.p12 b/dropwizard-client/src/test/resources/stores/client/keycert.p12 new file mode 100644 index 00000000000..b9292f84e93 Binary files /dev/null and b/dropwizard-client/src/test/resources/stores/client/keycert.p12 differ diff --git a/dropwizard-client/src/test/resources/stores/server/bad_host_keycert.p12 b/dropwizard-client/src/test/resources/stores/server/bad_host_keycert.p12 new file mode 100644 index 00000000000..7a2908b8077 Binary files /dev/null and b/dropwizard-client/src/test/resources/stores/server/bad_host_keycert.p12 differ diff --git a/dropwizard-client/src/test/resources/stores/server/ca_truststore.ts b/dropwizard-client/src/test/resources/stores/server/ca_truststore.ts new file mode 100644 index 00000000000..e4a7f77a055 Binary files /dev/null and b/dropwizard-client/src/test/resources/stores/server/ca_truststore.ts differ diff --git a/dropwizard-client/src/test/resources/stores/server/keycert.p12 b/dropwizard-client/src/test/resources/stores/server/keycert.p12 new file mode 100644 index 00000000000..c9115da7a9a Binary files /dev/null and b/dropwizard-client/src/test/resources/stores/server/keycert.p12 differ diff --git a/dropwizard-client/src/test/resources/stores/server/other_cert_truststore.ts b/dropwizard-client/src/test/resources/stores/server/other_cert_truststore.ts new file mode 100644 index 00000000000..7bce6a1d7a4 Binary files /dev/null and b/dropwizard-client/src/test/resources/stores/server/other_cert_truststore.ts differ diff --git a/dropwizard-client/src/test/resources/stores/server/self_sign_keycert.p12 b/dropwizard-client/src/test/resources/stores/server/self_sign_keycert.p12 new file mode 100644 index 00000000000..07939eab086 Binary files /dev/null and b/dropwizard-client/src/test/resources/stores/server/self_sign_keycert.p12 differ diff --git a/dropwizard-client/src/test/resources/yaml/bad_auth_password.yml b/dropwizard-client/src/test/resources/yaml/bad_auth_password.yml new file mode 100644 index 00000000000..df5413b794c --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/bad_auth_password.yml @@ -0,0 +1,6 @@ +proxy: + host: '192.168.52.11' + port: 8080 + auth: + username: 'username' + password: '' diff --git a/dropwizard-client/src/test/resources/yaml/bad_auth_username.yml b/dropwizard-client/src/test/resources/yaml/bad_auth_username.yml new file mode 100644 index 00000000000..373d4f757d7 --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/bad_auth_username.yml @@ -0,0 +1,6 @@ +proxy: + host: '192.168.52.11' + port: 8080 + auth: + username: '' + password: 'stuff' diff --git a/dropwizard-client/src/test/resources/yaml/bad_host.yml b/dropwizard-client/src/test/resources/yaml/bad_host.yml new file mode 100644 index 00000000000..88ebafc001a --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/bad_host.yml @@ -0,0 +1,4 @@ +proxy: + host : '' + port: 8080 + scheme : 'http' diff --git a/dropwizard-client/src/test/resources/yaml/bad_port.yml b/dropwizard-client/src/test/resources/yaml/bad_port.yml new file mode 100644 index 00000000000..9f05b61bd0e --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/bad_port.yml @@ -0,0 +1,3 @@ +proxy: + host: '192.168.52.11' + port: 100000 diff --git a/dropwizard-client/src/test/resources/yaml/bad_scheme.yml b/dropwizard-client/src/test/resources/yaml/bad_scheme.yml new file mode 100644 index 00000000000..e38b087edcd --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/bad_scheme.yml @@ -0,0 +1,4 @@ +proxy: + host: '192.168.52.11' + port: 8080 + schema : 'tcp' diff --git a/dropwizard-client/src/test/resources/yaml/dropwizardApacheConnectorTest.yml b/dropwizard-client/src/test/resources/yaml/dropwizardApacheConnectorTest.yml new file mode 100644 index 00000000000..e8dae193688 --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/dropwizardApacheConnectorTest.yml @@ -0,0 +1,8 @@ +# this is needed to start the application in the DropwizardApacheConnectorTest +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 diff --git a/dropwizard-client/src/test/resources/yaml/jersey-client.yml b/dropwizard-client/src/test/resources/yaml/jersey-client.yml new file mode 100644 index 00000000000..e40672250ef --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/jersey-client.yml @@ -0,0 +1,6 @@ +minThreads: 8 +maxThreads: 64 +gzipEnabled: false +workQueueSize: 16 +gzipEnabledForRequests: false +chunkedEncodingEnabled : false diff --git a/dropwizard-client/src/test/resources/yaml/jerseyIgnoreRequestUserAgentHeaderFilterTest.yml b/dropwizard-client/src/test/resources/yaml/jerseyIgnoreRequestUserAgentHeaderFilterTest.yml new file mode 100644 index 00000000000..ddfee9d59a7 --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/jerseyIgnoreRequestUserAgentHeaderFilterTest.yml @@ -0,0 +1,5 @@ +# this is needed to start the application in the JerseyIgnoreRequestUserAgentHeaderFilterTest +server: + applicationConnectors: + - type: http + port: 0 \ No newline at end of file diff --git a/dropwizard-client/src/test/resources/yaml/no_auth.yml b/dropwizard-client/src/test/resources/yaml/no_auth.yml new file mode 100644 index 00000000000..88b860a32a3 --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/no_auth.yml @@ -0,0 +1,4 @@ +proxy: + host: '192.168.52.11' + port: 8080 + scheme : 'HTTP' diff --git a/dropwizard-client/src/test/resources/yaml/no_port.yml b/dropwizard-client/src/test/resources/yaml/no_port.yml new file mode 100644 index 00000000000..992f57e106b --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/no_port.yml @@ -0,0 +1,2 @@ +proxy: + host: '192.168.52.11' diff --git a/dropwizard-client/src/test/resources/yaml/no_proxy.yml b/dropwizard-client/src/test/resources/yaml/no_proxy.yml new file mode 100644 index 00000000000..c7cb609433e --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/no_proxy.yml @@ -0,0 +1,3 @@ +timeout : 1000 ms +connectionTimeout : 5000 ms +timeToLive: 2 h diff --git a/dropwizard-client/src/test/resources/yaml/no_scheme.yml b/dropwizard-client/src/test/resources/yaml/no_scheme.yml new file mode 100644 index 00000000000..7c430cc1962 --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/no_scheme.yml @@ -0,0 +1,3 @@ +proxy: + host: '192.168.52.11' + port: 8080 diff --git a/dropwizard-client/src/test/resources/yaml/proxy.yml b/dropwizard-client/src/test/resources/yaml/proxy.yml new file mode 100644 index 00000000000..e56f9d0a69b --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/proxy.yml @@ -0,0 +1,11 @@ +proxy: + host: '192.168.52.11' + port: 8080 + scheme : 'https' + auth: + username: 'secret' + password: 'stuff' + nonProxyHosts: + - 'localhost' + - '192.168.52.*' + - '*.example.com' diff --git a/dropwizard-client/src/test/resources/yaml/ssl_connection_socket_factory_test.yml b/dropwizard-client/src/test/resources/yaml/ssl_connection_socket_factory_test.yml new file mode 100644 index 00000000000..0daf9c28d6c --- /dev/null +++ b/dropwizard-client/src/test/resources/yaml/ssl_connection_socket_factory_test.yml @@ -0,0 +1,49 @@ +server: + applicationConnectors: + - type: https + port: 0 + keyStoreType: PKCS12 + keyStorePassword: password + validateCerts: false + validatePeers: false + trustStoreType: PKCS12 + supportedProtocols: ["TLSv1.2"] + - type: https + port: 0 + keyStoreType: PKCS12 + keyStorePassword: password + validateCerts: false + validatePeers: false + trustStoreType: PKCS12 + - type: https + port: 0 + keyStoreType: PKCS12 + keyStorePassword: password + validateCerts: false + validatePeers: false + supportedProtocols: ["TLSv1.2"] + - type: https + port: 0 + keyStoreType: PKCS12 + keyStorePassword: password + validateCerts: false + validatePeers: false + trustStoreType: PKCS12 + supportedProtocols: ["TLSv1.2"] + - type: https + port: 0 + keyStoreType: PKCS12 + keyStorePassword: password + validateCerts: false + validatePeers: false + trustStoreType: PKCS12 + supportedProtocols: ["TLSv1.2"] + adminConnectors: + - type: http + port: 0 + + +logging: + level: INFO + appenders: + - type: console diff --git a/dropwizard-configuration/pom.xml b/dropwizard-configuration/pom.xml new file mode 100644 index 00000000000..febb202721f --- /dev/null +++ b/dropwizard-configuration/pom.xml @@ -0,0 +1,62 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-configuration + Dropwizard Configuration Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-jackson + + + io.dropwizard + dropwizard-validation + + + com.fasterxml.jackson.dataformat + jackson-dataformat-yaml + + + org.apache.commons + commons-lang3 + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + always + + test_value + 2 + alternative + + + + + + 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 00000000000..38166153422 --- /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. + *

    + * Refer to the implementations for different classes of problems: + *

      + *
    • Parsing errors: {@link ConfigurationParsingException}
    • + *
    • Validation errors: {@link ConfigurationValidationException}
    • + *
    + */ +public abstract class ConfigurationException extends Exception { + protected static final String NEWLINE = String.format("%n"); + + private final Collection 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 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 errors, Throwable cause) { + super(formatMessage(path, errors), cause); + this.errors = errors; + } + + public Collection getErrors() { + return errors; + } + + protected static String formatMessage(String file, Collection 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 00000000000..ef8a4b0f08d --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactory.java @@ -0,0 +1,39 @@ +package io.dropwizard.configuration; + +import java.io.File; +import java.io.IOException; + +public interface ConfigurationFactory { + + /** + * 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 + */ + T build(ConfigurationSourceProvider provider, String path) throws IOException, ConfigurationException; + + /** + * 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 + */ + default 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 + */ + T build() throws IOException, ConfigurationException; +} 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 00000000000..43dd0e2c069 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationFactoryFactory.java @@ -0,0 +1,12 @@ +package io.dropwizard.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.validation.Validator; + +public interface ConfigurationFactoryFactory { + ConfigurationFactory create(Class 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 00000000000..ab04bea7932 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ConfigurationParsingException.java @@ -0,0 +1,383 @@ +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.lang3.StringUtils; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +/** + * A {@link ConfigurationException} for errors parsing a configuration file. + */ +public class ConfigurationParsingException extends ConfigurationException { + private static final long serialVersionUID = 1L; + + static class Builder { + private static final int MAX_SUGGESTIONS = 5; + + private String summary; + private String detail = ""; + private List fieldPath = Collections.emptyList(); + private int line = -1; + private int column = -1; + private Exception cause = null; + private List 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 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. + *

    + * 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. + *

    + * 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. + *

    + * 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 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. + *

    + * 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. + *

    + * 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 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 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) { + final 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()) { + final List suggestions = getSuggestions(); + sb.append(NEWLINE).append(" Did you mean?:").append(NEWLINE); + final Iterator it = suggestions.iterator(); + int i = 0; + while (it.hasNext() && i < MAX_SUGGESTIONS) { + sb.append(" - ").append(it.next()); + i++; + if (it.hasNext()) { + sb.append(NEWLINE); + } + } + + final 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 path) { + final StringBuilder sb = new StringBuilder(); + if (path != null) { + final Iterator it = path.iterator(); + while (it.hasNext()) { + final JsonMappingException.Reference reference = it.next(); + final 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(); + } + + protected static class LevenshteinComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + private String base; + + public LevenshteinComparator(String base) { + this.base = base; + } + + /** + * Compares two Strings with respect to the base String, by Levenshtein distance. + *

    + * 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 Integer.compare(StringUtils.getLevenshteinDistance(a, base), + StringUtils.getLevenshteinDistance(b, base)); + } + + 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 00000000000..266e5d84eff --- /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} + */ + 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 00000000000..f725f315b53 --- /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> 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 ConfigurationValidationException(String path, Set> 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> 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 00000000000..40ea7195907 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/DefaultConfigurationFactoryFactory.java @@ -0,0 +1,34 @@ +package io.dropwizard.configuration; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +import javax.validation.Validator; + +public class DefaultConfigurationFactoryFactory implements ConfigurationFactoryFactory { + @Override + public ConfigurationFactory create( + Class klass, + Validator validator, + ObjectMapper objectMapper, + String propertyPrefix) { + return new YamlConfigurationFactory<>( + klass, + validator, + configureObjectMapper(objectMapper.copy()), + propertyPrefix); + } + + /** + * Provides additional configuration for the {@link ObjectMapper} used to read + * the configuration. By default {@link DeserializationFeature#FAIL_ON_UNKNOWN_PROPERTIES} + * is enabled to protect against misconfiguration. + * + * @param objectMapper template to be configured + * @return configured object mapper + */ + protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) { + return objectMapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + } + +} diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/EnvironmentVariableLookup.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/EnvironmentVariableLookup.java new file mode 100644 index 00000000000..48b5ae17e85 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/EnvironmentVariableLookup.java @@ -0,0 +1,48 @@ +package io.dropwizard.configuration; + +import org.apache.commons.lang3.text.StrLookup; + +/** + * A custom {@link org.apache.commons.lang3.text.StrLookup} implementation using environment variables as lookup source. + */ +public class EnvironmentVariableLookup extends StrLookup { + private final boolean strict; + + /** + * Create a new instance with strict behavior. + */ + public EnvironmentVariableLookup() { + this(true); + } + + /** + * Create a new instance. + * + * @param strict {@code true} if looking up undefined environment variables should throw a + * {@link UndefinedEnvironmentVariableException}, {@code false} otherwise. + * @throws UndefinedEnvironmentVariableException if the environment variable doesn't exist and strict behavior + * is enabled. + */ + public EnvironmentVariableLookup(boolean strict) { + this.strict = strict; + } + + /** + * {@inheritDoc} + * + * @throws UndefinedEnvironmentVariableException if the environment variable doesn't exist and strict behavior + * is enabled. + */ + @Override + public String lookup(String key) { + final String value = System.getenv(key); + + if (value == null && strict) { + throw new UndefinedEnvironmentVariableException("The environment variable '" + key + + "' is not defined; could not substitute the expression '${" + + key + "}'."); + } + + return value; + } +} diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/EnvironmentVariableSubstitutor.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/EnvironmentVariableSubstitutor.java new file mode 100644 index 00000000000..ae7ecafe543 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/EnvironmentVariableSubstitutor.java @@ -0,0 +1,28 @@ +package io.dropwizard.configuration; + +import org.apache.commons.lang3.text.StrSubstitutor; + +/** + * A custom {@link StrSubstitutor} using environment variables as lookup source. + */ +public class EnvironmentVariableSubstitutor extends StrSubstitutor { + public EnvironmentVariableSubstitutor() { + this(true, false); + } + + public EnvironmentVariableSubstitutor(boolean strict) { + this(strict, false); + } + + /** + * @param strict {@code true} if looking up undefined environment variables should throw a + * {@link UndefinedEnvironmentVariableException}, {@code false} otherwise. + * @param substitutionInVariables a flag whether substitution is done in variable names. + * @see io.dropwizard.configuration.EnvironmentVariableLookup#EnvironmentVariableLookup(boolean) + * @see org.apache.commons.lang3.text.StrSubstitutor#setEnableSubstitutionInVariables(boolean) + */ + public EnvironmentVariableSubstitutor(boolean strict, boolean substitutionInVariables) { + super(new EnvironmentVariableLookup(strict)); + this.setEnableSubstitutionInVariables(substitutionInVariables); + } +} 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 00000000000..ad933c22de1 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/FileConfigurationSourceProvider.java @@ -0,0 +1,23 @@ +package io.dropwizard.configuration; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +/** + * 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/ResourceConfigurationSourceProvider.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ResourceConfigurationSourceProvider.java new file mode 100644 index 00000000000..5004ef2cc39 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/ResourceConfigurationSourceProvider.java @@ -0,0 +1,11 @@ +package io.dropwizard.configuration; + +import java.io.IOException; +import java.io.InputStream; + +public class ResourceConfigurationSourceProvider implements ConfigurationSourceProvider { + @Override + public InputStream open(String path) throws IOException { + return getClass().getClassLoader().getResourceAsStream(path); + } +} diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/SubstitutingSourceProvider.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/SubstitutingSourceProvider.java new file mode 100644 index 00000000000..580c19140e9 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/SubstitutingSourceProvider.java @@ -0,0 +1,44 @@ +package io.dropwizard.configuration; + +import com.google.common.io.ByteStreams; +import org.apache.commons.lang3.text.StrSubstitutor; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static java.util.Objects.requireNonNull; + +/** + * A delegating {@link ConfigurationSourceProvider} which replaces variables in the underlying configuration + * source according to the rules of a custom {@link org.apache.commons.lang3.text.StrSubstitutor}. + */ +public class SubstitutingSourceProvider implements ConfigurationSourceProvider { + private final ConfigurationSourceProvider delegate; + private final StrSubstitutor substitutor; + + /** + * Create a new instance. + * + * @param delegate The underlying {@link io.dropwizard.configuration.ConfigurationSourceProvider}. + * @param substitutor The custom {@link org.apache.commons.lang3.text.StrSubstitutor} implementation. + */ + public SubstitutingSourceProvider(ConfigurationSourceProvider delegate, StrSubstitutor substitutor) { + this.delegate = requireNonNull(delegate); + this.substitutor = requireNonNull(substitutor); + } + + /** + * {@inheritDoc} + */ + @Override + public InputStream open(String path) throws IOException { + try (final InputStream in = delegate.open(path)) { + final String config = new String(ByteStreams.toByteArray(in), StandardCharsets.UTF_8); + final String substituted = substitutor.replace(config); + + return new ByteArrayInputStream(substituted.getBytes(StandardCharsets.UTF_8)); + } + } +} diff --git a/dropwizard-configuration/src/main/java/io/dropwizard/configuration/UndefinedEnvironmentVariableException.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/UndefinedEnvironmentVariableException.java new file mode 100644 index 00000000000..f8deaccf04d --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/UndefinedEnvironmentVariableException.java @@ -0,0 +1,9 @@ +package io.dropwizard.configuration; + +public class UndefinedEnvironmentVariableException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public UndefinedEnvironmentVariableException(String errorMessage) { + super(errorMessage); + } +} 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 00000000000..991109c5ac1 --- /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/main/java/io/dropwizard/configuration/YamlConfigurationFactory.java b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/YamlConfigurationFactory.java new file mode 100644 index 00000000000..cf301285510 --- /dev/null +++ b/dropwizard-configuration/src/main/java/io/dropwizard/configuration/YamlConfigurationFactory.java @@ -0,0 +1,237 @@ +package io.dropwizard.configuration; + +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.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 javax.validation.ConstraintViolation; +import javax.validation.Validator; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import static java.util.Objects.requireNonNull; + +/** + * A factory class for loading YAML configuration files, binding them to configuration objects, and + * validating their constraints. Allows for overriding configuration parameters from system properties. + * + * @param the type of the configuration objects to produce + */ +public class YamlConfigurationFactory implements ConfigurationFactory { + + private static final Pattern ESCAPED_COMMA_PATTERN = Pattern.compile("\\\\,"); + private static final Splitter ESCAPED_COMMA_SPLITTER = Splitter.on(Pattern.compile("(? 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 YamlConfigurationFactory(Class klass, + Validator validator, + ObjectMapper objectMapper, + String propertyPrefix) { + this.klass = klass; + this.propertyPrefix = (propertyPrefix == null || propertyPrefix.endsWith(".")) + ? propertyPrefix : (propertyPrefix + '.'); + // Sub-classes may choose to omit data-binding; if so, null ObjectMapper passed: + if (objectMapper == null) { // sub-class has no need for mapper + mapper = null; + yamlFactory = null; + } else { + mapper = objectMapper; + yamlFactory = new YAMLFactory(); + } + this.validator = validator; + } + + @Override + public T build(ConfigurationSourceProvider provider, String path) throws IOException, ConfigurationException { + try (InputStream input = provider.open(requireNonNull(path))) { + final JsonNode node = mapper.readTree(yamlFactory.createParser(input)); + + if (node == null) { + throw ConfigurationParsingException + .builder("Configuration at " + path + " must not be empty") + .build(path); + } + + return build(node, path); + } catch (YAMLException e) { + final 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); + } + } + + @Override + public T build() throws IOException, ConfigurationException { + try { + final JsonNode node = mapper.valueToTree(klass.newInstance()); + return build(node, "default configuration"); + } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | SecurityException e) { + throw new IllegalArgumentException("Unable create an instance " + + "of the configuration class: '" + klass.getCanonicalName() + "'", e); + } + } + + protected T build(JsonNode node, String path) throws IOException, ConfigurationException { + for (Map.Entry 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) { + final List properties = e.getKnownPropertyIds().stream() + .map(Object::toString) + .collect(Collectors.toList()); + throw ConfigurationParsingException.builder("Unrecognized field") + .setFieldPath(e.getPath()) + .setLocation(e.getLocation()) + .addSuggestions(properties) + .setSuggestionBase(e.getPropertyName()) + .setCause(e) + .build(path); + } catch (InvalidFormatException e) { + final String sourceType = e.getValue().getClass().getSimpleName(); + final 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); + } + } + + protected void addOverride(JsonNode root, String name, String value) { + JsonNode node = root; + final List parts = ESCAPED_DOT_SPLITTER.splitToList(name).stream() + .map(key -> ESCAPED_DOT_PATTERN.matcher(key).replaceAll(".")) + .collect(Collectors.toList()); + for (int i = 0; i < parts.size(); i++) { + final String key = parts.get(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(parts.subList(i, parts.size())); + if (obj.has(remainingPath) && !remainingPath.equals(key)) { + if (obj.get(remainingPath).isValueNode()) { + obj.put(remainingPath, value); + return; + } + } + + JsonNode child; + final boolean moreParts = i < parts.size() - 1; + + if (key.matches(".+\\[\\d+\\]$")) { + final int s = key.indexOf('['); + final int index = Integer.parseInt(key.substring(s + 1, key.length() - 1)); + child = obj.get(key.substring(0, s)); + 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 { + final 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.set(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()) { + final ArrayNode arrayNode = (ArrayNode) obj.get(key); + arrayNode.removeAll(); + for (String val : ESCAPED_COMMA_SPLITTER.split(value)) { + arrayNode.add(ESCAPED_COMMA_PATTERN.matcher(val).replaceAll(",")); + } + } else { + obj.put(key, value); + } + } + } + } + + private void validate(String path, T config) throws ConfigurationValidationException { + if (validator != null) { + final Set> violations = validator.validate(config); + if (!violations.isEmpty()) { + throw new ConfigurationValidationException(path, violations); + } + } + } +} 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 00000000000..3ca7fcab0c1 --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryFactoryTest.java @@ -0,0 +1,72 @@ +package io.dropwizard.configuration; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import io.dropwizard.configuration.ConfigurationFactoryTest.Example; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.validation.BaseValidator; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import javax.validation.Validator; +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class ConfigurationFactoryFactoryTest { + + @Rule + public final ExpectedException expectedException = ExpectedException.none(); + + private final ConfigurationFactoryFactory factoryFactory = new DefaultConfigurationFactoryFactory<>(); + private final Validator validator = BaseValidator.newValidator(); + + @Test + public void createDefaultFactory() throws Exception { + File validFile = new File(Resources.getResource("factory-test-valid.yml").toURI()); + ConfigurationFactory factory = + factoryFactory.create(Example.class, validator, Jackson.newObjectMapper(), "dw"); + final Example example = factory.build(validFile); + assertThat(example.getName()) + .isEqualTo("Coda Hale"); + } + + @Test + public void createDefaultFactoryFailsUnknownProperty() throws Exception { + File validFileWithUnknownProp = new File( + Resources.getResource("factory-test-unknown-property.yml").toURI()); + ConfigurationFactory factory = + factoryFactory.create(Example.class, validator, Jackson.newObjectMapper(), "dw"); + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage("Unrecognized field at: trait"); + factory.build(validFileWithUnknownProp); + } + + @Test + public void createFactoryAllowingUnknownProperties() throws Exception { + ConfigurationFactoryFactory customFactory = new PassThroughConfigurationFactoryFactory(); + File validFileWithUnknownProp = new File( + Resources.getResource("factory-test-unknown-property.yml").toURI()); + ConfigurationFactory factory = + customFactory.create( + Example.class, + validator, + Jackson.newObjectMapper().disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES), + "dw"); + Example example = factory.build(validFileWithUnknownProp); + assertThat(example.getName()) + .isEqualTo("Mighty Wizard"); + } + + private static final class PassThroughConfigurationFactoryFactory + extends DefaultConfigurationFactoryFactory { + @Override + protected ObjectMapper configureObjectMapper(ObjectMapper objectMapper) { + return objectMapper; + } + } +} 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 00000000000..b26a6ba8cfd --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationFactoryTest.java @@ -0,0 +1,479 @@ +package io.dropwizard.configuration; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.validation.BaseValidator; +import org.assertj.core.data.MapEntry; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.Validator; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import java.io.File; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class ConfigurationFactoryTest { + + private static final String NEWLINE = System.lineSeparator(); + + @SuppressWarnings("UnusedDeclaration") + public static class ExampleServer { + + @JsonProperty + private int port = 8000; + + public int getPort() { + return port; + } + + public static ExampleServer create(int port) { + ExampleServer server = new ExampleServer(); + server.port = port; + return server; + } + + } + + @SuppressWarnings("UnusedDeclaration") + public static class Example { + + @NotNull + @Pattern(regexp = "[\\w]+[\\s]+[\\w]+([\\s][\\w]+)?") + private String name; + + @JsonProperty + private int age = 1; + + List type; + + @JsonProperty + private Map properties = new LinkedHashMap<>(); + + @JsonProperty + private List servers = new ArrayList<>(); + + private boolean admin; + + @JsonProperty("my.logger") + private Map logger = new LinkedHashMap<>(); + + public String getName() { + return name; + } + + public List getType() { + return type; + } + + public Map getProperties() { + return properties; + } + + public List getServers() { + return servers; + } + + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public Map getLogger() { + return logger; + } + } + + static class ExampleWithDefaults { + + @NotNull + @Pattern(regexp = "[\\w]+[\\s]+[\\w]+([\\s][\\w]+)?") + @JsonProperty + String name = "Coda Hale"; + + @JsonProperty + List type = ImmutableList.of("coder", "wizard"); + + @JsonProperty + Map properties = ImmutableMap.of("debug", "true", "settings.enabled", "false"); + + @JsonProperty + List servers = ImmutableList.of( + ExampleServer.create(8080), ExampleServer.create(8081), ExampleServer.create(8082)); + + @JsonProperty + @Valid + CacheBuilderSpec cacheBuilderSpec = CacheBuilderSpec.disableCaching(); + } + + static class NonInsatiableExample { + + @JsonProperty + String name = "Code Hale"; + + NonInsatiableExample(@JsonProperty("name") String name) { + this.name = name; + } + } + + private final Validator validator = BaseValidator.newValidator(); + private final YamlConfigurationFactory factory = + new YamlConfigurationFactory<>(Example.class, validator, Jackson.newObjectMapper(), "dw"); + private File malformedFile; + private File emptyFile; + private File invalidFile; + private File validFile; + + private static File resourceFileName(String resourceName) throws URISyntaxException { + return new File(Resources.getResource(resourceName).toURI()); + } + + @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 = resourceFileName("factory-test-malformed.yml"); + this.emptyFile = resourceFileName("factory-test-empty.yml"); + this.invalidFile = resourceFileName("factory-test-invalid.yml"); + this.validFile = resourceFileName("factory-test-valid.yml"); + } + + @Test + public void usesDefaultedCacheBuilderSpec() throws Exception { + final ExampleWithDefaults example = + new YamlConfigurationFactory<>(ExampleWithDefaults.class, validator, Jackson.newObjectMapper(), "dw") + .build(); + assertThat(example.cacheBuilderSpec) + .isNotNull(); + assertThat(example.cacheBuilderSpec) + .isEqualTo(CacheBuilderSpec.disableCaching()); + } + + @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 handlesExistingOverrideWithPeriod() throws Exception { + System.setProperty("dw.my\\.logger.level", "debug"); + final Example example = factory.build(validFile); + assertThat(example.getLogger().get("level")) + .isEqualTo("debug"); + } + + @Test + public void handlesNewOverrideWithPeriod() throws Exception { + System.setProperty("dw.my\\.logger.com\\.example", "error"); + final Example example = factory.build(validFile); + assertThat(example.getLogger().get("com.example")) + .isEqualTo("error"); + } + + @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 throwsAnExceptionOnEmptyFiles() throws Exception { + try { + factory.build(emptyFile); + failBecauseExceptionWasNotThrown(ConfigurationParsingException.class); + } catch (ConfigurationParsingException e) { + assertThat(e.getMessage()) + .containsOnlyOnce(" * Configuration at " + emptyFile.toString() + " must not be empty"); + } + } + + @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]+)?\"%n")); + } + } + } + + @Test + public void handleOverrideDefaultConfiguration() throws Exception { + System.setProperty("dw.name", "Coda Hale Overridden"); + System.setProperty("dw.type", "coder,wizard,overridden"); + System.setProperty("dw.properties.settings.enabled", "true"); + System.setProperty("dw.servers[0].port", "8090"); + System.setProperty("dw.servers[2].port", "8092"); + + final ExampleWithDefaults example = + new YamlConfigurationFactory<>(ExampleWithDefaults.class, validator, Jackson.newObjectMapper(), "dw") + .build(); + + assertThat(example.name).isEqualTo("Coda Hale Overridden"); + assertThat(example.type.get(2)).isEqualTo("overridden"); + assertThat(example.type.size()).isEqualTo(3); + assertThat(example.properties).containsEntry("settings.enabled", "true"); + assertThat(example.servers.get(0).getPort()).isEqualTo(8090); + assertThat(example.servers.get(2).getPort()).isEqualTo(8092); + } + + @Test + public void handleDefaultConfigurationWithoutOverriding() throws Exception { + final ExampleWithDefaults example = + new YamlConfigurationFactory<>(ExampleWithDefaults.class, validator, Jackson.newObjectMapper(), "dw") + .build(); + + assertThat(example.name).isEqualTo("Coda Hale"); + assertThat(example.type).isEqualTo(ImmutableList.of("coder", "wizard")); + assertThat(example.properties).isEqualTo(ImmutableMap.of("debug", "true", "settings.enabled", "false")); + assertThat(example.servers.get(0).getPort()).isEqualTo(8080); + assertThat(example.servers.get(1).getPort()).isEqualTo(8081); + assertThat(example.servers.get(2).getPort()).isEqualTo(8082); + } + + @Test + public void throwsAnExceptionIfDefaultConfigurationCantBeInstantiated() throws Exception { + System.setProperty("dw.name", "Coda Hale Overridden"); + try { + new YamlConfigurationFactory<>(NonInsatiableExample.class, validator, Jackson.newObjectMapper(), "dw").build(); + Assert.fail("Configuration is parsed, but shouldn't be"); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Unable create an instance of the configuration class: " + + "'io.dropwizard.configuration.ConfigurationFactoryTest.NonInsatiableExample'"); + } + + } + + @Test + public void printsDidYouMeanOnUnrecognizedField() throws Exception { + final File resourceFileName = resourceFileName("factory-test-typo.yml"); + try { + factory.build(resourceFileName); + fail("Typo in a configuration should be caught"); + } catch (ConfigurationParsingException e) { + assertThat(e.getMessage()).isEqualTo(resourceFileName + " has an error:" + NEWLINE + + " * Unrecognized field at: propertis" + NEWLINE + + " Did you mean?:" + NEWLINE + + " - properties" + NEWLINE + + " - servers" + NEWLINE + + " - type" + NEWLINE + + " - name" + NEWLINE + + " - age" + NEWLINE + + " [2 more]" + NEWLINE); + } + } + + @Test + public void incorrectTypeIsFound() throws Exception { + final File resourceFileName = resourceFileName("factory-test-wrong-type.yml"); + try { + factory.build(resourceFileName); + fail("Incorrect type in a configuration should be found"); + } catch (ConfigurationParsingException e) { + assertThat(e.getMessage()).isEqualTo(resourceFileName + " has an error:" + NEWLINE + + " * Incorrect type of value at: age; is of type: String, expected: int" + NEWLINE); + } + } + + @Test + public void printsDetailedInformationOnMalformedYaml() throws Exception { + final File resourceFileName = resourceFileName("factory-test-malformed-advanced.yml"); + try { + factory.build(resourceFileName); + fail("Should print a detailed error on a malformed YAML file"); + } catch (Exception e) { + assertThat(e.getMessage()).isEqualTo( + "YAML decoding problem: while parsing a flow sequence\n" + + " in 'reader', line 2, column 7:\n" + + " type: [ coder,wizard\n" + + " ^\n" + + "expected ',' or ']', but got StreamEnd\n" + + " in 'reader', line 2, column 21:\n" + + " wizard\n" + + " ^\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 00000000000..ea343d0eeff --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ConfigurationValidationExceptionTest.java @@ -0,0 +1,48 @@ +package io.dropwizard.configuration; + +import io.dropwizard.validation.BaseValidator; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.constraints.NotNull; +import java.util.Locale; +import java.util.Set; + +import static org.assertj.core.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 = BaseValidator.newValidator(); + final Set> 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%n" + )); + } + + @Test + public void retainsTheSetOfExceptions() throws Exception { + assertThat(e.getConstraintViolations()) + .isNotEmpty(); + } +} diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/EnvironmentVariableLookupTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/EnvironmentVariableLookupTest.java new file mode 100644 index 00000000000..6dd4ca3dfab --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/EnvironmentVariableLookupTest.java @@ -0,0 +1,34 @@ +package io.dropwizard.configuration; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assume.assumeThat; + +public class EnvironmentVariableLookupTest { + @Test(expected = UndefinedEnvironmentVariableException.class) + public void defaultConstructorEnablesStrict() { + assumeThat(System.getenv("nope"), nullValue()); + + EnvironmentVariableLookup lookup = new EnvironmentVariableLookup(); + lookup.lookup("nope"); + } + + @Test + public void lookupReplacesWithEnvironmentVariables() { + EnvironmentVariableLookup lookup = new EnvironmentVariableLookup(false); + + // Let's hope this doesn't break on Windows + assertThat(lookup.lookup("TEST")).isEqualTo(System.getenv("TEST")); + assertThat(lookup.lookup("nope")).isNull(); + } + + @Test(expected = UndefinedEnvironmentVariableException.class) + public void lookupThrowsExceptionInStrictMode() { + assumeThat(System.getenv("nope"), nullValue()); + + EnvironmentVariableLookup lookup = new EnvironmentVariableLookup(true); + lookup.lookup("nope"); + } +} \ No newline at end of file diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/EnvironmentVariableSubstitutorTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/EnvironmentVariableSubstitutorTest.java new file mode 100644 index 00000000000..347ca0cf2a4 --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/EnvironmentVariableSubstitutorTest.java @@ -0,0 +1,61 @@ +package io.dropwizard.configuration; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.Assume.assumeThat; + +public class EnvironmentVariableSubstitutorTest { + @Test + public void defaultConstructorDisablesSubstitutionInVariables() { + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(); + assertThat(substitutor.isEnableSubstitutionInVariables()).isFalse(); + } + + @Test(expected = UndefinedEnvironmentVariableException.class) + public void defaultConstructorEnablesStrict() { + assumeThat(System.getenv("DOES_NOT_EXIST"), nullValue()); + + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(); + substitutor.replace("${DOES_NOT_EXIST}"); + } + + @Test + public void constructorEnablesSubstitutionInVariables() { + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(true, true); + assertThat(substitutor.isEnableSubstitutionInVariables()).isTrue(); + } + + @Test + public void substitutorUsesEnvironmentVariableLookup() { + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(); + assertThat(substitutor.getVariableResolver()).isInstanceOf(EnvironmentVariableLookup.class); + } + + @Test + public void substitutorReplacesWithEnvironmentVariables() { + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(false); + + assertThat(substitutor.replace("${TEST}")).isEqualTo(System.getenv("TEST")); + assertThat(substitutor.replace("no replacement")).isEqualTo("no replacement"); + assertThat(substitutor.replace("${DOES_NOT_EXIST}")).isEqualTo("${DOES_NOT_EXIST}"); + assertThat(substitutor.replace("${DOES_NOT_EXIST:-default}")).isEqualTo("default"); + } + + @Test(expected = UndefinedEnvironmentVariableException.class) + public void substitutorThrowsExceptionInStrictMode() { + assumeThat(System.getenv("DOES_NOT_EXIST"), nullValue()); + + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(true); + substitutor.replace("${DOES_NOT_EXIST}"); + } + + @Test + public void substitutorReplacesRecursively() { + EnvironmentVariableSubstitutor substitutor = new EnvironmentVariableSubstitutor(false, true); + + assertThat(substitutor.replace("$${${TEST}}")).isEqualTo("${test_value}"); + assertThat(substitutor.replace("${TEST${TEST_SUFFIX}}")).isEqualTo(System.getenv("TEST2")); + } +} \ No newline at end of file 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 00000000000..f3b0ab43646 --- /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.io.ByteStreams; +import com.google.common.io.Resources; +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.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), StandardCharsets.UTF_8).trim()) + .isEqualTo("whee"); + } + } +} diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/LevenshteinComparatorTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/LevenshteinComparatorTest.java new file mode 100644 index 00000000000..f87465ca1bb --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/LevenshteinComparatorTest.java @@ -0,0 +1,41 @@ +package io.dropwizard.configuration; + +import org.junit.Test; + +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LevenshteinComparatorTest { + private final ConfigurationParsingException.Builder.LevenshteinComparator c = new ConfigurationParsingException.Builder.LevenshteinComparator("base"); + + /** + * An "java.lang.IllegalArgumentException: Comparison method violates its general contract!" + * is triggered by this test with a previous version of LevenshteinComparator + * + * It is triggered by a certain condition in TimSort that only happens if 32 or more + * values are in an array. As such, it may not be a thorough test... it depends on the + * specifics of the environment / JVM. + */ + @Test + public void testLevenshteinComparatorSort() { + // no assertions, just making sure we don't violate the compare contract + Arrays.sort(new String[]{ + "y", "w", "y", "e", + "s", "u", "h", "o", + "d", "t", "d", "f", + "z", "j", "c", "k", + "f", "z", "o", "e", + "r", "t", "v", "d", + "l", "r", "w", "u", + "v", "a", "m", "o"}, c); + } + + @Test + public void testLevenshteinCompare() { + assertThat(c.compare("z", "v")).isEqualTo(0); + assertThat(c.compare("b", "v")).isEqualTo(-1); + assertThat(c.compare("v", "b")).isEqualTo(1); + } + +} diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ResourceConfigurationSourceProviderTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ResourceConfigurationSourceProviderTest.java new file mode 100644 index 00000000000..e6dd9e2e399 --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/ResourceConfigurationSourceProviderTest.java @@ -0,0 +1,20 @@ +package io.dropwizard.configuration; + +import com.google.common.io.ByteStreams; +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ResourceConfigurationSourceProviderTest { + private final ConfigurationSourceProvider provider = new ResourceConfigurationSourceProvider(); + + @Test + public void readsFileContents() throws Exception { + try (InputStream input = provider.open("example.txt")) { + assertThat(new String(ByteStreams.toByteArray(input), StandardCharsets.UTF_8).trim()).isEqualTo("whee"); + } + } +} diff --git a/dropwizard-configuration/src/test/java/io/dropwizard/configuration/SubstitutingSourceProviderTest.java b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/SubstitutingSourceProviderTest.java new file mode 100644 index 00000000000..7614eef5353 --- /dev/null +++ b/dropwizard-configuration/src/test/java/io/dropwizard/configuration/SubstitutingSourceProviderTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.configuration; + +import com.google.common.io.ByteStreams; +import org.apache.commons.lang3.text.StrLookup; +import org.apache.commons.lang3.text.StrSubstitutor; +import org.junit.Test; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class SubstitutingSourceProviderTest { + @Test + public void shouldSubstituteCorrectly() throws IOException { + StrLookup dummyLookup = new StrLookup() { + @Override + public String lookup(String key) { + return "baz"; + } + }; + DummySourceProvider dummyProvider = new DummySourceProvider(); + SubstitutingSourceProvider provider = new SubstitutingSourceProvider(dummyProvider, new StrSubstitutor(dummyLookup)); + String results = new String(ByteStreams.toByteArray(provider.open("foo: ${bar}")), StandardCharsets.UTF_8); + + assertThat(results).isEqualTo("foo: baz"); + + // ensure that opened streams are closed + try { + dummyProvider.lastStream.read(); + failBecauseExceptionWasNotThrown(IOException.class); + } catch (IOException e) { + assertThat(e).hasMessage("Stream closed"); + } + } + + @Test + public void shouldSubstituteOnlyExistingVariables() throws IOException { + StrLookup dummyLookup = new StrLookup() { + @Override + public String lookup(String key) { + return null; + } + }; + SubstitutingSourceProvider provider = new SubstitutingSourceProvider(new DummySourceProvider(), new StrSubstitutor(dummyLookup)); + String results = new String(ByteStreams.toByteArray(provider.open("foo: ${bar}")), StandardCharsets.UTF_8); + + assertThat(results).isEqualTo("foo: ${bar}"); + } + + @Test + public void shouldSubstituteWithDefaultValue() throws IOException { + StrLookup dummyLookup = new StrLookup() { + @Override + public String lookup(String key) { + return null; + } + }; + SubstitutingSourceProvider provider = new SubstitutingSourceProvider(new DummySourceProvider(), new StrSubstitutor(dummyLookup)); + String results = new String(ByteStreams.toByteArray(provider.open("foo: ${bar:-default}")), StandardCharsets.UTF_8); + + assertThat(results).isEqualTo("foo: default"); + } + + private static class DummySourceProvider implements ConfigurationSourceProvider { + public InputStream lastStream; + + @Override + public InputStream open(String s) throws IOException { + // used to test that the stream is properly closed + lastStream = new BufferedInputStream(new ByteArrayInputStream(s.getBytes(StandardCharsets.UTF_8))); + return lastStream; + } + } +} 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 00000000000..327606956fb --- /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.io.ByteStreams; +import com.google.common.io.Resources; +import org.junit.Test; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.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), StandardCharsets.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 00000000000..c6284d24b6d --- /dev/null +++ b/dropwizard-configuration/src/test/resources/example.txt @@ -0,0 +1 @@ +whee diff --git a/dropwizard-configuration/src/test/resources/factory-test-empty.yml b/dropwizard-configuration/src/test/resources/factory-test-empty.yml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dropwizard/src/test/resources/factory-test-invalid.yml b/dropwizard-configuration/src/test/resources/factory-test-invalid.yml similarity index 100% rename from dropwizard/src/test/resources/factory-test-invalid.yml rename to dropwizard-configuration/src/test/resources/factory-test-invalid.yml diff --git a/dropwizard-configuration/src/test/resources/factory-test-malformed-advanced.yml b/dropwizard-configuration/src/test/resources/factory-test-malformed-advanced.yml new file mode 100644 index 00000000000..4903da9b768 --- /dev/null +++ b/dropwizard-configuration/src/test/resources/factory-test-malformed-advanced.yml @@ -0,0 +1,2 @@ +name: Mighty Wizard +type: [ coder,wizard \ No newline at end of file diff --git a/dropwizard/src/test/resources/factory-test-malformed.yml b/dropwizard-configuration/src/test/resources/factory-test-malformed.yml similarity index 100% rename from dropwizard/src/test/resources/factory-test-malformed.yml rename to dropwizard-configuration/src/test/resources/factory-test-malformed.yml diff --git a/dropwizard-configuration/src/test/resources/factory-test-typo.yml b/dropwizard-configuration/src/test/resources/factory-test-typo.yml new file mode 100644 index 00000000000..d52ba8a167a --- /dev/null +++ b/dropwizard-configuration/src/test/resources/factory-test-typo.yml @@ -0,0 +1,7 @@ +name: Migty Wizard +type: + - wizard +propertis: + admin: true +servers: + - port: 8080 diff --git a/dropwizard-configuration/src/test/resources/factory-test-unknown-property.yml b/dropwizard-configuration/src/test/resources/factory-test-unknown-property.yml new file mode 100644 index 00000000000..2f02c36fe32 --- /dev/null +++ b/dropwizard-configuration/src/test/resources/factory-test-unknown-property.yml @@ -0,0 +1,12 @@ +name: Mighty Wizard +type: + - coder + - wizard +trait: Nearly Bald +properties: + debug: true + settings.enabled: false +servers: + - port: 8080 + - port: 8081 + - port: 8082 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 00000000000..561416b2a7a --- /dev/null +++ b/dropwizard-configuration/src/test/resources/factory-test-valid.yml @@ -0,0 +1,13 @@ +name: Coda Hale +type: + - coder + - wizard +properties: + debug: true + settings.enabled: false +servers: + - port: 8080 + - port: 8081 + - port: 8082 +my.logger: + level: info diff --git a/dropwizard-configuration/src/test/resources/factory-test-wrong-type.yml b/dropwizard-configuration/src/test/resources/factory-test-wrong-type.yml new file mode 100644 index 00000000000..c5c29e8adaf --- /dev/null +++ b/dropwizard-configuration/src/test/resources/factory-test-wrong-type.yml @@ -0,0 +1,2 @@ +name: Mighty Wizard +age: one hundred and two 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-configuration/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-core/pom.xml b/dropwizard-core/pom.xml new file mode 100644 index 00000000000..4f4ad8b3fcf --- /dev/null +++ b/dropwizard-core/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-core + Dropwizard + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-util + + + io.dropwizard + dropwizard-jackson + + + io.dropwizard + dropwizard-validation + + + io.dropwizard + dropwizard-configuration + + + io.dropwizard + dropwizard-logging + + + io.dropwizard + dropwizard-metrics + + + io.dropwizard + dropwizard-jersey + + + io.dropwizard + dropwizard-servlets + + + io.dropwizard + dropwizard-jetty + + + io.dropwizard + dropwizard-lifecycle + + + io.dropwizard.metrics + metrics-core + + + io.dropwizard.metrics + metrics-jvm + + + io.dropwizard.metrics + metrics-servlets + + + io.dropwizard.metrics + metrics-healthchecks + + + io.dropwizard + dropwizard-request-logging + + + net.sourceforge.argparse4j + argparse4j + + + org.eclipse.jetty.toolchain.setuid + jetty-setuid-java + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + test + + + 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 00000000000..7d5b13c693b --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/Application.java @@ -0,0 +1,114 @@ +package io.dropwizard; + +import ch.qos.logback.classic.Level; +import io.dropwizard.cli.CheckCommand; +import io.dropwizard.cli.Cli; +import io.dropwizard.cli.ServerCommand; +import io.dropwizard.logging.BootstrapLogging; +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. + * + * Because the default constructor will be inherited by all + * subclasses, {BootstrapLogging.bootstrap()} will always be + * invoked. The log level used during the bootstrap process can be + * configured by {Application} subclasses by overriding + * {#bootstrapLogLevel}. + * + * @param the type of configuration class for this application + */ +public abstract class Application { + protected Application() { + // make sure spinning up Hibernate Validator doesn't yell at us + BootstrapLogging.bootstrap(bootstrapLogLevel()); + } + + /** + * The log level at which to bootstrap logging on application startup. + */ + protected Level bootstrapLogLevel() { + return Level.WARN; + } + + /** + * Returns the {@link Class} of the configuration class type parameter. + * + * @return the configuration class + * @see Generics#getTypeParameter(Class, Class) + */ + public Class 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 void initialize(Bootstrap 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 void run(String... arguments) throws Exception { + final Bootstrap bootstrap = new Bootstrap<>(this); + addDefaultCommands(bootstrap); + initialize(bootstrap); + // Should by called after initialize to give an opportunity to set a custom metric registry + bootstrap.registerMetrics(); + + 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 + onFatalError(); + } + } + + /** + * Called by {@link #run(String...)} to add the standard "server" and "check" commands + * + * @param bootstrap the bootstrap instance + */ + protected void addDefaultCommands(Bootstrap bootstrap) { + bootstrap.addCommand(new ServerCommand<>(this)); + bootstrap.addCommand(new CheckCommand<>(this)); + } + + /** + * Called by {@link #run(String...)} to indicate there was a fatal error running the requested command. + * + * The default implementation calls {@link System#exit(int)} with a non-zero status code to terminate the + * application. + */ + protected void onFatalError() { + 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 00000000000..ffa6b6ded13 --- /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 00000000000..4f0b8724cbc --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/Configuration.java @@ -0,0 +1,129 @@ +package io.dropwizard; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +import io.dropwizard.logging.DefaultLoggingFactory; +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. + *

    + * For example, given a YAML file with this: + *

    + * name: "Random Person"
    + * age: 43
    + * # ... etc ...
    + * 
    + * And a configuration like this: + *
    + * 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;
    + *     }
    + * }
    + * 
    + *

    + * 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 YAML Cookbook + */ +public class Configuration { + @Valid + @NotNull + private ServerFactory server = new DefaultServerFactory(); + + @Valid + @NotNull + private LoggingFactory logging = new DefaultLoggingFactory(); + + @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 MoreObjects.toStringHelper(this) + .add("server", server) + .add("logging", logging) + .add("metrics", metrics) + .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 00000000000..fb626a2f38f --- /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 the required configuration interface + */ +public interface ConfiguredBundle { + /** + * 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 00000000000..54f9b01684c --- /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 the {@link Configuration} subclass which is loaded from the configuration file + */ +public class CheckCommand extends ConfiguredCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(CheckCommand.class); + + private final Class configurationClass; + + public CheckCommand(Application 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 getConfigurationClass() { + return configurationClass; + } + + @Override + protected void run(Bootstrap 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 00000000000..c1f94b58d0a --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/cli/Cli.java @@ -0,0 +1,156 @@ +package io.dropwizard.cli; + +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.Argument; +import net.sourceforge.argparse4j.inf.ArgumentAction; +import net.sourceforge.argparse4j.inf.ArgumentParser; +import net.sourceforge.argparse4j.inf.ArgumentParserException; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import net.sourceforge.argparse4j.internal.HelpScreenException; + +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * 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 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, StandardCharsets.UTF_8), true); + this.stdErr = new PrintWriter(new OutputStreamWriter(stdErr, StandardCharsets.UTF_8), true); + this.commands = new TreeMap<>(); + 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); + final Command command = commands.get(namespace.getString(COMMAND_NAME_ATTR)); + command.run(bootstrap, namespace); + } + return true; + } catch (HelpScreenException ignored) { + // This exception is triggered when the user passes in a help flag. + // Return true to signal that the process executed normally. + return true; + } catch (ArgumentParserException e) { + stdErr.println(e.getMessage()); + e.getParser().printHelp(stdErr); + return false; + } catch (Throwable t) { + // Unexpected exceptions should result in non-zero exit status of the process + stdErr.println(t.getMessage()); + return false; + } + } + + private static 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().orElse( + "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 attrs, String flag, Object value) + throws ArgumentParserException { + parser.printHelp(out); + throw new HelpScreenException(parser); + } + + @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 00000000000..d10db1dd8b8 --- /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 00000000000..d96880b0192 --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/cli/ConfiguredCommand.java @@ -0,0 +1,128 @@ +package io.dropwizard.cli; + +import com.fasterxml.jackson.databind.ObjectMapper; +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 net.sourceforge.argparse4j.inf.Argument; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; + +import javax.validation.Validator; +import java.io.IOException; + +/** + * 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 the {@link Configuration} subclass which is loaded from the configuration file + * @see Configuration + */ +public abstract class ConfiguredCommand 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 getConfigurationClass() { + return Generics.getTypeParameter(getClass(), Configuration.class); + } + + /** + * Configure the command's {@link Subparser}.

    N.B.: if you override this method, you + * must call {@code super.override(subparser)} in order to preserve the configuration + * file parameter in the subparser.

    + * + * @param subparser the {@link Subparser} specific to the command + */ + @Override + public void configure(Subparser subparser) { + addFileArgument(subparser); + } + + /** + * Adds the configuration file argument for the configured command. + * @param subparser The subparser to register the argument on + * @return the register argument + */ + protected Argument addFileArgument(Subparser subparser) { + return subparser.addArgument("file") + .nargs("?") + .help("application configuration file"); + } + + @Override + @SuppressWarnings("unchecked") + public void run(Bootstrap wildcardBootstrap, Namespace namespace) throws Exception { + final Bootstrap bootstrap = (Bootstrap) wildcardBootstrap; + configuration = parseConfiguration(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, 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 bootstrap, + Namespace namespace, + T configuration) throws Exception; + + private T parseConfiguration(ConfigurationFactoryFactory configurationFactoryFactory, + ConfigurationSourceProvider provider, + Validator validator, + String path, + Class klass, + ObjectMapper objectMapper) throws IOException, ConfigurationException { + final ConfigurationFactory 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 00000000000..709ae240740 --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/cli/EnvironmentCommand.java @@ -0,0 +1,56 @@ +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; + +/** + * A command which executes with a configured {@link Environment}. + * + * @param the {@link Configuration} subclass which is loaded from the configuration file + * @see Configuration + */ +public abstract class EnvironmentCommand extends ConfiguredCommand { + private final Application 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 application, String name, String description) { + super(name, description); + this.application = application; + } + + @Override + protected void run(Bootstrap bootstrap, Namespace namespace, T configuration) throws Exception { + final Environment environment = new Environment(bootstrap.getApplication().getName(), + bootstrap.getObjectMapper(), + bootstrap.getValidatorFactory().getValidator(), + bootstrap.getMetricRegistry(), + bootstrap.getClassLoader(), + bootstrap.getHealthCheckRegistry()); + configuration.getMetricsFactory().configure(environment.lifecycle(), + bootstrap.getMetricRegistry()); + configuration.getServerFactory().configure(environment); + + 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 00000000000..8f9b828b708 --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/cli/ServerCommand.java @@ -0,0 +1,76 @@ +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 the {@link Configuration} subclass which is loaded from the configuration file + */ +public class ServerCommand extends EnvironmentCommand { + private static final Logger LOGGER = LoggerFactory.getLogger(ServerCommand.class); + + private final Class configurationClass; + + public ServerCommand(Application application) { + this(application, "server", "Runs the Dropwizard application as an HTTP server"); + } + + /** + * A constructor to allow reuse of the server command as a different name + * @param application the application using this command + * @param name the argument name to invoke this command + * @param description a summary of what the command does + */ + protected ServerCommand(final Application application, final String name, final String description) { + super(application, name, description); + this.configurationClass = application.getConfigurationClass(); + } + + /* + * Since we don't subclass ServerCommand, we need a concrete reference to the configuration + * class. + */ + @Override + protected Class 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); + try { + server.stop(); + } catch (Exception e1) { + LOGGER.warn("Failure during stop server", e1); + } + try { + cleanup(); + } catch (Exception e2) { + LOGGER.warn("Failure during cleanup", e2); + } + 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/server/AbstractServerFactory.java b/dropwizard-core/src/main/java/io/dropwizard/server/AbstractServerFactory.java new file mode 100644 index 00000000000..50ed56dd42e --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/server/AbstractServerFactory.java @@ -0,0 +1,615 @@ +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.Joiner; +import com.google.common.io.Resources; +import io.dropwizard.jersey.errors.EarlyEofExceptionMapper; +import io.dropwizard.jersey.errors.LoggingExceptionMapper; +import io.dropwizard.jersey.filter.AllowedMethodsFilter; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper; +import io.dropwizard.jersey.setup.JerseyEnvironment; +import io.dropwizard.jersey.validation.HibernateValidationFeature; +import io.dropwizard.jersey.validation.JerseyViolationExceptionMapper; +import io.dropwizard.jetty.GzipHandlerFactory; +import io.dropwizard.jetty.MutableServletContextHandler; +import io.dropwizard.jetty.NonblockingServletHolder; +import io.dropwizard.jetty.ServerPushFilterFactory; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.request.logging.LogbackAccessRequestLogFactory; +import io.dropwizard.request.logging.RequestLogFactory; +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.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.nio.charset.StandardCharsets; +import java.util.EnumSet; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.BlockingQueue; +import java.util.regex.Pattern; + +/** + * A base class for {@link ServerFactory} implementations. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code requestLog}The {@link RequestLogFactory request log} configuration.
    {@code gzip}The {@link GzipHandlerFactory GZIP} configuration.
    {@code serverPush}The {@link ServerPushFilterFactory} configuration.
    {@code maxThreads}1024The maximum number of threads to use for requests.
    {@code minThreads}8The minimum number of threads to use for requests.
    {@code maxQueuedRequests}1024The maximum number of requests to queue before blocking the acceptors.
    {@code idleThreadTimeout}1 minuteThe amount of time a worker thread can be idle before being stopped.
    {@code nofileSoftLimit}(none) + * The number of open file descriptors before a soft error is issued. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code nofileHardLimit}(none) + * The number of open file descriptors before a hard error is issued. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code gid}(none) + * The group ID to switch to once the connectors have started. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code uid}(none) + * The user ID to switch to once the connectors have started. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code user}(none) + * The username to switch to once the connectors have started. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code group}(none) + * The group to switch to once the connectors have started. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code umask}(none) + * The umask to switch to once the connectors have started. Requires Jetty's + * {@code libsetuid.so} on {@code java.library.path}. + *
    {@code startsAsRoot}(none) + * Whether or not the Dropwizard application is started as a root user. Requires + * Jetty's {@code libsetuid.so} on {@code java.library.path}. + *
    {@code registerDefaultExceptionMappers}true + * Whether or not the default Jersey ExceptionMappers should be registered. + * Set this to false if you want to register your own. + *
    {@code shutdownGracePeriod}30 seconds + * The maximum time to wait for Jetty, and all Managed instances, to cleanly shutdown + * before forcibly terminating them. + *
    {@code allowedMethods}GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH + * The set of allowed HTTP methods. Others will be rejected with a + * 405 Method Not Allowed response. + *
    {@code rootPath}/* + * The URL pattern relative to {@code applicationContextPath} from which the JAX-RS resources will be served. + *
    + * + * @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 LogbackAccessRequestLogFactory(); + + @Valid + @NotNull + private GzipHandlerFactory gzip = new GzipHandlerFactory(); + + @Valid + @NotNull + private ServerPushFilterFactory serverPush = new ServerPushFilterFactory(); + + @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 Boolean registerDefaultExceptionMappers = Boolean.TRUE; + + private Duration shutdownGracePeriod = Duration.seconds(30); + + @NotNull + private Set allowedMethods = AllowedMethodsFilter.DEFAULT_ALLOWED_METHODS; + + private Optional jerseyRootPath = Optional.empty(); + + @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 GzipHandlerFactory getGzipFilterFactory() { + return gzip; + } + + @JsonProperty("gzip") + public void setGzipFilterFactory(GzipHandlerFactory gzip) { + this.gzip = gzip; + } + + @JsonProperty("serverPush") + public ServerPushFilterFactory getServerPush() { + return serverPush; + } + + @JsonProperty("serverPush") + public void setServerPush(ServerPushFilterFactory serverPush) { + this.serverPush = serverPush; + } + + @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; + } + + public Boolean getRegisterDefaultExceptionMappers() { + return registerDefaultExceptionMappers; + } + + public void setRegisterDefaultExceptionMappers(Boolean registerDefaultExceptionMappers) { + this.registerDefaultExceptionMappers = registerDefaultExceptionMappers; + } + + @JsonProperty + public Duration getShutdownGracePeriod() { + return shutdownGracePeriod; + } + + @JsonProperty + public void setShutdownGracePeriod(Duration shutdownGracePeriod) { + this.shutdownGracePeriod = shutdownGracePeriod; + } + + @JsonProperty + public Set getAllowedMethods() { + return allowedMethods; + } + + @JsonProperty + public void setAllowedMethods(Set allowedMethods) { + this.allowedMethods = allowedMethods; + } + + @JsonProperty("rootPath") + public Optional getJerseyRootPath() { + return jerseyRootPath; + } + + @JsonProperty("rootPath") + public void setJerseyRootPath(String jerseyRootPath) { + this.jerseyRootPath = Optional.ofNullable(jerseyRootPath); + } + + 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) { + handler.setServer(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 Servlet 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)); + serverPush.addFilter(handler); + if (jerseyContainer != null) { + if (jerseyRootPath.isPresent()) { + jersey.setUrlPattern(jerseyRootPath.get()); + } + jersey.register(new JacksonMessageBodyProvider(objectMapper)); + jersey.register(new HibernateValidationFeature(validator)); + if (registerDefaultExceptionMappers == null || registerDefaultExceptionMappers) { + jersey.register(new LoggingExceptionMapper() { + }); + jersey.register(new JerseyViolationExceptionMapper()); + jersey.register(new JsonProcessingExceptionMapper()); + jersey.register(new EarlyEofExceptionMapper()); + } + 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 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 + final StatisticsHandler statisticsHandler = new StatisticsHandler(); + statisticsHandler.setHandler(handler); + return statisticsHandler; + } + + protected Handler buildGzipHandler(Handler handler) { + return gzip.isEnabled() ? gzip.build(handler) : handler; + } + + protected void printBanner(String name) { + try { + final String banner = WINDOWS_NEWLINE.matcher(Resources.toString(Resources.getResource("banner.txt"), + StandardCharsets.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 00000000000..0fa7db0eb42 --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/server/DefaultServerFactory.java @@ -0,0 +1,252 @@ +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.base.MoreObjects; +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 org.hibernate.validator.constraints.NotEmpty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 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). + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code applicationConnectors}An {@link HttpConnectorFactory HTTP connector} listening on port 8080.A set of {@link ConnectorFactory connectors} which will handle application requests.
    {@code adminConnectors}An {@link HttpConnectorFactory HTTP connector} listening on port 8081.A set of {@link ConnectorFactory connectors} which will handle admin requests.
    {@code adminMaxThreads}64The maximum number of threads to use for admin requests.
    {@code adminMinThreads}1The minimum number of threads to use for admin requests.
    + *

    + * For more configuration parameters, see {@link AbstractServerFactory}. + * + * @see ServerFactory + * @see AbstractServerFactory + */ +@JsonTypeName("default") +public class DefaultServerFactory extends AbstractServerFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultServerFactory.class); + + @Valid + @NotNull + private List applicationConnectors = Collections.singletonList(HttpConnectorFactory.application()); + + @Valid + @NotNull + private List adminConnectors = Collections.singletonList(HttpConnectorFactory.admin()); + + @Min(2) + private int adminMaxThreads = 64; + + @Min(1) + private int adminMinThreads = 1; + + @NotEmpty + private String applicationContextPath = "/"; + + @NotEmpty + private String adminContextPath = "/"; + + @JsonProperty + public List getApplicationConnectors() { + return applicationConnectors; + } + + @JsonProperty + public void setApplicationConnectors(List connectors) { + this.applicationConnectors = connectors; + } + + @JsonProperty + public List getAdminConnectors() { + return adminConnectors; + } + + @JsonProperty + public void setAdminConnectors(List 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; + } + + @JsonProperty + public String getApplicationContextPath() { + return applicationContextPath; + } + + @JsonProperty + public void setApplicationContextPath(final String applicationContextPath) { + this.applicationContextPath = applicationContextPath; + } + + @JsonProperty + public String getAdminContextPath() { + return adminContextPath; + } + + @JsonProperty + public void setAdminContextPath(final String adminContextPath) { + this.adminContextPath = adminContextPath; + } + + @Override + public Server build(Environment environment) { + // ensures that the environment is configured before the server is built + configure(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); + final Handler gzipHandler = buildGzipHandler(routingHandler); + server.setHandler(addStatsHandler(addRequestLog(server, gzipHandler, environment.getName()))); + return server; + } + + @Override + public void configure(Environment environment) { + LOGGER.info("Registering jersey handler with root path prefix: {}", applicationContextPath); + environment.getApplicationContext().setContextPath(applicationContextPath); + + LOGGER.info("Registering admin handler with root path prefix: {}", adminContextPath); + environment.getAdminContext().setContextPath(adminContextPath); + } + + private RoutingHandler buildRoutingHandler(MetricRegistry metricRegistry, + Server server, + Handler applicationHandler, + Handler adminHandler) { + final List appConnectors = buildAppConnectors(metricRegistry, server); + + final List adConnectors = buildAdminConnectors(metricRegistry, server); + + final Map handlers = new LinkedHashMap<>(); + + 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 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 connectors = new ArrayList<>(); + for (ConnectorFactory factory : adminConnectors) { + final Connector connector = factory.build(server, metricRegistry, "admin", threadPool); + if (connector instanceof ContainerLifeCycle) { + ((ContainerLifeCycle) connector).unmanage(threadPool); + } + connectors.add(connector); + } + return connectors; + } + + private List buildAppConnectors(MetricRegistry metricRegistry, Server server) { + final List connectors = new ArrayList<>(); + for (ConnectorFactory factory : applicationConnectors) { + connectors.add(factory.build(server, metricRegistry, "application", null)); + } + return connectors; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("applicationConnectors", applicationConnectors) + .add("adminConnectors", adminConnectors) + .add("adminMaxThreads", adminMaxThreads) + .add("adminMinThreads", adminMinThreads) + .add("applicationContextPath", applicationContextPath) + .add("adminContextPath", adminContextPath) + .toString(); + } +} 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 00000000000..d00a26e183b --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/server/ServerFactory.java @@ -0,0 +1,29 @@ +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 + */ +@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); + + /** + * Configures the given environment with settings defined in the factory. + * + * @param environment the application's environment + */ + void configure(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 00000000000..fa5ed909f7b --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/server/SimpleServerFactory.java @@ -0,0 +1,147 @@ +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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +/** + * 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}. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code connector}An {@link HttpConnectorFactory HTTP connector} listening on port {@code 8080}.The {@link ConnectorFactory connector} which will handle both application and admin requests.
    {@code applicationContextPath}{@code /application}The context path of the application servlets, including Jersey.
    {@code adminContextPath}{@code /admin}The context path of the admin servlets, including metrics and tasks.
    + *

    + * For more configuration parameters, see {@link AbstractServerFactory}. + * + * @see ServerFactory + * @see AbstractServerFactory + */ +@JsonTypeName("simple") +public class SimpleServerFactory extends AbstractServerFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(SimpleServerFactory.class); + + @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) { + // ensures that the environment is configured before the server is built + configure(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 Connector conn = connector.build(server, + environment.metrics(), + environment.getName(), + null); + + server.addConnector(conn); + + final ContextRoutingHandler routingHandler = new ContextRoutingHandler(ImmutableMap.of( + applicationContextPath, applicationHandler, + adminContextPath, adminHandler + )); + final Handler gzipHandler = buildGzipHandler(routingHandler); + server.setHandler(addStatsHandler(addRequestLog(server, gzipHandler, environment.getName()))); + + return server; + } + + @Override + public void configure(Environment environment) { + LOGGER.info("Registering jersey handler with root path prefix: {}", applicationContextPath); + environment.getApplicationContext().setContextPath(applicationContextPath); + + LOGGER.info("Registering admin handler with root path prefix: {}", adminContextPath); + environment.getAdminContext().setContextPath(adminContextPath); + } +} 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 00000000000..739a568772c --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/setup/AdminEnvironment.java @@ -0,0 +1,92 @@ +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.LogConfigurationTask; +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.MoreObjects.firstNonNull; +import static java.util.Objects.requireNonNull; + +/** + * 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()); + tasks.add(new LogConfigurationTask()); + 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(requireNonNull(task)); + } + + private void logTasks() { + final StringBuilder stringBuilder = new StringBuilder(1024).append(String.format("%n%n")); + + for (Task task : tasks.getTasks()) { + final String taskClassName = firstNonNull(task.getClass().getCanonicalName(), task.getClass().getName()); + stringBuilder.append(String.format(" %-7s /tasks/%s (%s)%n", + "POST", + task.getName(), + taskClassName)); + } + + 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 00000000000..7de8a37aaac --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/setup/Bootstrap.java @@ -0,0 +1,256 @@ +package io.dropwizard.setup; + +import com.codahale.metrics.JmxReporter; +import com.codahale.metrics.JvmAttributeGaugeSet; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.codahale.metrics.jvm.BufferPoolMetricSet; +import com.codahale.metrics.jvm.ClassLoadingGaugeSet; +import com.codahale.metrics.jvm.FileDescriptorRatioGauge; +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 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 io.dropwizard.jersey.validation.Validators; + +import javax.validation.ValidatorFactory; +import java.lang.management.ManagementFactory; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * The pre-start application environment, containing everything required to bootstrap a Dropwizard + * command. + * + * @param the configuration type + */ +public class Bootstrap { + private final Application application; + private final List bundles; + private final List> configuredBundles; + private final List commands; + + private ObjectMapper objectMapper; + private MetricRegistry metricRegistry; + private ConfigurationSourceProvider configurationSourceProvider; + private ClassLoader classLoader; + private ConfigurationFactoryFactory configurationFactoryFactory; + private ValidatorFactory validatorFactory; + + private boolean metricsAreRegistered; + private HealthCheckRegistry healthCheckRegistry; + + /** + * Creates a new {@link Bootstrap} for the given application. + * + * @param application a Dropwizard {@link Application} + */ + public Bootstrap(Application application) { + this.application = application; + this.objectMapper = Jackson.newObjectMapper(); + this.bundles = new ArrayList<>(); + this.configuredBundles = new ArrayList<>(); + this.commands = new ArrayList<>(); + this.validatorFactory = Validators.newValidatorFactory(); + this.metricRegistry = new MetricRegistry(); + this.configurationSourceProvider = new FileConfigurationSourceProvider(); + this.classLoader = Thread.currentThread().getContextClassLoader(); + this.configurationFactoryFactory = new DefaultConfigurationFactoryFactory<>(); + this.healthCheckRegistry = new HealthCheckRegistry(); + } + + /** + * Registers the JVM metrics to the metric registry and start to report + * the registry metrics via JMX. + */ + public void registerMetrics() { + if (metricsAreRegistered) { + return; + } + getMetricRegistry().register("jvm.attribute", new JvmAttributeGaugeSet()); + getMetricRegistry().register("jvm.buffers", new BufferPoolMetricSet(ManagementFactory + .getPlatformMBeanServer())); + getMetricRegistry().register("jvm.classloader", new ClassLoadingGaugeSet()); + getMetricRegistry().register("jvm.filedescriptor", new FileDescriptorRatioGauge()); + getMetricRegistry().register("jvm.gc", new GarbageCollectorMetricSet()); + getMetricRegistry().register("jvm.memory", new MemoryUsageGaugeSet()); + getMetricRegistry().register("jvm.threads", new ThreadStatesGaugeSet()); + + JmxReporter.forRegistry(metricRegistry).build().start(); + metricsAreRegistered = true; + } + + /** + * Returns the bootstrap's {@link Application}. + */ + public Application 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 = requireNonNull(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 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 command) { + commands.add(command); + } + + /** + * Returns the bootstrap's {@link ObjectMapper}. + */ + public ObjectMapper getObjectMapper() { + return objectMapper; + } + + /** + * Sets the given {@link ObjectMapper} to the bootstrap. + * WARNING: The mapper should be created by {@link Jackson#newMinimalObjectMapper()} + * or {@link Jackson#newObjectMapper()}, otherwise it will not work with Dropwizard.

    + * + * @param objectMapper an {@link ObjectMapper} + */ + public void setObjectMapper(ObjectMapper objectMapper) { + this.objectMapper = 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 bundle : configuredBundles) { + bundle.run(configuration, environment); + } + } + + /** + * Returns the application's commands. + */ + public ImmutableList getCommands() { + return ImmutableList.copyOf(commands); + } + + /** + * Returns the application metrics. + */ + public MetricRegistry getMetricRegistry() { + return metricRegistry; + } + + /** + * Sets a custom registry for the application metrics. + * + * @param metricRegistry a custom metric registry + */ + public void setMetricRegistry(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + } + + /** + * Returns the application's validator factory. + */ + public ValidatorFactory getValidatorFactory() { + return validatorFactory; + } + + public void setValidatorFactory(ValidatorFactory validatorFactory) { + this.validatorFactory = validatorFactory; + } + + public ConfigurationFactoryFactory getConfigurationFactoryFactory() { + return configurationFactoryFactory; + } + + public void setConfigurationFactoryFactory(ConfigurationFactoryFactory configurationFactoryFactory) { + this.configurationFactoryFactory = configurationFactoryFactory; + } + + /** + * returns the health check registry + */ + public HealthCheckRegistry getHealthCheckRegistry() { + return healthCheckRegistry; + } + + public void setHealthCheckRegistry(HealthCheckRegistry healthCheckRegistry) { + this.healthCheckRegistry = healthCheckRegistry; + } +} 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 00000000000..870c6013549 --- /dev/null +++ b/dropwizard-core/src/main/java/io/dropwizard/setup/Environment.java @@ -0,0 +1,196 @@ +package io.dropwizard.setup; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.SharedMetricRegistries; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.setup.JerseyContainerHolder; +import io.dropwizard.jersey.setup.JerseyEnvironment; +import io.dropwizard.jersey.setup.JerseyServletContainer; +import io.dropwizard.jetty.MutableServletContextHandler; +import io.dropwizard.jetty.setup.ServletEnvironment; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; + +import javax.servlet.Servlet; +import javax.validation.Validator; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ThreadPoolExecutor; + +import static java.util.Objects.requireNonNull; + +/** + * 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; + + private final ExecutorService healthCheckExecutorService; + + /** + * 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, + HealthCheckRegistry healthCheckRegistry) { + this.name = name; + this.objectMapper = objectMapper; + this.metricRegistry = metricRegistry; + this.healthCheckRegistry = 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); + + this.jerseyServletContainer = new JerseyContainerHolder(new JerseyServletContainer(jerseyConfig)); + this.jerseyEnvironment = new JerseyEnvironment(jerseyServletContainer, jerseyConfig); + + + this.healthCheckExecutorService = this.lifecycle().executorService("TimeBoundHealthCheck-pool-%d") + .workQueue(new ArrayBlockingQueue<>(1)) + .minThreads(1) + .maxThreads(4) + .threadFactory(new ThreadFactoryBuilder().setDaemon(true).build()) + .rejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()) + .build(); + + SharedMetricRegistries.add("default", metricRegistry); + } + + /** + * Creates an environment with default health check registry + */ + public Environment(String name, + ObjectMapper objectMapper, + Validator validator, + MetricRegistry metricRegistry, + ClassLoader classLoader) { + this(name, objectMapper, validator, metricRegistry, classLoader, new HealthCheckRegistry()); + } + + /** + * Returns the application's {@link JerseyEnvironment}. + */ + public JerseyEnvironment jersey() { + return jerseyEnvironment; + } + + /** + * Returns an {@link ExecutorService} to run time bound health checks + */ + public ExecutorService getHealthCheckExecutorService() { + return healthCheckExecutorService; + } + + /** + * 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 = requireNonNull(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 + */ + + public MutableServletContextHandler getApplicationContext() { + return servletContext; + } + + public Servlet 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 00000000000..ae5ceddda3a --- /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 00000000000..a779ed48b23 --- /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 00000000000..560b0cd2e5a --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/ApplicationTest.java @@ -0,0 +1,78 @@ +package io.dropwizard; + +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ApplicationTest { + private static class FakeConfiguration extends Configuration { + } + + private static class FakeApplication extends Application { + boolean fatalError = false; + + @Override + public void run(FakeConfiguration configuration, Environment environment) {} + + @Override + protected void onFatalError() { + fatalError = true; + } + } + + private static class PoserApplication extends FakeApplication { + } + + private static class WrapperApplication extends Application { + private final Application application; + + private WrapperApplication(Application application) { + this.application = application; + } + + @Override + public void initialize(Bootstrap 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); + } + + @Test + public void exitWithFatalErrorWhenCommandFails() throws Exception { + final File configFile = File.createTempFile("dropwizard-invalid-config", ".yml"); + try { + final FakeApplication application = new FakeApplication(); + application.run("server", configFile.getAbsolutePath()); + assertThat(application.fatalError).isTrue(); + } finally { + configFile.delete(); + } + } +} 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 00000000000..f8b15659f2f --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/ConfigurationTest.java @@ -0,0 +1,57 @@ +package io.dropwizard; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jetty.ConnectorFactory; +import io.dropwizard.logging.AppenderFactory; +import org.junit.Test; + +import java.util.ServiceLoader; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.assertj.core.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(); + Class[] dummyArray = {}; + + mapper.getSubtypeResolver() + .registerSubtypes(StreamSupport.stream(ServiceLoader.load(AppenderFactory.class).spliterator(), false) + .map(Object::getClass) + .collect(Collectors.toList()) + .toArray(dummyArray)); + + mapper.getSubtypeResolver() + .registerSubtypes(StreamSupport.stream(ServiceLoader.load(ConnectorFactory.class).spliterator(), false) + .map(Object::getClass) + .collect(Collectors.toList()) + .toArray(dummyArray)); + + // 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 00000000000..b1b876e5235 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/cli/CheckCommandTest.java @@ -0,0 +1,47 @@ +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.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class CheckCommandTest { + private static class MyApplication extends Application { + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + private final MyApplication application = new MyApplication(); + private final CheckCommand command = new CheckCommand<>(application); + + @SuppressWarnings("unchecked") + private final Bootstrap 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 00000000000..a57458e4d55 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/cli/CliTest.java @@ -0,0 +1,298 @@ +package io.dropwizard.cli; + +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.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.util.Locale; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CliTest { + private static final Locale DEFAULT_LOCALE = Locale.getDefault(); + + private final JarLocation location = mock(JarLocation.class); + private final Application app = new Application() { + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + }; + private final Bootstrap bootstrap = new Bootstrap<>(app); + private final ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); + private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + private final CheckCommand command = spy(new CheckCommand<>(app)); + private Cli cli; + + @BeforeClass + public static void init() { + // Set default locale to English because some tests assert localized error messages + Locale.setDefault(Locale.ENGLISH); + } + + @AfterClass + public static void shutdown() { + Locale.setDefault(DEFAULT_LOCALE); + } + + @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.empty()); + 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/CommandTest.java b/dropwizard-core/src/test/java/io/dropwizard/cli/CommandTest.java new file mode 100644 index 00000000000..f178e5f0147 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/cli/CommandTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.cli; + +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 net.sourceforge.argparse4j.inf.Subparser; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class CommandTest { + private static class TestCommand extends Command { + protected TestCommand() { + super("test", "test"); + } + + @Override + public void configure(Subparser subparser) { + subparser.addArgument("types").choices("a", "b", "c").help("Type to use"); + } + + @Override + public void run(Bootstrap bootstrap, Namespace namespace) throws Exception { + } + } + + private final Application app = new Application() { + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + }; + + private final ByteArrayOutputStream stdOut = new ByteArrayOutputStream(); + private final ByteArrayOutputStream stdErr = new ByteArrayOutputStream(); + private final Command command = new TestCommand(); + private Cli cli; + + @Before + public void setUp() throws Exception { + final JarLocation location = mock(JarLocation.class); + final Bootstrap bootstrap = new Bootstrap<>(app); + when(location.toString()).thenReturn("dw-thing.jar"); + when(location.getVersion()).thenReturn(Optional.of("1.0.0")); + bootstrap.addCommand(command); + + cli = new Cli(location, bootstrap, stdOut, stdErr); + } + + @Test + public void listHelpOnceOnArgumentOmission() throws Exception { + assertThat(cli.run("test", "-h")) + .isTrue(); + + assertThat(stdOut.toString()) + .isEqualTo(String.format( + "usage: java -jar dw-thing.jar test [-h] {a,b,c}%n" + + "%n" + + "test%n" + + "%n" + + "positional arguments:%n" + + " {a,b,c} Type to use%n" + + "%n" + + "optional arguments:%n" + + " -h, --help show this help message and exit%n" + )); + + assertThat(stdErr.toString()) + .isEmpty(); + } +} 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 00000000000..a3344b23302 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/cli/ConfiguredCommandTest.java @@ -0,0 +1,54 @@ +package io.dropwizard.cli; + +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.configuration.ConfigurationFactory; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import net.sourceforge.argparse4j.inf.Namespace; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class ConfiguredCommandTest { + private static class TestCommand extends ConfiguredCommand { + protected TestCommand() { + super("test", "test"); + } + + @Override + protected void run(Bootstrap bootstrap, Namespace namespace, Configuration configuration) throws Exception { + + } + } + + private static class MyApplication extends Application { + @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 bootstrap = new Bootstrap<>(application); + private final Namespace namespace = mock(Namespace.class); + + @SuppressWarnings("unchecked") + @Test + public void canUseCustomConfigurationFactory() throws Exception { + + ConfigurationFactory factory = Mockito.mock(ConfigurationFactory.class); + when(factory.build()).thenReturn(null); + + bootstrap.setConfigurationFactoryFactory( + (klass, validator, objectMapper, propertyPrefix) -> factory + ); + + command.run(bootstrap, namespace); + + Mockito.verify(factory).build(); + } +} diff --git a/dropwizard-core/src/test/java/io/dropwizard/cli/InheritedServerCommandTest.java b/dropwizard-core/src/test/java/io/dropwizard/cli/InheritedServerCommandTest.java new file mode 100644 index 00000000000..687aaa19da2 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/cli/InheritedServerCommandTest.java @@ -0,0 +1,133 @@ +package io.dropwizard.cli; + +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.configuration.ConfigurationException; +import io.dropwizard.configuration.ConfigurationFactory; +import io.dropwizard.configuration.ConfigurationSourceProvider; +import io.dropwizard.logging.LoggingFactory; +import io.dropwizard.server.ServerFactory; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.dropwizard.util.JarLocation; +import net.sourceforge.argparse4j.inf.Argument; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.eclipse.jetty.server.Server; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class InheritedServerCommandTest { + private static class ApiCommand extends ServerCommand { + + protected ApiCommand(final Application application) { + super(application, "api", "Runs the Dropwizard application as an API HTTP server"); + } + } + + private static class MyApplication extends Application { + @Override + public void initialize(final Bootstrap bootstrap) { + bootstrap.addCommand(new ApiCommand(this)); + super.initialize(bootstrap); + } + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + private final MyApplication application = new MyApplication(); + private final ApiCommand command = new ApiCommand(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("api"); + } + + @Test + public void hasADescription() throws Exception { + assertThat(command.getDescription()) + .isEqualTo("Runs the Dropwizard application as an API HTTP server"); + } + + @Test + public void buildsAndRunsAConfiguredServer() throws Exception { + command.run(environment, namespace, configuration); + + assertThat(server.isStarted()) + .isTrue(); + } + + @Test + public void usesDefaultConfigPath() throws Exception { + + class SingletonConfigurationFactory implements ConfigurationFactory { + @Override + public Object build(final ConfigurationSourceProvider provider, final String path) throws IOException, ConfigurationException { + return configuration; + } + + @Override + public Object build() throws IOException, ConfigurationException { + throw new AssertionError("Didn't use the default config path variable"); + } + } + + when(configuration.getLoggingFactory()).thenReturn(mock(LoggingFactory.class)); + + final Bootstrap bootstrap = new Bootstrap<>(application); + + bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) -> new SingletonConfigurationFactory()); + + bootstrap.addCommand(new ConfiguredCommand("test", "a test command") { + + @Override + protected void run(final Bootstrap bootstrap, final Namespace namespace, final Configuration configuration) throws Exception { + assertThat(namespace.getString("file")) + .isNotEmpty() + .isEqualTo("yaml/server.yml"); + } + + @Override + protected Argument addFileArgument(final Subparser subparser) { + return super.addFileArgument(subparser) + .setDefault("yaml/server.yml"); + } + }); + + final JarLocation location = mock(JarLocation.class); + + when(location.toString()).thenReturn("dw-thing.jar"); + when(location.getVersion()).thenReturn(Optional.of("1.0.0")); + + Cli cli = new Cli(location, bootstrap, System.out, System.err); + cli.run("test"); + } +} 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 00000000000..5350aecf091 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/cli/ServerCommandTest.java @@ -0,0 +1,106 @@ +package io.dropwizard.cli; + +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.server.ServerFactory; +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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ServerCommandTest { + private static class MyApplication extends Application { + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + } + + private final MyApplication application = new MyApplication(); + private final ServerCommand command = new ServerCommand<>(application); + private final Server server = new Server(0) { + @Override + protected void doStop() throws Exception { + super.doStop(); + if (ServerCommandTest.this.throwException) { + System.out.println("throw NullPointerException, see Issue#1557"); + throw new NullPointerException(); + } + } + }; + + 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); + private boolean throwException = false; + + @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 { + this.throwException = true; + 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(); + this.throwException = false; + } +} diff --git a/dropwizard-core/src/test/java/io/dropwizard/server/AbstractServerFactoryTest.java b/dropwizard-core/src/test/java/io/dropwizard/server/AbstractServerFactoryTest.java new file mode 100644 index 00000000000..eb962bffa17 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/server/AbstractServerFactoryTest.java @@ -0,0 +1,96 @@ +package io.dropwizard.server; + +import io.dropwizard.Configuration; +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.setup.Environment; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests that the {@link JerseyEnvironment#getUrlPattern()} is set by the following priority order: + *
      + *
    1. YAML defined value
    2. + *
    3. {@link io.dropwizard.Application#run(Configuration, Environment)} defined value
    4. + *
    5. Default value defined by {@link DropwizardResourceConfig#urlPattern}
    6. + *
    + */ +public class AbstractServerFactoryTest { + + private final JerseyContainerHolder holder = mock(JerseyContainerHolder.class); + private final DropwizardResourceConfig config = new DropwizardResourceConfig(); + private final JerseyEnvironment jerseyEnvironment = new JerseyEnvironment(holder, config); + private final Environment environment = mock(Environment.class, RETURNS_DEEP_STUBS); + private final TestServerFactory serverFactory = new TestServerFactory(); + + private static final String DEFAULT_PATTERN = "/*"; + private static final String RUN_SET_PATTERN = "/set/from/run/*"; + private static final String YAML_SET_PATTERN = "/set/from/yaml/*"; + + @Before + public void before() { + when(environment.jersey()).thenReturn(jerseyEnvironment); + when(environment.getApplicationContext()).thenReturn(new MutableServletContextHandler()); + } + + @Test + public void usesYamlDefinedPattern() { + serverFactory.setJerseyRootPath(YAML_SET_PATTERN); + jerseyEnvironment.setUrlPattern(RUN_SET_PATTERN); + + serverFactory.build(environment); + + assertThat(jerseyEnvironment.getUrlPattern()).isEqualTo(YAML_SET_PATTERN); + } + + @Test + public void usesRunDefinedPatternWhenNoYaml() { + jerseyEnvironment.setUrlPattern(RUN_SET_PATTERN); + + serverFactory.build(environment); + + assertThat(jerseyEnvironment.getUrlPattern()).isEqualTo(RUN_SET_PATTERN); + } + + @Test + public void usesDefaultPatternWhenNoneSet() { + serverFactory.build(environment); + + assertThat(jerseyEnvironment.getUrlPattern()).isEqualTo(DEFAULT_PATTERN); + } + + /** + * Test implementation of {@link AbstractServerFactory} used to run {@link #createAppServlet}, which triggers the + * setting of {@link JerseyEnvironment#setUrlPattern(String)}. + */ + public static class TestServerFactory extends AbstractServerFactory { + @Override + public Server build(Environment environment) { + // mimics the current default + simple server factory build() methods + ThreadPool threadPool = createThreadPool(environment.metrics()); + Server server = buildServer(environment.lifecycle(), threadPool); + createAppServlet(server, + environment.jersey(), + environment.getObjectMapper(), + environment.getValidator(), + environment.getApplicationContext(), + environment.getJerseyServletContainer(), + environment.metrics()); + return server; + } + + @Override + public void configure(Environment environment) { + // left blank intentionally + } + } +} 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 00000000000..81f7491fda7 --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/server/DefaultServerFactoryTest.java @@ -0,0 +1,228 @@ +package io.dropwizard.server; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.errors.EarlyEofExceptionMapper; +import io.dropwizard.jersey.errors.LoggingExceptionMapper; +import io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper; +import io.dropwizard.jersey.validation.JerseyViolationExceptionMapper; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.jetty.HttpConnectorFactory; +import io.dropwizard.jetty.ServerPushFilterFactory; +import io.dropwizard.logging.ConsoleAppenderFactory; +import io.dropwizard.logging.FileAppenderFactory; +import io.dropwizard.logging.SyslogAppenderFactory; +import io.dropwizard.setup.Environment; +import io.dropwizard.validation.BaseValidator; +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 javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.ext.ExceptionMapper; +import java.io.File; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLConnection; +import java.nio.charset.StandardCharsets; +import java.util.Set; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +public class DefaultServerFactoryTest { + private Environment environment = new Environment("test", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), + ClassLoader.getSystemClassLoader()); + 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); + + http = new YamlConfigurationFactory<>(DefaultServerFactory.class, + BaseValidator.newValidator(), + 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 loadsServerPushConfig() throws Exception { + final ServerPushFilterFactory serverPush = http.getServerPush(); + assertThat(serverPush.isEnabled()).isTrue(); + assertThat(serverPush.getRefererHosts()).contains("dropwizard.io"); + assertThat(serverPush.getRefererPorts()).contains(8445); + } + + @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 hasApplicationContextPath() throws Exception { + assertThat(http.getApplicationContextPath()).isEqualTo("/app"); + } + + @Test + public void hasAdminContextPath() throws Exception { + assertThat(http.getAdminContextPath()).isEqualTo("/admin"); + } + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(DefaultServerFactory.class); + } + + @Test + public void registersDefaultExceptionMappers() throws Exception { + assertThat(http.getRegisterDefaultExceptionMappers()).isTrue(); + + http.build(environment); + Set singletons = environment.jersey().getResourceConfig().getSingletons(); + assertThat(singletons).hasAtLeastOneElementOfType(LoggingExceptionMapper.class); + assertThat(singletons).hasAtLeastOneElementOfType(JsonProcessingExceptionMapper.class); + assertThat(singletons).hasAtLeastOneElementOfType(EarlyEofExceptionMapper.class); + assertThat(singletons).hasAtLeastOneElementOfType(JerseyViolationExceptionMapper.class); + + } + + @Test + public void doesNotDefaultExceptionMappers() throws Exception { + http.setRegisterDefaultExceptionMappers(false); + assertThat(http.getRegisterDefaultExceptionMappers()).isFalse(); + Environment environment = new Environment("test", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), + ClassLoader.getSystemClassLoader()); + http.build(environment); + for (Object singleton : environment.jersey().getResourceConfig().getSingletons()) { + assertThat(singleton).isNotInstanceOf(ExceptionMapper.class); + } + } + + @Test + public void testGracefulShutdown() throws Exception { + 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 cleanup = executor.schedule(() -> { + if (!server.isStopped()) { + server.stop(); + } + executor.shutdownNow(); + return null; + }, 5, TimeUnit.SECONDS); + + + server.start(); + + final int port = ((AbstractNetworkConnector) server.getConnectors()[0]).getLocalPort(); + + Future futureResult = executor.submit(() -> { + URL url = new URL("http://localhost:" + port + "/app/test"); + URLConnection connection = url.openConnection(); + connection.connect(); + return CharStreams.toString(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8)); + }); + + requestReceived.await(10, TimeUnit.SECONDS); + + Future serverStopped = executor.submit(() -> { + 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(); + } + + @Test + public void testConfiguredEnvironment() { + http.configure(environment); + + assertEquals(http.getAdminContextPath(), environment.getAdminContext().getContextPath()); + assertEquals(http.getApplicationContextPath(), environment.getApplicationContext().getContextPath()); + } + + @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 00000000000..a11f98ff68e --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/server/SimpleServerFactoryTest.java @@ -0,0 +1,132 @@ +package io.dropwizard.server; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +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.servlets.tasks.Task; +import io.dropwizard.setup.Environment; +import io.dropwizard.validation.BaseValidator; +import org.eclipse.jetty.server.AbstractNetworkConnector; +import org.eclipse.jetty.server.Server; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.Validator; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import java.io.File; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +public class SimpleServerFactoryTest { + + private SimpleServerFactory http; + private final ObjectMapper objectMapper = Jackson.newObjectMapper(); + private Validator validator = BaseValidator.newValidator(); + private Environment environment = new Environment("testEnvironment", objectMapper, validator, new MetricRegistry(), + ClassLoader.getSystemClassLoader()); + + @Before + public void setUp() throws Exception { + objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class, + FileAppenderFactory.class, SyslogAppenderFactory.class, HttpConnectorFactory.class); + http = (SimpleServerFactory) new YamlConfigurationFactory<>(ServerFactory.class, validator, objectMapper, "dw") + .build(new File(Resources.getResource("yaml/simple_server.yml").toURI())); + } + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(SimpleServerFactory.class); + } + + @Test + public void testGetAdminContext() { + assertThat(http.getAdminContextPath()).isEqualTo("/secret"); + } + + @Test + public void testGetApplicationContext() { + assertThat(http.getApplicationContextPath()).isEqualTo("/service"); + } + + @Test + public void testGetPort() { + final HttpConnectorFactory connector = (HttpConnectorFactory) http.getConnector(); + assertThat(connector.getPort()).isEqualTo(0); + } + + @Test + public void testBuild() throws Exception { + environment.jersey().register(new TestResource()); + environment.admin().addTask(new TestTask()); + + final Server server = http.build(environment); + server.start(); + + final int port = ((AbstractNetworkConnector) server.getConnectors()[0]).getLocalPort(); + assertThat(httpRequest("GET", "http://localhost:" + port + "/service/test")) + .isEqualTo("{\"hello\": \"World\"}"); + assertThat(httpRequest("POST", "http://localhost:" + port + "/secret/tasks/hello?name=test_user")) + .isEqualTo("Hello, test_user!"); + + server.stop(); + } + + @Test + public void testConfiguredEnvironment() { + http.configure(environment); + + assertEquals(http.getAdminContextPath(), environment.getAdminContext().getContextPath()); + assertEquals(http.getApplicationContextPath(), environment.getApplicationContext().getContextPath()); + } + + private static String httpRequest(String requestMethod, String url) throws Exception { + final HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setRequestMethod(requestMethod); + connection.connect(); + try (InputStream inputStream = connection.getInputStream()) { + return CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + } + } + + @Path("/test") + @Produces("application/json") + public static class TestResource { + + @GET + public String get() throws Exception { + return "{\"hello\": \"World\"}"; + } + } + + public static class TestTask extends Task { + + public TestTask() { + super("hello"); + } + + @Override + public void execute(ImmutableMultimap parameters, PrintWriter output) throws Exception { + final String name = parameters.get("name").iterator().next(); + output.print("Hello, " + name + "!"); + output.flush(); + } + } +} 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 00000000000..e604a407d0e --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/setup/AdminEnvironmentTest.java @@ -0,0 +1,45 @@ +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.logging.BootstrapLogging; +import io.dropwizard.servlets.tasks.Task; +import org.eclipse.jetty.server.Server; +import org.junit.Test; + +import javax.servlet.ServletRegistration; +import java.io.PrintWriter; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AdminEnvironmentTest { + static { + BootstrapLogging.bootstrap(); + } + + 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 parameters, PrintWriter output) throws Exception { + } + }; + env.addTask(task); + + handler.setServer(new Server()); + 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 00000000000..d44cf9e411e --- /dev/null +++ b/dropwizard-core/src/test/java/io/dropwizard/setup/BootstrapTest.java @@ -0,0 +1,148 @@ +package io.dropwizard.setup; + +import com.codahale.metrics.Histogram; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.UniformReservoir; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.configuration.DefaultConfigurationFactoryFactory; +import io.dropwizard.configuration.FileConfigurationSourceProvider; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.NonEmptyStringParamUnwrapper; +import io.dropwizard.jersey.validation.ParamValidatorUnwrapper; +import io.dropwizard.validation.valuehandling.GuavaOptionalValidatedValueUnwrapper; +import io.dropwizard.validation.valuehandling.OptionalDoubleValidatedValueUnwrapper; +import io.dropwizard.validation.valuehandling.OptionalIntValidatedValueUnwrapper; +import io.dropwizard.validation.valuehandling.OptionalLongValidatedValueUnwrapper; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.internal.engine.ValidatorFactoryImpl; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.Validation; +import javax.validation.ValidatorFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BootstrapTest { + private final Application application = new Application() { + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + } + }; + private Bootstrap bootstrap; + + @Before + public void setUp() { + bootstrap = new Bootstrap<>(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 hasHealthCheckRegistry() { + assertThat(bootstrap.getHealthCheckRegistry()) + .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 { + bootstrap.registerMetrics(); + assertThat(bootstrap.getMetricRegistry().getNames()) + .contains("jvm.buffers.mapped.capacity", "jvm.threads.count", "jvm.memory.heap.usage", + "jvm.attribute.vendor", "jvm.classloader.loaded", "jvm.filedescriptor"); + } + + @Test + public void defaultsToDefaultConfigurationFactoryFactory() throws Exception { + assertThat(bootstrap.getConfigurationFactoryFactory()) + .isInstanceOf(DefaultConfigurationFactoryFactory.class); + } + + @Test + public void bringsYourOwnMetricRegistry() { + final MetricRegistry newRegistry = new MetricRegistry() { + @Override + public Histogram histogram(String name) { + Histogram existed = (Histogram) getMetrics().get(name); + return existed != null ? existed : new Histogram(new UniformReservoir()); + } + }; + bootstrap.setMetricRegistry(newRegistry); + bootstrap.registerMetrics(); + + assertThat(newRegistry.getNames()) + .contains("jvm.buffers.mapped.capacity", "jvm.threads.count", "jvm.memory.heap.usage", + "jvm.attribute.vendor", "jvm.classloader.loaded", "jvm.filedescriptor"); + } + + @Test + public void defaultsToDefaultValidatorFactory() throws Exception { + assertThat(bootstrap.getValidatorFactory()).isInstanceOf(ValidatorFactoryImpl.class); + + ValidatorFactoryImpl validatorFactory = (ValidatorFactoryImpl) bootstrap.getValidatorFactory(); + + // It's imperative that the NonEmptyString validator come before the general param validator + // because a NonEmptyString is a param that wraps an optional and the Hibernate Validator + // can't unwrap nested classes it knows how to unwrap. + // https://hibernate.atlassian.net/browse/HV-904 + assertThat(validatorFactory.getValidatedValueHandlers()) + .extractingResultOf("getClass") + .containsSubsequence(GuavaOptionalValidatedValueUnwrapper.class, + OptionalDoubleValidatedValueUnwrapper.class, + OptionalIntValidatedValueUnwrapper.class, + OptionalLongValidatedValueUnwrapper.class, + NonEmptyStringParamUnwrapper.class, + ParamValidatorUnwrapper.class); + } + + @Test + public void canUseCustomValidatorFactory() throws Exception { + ValidatorFactory factory = Validation + .byProvider(HibernateValidator.class) + .configure() + .buildValidatorFactory(); + bootstrap.setValidatorFactory(factory); + + assertThat(bootstrap.getValidatorFactory()).isSameAs(factory); + } + + @Test + public void canUseCustomObjectMapper() { + final ObjectMapper minimalObjectMapper = Jackson.newMinimalObjectMapper(); + bootstrap.setObjectMapper(minimalObjectMapper); + assertThat(bootstrap.getObjectMapper()).isSameAs(minimalObjectMapper); + } + + @Test + public void canUseCustomHealthCheckRegistry() { + final HealthCheckRegistry healthCheckRegistry = new HealthCheckRegistry(); + bootstrap.setHealthCheckRegistry(healthCheckRegistry); + assertThat(bootstrap.getHealthCheckRegistry()).isSameAs(healthCheckRegistry); + } + +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-core/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + 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 00000000000..64bc1495a5d --- /dev/null +++ b/dropwizard-core/src/test/resources/yaml/server.yml @@ -0,0 +1,28 @@ +requestLog: + appenders: + - type: console + - type: file + currentLogFilename: ./logs/requests.log + archivedLogFilenamePattern: ./logs/requests-%d.log.gz + archivedFileCount: 5 +gzip: + enabled: false +serverPush: + enabled: true + refererHosts: ["dropwizard.io"] + refererPorts: [8445] +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 +applicationContextPath: /app +adminContextPath: /admin diff --git a/dropwizard-core/src/test/resources/yaml/simple_server.yml b/dropwizard-core/src/test/resources/yaml/simple_server.yml new file mode 100644 index 00000000000..90a104ae080 --- /dev/null +++ b/dropwizard-core/src/test/resources/yaml/simple_server.yml @@ -0,0 +1,6 @@ +type: simple +connector: + type: http + port: 0 +applicationContextPath: /service +adminContextPath: /secret diff --git a/dropwizard-db/pom.xml b/dropwizard-db/pom.xml new file mode 100644 index 00000000000..2aff6932625 --- /dev/null +++ b/dropwizard-db/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-db + Dropwizard Database Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + org.apache.tomcat + tomcat-jdbc + + + com.h2database + h2 + test + + + 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 00000000000..20661fe76d1 --- /dev/null +++ b/dropwizard-db/src/main/java/io/dropwizard/db/DataSourceFactory.java @@ -0,0 +1,867 @@ +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.primitives.Ints; +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.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +/** + * A factory for pooled {@link ManagedDataSource}s. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code driverClass}REQUIREDThe full name of the JDBC driver class.
    {@code url}REQUIREDThe URL of the server.
    {@code user}noneThe username used to connect to the server.
    {@code password}noneThe password used to connect to the server.
    {@code removeAbandoned}{@code false} + * Remove abandoned connections if they exceed the {@code removeAbandonedTimeout}. + * If set to {@code true} a connection is considered abandoned and eligible for removal if it has + * been in use longer than the {@code removeAbandonedTimeout} and the condition for + * {@code abandonWhenPercentageFull} is met. + *
    {@code removeAbandonedTimeout}60 seconds + * The time before a database connection can be considered abandoned. + *
    {@code abandonWhenPercentageFull}0 + * 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. + *
    {@code alternateUsernamesAllowed}{@code false} + * 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. + *
    {@code commitOnReturn}{@code false} + * Set to true if you want the connection pool to commit any pending transaction when a + * connection is returned. + *
    {@code rollbackOnReturn}{@code false} + * Set to true if you want the connection pool to rollback any pending transaction when a + * connection is returned. + *
    {@code autoCommitByDefault}JDBC driver's defaultThe default auto-commit state of the connections.
    {@code readOnlyByDefault}JDBC driver's defaultThe default read-only state of the connections.
    {@code properties}noneAny additional JDBC driver parameters.
    {@code defaultCatalog}noneThe default catalog to use for the connections.
    {@code defaultTransactionIsolation}JDBC driver default + * 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}. + *
    {@code useFairQueue}{@code true} + * If {@code true}, calls to {@code getConnection} are handled in a FIFO manner. + *
    {@code initialSize}10 + * The initial size of the connection pool. May be zero, which will allow you to start + * the connection pool without requiring the DB to be up. In the latter case the {@link #minSize} + * must also be set to zero. + *
    {@code minSize}10 + * The minimum size of the connection pool. + *
    {@code maxSize}100 + * The maximum size of the connection pool. + *
    {@code initializationQuery}none + * A custom query to be run when a connection is first created. + *
    {@code logAbandonedConnections}{@code false} + * If {@code true}, logs stack traces of abandoned connections. + *
    {@code logValidationErrors}{@code false} + * If {@code true}, logs errors when connections fail validation. + *
    {@code maxConnectionAge}none + * If set, connections which have been open for longer than {@code maxConnectionAge} are + * closed when returned. + *
    {@code maxWaitForConnection}30 seconds + * If a request for a connection is blocked for longer than this period, an exception + * will be thrown. + *
    {@code minIdleTime}1 minute + * The minimum amount of time an connection must sit idle in the pool before it is + * eligible for eviction. + *
    {@code validationQuery}{@code SELECT 1} + * 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. + *
    {@code validationQueryTimeout}none + * The timeout before a connection validation queries fail. + *
    {@code checkConnectionWhileIdle}{@code true} + * Set to true if query validation should take place while the connection is idle. + *
    {@code checkConnectionOnBorrow}{@code false} + * 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. + *
    {@code checkConnectionOnConnect}{@code false} + * 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. + *
    {@code checkConnectionOnReturn}{@code false} + * 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. + *
    {@code autoCommentsEnabled}{@code true} + * Whether or not ORMs should automatically add comments. + *
    {@code evictionInterval}5 seconds + * The amount of time to sleep between runs of the idle connection validation, abandoned + * cleaner and idle pool resizing. + *
    {@code validationInterval}30 seconds + * To avoid excess validation, only run validation once every interval. + *
    {@code validatorClassName}(none) + * Name of a class of a custom {@link org.apache.tomcat.jdbc.pool.Validator} + * implementation, which will be used for validating connections. + *
    + */ +public class DataSourceFactory implements PooledDataSourceFactory { + @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; + + 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 rollbackOnReturn = false; + + private Boolean autoCommitByDefault; + + private Boolean readOnlyByDefault; + + private String user = null; + + private String password = null; + + @NotNull + private String url = null; + + @NotNull + private Map properties = new LinkedHashMap<>(); + + private String defaultCatalog; + + @NotNull + private TransactionIsolation defaultTransactionIsolation = TransactionIsolation.DEFAULT; + + private boolean useFairQueue = true; + + @Min(0) + private int initialSize = 10; + + @Min(0) + 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"; + + @MinDuration(value = 1, unit = TimeUnit.SECONDS) + private Duration validationQueryTimeout; + + 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); + + private Optional validatorClassName = Optional.empty(); + + private boolean removeAbandoned = false; + + @NotNull + @MinDuration(1) + private Duration removeAbandonedTimeout = Duration.seconds(60L); + + @JsonProperty + @Override + public boolean isAutoCommentsEnabled() { + return autoCommentsEnabled; + } + + @JsonProperty + public void setAutoCommentsEnabled(boolean autoCommentsEnabled) { + this.autoCommentsEnabled = autoCommentsEnabled; + } + + @JsonProperty + @Override + 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 + @Override + public String getUrl() { + return url; + } + + @JsonProperty + public void setUrl(String url) { + this.url = url; + } + + @JsonProperty + @Override + public Map getProperties() { + return properties; + } + + @JsonProperty + public void setProperties(Map properties) { + this.properties = properties; + } + + @JsonProperty + public Duration getMaxWaitForConnection() { + return maxWaitForConnection; + } + + @JsonProperty + public void setMaxWaitForConnection(Duration maxWaitForConnection) { + this.maxWaitForConnection = maxWaitForConnection; + } + + @Override + @JsonProperty + public String getValidationQuery() { + return validationQuery; + } + + @Override + @Deprecated + @JsonIgnore + public String getHealthCheckValidationQuery() { + return getValidationQuery(); + } + + @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 = 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 boolean getRollbackOnReturn() { + return rollbackOnReturn; + } + + @JsonProperty + public void setCommitOnReturn(boolean commitOnReturn) { + this.commitOnReturn = commitOnReturn; + } + + @JsonProperty + public void setRollbackOnReturn(boolean rollbackOnReturn) { + this.rollbackOnReturn = rollbackOnReturn; + } + + @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 getMaxConnectionAge() { + return Optional.ofNullable(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; + } + + @Override + @JsonProperty + public Optional getValidationQueryTimeout() { + return Optional.ofNullable(validationQueryTimeout); + } + + @JsonProperty + public Optional getValidatorClassName() { + return validatorClassName; + } + + @JsonProperty + public void setValidatorClassName(Optional validatorClassName) { + this.validatorClassName = validatorClassName; + } + + @Override + @Deprecated + @JsonIgnore + public Optional getHealthCheckValidationTimeout() { + return getValidationQueryTimeout(); + } + + @JsonProperty + public void setValidationQueryTimeout(Duration validationQueryTimeout) { + this.validationQueryTimeout = validationQueryTimeout; + } + + @JsonProperty + public boolean isRemoveAbandoned() { + return removeAbandoned; + } + + @JsonProperty + public void setRemoveAbandoned(boolean removeAbandoned) { + this.removeAbandoned = removeAbandoned; + } + + @JsonProperty + public Duration getRemoveAbandonedTimeout() { + return removeAbandonedTimeout; + } + + @JsonProperty + public void setRemoveAbandonedTimeout(Duration removeAbandonedTimeout) { + this.removeAbandonedTimeout = Objects.requireNonNull(removeAbandonedTimeout); + } + + @Override + public void asSingleConnectionPool() { + minSize = 1; + maxSize = 1; + initialSize = 1; + } + + @Override + public ManagedDataSource build(MetricRegistry metricRegistry, String name) { + final Properties properties = new Properties(); + for (Map.Entry 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.setRollbackOnReturn(rollbackOnReturn); + 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 (getMaxConnectionAge().isPresent()) { + 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(user != null && password == null ? "" : password); + poolConfig.setRemoveAbandoned(removeAbandoned); + poolConfig.setRemoveAbandonedTimeout(Ints.saturatedCast(removeAbandonedTimeout.toSeconds())); + + poolConfig.setTestWhileIdle(checkConnectionWhileIdle); + poolConfig.setValidationQuery(validationQuery); + poolConfig.setTestOnBorrow(checkConnectionOnBorrow); + poolConfig.setTestOnConnect(checkConnectionOnConnect); + poolConfig.setTestOnReturn(checkConnectionOnReturn); + poolConfig.setTimeBetweenEvictionRunsMillis((int) evictionInterval.toMilliseconds()); + poolConfig.setValidationInterval(validationInterval.toMilliseconds()); + + if (getValidationQueryTimeout().isPresent()) { + poolConfig.setValidationQueryTimeout((int) validationQueryTimeout.toSeconds()); + } + if (validatorClassName.isPresent()) { + poolConfig.setValidatorClassName(validatorClassName.get()); + } + + 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 00000000000..161beb9e75b --- /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 { + PooledDataSourceFactory 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 00000000000..348dcc589cb --- /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 00000000000..392f4a6ca2c --- /dev/null +++ b/dropwizard-db/src/main/java/io/dropwizard/db/ManagedPooledDataSource.java @@ -0,0 +1,56 @@ +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"), + (Gauge) connectionPool::getActive); + + metricRegistry.register(name(getClass(), connectionPool.getName(), "idle"), + (Gauge) connectionPool::getIdle); + + metricRegistry.register(name(getClass(), connectionPool.getName(), "waiting"), + (Gauge) connectionPool::getWaitCount); + + metricRegistry.register(name(getClass(), connectionPool.getName(), "size"), + (Gauge) connectionPool::getSize); + } + + @Override + public void stop() throws Exception { + close(); + } +} diff --git a/dropwizard-db/src/main/java/io/dropwizard/db/PooledDataSourceFactory.java b/dropwizard-db/src/main/java/io/dropwizard/db/PooledDataSourceFactory.java new file mode 100644 index 00000000000..e6c8cfaf68a --- /dev/null +++ b/dropwizard-db/src/main/java/io/dropwizard/db/PooledDataSourceFactory.java @@ -0,0 +1,95 @@ +package io.dropwizard.db; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.util.Duration; + +import java.util.Map; +import java.util.Optional; + +/** + * Interface of a factory that produces JDBC data sources + * backed by the connection pool. + */ +public interface PooledDataSourceFactory { + + /** + * Whether ORM tools allowed to add comments to SQL queries. + * + * @return {@code true}, if allowed + */ + boolean isAutoCommentsEnabled(); + + /** + * Returns the configuration properties for ORM tools. + * + * @return configuration properties as a map + */ + Map getProperties(); + + /** + * Returns the timeout for awaiting a response from the database + * during connection health checks. + * + * @return the timeout as {@code Duration} + */ + Optional getValidationQueryTimeout(); + + /** + * Returns the timeout for awaiting a response from the database + * during connection health checks. + * + * @return the timeout as {@code Duration} + * @deprecated Use {@link #getValidationQueryTimeout()} + */ + @Deprecated + Optional getHealthCheckValidationTimeout(); + + /** + * Returns the SQL query, which is being used for the database + * connection health check. + * + * @return the SQL query as a string + */ + String getValidationQuery(); + + /** + * Returns the SQL query, which is being used for the database + * connection health check. + * + * @return the SQL query as a string + * @deprecated Use {@link #getValidationQuery()} + */ + @Deprecated + String getHealthCheckValidationQuery(); + + /** + * Returns the Java class of the database driver. + * + * @return the JDBC driver class as a string + */ + String getDriverClass(); + + /** + * Returns the JDBC connection URL. + * + * @return the JDBC connection URL as a string + */ + String getUrl(); + + /** + * Configures the pool as a single connection pool. + * It's useful for tools that use only one database connection, + * such as database migrations. + */ + void asSingleConnectionPool(); + + /** + * Builds a new JDBC data source backed by the connection pool + * and managed by Dropwizard. + * + * @param metricRegistry the application metric registry + * @param name name of the connection pool + * @return a new JDBC data source as {@code ManagedDataSource} + */ + ManagedDataSource build(MetricRegistry metricRegistry, String name); +} diff --git a/dropwizard-db/src/main/java/io/dropwizard/db/TimeBoundHealthCheck.java b/dropwizard-db/src/main/java/io/dropwizard/db/TimeBoundHealthCheck.java new file mode 100644 index 00000000000..af567a3a340 --- /dev/null +++ b/dropwizard-db/src/main/java/io/dropwizard/db/TimeBoundHealthCheck.java @@ -0,0 +1,27 @@ +package io.dropwizard.db; + +import com.codahale.metrics.health.HealthCheck; +import io.dropwizard.util.Duration; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; + +public class TimeBoundHealthCheck { + private final ExecutorService executorService; + private final Duration duration; + + public TimeBoundHealthCheck(ExecutorService executorService, Duration duration) { + this.executorService = executorService; + this.duration = duration; + } + + public HealthCheck.Result check(Callable c) { + HealthCheck.Result result; + try { + result = executorService.submit(c).get(duration.getQuantity(), duration.getUnit()); + } catch (Exception e) { + result = HealthCheck.Result.unhealthy("Unable to successfully check in %s", duration); + } + return result; + } +} diff --git a/dropwizard-db/src/test/java/io/dropwizard/db/CustomConnectionValidator.java b/dropwizard-db/src/test/java/io/dropwizard/db/CustomConnectionValidator.java new file mode 100644 index 00000000000..a06d71116aa --- /dev/null +++ b/dropwizard-db/src/test/java/io/dropwizard/db/CustomConnectionValidator.java @@ -0,0 +1,17 @@ +package io.dropwizard.db; + +import org.apache.tomcat.jdbc.pool.Validator; + +import java.sql.Connection; + +public class CustomConnectionValidator implements Validator { + + // It's used only once, so static access should be fine + static volatile boolean loaded; + + @Override + public boolean validate(Connection connection, int validateAction) { + loaded = true; + return true; + } +} diff --git a/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceConfigurationTest.java b/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceConfigurationTest.java new file mode 100644 index 00000000000..5752ad20984 --- /dev/null +++ b/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceConfigurationTest.java @@ -0,0 +1,120 @@ +package io.dropwizard.db; + +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.util.Duration; +import org.junit.Test; + +import java.io.File; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DataSourceConfigurationTest { + + @Test + public void testFullConfiguration() throws Exception { + DataSourceFactory ds = getDataSourceFactory("yaml/full_db_pool.yml"); + + assertThat(ds.getDriverClass()).isEqualTo("org.postgresql.Driver"); + assertThat(ds.getUser()).isEqualTo("pg-user"); + assertThat(ds.getUrl()).isEqualTo("jdbc:postgresql://db.example.com/db-prod"); + assertThat(ds.getPassword()).isEqualTo("iAMs00perSecrEET"); + assertThat(ds.getProperties()).containsEntry("charSet", "UTF-8"); + assertThat(ds.getMaxWaitForConnection()).isEqualTo(Duration.seconds(1)); + assertThat(ds.getValidationQuery()).isEqualTo("/* MyService Health Check */ SELECT 1"); + assertThat(ds.getMinSize()).isEqualTo(8); + assertThat(ds.getInitialSize()).isEqualTo(15); + assertThat(ds.getMaxSize()).isEqualTo(32); + assertThat(ds.getCheckConnectionWhileIdle()).isFalse(); + assertThat(ds.getEvictionInterval()).isEqualTo(Duration.seconds(10)); + assertThat(ds.getMinIdleTime()).isEqualTo(Duration.minutes(1)); + assertThat(ds.getValidationInterval()).isEqualTo(Duration.minutes(1)); + assertThat(ds.isAutoCommentsEnabled()).isFalse(); + assertThat(ds.getReadOnlyByDefault()).isFalse(); + assertThat(ds.isRemoveAbandoned()).isTrue(); + assertThat(ds.getRemoveAbandonedTimeout()).isEqualTo(Duration.seconds(15L)); + assertThat(ds.getAbandonWhenPercentageFull()).isEqualTo(75); + assertThat(ds.isAlternateUsernamesAllowed()).isTrue(); + assertThat(ds.getCommitOnReturn()).isTrue(); + assertThat(ds.getRollbackOnReturn()).isTrue(); + assertThat(ds.getAutoCommitByDefault()).isFalse(); + assertThat(ds.getDefaultCatalog()).isEqualTo("test_catalog"); + assertThat(ds.getDefaultTransactionIsolation()) + .isEqualTo(DataSourceFactory.TransactionIsolation.READ_COMMITTED); + assertThat(ds.getUseFairQueue()).isFalse(); + assertThat(ds.getInitializationQuery()).isEqualTo("insert into connections_log(ts) values (now())"); + assertThat(ds.getLogAbandonedConnections()).isEqualTo(true); + assertThat(ds.getLogValidationErrors()).isEqualTo(true); + assertThat(ds.getMaxConnectionAge()).isEqualTo(Optional.of(Duration.hours(1))); + assertThat(ds.getCheckConnectionOnBorrow()).isEqualTo(true); + assertThat(ds.getCheckConnectionOnConnect()).isEqualTo(false); + assertThat(ds.getCheckConnectionOnReturn()).isEqualTo(true); + assertThat(ds.getValidationQueryTimeout()).isEqualTo(Optional.of(Duration.seconds(3))); + assertThat(ds.getValidatorClassName()).isEqualTo(Optional.of("io.dropwizard.db.CustomConnectionValidator")); + } + + @Test + public void testMinimalConfiguration() throws Exception { + DataSourceFactory ds = getDataSourceFactory("yaml/minimal_db_pool.yml"); + + assertThat(ds.getDriverClass()).isEqualTo("org.postgresql.Driver"); + assertThat(ds.getUser()).isEqualTo("pg-user"); + assertThat(ds.getUrl()).isEqualTo("jdbc:postgresql://db.example.com/db-prod"); + assertThat(ds.getPassword()).isEqualTo("iAMs00perSecrEET"); + assertThat(ds.getProperties()).isEmpty(); + assertThat(ds.getMaxWaitForConnection()).isEqualTo(Duration.seconds(30)); + assertThat(ds.getValidationQuery()).isEqualTo("/* Health Check */ SELECT 1"); + assertThat(ds.getMinSize()).isEqualTo(10); + assertThat(ds.getInitialSize()).isEqualTo(10); + assertThat(ds.getMaxSize()).isEqualTo(100); + assertThat(ds.getCheckConnectionWhileIdle()).isTrue(); + assertThat(ds.getEvictionInterval()).isEqualTo(Duration.seconds(5)); + assertThat(ds.getMinIdleTime()).isEqualTo(Duration.minutes(1)); + assertThat(ds.getValidationInterval()).isEqualTo(Duration.seconds(30)); + assertThat(ds.isAutoCommentsEnabled()).isTrue(); + assertThat(ds.getReadOnlyByDefault()).isNull(); + assertThat(ds.isRemoveAbandoned()).isFalse(); + assertThat(ds.getRemoveAbandonedTimeout()).isEqualTo(Duration.seconds(60L)); + assertThat(ds.getAbandonWhenPercentageFull()).isEqualTo(0); + assertThat(ds.isAlternateUsernamesAllowed()).isFalse(); + assertThat(ds.getCommitOnReturn()).isFalse(); + assertThat(ds.getRollbackOnReturn()).isFalse(); + assertThat(ds.getAutoCommitByDefault()).isNull(); + assertThat(ds.getDefaultCatalog()).isNull(); + assertThat(ds.getDefaultTransactionIsolation()) + .isEqualTo(DataSourceFactory.TransactionIsolation.DEFAULT); + assertThat(ds.getUseFairQueue()).isTrue(); + assertThat(ds.getInitializationQuery()).isNull(); + assertThat(ds.getLogAbandonedConnections()).isEqualTo(false); + assertThat(ds.getLogValidationErrors()).isEqualTo(false); + assertThat(ds.getMaxConnectionAge()).isEqualTo(Optional.empty()); + assertThat(ds.getCheckConnectionOnBorrow()).isEqualTo(false); + assertThat(ds.getCheckConnectionOnConnect()).isEqualTo(true); + assertThat(ds.getCheckConnectionOnReturn()).isEqualTo(false); + assertThat(ds.getValidationQueryTimeout()).isEqualTo(Optional.empty()); + } + + @Test + public void testInlineUserPasswordConfiguration() throws Exception { + DataSourceFactory ds = getDataSourceFactory("yaml/inline_user_pass_db_pool.yml"); + + assertThat(ds.getDriverClass()).isEqualTo("org.postgresql.Driver"); + assertThat(ds.getUrl()).isEqualTo("jdbc:postgresql://db.example.com/db-prod?user=scott&password=tiger"); + assertThat(ds.getUser()).isNull(); + assertThat(ds.getPassword()).isNull(); + } + @Test + public void testInitialSizeZeroIsAllowed() throws Exception { + DataSourceFactory ds = getDataSourceFactory("yaml/empty_initial_pool.yml"); + assertThat(ds.getInitialSize()).isEqualTo(0); + } + + private DataSourceFactory getDataSourceFactory(String resourceName) throws Exception { + return new YamlConfigurationFactory<>(DataSourceFactory.class, + Validators.newValidator(), Jackson.newObjectMapper(), "dw") + .build(new File(Resources.getResource(resourceName).toURI())); + } +} 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 00000000000..b5db5c5fca7 --- /dev/null +++ b/dropwizard-db/src/test/java/io/dropwizard/db/DataSourceFactoryTest.java @@ -0,0 +1,124 @@ +package io.dropwizard.db; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.configuration.ResourceConfigurationSourceProvider; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.util.Duration; +import io.dropwizard.validation.BaseValidator; +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 java.sql.SQLException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DataSourceFactoryTest { + private final MetricRegistry metricRegistry = new MetricRegistry(); + + private DataSourceFactory factory; + private ManagedDataSource dataSource; + + @Before + public void setUp() { + factory = new DataSourceFactory(); + factory.setUrl("jdbc:h2:mem:DbTest-" + System.currentTimeMillis() + ";user=sa"); + factory.setDriverClass("org.h2.Driver"); + factory.setValidationQuery("SELECT 1"); + } + + @After + public void tearDown() throws Exception { + if (null != dataSource) { + dataSource.stop(); + } + } + + private ManagedDataSource dataSource() throws Exception { + dataSource = factory.build(metricRegistry, "test"); + dataSource.start(); + return dataSource; + } + + @Test + public void testInitialSizeIsZero() throws Exception { + factory.setUrl("nonsense invalid url"); + factory.setInitialSize(0); + ManagedDataSource dataSource = factory.build(metricRegistry, "test"); + dataSource.start(); + } + + @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); + } + } + } + } + } + + @Test + public void testNoValidationQueryTimeout() throws Exception { + try (Connection connection = dataSource().getConnection()) { + try (PreparedStatement statement = connection.prepareStatement("select 1")) { + assertThat(statement.getQueryTimeout()).isEqualTo(0); + } + } + } + + @Test + public void testValidationQueryTimeoutIsSet() throws Exception { + factory.setValidationQueryTimeout(Duration.seconds(3)); + + try (Connection connection = dataSource().getConnection()) { + try (PreparedStatement statement = connection.prepareStatement("select 1")) { + assertThat(statement.getQueryTimeout()).isEqualTo(3); + } + } + } + + @Test(expected = SQLException.class) + public void invalidJDBCDriverClassThrowsSQLException() throws SQLException { + final DataSourceFactory factory = new DataSourceFactory(); + factory.setDriverClass("org.example.no.driver.here"); + + factory.build(metricRegistry, "test").getConnection(); + } + + @Test + public void testCustomValidator() throws Exception { + factory.setValidatorClassName(Optional.of(CustomConnectionValidator.class.getName())); + try (Connection connection = dataSource().getConnection()) { + try (PreparedStatement statement = connection.prepareStatement("select 1")) { + try (ResultSet rs = statement.executeQuery()) { + assertThat(rs.next()); + assertThat(rs.getInt(1)).isEqualTo(1); + } + } + } + assertThat(CustomConnectionValidator.loaded).isTrue(); + } + + @Test + public void createDefaultFactory() throws Exception { + final DataSourceFactory factory = new YamlConfigurationFactory<>(DataSourceFactory.class, + BaseValidator.newValidator(), Jackson.newObjectMapper(), "dw") + .build(new ResourceConfigurationSourceProvider(), "yaml/minimal_db_pool.yml"); + + assertThat(factory.getDriverClass()).isEqualTo("org.postgresql.Driver"); + assertThat(factory.getUser()).isEqualTo("pg-user"); + assertThat(factory.getPassword()).isEqualTo("iAMs00perSecrEET"); + assertThat(factory.getUrl()).isEqualTo("jdbc:postgresql://db.example.com/db-prod"); + assertThat(factory.getValidationQuery()).isEqualTo("/* Health Check */ SELECT 1"); + assertThat(factory.getValidationQueryTimeout()).isEqualTo(Optional.empty()); + } +} 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 00000000000..b1a5fdffc24 --- /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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.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/java/io/dropwizard/db/TimeBoundHealthCheckTest.java b/dropwizard-db/src/test/java/io/dropwizard/db/TimeBoundHealthCheckTest.java new file mode 100644 index 00000000000..3cf611fa7ab --- /dev/null +++ b/dropwizard-db/src/test/java/io/dropwizard/db/TimeBoundHealthCheckTest.java @@ -0,0 +1,37 @@ +package io.dropwizard.db; + +import com.codahale.metrics.health.HealthCheck; +import io.dropwizard.util.Duration; +import org.junit.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TimeBoundHealthCheckTest { + + @Test + @SuppressWarnings("unchecked") + public void testCheck() throws InterruptedException, ExecutionException, TimeoutException { + final ExecutorService executorService = mock(ExecutorService.class); + final Duration duration = mock(Duration.class); + when(duration.getQuantity()).thenReturn(5L); + when(duration.getUnit()).thenReturn(TimeUnit.SECONDS); + + final Callable callable = mock(Callable.class); + final Future future = mock(Future.class); + when(executorService.submit(callable)).thenReturn(future); + + new TimeBoundHealthCheck(executorService, duration).check(callable); + verify(executorService, times(1)).submit(callable); + verify(future, times(1)).get(duration.getQuantity(), duration.getUnit()); + } +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-db/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-db/src/test/resources/yaml/empty_initial_pool.yml b/dropwizard-db/src/test/resources/yaml/empty_initial_pool.yml new file mode 100644 index 00000000000..ab69c548955 --- /dev/null +++ b/dropwizard-db/src/test/resources/yaml/empty_initial_pool.yml @@ -0,0 +1,6 @@ +driverClass: org.postgresql.Driver +user: pg-user +password: iAMs00perSecrEET +url: jdbc:postgresql://db.example.com/db-prod +initialSize: 0 +minSize: 0 diff --git a/dropwizard-db/src/test/resources/yaml/full_db_pool.yml b/dropwizard-db/src/test/resources/yaml/full_db_pool.yml new file mode 100644 index 00000000000..396fa21c242 --- /dev/null +++ b/dropwizard-db/src/test/resources/yaml/full_db_pool.yml @@ -0,0 +1,46 @@ +driverClass: org.postgresql.Driver +user: pg-user +password: iAMs00perSecrEET +url: jdbc:postgresql://db.example.com/db-prod +properties: + charSet: UTF-8 +maxWaitForConnection: 1s +maxConnectionAge: 1h +minIdleTime: 1 minute + +validationQuery: "/* MyService Health Check */ SELECT 1" +initializationQuery: "insert into connections_log(ts) values (now())" +validationQueryTimeout: 3s + +minSize: 8 +maxSize: 32 +initialSize: 15 + +evictionInterval: 10s +validationInterval: 1m + +readOnlyByDefault: false +# deprecated in favour readOnlyByDefault +defaultReadOnly: false + +autoCommentsEnabled: false +abandonWhenPercentageFull: 75 +alternateUsernamesAllowed: true +commitOnReturn: true +rollbackOnReturn: true +autoCommitByDefault: false +defaultCatalog: test_catalog +defaultTransactionIsolation: read-committed +removeAbandoned: true +removeAbandonedTimeout: 15s +useFairQueue: false + +logAbandonedConnections: true +logValidationErrors: true + +checkConnectionWhileIdle: false +checkConnectionOnBorrow: true +checkConnectionOnConnect: false +checkConnectionOnReturn: true + +validatorClassName: io.dropwizard.db.CustomConnectionValidator diff --git a/dropwizard-db/src/test/resources/yaml/inline_user_pass_db_pool.yml b/dropwizard-db/src/test/resources/yaml/inline_user_pass_db_pool.yml new file mode 100644 index 00000000000..8130f31aa2f --- /dev/null +++ b/dropwizard-db/src/test/resources/yaml/inline_user_pass_db_pool.yml @@ -0,0 +1,2 @@ +driverClass: org.postgresql.Driver +url: jdbc:postgresql://db.example.com/db-prod?user=scott&password=tiger diff --git a/dropwizard-db/src/test/resources/yaml/minimal_db_pool.yml b/dropwizard-db/src/test/resources/yaml/minimal_db_pool.yml new file mode 100644 index 00000000000..a0907309ce4 --- /dev/null +++ b/dropwizard-db/src/test/resources/yaml/minimal_db_pool.yml @@ -0,0 +1,4 @@ +driverClass: org.postgresql.Driver +user: pg-user +password: iAMs00perSecrEET +url: jdbc:postgresql://db.example.com/db-prod diff --git a/dropwizard-example/README.md b/dropwizard-example/README.md new file mode 100644 index 00000000000..f321211ea1d --- /dev/null +++ b/dropwizard-example/README.md @@ -0,0 +1,52 @@ +# Introduction + +The Dropwizard example application was developed to, as its name implies, provide examples of some of the features +present in Dropwizard. + +# 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 [Hibernate](http://hibernate.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 Data Access Object pattern with assisting of Hibernate. + +* The `Person` illustrates mapping of Java classes to database tables with assisting of JPA annotations. + +* All the JPQL statements for use in the `PersonDAO` are located in the `Person` class. + +* `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-1.0.0-rc3-SNAPSHOT.jar db migrate example.yml + +* To run the server run. + + java -jar target/dropwizard-example-1.0.0-rc3-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 00000000000..68186ea4fba Binary files /dev/null and b/dropwizard-example/example.keystore differ diff --git a/dropwizard-example/example.yml b/dropwizard-example/example.yml index 868cb1de5cc..e240e52b578 100644 --- a/dropwizard-example/example.yml +++ b/dropwizard-example/example.yml @@ -1,102 +1,62 @@ template: Hello, %s! -defaultName: Stranger - -# HTTP-specific options. -http: - - # The port on which the HTTP server listens for service requests. - port: 8080 - - # The port on which the HTTP server listens for administrative requests. - adminPort: 8081 - - # Maximum number of threads. - maxThreads: 100 - - # Minimum number of thread to keep alive. - minThreads: 10 - - # The type of connector to use. Other valid values are "nonblocking" or "legacy". In general, the - # blocking connector should be used for low-latency services with short request durations. The - # nonblocking connector should be used for services with long request durations or which - # specifically take advantage of Jetty's continuation support. - connectorType: blocking - - # The maximum amount of time a connection is allowed to be idle before being closed. - maxIdleTime: 1s - - # The number of threads dedicated to accepting connections. If omitted, this defaults to the - # number of logical CPUs on the current machine. - acceptorThreadCount: 3 - - # The offset of the acceptor threads' priorities. Can be [-5...5], with -5 dropping the acceptor - # threads to the lowest possible priority and with 5 raising them to the highest priority. - acceptorThreadPriorityOffset: 0 - - # The number of unaccepted requests to keep in the accept queue before refusing connections. If - # set to -1 or omitted, the system default is used. - acceptQueueSize: 100 - - # The maximum number of buffers to keep in memory. - maxBufferCount: 1024 - - # The initial buffer size for reading requests. - requestBufferSize: 32KB - - # The initial buffer size for reading request headers. - requestHeaderBufferSize: 6KB - - # The initial buffer size for writing responses. - responseBufferSize: 32KB - - # The initial buffer size for writing response headers. - responseHeaderBufferSize: 6KB - - # Enables SO_REUSEADDR on the server socket. - reuseAddress: true - - # Enables SO_LINGER on the server socket with the specified linger time. - soLingerTime: 1s - - # The number of open connections at which the server transitions to a "low-resources" mode. - lowResourcesConnectionThreshold: 25000 - - # When in low-resources mode, the maximum amount of time a connection is allowed to be idle before - # being closed. Overrides maxIdleTime. - lowResourcesMaxIdleTime: 5s - - # If non-zero, the server will allow worker threads to finish processing requests after the server - # socket has been closed for the given amount of time. - shutdownGracePeriod: 2s - - # If true, the HTTP server will prefer X-Forwarded headers over their non-forwarded equivalents. - useForwardedHeaders: true - - # If true, forces the HTTP connector to use off-heap, direct buffers. - useDirectBuffers: true - - # The hostname of the interface to which the HTTP server socket wil be found. If omitted, the - # socket will listen on all interfaces. - # bindHost: app1.example.com - - # HTTP request log settings - requestLog: - - # Whether or not to log HTTP requests. - enabled: false - - # The filename to which HTTP requests will be logged. The string 'yyyy_mm_dd' will be replaced - # with the current date, and the logs will be rolled accordingly. - filenamePattern: ./logs/yyyy_mm_dd.log - - # The maximum number of old log files to retain. - retainedFileCount: 5 +defaultName: ${DW_DEFAULT_NAME:-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 + validatePeers: false + #this requires the alpn-boot library on the JVM's boot classpath + #- type: h2 + # port: 8445 + # keyStorePath: example.keystore + # keyStorePassword: example + # validateCerts: false + # validatePeers: false + adminConnectors: + - type: http + port: 8081 + - type: https + port: 8444 + keyStorePath: example.keystore + keyStorePassword: example + validateCerts: false + validatePeers: false # Logging settings. logging: - # The default level of all loggers. Can be OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. + # The default level of all loggers. Can be OFF, ERROR, WARN, INFO, DEBUG, TRACE, or ALL. level: INFO # Logger-specific levels. @@ -105,45 +65,35 @@ logging: # Sets the level for 'com.example.app' to DEBUG. com.example.app: DEBUG - # Settings for logging to stdout. - console: - - # If true, write log statements to stdout. - enabled: true - - # Do not display log statements below this threshold to stdout. - threshold: ALL - - # Settings for logging to a file. - file: - - # If true, write log statements to a file. - enabled: false - - # Do not write log statements below this threshold to the file. - threshold: ALL - - # The file to which statements will be logged. When the log file reaches the maximum size, the - # file will be renamed example.log.1, example.log will be truncated, and new statements written - # to it. - filenamePattern: ./logs/example.log - - # The maximum size of any log file. - maxFileSize: 50MB - - # The maximum number of log files to retain. - retainedFileCount: 5 - - # Settings for logging to syslog. - syslog: - - # If true, write log statements to syslog. - enabled: false - - # 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 + # Redirects SQL logs to a separate file + org.hibernate.SQL: + level: DEBUG + +# Logback's Time Based Rolling Policy - archivedLogFilenamePattern: /tmp/application-%d{yyyy-MM-dd}.log.gz +# Logback's Size and Time Based Rolling Policy - archivedLogFilenamePattern: /tmp/application-%d{yyyy-MM-dd}-%i.log.gz +# Logback's Fixed Window Rolling Policy - archivedLogFilenamePattern: /tmp/application-%i.log.gz + + appenders: + - type: console + - type: file + threshold: INFO + logFormat: "%-6level [%d{HH:mm:ss.SSS}] [%t] %logger{5} - %X{code} %msg %n" + currentLogFilename: /tmp/application.log + archivedLogFilenamePattern: /tmp/application-%d{yyyy-MM-dd}-%i.log.gz + archivedFileCount: 7 + timeZone: UTC + maxFileSize: 10MB + +# the key needs to match the suffix of the renderer +viewRendererConfiguration: + .ftl: + strict_syntax: yes + whitespace_stripping: yes + +metrics: + reporters: + - type: graphite + host: localhost + port: 2003 + prefix: example + frequency: 10s diff --git a/dropwizard-example/pom.xml b/dropwizard-example/pom.xml index 808083f0a9f..c08473f5d69 100644 --- a/dropwizard-example/pom.xml +++ b/dropwizard-example/pom.xml @@ -1,55 +1,209 @@ - + 4.0.0 + + 3.0.0 + - com.yammer dropwizard-example - 0.1.0-SNAPSHOT + 1.0.1-SNAPSHOT + io.dropwizard + Dropwizard Example Application + + + UTF-8 + UTF-8 + 1.8 + 1.8 + + + true + + true + true + + + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + http://oss.sonatype.org/content/repositories/snapshots + + + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + - com.yammer - dropwizard - ${project.version} + io.dropwizard + dropwizard-core + + + io.dropwizard + dropwizard-auth + + + io.dropwizard + dropwizard-assets + + + io.dropwizard + dropwizard-http2 + + + io.dropwizard + dropwizard-hibernate + + + io.dropwizard + dropwizard-migrations + + + io.dropwizard + dropwizard-views-freemarker + + + io.dropwizard + dropwizard-views-mustache + + + io.dropwizard + dropwizard-metrics-graphite - junit - junit - 4.10 + com.h2database + h2 + + + io.dropwizard + dropwizard-testing test - org.mockito - mockito-all - 1.9.0-rc1 + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory test + + + javax.servlet + javax.servlet-api + + + junit + junit + + - org.hamcrest - hamcrest-all - 1.1 + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 test + + + javax.servlet + javax.servlet-api + + + junit + junit + + + + + + org.apache.maven.plugins + maven-clean-plugin + 2.6.1 + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + + + org.apache.maven.plugins + maven-surefire-plugin + 2.18.1 + + + org.apache.maven.plugins + maven-resources-plugin + 2.7 + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + + org.apache.maven.plugins + maven-source-plugin + 2.4 + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.1 + + + org.apache.maven.plugins + maven-deploy-plugin + 2.8.2 + + + org.apache.maven.plugins + maven-site-plugin + 3.4 + + + org.apache.maven.plugins - maven-compiler-plugin - 2.3.2 - - 1.6 - 1.6 - UTF-8 - + maven-enforcer-plugin + + + enforce + + + + + + + enforce + + + org.apache.maven.plugins maven-source-plugin - 2.1.2 attach-sources @@ -61,19 +215,36 @@ org.apache.maven.plugins - maven-resources-plugin - 2.5 + maven-jar-plugin - - UTF-8 + + + true + + org.apache.maven.plugins maven-shade-plugin - 1.4 true + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + com.example.helloworld.HelloWorldApplication + + @@ -81,27 +252,28 @@ shade - - - - - com.example.helloworld.HelloWorldService - - - - - - org.apache.maven.plugins - maven-deploy-plugin - 2.7 - - true - - + + + + dev + + + + org.apache.maven.plugins + maven-shade-plugin + + + none + + + + + + + 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 00000000000..b37835c65e3 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldApplication.java @@ -0,0 +1,103 @@ +package com.example.helloworld; + +import com.example.helloworld.auth.ExampleAuthenticator; +import com.example.helloworld.auth.ExampleAuthorizer; +import com.example.helloworld.cli.RenderCommand; +import com.example.helloworld.core.Person; +import com.example.helloworld.core.Template; +import com.example.helloworld.core.User; +import com.example.helloworld.db.PersonDAO; +import com.example.helloworld.filter.DateRequiredFeature; +import com.example.helloworld.health.TemplateHealthCheck; +import com.example.helloworld.resources.FilteredResource; +import com.example.helloworld.resources.HelloWorldResource; +import com.example.helloworld.resources.PeopleResource; +import com.example.helloworld.resources.PersonResource; +import com.example.helloworld.resources.ProtectedResource; +import com.example.helloworld.resources.ViewResource; +import com.example.helloworld.tasks.EchoTask; +import io.dropwizard.Application; +import io.dropwizard.assets.AssetsBundle; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.configuration.EnvironmentVariableSubstitutor; +import io.dropwizard.configuration.SubstitutingSourceProvider; +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; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; + +import java.util.Map; + +public class HelloWorldApplication extends Application { + public static void main(String[] args) throws Exception { + new HelloWorldApplication().run(args); + } + + private final HibernateBundle hibernateBundle = + new HibernateBundle(Person.class) { + @Override + public DataSourceFactory getDataSourceFactory(HelloWorldConfiguration configuration) { + return configuration.getDataSourceFactory(); + } + }; + + @Override + public String getName() { + return "hello-world"; + } + + @Override + public void initialize(Bootstrap bootstrap) { + // Enable variable substitution with environment variables + bootstrap.setConfigurationSourceProvider( + new SubstitutingSourceProvider( + bootstrap.getConfigurationSourceProvider(), + new EnvironmentVariableSubstitutor(false) + ) + ); + + bootstrap.addCommand(new RenderCommand()); + bootstrap.addBundle(new AssetsBundle()); + bootstrap.addBundle(new MigrationsBundle() { + @Override + public DataSourceFactory getDataSourceFactory(HelloWorldConfiguration configuration) { + return configuration.getDataSourceFactory(); + } + }); + bootstrap.addBundle(hibernateBundle); + bootstrap.addBundle(new ViewBundle() { + @Override + public Map> getViewConfiguration(HelloWorldConfiguration configuration) { + return configuration.getViewRendererConfiguration(); + } + }); + } + + @Override + public void run(HelloWorldConfiguration configuration, Environment environment) { + final PersonDAO dao = new PersonDAO(hibernateBundle.getSessionFactory()); + final Template template = configuration.buildTemplate(); + + environment.healthChecks().register("template", new TemplateHealthCheck(template)); + environment.admin().addTask(new EchoTask()); + environment.jersey().register(DateRequiredFeature.class); + environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setRealm("SUPER SECRET STUFF") + .buildAuthFilter())); + environment.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class)); + environment.jersey().register(RolesAllowedDynamicFeature.class); + 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)); + environment.jersey().register(new FilteredResource()); + } +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java index 4e0a57e1cf1..e9ff79d9d82 100644 --- a/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java +++ b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldConfiguration.java @@ -1,29 +1,47 @@ package com.example.helloworld; import com.example.helloworld.core.Template; -import com.yammer.dropwizard.config.Configuration; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.Configuration; +import io.dropwizard.db.DataSourceFactory; +import org.hibernate.validator.constraints.NotEmpty; +import javax.validation.Valid; import javax.validation.constraints.NotNull; +import java.util.Collections; +import java.util.Map; public class HelloWorldConfiguration extends Configuration { - @NotNull + @NotEmpty private String template; - - @NotNull + + @NotEmpty private String defaultName = "Stranger"; + @Valid + @NotNull + private DataSourceFactory database = new DataSourceFactory(); + + @NotNull + private Map> viewRendererConfiguration = Collections.emptyMap(); + + @JsonProperty public String getTemplate() { return template; } - public String getDefaultName() { - return defaultName; - } - + @JsonProperty public void setTemplate(String template) { this.template = template; } + @JsonProperty + public String getDefaultName() { + return defaultName; + } + + @JsonProperty public void setDefaultName(String defaultName) { this.defaultName = defaultName; } @@ -31,4 +49,28 @@ public void setDefaultName(String 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; + } + + @JsonProperty("viewRendererConfiguration") + public Map> getViewRendererConfiguration() { + return viewRendererConfiguration; + } + + @JsonProperty("viewRendererConfiguration") + public void setViewRendererConfiguration(Map> viewRendererConfiguration) { + final ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry> entry : viewRendererConfiguration.entrySet()) { + builder.put(entry.getKey(), ImmutableMap.copyOf(entry.getValue())); + } + this.viewRendererConfiguration = builder.build(); + } } diff --git a/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldService.java b/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldService.java deleted file mode 100644 index a65ecbc0a24..00000000000 --- a/dropwizard-example/src/main/java/com/example/helloworld/HelloWorldService.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.example.helloworld; - -import com.example.helloworld.cli.RenderCommand; -import com.example.helloworld.core.Template; -import com.example.helloworld.health.TemplateHealthCheck; -import com.example.helloworld.resources.HelloWorldResource; -import com.yammer.dropwizard.Service; -import com.yammer.dropwizard.config.Environment; - -public class HelloWorldService extends Service { - public static void main(String[] args) throws Exception { - new HelloWorldService().run(args); - } - - private HelloWorldService() { - super("hello-world"); - addCommand(new RenderCommand()); - } - - @Override - public void initialize(HelloWorldConfiguration configuration, - Environment environment) { - final Template template = configuration.buildTemplate(); - - environment.addHealthCheck(new TemplateHealthCheck(template)); - environment.addResource(new HelloWorldResource(template)); - } -} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/api/Saying.java b/dropwizard-example/src/main/java/com/example/helloworld/api/Saying.java new file mode 100644 index 00000000000..3b943093077 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/api/Saying.java @@ -0,0 +1,39 @@ +package com.example.helloworld.api; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +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; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("content", content) + .toString(); + } +} 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 00000000000..a0138ad571a --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthenticator.java @@ -0,0 +1,31 @@ +package com.example.helloworld.auth; + +import com.example.helloworld.core.User; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.auth.AuthenticationException; +import io.dropwizard.auth.Authenticator; +import io.dropwizard.auth.basic.BasicCredentials; + +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +public class ExampleAuthenticator implements Authenticator { + /** + * Valid users with mapping user -> roles + */ + private static final Map> VALID_USERS = ImmutableMap.of( + "guest", ImmutableSet.of(), + "good-guy", ImmutableSet.of("BASIC_GUY"), + "chief-wizard", ImmutableSet.of("ADMIN", "BASIC_GUY") + ); + + @Override + public Optional authenticate(BasicCredentials credentials) throws AuthenticationException { + if (VALID_USERS.containsKey(credentials.getUsername()) && "secret".equals(credentials.getPassword())) { + return Optional.of(new User(credentials.getUsername(), VALID_USERS.get(credentials.getUsername()))); + } + return Optional.empty(); + } +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthorizer.java b/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthorizer.java new file mode 100644 index 00000000000..769e755bc7e --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/auth/ExampleAuthorizer.java @@ -0,0 +1,12 @@ +package com.example.helloworld.auth; + +import com.example.helloworld.core.User; +import io.dropwizard.auth.Authorizer; + +public class ExampleAuthorizer implements Authorizer { + + @Override + public boolean authorize(User user, String role) { + return user.getRoles() != null && user.getRoles().contains(role); + } +} 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 index aa0599d6e88..8481ad1c65b 100644 --- a/dropwizard-example/src/main/java/com/example/helloworld/cli/RenderCommand.java +++ b/dropwizard-example/src/main/java/com/example/helloworld/cli/RenderCommand.java @@ -2,46 +2,48 @@ import com.example.helloworld.HelloWorldConfiguration; import com.example.helloworld.core.Template; -import com.google.common.base.Optional; -import com.yammer.dropwizard.AbstractService; -import com.yammer.dropwizard.cli.ConfiguredCommand; -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.Options; +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; +import java.util.Optional; + public class RenderCommand extends ConfiguredCommand { private static final Logger LOGGER = LoggerFactory.getLogger(RenderCommand.class); public RenderCommand() { - super("render", "Renders the configured template to the console."); - } - - @Override - protected String getConfiguredSyntax() { - return "[name1 name2]"; + super("render", "Render the template data to console"); } @Override - public Options getOptions() { - final Options options = new Options(); - options.addOption("i", "include-default", false, - "Also render the template with the default name"); - return options; + 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(AbstractService service, - HelloWorldConfiguration configuration, - CommandLine params) throws Exception { + protected void run(Bootstrap bootstrap, + Namespace namespace, + HelloWorldConfiguration configuration) throws Exception { final Template template = configuration.buildTemplate(); - if (params.hasOption("include-default")) { - LOGGER.info("DEFAULT => {}", template.render(Optional.absent())); + if (namespace.getBoolean("include-default")) { + LOGGER.info("DEFAULT => {}", template.render(Optional.empty())); } - for (String name : params.getArgs()) { - LOGGER.info("{} => {}", name, template.render(Optional.of(name))); + for (String name : namespace.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 00000000000..de8dfc7a83c --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/core/Person.java @@ -0,0 +1,86 @@ +package com.example.helloworld.core; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import java.util.Objects; + +@Entity +@Table(name = "people") +@NamedQueries( + { + @NamedQuery( + name = "com.example.helloworld.core.Person.findAll", + query = "SELECT p FROM Person p" + ) + } +) +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 Person() { + } + + public Person(String fullName, String jobTitle) { + this.fullName = fullName; + this.jobTitle = 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; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Person)) { + return false; + } + + final Person that = (Person) o; + + return Objects.equals(this.id, that.id) && + Objects.equals(this.fullName, that.fullName) && + Objects.equals(this.jobTitle, that.jobTitle); + } + + @Override + public int hashCode() { + return Objects.hash(id, fullName, 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 deleted file mode 100644 index f601c4f92a5..00000000000 --- a/dropwizard-example/src/main/java/com/example/helloworld/core/Saying.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.example.helloworld.core; - -public class Saying { - private final long id; - private final String content; - - public Saying(long id, String content) { - this.id = id; - this.content = content; - } - - public long getId() { - return id; - } - - 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 index 897260b8ca4..1dccf722a9f 100644 --- a/dropwizard-example/src/main/java/com/example/helloworld/core/Template.java +++ b/dropwizard-example/src/main/java/com/example/helloworld/core/Template.java @@ -1,6 +1,6 @@ package com.example.helloworld.core; -import com.google.common.base.Optional; +import java.util.Optional; import static java.lang.String.format; @@ -12,8 +12,8 @@ public Template(String content, String defaultName) { this.content = content; this.defaultName = defaultName; } - + public String render(Optional name) { - return format(content, name.or(defaultName)); + return format(content, name.orElse(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 00000000000..1340d8ca4b1 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/core/User.java @@ -0,0 +1,32 @@ +package com.example.helloworld.core; + +import java.security.Principal; +import java.util.Set; + +public class User implements Principal { + private final String name; + + private final Set roles; + + public User(String name) { + this.name = name; + this.roles = null; + } + + public User(String name, Set roles) { + this.name = name; + this.roles = roles; + } + + public String getName() { + return name; + } + + public int getId() { + return (int) (Math.random() * 100); + } + + public Set getRoles() { + return roles; + } +} 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 00000000000..0466a962971 --- /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 io.dropwizard.hibernate.AbstractDAO; +import org.hibernate.SessionFactory; + +import java.util.List; +import java.util.Optional; + +public class PersonDAO extends AbstractDAO { + public PersonDAO(SessionFactory factory) { + super(factory); + } + + public Optional findById(Long id) { + return Optional.ofNullable(get(id)); + } + + public Person create(Person person) { + return persist(person); + } + + public List findAll() { + return list(namedQuery("com.example.helloworld.core.Person.findAll")); + } +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/filter/DateNotSpecifiedFilter.java b/dropwizard-example/src/main/java/com/example/helloworld/filter/DateNotSpecifiedFilter.java new file mode 100644 index 00000000000..3b6e67ca501 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/filter/DateNotSpecifiedFilter.java @@ -0,0 +1,21 @@ +package com.example.helloworld.filter; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; +import java.io.IOException; + +@Provider +public class DateNotSpecifiedFilter implements ContainerRequestFilter { + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + final String dateHeader = requestContext.getHeaderString(HttpHeaders.DATE); + if (dateHeader == null) { + throw new WebApplicationException(new IllegalArgumentException("Date Header was not specified"), + Response.Status.BAD_REQUEST); + } + } +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/filter/DateRequired.java b/dropwizard-example/src/main/java/com/example/helloworld/filter/DateRequired.java new file mode 100644 index 00000000000..1ade22e178f --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/filter/DateRequired.java @@ -0,0 +1,11 @@ +package com.example.helloworld.filter; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface DateRequired { +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/filter/DateRequiredFeature.java b/dropwizard-example/src/main/java/com/example/helloworld/filter/DateRequiredFeature.java new file mode 100644 index 00000000000..11685dd9e5e --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/filter/DateRequiredFeature.java @@ -0,0 +1,16 @@ +package com.example.helloworld.filter; + +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.Provider; + +@Provider +public class DateRequiredFeature implements DynamicFeature { + @Override + public void configure(ResourceInfo resourceInfo, FeatureContext context) { + if (resourceInfo.getResourceMethod().getAnnotation(DateRequired.class) != null) { + context.register(DateNotSpecifiedFilter.class); + } + } +} 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 index ebc40c03e09..fc7131b5f2f 100644 --- a/dropwizard-example/src/main/java/com/example/helloworld/health/TemplateHealthCheck.java +++ b/dropwizard-example/src/main/java/com/example/helloworld/health/TemplateHealthCheck.java @@ -1,8 +1,9 @@ package com.example.helloworld.health; +import com.codahale.metrics.health.HealthCheck; import com.example.helloworld.core.Template; -import com.google.common.base.Optional; -import com.yammer.metrics.core.HealthCheck; + +import java.util.Optional; public class TemplateHealthCheck extends HealthCheck { private final Template template; @@ -12,14 +13,9 @@ public TemplateHealthCheck(Template template) { } @Override - public String name() { - return "template"; - } - - @Override - public Result check() throws Exception { + protected Result check() throws Exception { template.render(Optional.of("woo")); - template.render(Optional.absent()); + template.render(Optional.empty()); return Result.healthy(); } } diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/FilteredResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/FilteredResource.java new file mode 100644 index 00000000000..7292ee47680 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/FilteredResource.java @@ -0,0 +1,17 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.filter.DateRequired; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/filtered") +public class FilteredResource { + + @GET + @DateRequired + @Path("hello") + public String sayHello() { + return "hello"; + } +} 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 index a782a2c3f5a..8cf703a4e30 100644 --- a/dropwizard-example/src/main/java/com/example/helloworld/resources/HelloWorldResource.java +++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/HelloWorldResource.java @@ -1,24 +1,28 @@ package com.example.helloworld.resources; -import com.example.helloworld.core.Saying; +import com.codahale.metrics.annotation.Timed; +import com.example.helloworld.api.Saying; import com.example.helloworld.core.Template; -import com.google.common.base.Optional; -import com.yammer.metrics.Metrics; -import com.yammer.metrics.core.TimerContext; -import com.yammer.metrics.core.TimerMetric; +import io.dropwizard.jersey.caching.CacheControl; +import io.dropwizard.jersey.params.DateTimeParam; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.validation.Valid; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; +import java.util.Optional; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; @Path("/hello-world") @Produces(MediaType.APPLICATION_JSON) public class HelloWorldResource { - private static final TimerMetric GETS = Metrics.newTimer(HelloWorldResource.class, - "get-requests"); + private static final Logger LOGGER = LoggerFactory.getLogger(HelloWorldResource.class); private final Template template; private final AtomicLong counter; @@ -29,12 +33,28 @@ public HelloWorldResource(Template template) { } @GET + @Timed(name = "get-requests") + @CacheControl(maxAge = 1, maxAgeUnit = TimeUnit.DAYS) public Saying sayHello(@QueryParam("name") Optional name) { - final TimerContext context = GETS.time(); - try { - return new Saying(counter.incrementAndGet(), template.render(name)); - } finally { - context.stop(); + return new Saying(counter.incrementAndGet(), template.render(name)); + } + + @POST + public void receiveHello(@Valid Saying saying) { + LOGGER.info("Received a saying: {}", saying); + } + + @GET + @Path("/date") + @Produces(MediaType.TEXT_PLAIN) + public String receiveDate(@QueryParam("date") Optional dateTimeParam) { + if (dateTimeParam.isPresent()) { + final DateTimeParam actualDateTimeParam = dateTimeParam.get(); + LOGGER.info("Received a date: {}", actualDateTimeParam); + return actualDateTimeParam.get().toString(); + } else { + LOGGER.warn("No received date"); + return null; } } } 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 00000000000..40ad6d29b3f --- /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; + +@Path("/people") +@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 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 00000000000..2672a4edd65 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/PersonResource.java @@ -0,0 +1,51 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.core.Person; +import com.example.helloworld.db.PersonDAO; +import com.example.helloworld.views.PersonView; +import io.dropwizard.hibernate.UnitOfWork; +import io.dropwizard.jersey.params.LongParam; + +import javax.ws.rs.GET; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/people/{personId}") +@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()); + } + + @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())); + } + + private Person findSafely(long personId) { + return peopleDAO.findById(personId).orElseThrow(() -> new NotFoundException("No such user.")); + } +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedClassResource.java b/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedClassResource.java new file mode 100644 index 00000000000..ff0777356c2 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedClassResource.java @@ -0,0 +1,43 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.core.User; +import io.dropwizard.auth.Auth; + +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.SecurityContext; + +/** + * {@link RolesAllowed}, {@link PermitAll} are supported on the class level.

    + * Method level annotations take precedence over the class level ones + */ + +@Path("/protected") +@RolesAllowed("BASIC_GUY") +public final class ProtectedClassResource { + + @GET + @PermitAll + @Path("guest") + public String showSecret(@Auth User user) { + return String.format("Hey there, %s. You know the secret! %d", user.getName(), user.getId()); + } + + /* Access to this method is authorized by the class level annotation */ + @GET + public String showBasicUserSecret(@Context SecurityContext context) { + User user = (User) context.getUserPrincipal(); + return String.format("Hey there, %s. You seem to be a basic user. %d", user.getName(), user.getId()); + } + + @GET + @RolesAllowed("ADMIN") + @Path("admin") + public String showAdminSecret(@Auth User user) { + return String.format("Hey there, %s. It looks like you are an admin. %d", user.getName(), user.getId()); + } + +} 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 00000000000..d70051dc8a6 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/ProtectedResource.java @@ -0,0 +1,29 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.core.User; +import io.dropwizard.auth.Auth; + +import javax.annotation.security.PermitAll; +import javax.annotation.security.RolesAllowed; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/protected") +@Produces(MediaType.TEXT_PLAIN) +public class ProtectedResource { + + @PermitAll + @GET + public String showSecret(@Auth User user) { + return String.format("Hey there, %s. You know the secret! %d", user.getName(), user.getId()); + } + + @RolesAllowed("ADMIN") + @GET + @Path("admin") + public String showAdminSecret(@Auth User user) { + return String.format("Hey there, %s. It looks like you are an admin. %d", user.getName(), user.getId()); + } +} 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 00000000000..b7e0bc460f1 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/resources/ViewResource.java @@ -0,0 +1,43 @@ +package com.example.helloworld.resources; + +import io.dropwizard.views.View; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import java.nio.charset.StandardCharsets; + +@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", StandardCharsets.UTF_8) { + }; + } + + @GET + @Produces("text/html;charset=ISO-8859-1") + @Path("/iso88591.ftl") + public View freemarkerISO88591() { + return new View("/views/ftl/iso88591.ftl", StandardCharsets.ISO_8859_1) { + }; + } + + @GET + @Produces("text/html;charset=UTF-8") + @Path("/utf8.mustache") + public View mustacheUTF8() { + return new View("/views/mustache/utf8.mustache", StandardCharsets.UTF_8) { + }; + } + + @GET + @Produces("text/html;charset=ISO-8859-1") + @Path("/iso88591.mustache") + public View mustacheISO88591() { + return new View("/views/mustache/iso88591.mustache", StandardCharsets.ISO_8859_1) { + }; + } +} diff --git a/dropwizard-example/src/main/java/com/example/helloworld/tasks/EchoTask.java b/dropwizard-example/src/main/java/com/example/helloworld/tasks/EchoTask.java new file mode 100644 index 00000000000..10a62a0af1d --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/tasks/EchoTask.java @@ -0,0 +1,18 @@ +package com.example.helloworld.tasks; + +import com.google.common.collect.ImmutableMultimap; +import io.dropwizard.servlets.tasks.PostBodyTask; + +import java.io.PrintWriter; + +public class EchoTask extends PostBodyTask { + public EchoTask() { + super("echo"); + } + + @Override + public void execute(ImmutableMultimap parameters, String body, PrintWriter output) throws Exception { + output.print(body); + output.flush(); + } +} 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 00000000000..3b52c906686 --- /dev/null +++ b/dropwizard-example/src/main/java/com/example/helloworld/views/PersonView.java @@ -0,0 +1,32 @@ +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; + + 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; + } +} 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 00000000000..e71db1d01c4 --- /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/assets/pure-min.css b/dropwizard-example/src/main/resources/assets/pure-min.css new file mode 100644 index 00000000000..d0102d7ed80 --- /dev/null +++ b/dropwizard-example/src/main/resources/assets/pure-min.css @@ -0,0 +1,11 @@ +/*! +Pure v0.6.0 +Copyright 2014 Yahoo! Inc. All rights reserved. +Licensed under the BSD License. +https://github.com/yahoo/pure/blob/master/LICENSE.md +*/ +/*! +normalize.css v^3.0 | MIT License | git.io/normalize +Copyright (c) Nicolas Gallagher and Jonathan Neal +*/ +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{letter-spacing:-.31em;*letter-spacing:normal;*word-spacing:-.43em;text-rendering:optimizespeed;font-family:FreeSans,Arimo,"Droid Sans",Helvetica,Arial,sans-serif;display:-webkit-flex;-webkit-flex-flow:row wrap;display:-ms-flexbox;-ms-flex-flow:row wrap;-ms-align-content:flex-start;-webkit-align-content:flex-start;align-content:flex-start}.opera-only :-o-prefocus,.pure-g{word-spacing:-.43em}.pure-u{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-g [class *="pure-u"]{font-family:sans-serif}.pure-u-1,.pure-u-1-1,.pure-u-1-2,.pure-u-1-3,.pure-u-2-3,.pure-u-1-4,.pure-u-3-4,.pure-u-1-5,.pure-u-2-5,.pure-u-3-5,.pure-u-4-5,.pure-u-5-5,.pure-u-1-6,.pure-u-5-6,.pure-u-1-8,.pure-u-3-8,.pure-u-5-8,.pure-u-7-8,.pure-u-1-12,.pure-u-5-12,.pure-u-7-12,.pure-u-11-12,.pure-u-1-24,.pure-u-2-24,.pure-u-3-24,.pure-u-4-24,.pure-u-5-24,.pure-u-6-24,.pure-u-7-24,.pure-u-8-24,.pure-u-9-24,.pure-u-10-24,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24{display:inline-block;*display:inline;zoom:1;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%;*width:4.1357%}.pure-u-1-12,.pure-u-2-24{width:8.3333%;*width:8.3023%}.pure-u-1-8,.pure-u-3-24{width:12.5%;*width:12.469%}.pure-u-1-6,.pure-u-4-24{width:16.6667%;*width:16.6357%}.pure-u-1-5{width:20%;*width:19.969%}.pure-u-5-24{width:20.8333%;*width:20.8023%}.pure-u-1-4,.pure-u-6-24{width:25%;*width:24.969%}.pure-u-7-24{width:29.1667%;*width:29.1357%}.pure-u-1-3,.pure-u-8-24{width:33.3333%;*width:33.3023%}.pure-u-3-8,.pure-u-9-24{width:37.5%;*width:37.469%}.pure-u-2-5{width:40%;*width:39.969%}.pure-u-5-12,.pure-u-10-24{width:41.6667%;*width:41.6357%}.pure-u-11-24{width:45.8333%;*width:45.8023%}.pure-u-1-2,.pure-u-12-24{width:50%;*width:49.969%}.pure-u-13-24{width:54.1667%;*width:54.1357%}.pure-u-7-12,.pure-u-14-24{width:58.3333%;*width:58.3023%}.pure-u-3-5{width:60%;*width:59.969%}.pure-u-5-8,.pure-u-15-24{width:62.5%;*width:62.469%}.pure-u-2-3,.pure-u-16-24{width:66.6667%;*width:66.6357%}.pure-u-17-24{width:70.8333%;*width:70.8023%}.pure-u-3-4,.pure-u-18-24{width:75%;*width:74.969%}.pure-u-19-24{width:79.1667%;*width:79.1357%}.pure-u-4-5{width:80%;*width:79.969%}.pure-u-5-6,.pure-u-20-24{width:83.3333%;*width:83.3023%}.pure-u-7-8,.pure-u-21-24{width:87.5%;*width:87.469%}.pure-u-11-12,.pure-u-22-24{width:91.6667%;*width:91.6357%}.pure-u-23-24{width:95.8333%;*width:95.8023%}.pure-u-1,.pure-u-1-1,.pure-u-5-5,.pure-u-24-24{width:100%}.pure-button{display:inline-block;zoom:1;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:#444;color:rgba(0,0,0,.8);border:1px solid #999;border:0 rgba(0,0,0,0);background-color:#E6E6E6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:hover,.pure-button:focus{filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#1a000000', GradientType=0);background-image:-webkit-gradient(linear,0 0,0 100%,from(transparent),color-stop(40%,rgba(0,0,0,.05)),to(rgba(0,0,0,.1)));background-image:-webkit-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:-moz-linear-gradient(top,rgba(0,0,0,.05) 0,rgba(0,0,0,.1));background-image:-o-linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1));background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000\9}.pure-button[disabled],.pure-button-disabled,.pure-button-disabled:hover,.pure-button-disabled:focus,.pure-button-disabled:active{border:0;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);filter:alpha(opacity=40);-khtml-opacity:.4;-moz-opacity:.4;opacity:.4;cursor:not-allowed;box-shadow:none}.pure-button-hidden{display:none}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-primary,.pure-button-selected,a.pure-button-primary,a.pure-button-selected{background-color:#0078e7;color:#fff}.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form select,.pure-form textarea{padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;vertical-align:middle;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input:not([type]){padding:.5em .6em;display:inline-block;border:1px solid #ccc;box-shadow:inset 0 1px 3px #ddd;border-radius:4px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-form input[type=color]{padding:.2em .5em}.pure-form input[type=text]:focus,.pure-form input[type=password]:focus,.pure-form input[type=email]:focus,.pure-form input[type=url]:focus,.pure-form input[type=date]:focus,.pure-form input[type=month]:focus,.pure-form input[type=time]:focus,.pure-form input[type=datetime]:focus,.pure-form input[type=datetime-local]:focus,.pure-form input[type=week]:focus,.pure-form input[type=number]:focus,.pure-form input[type=search]:focus,.pure-form input[type=tel]:focus,.pure-form input[type=color]:focus,.pure-form select:focus,.pure-form textarea:focus{outline:0;border-color:#129FEA}.pure-form input:not([type]):focus{outline:0;border-color:#129FEA}.pure-form input[type=file]:focus,.pure-form input[type=radio]:focus,.pure-form input[type=checkbox]:focus{outline:thin solid #129FEA;outline:1px auto #129FEA}.pure-form .pure-checkbox,.pure-form .pure-radio{margin:.5em 0;display:block}.pure-form input[type=text][disabled],.pure-form input[type=password][disabled],.pure-form input[type=email][disabled],.pure-form input[type=url][disabled],.pure-form input[type=date][disabled],.pure-form input[type=month][disabled],.pure-form input[type=time][disabled],.pure-form input[type=datetime][disabled],.pure-form input[type=datetime-local][disabled],.pure-form input[type=week][disabled],.pure-form input[type=number][disabled],.pure-form input[type=search][disabled],.pure-form input[type=tel][disabled],.pure-form input[type=color][disabled],.pure-form select[disabled],.pure-form textarea[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input:not([type])[disabled]{cursor:not-allowed;background-color:#eaeded;color:#cad2d3}.pure-form input[readonly],.pure-form select[readonly],.pure-form textarea[readonly]{background-color:#eee;color:#777;border-color:#ccc}.pure-form input:focus:invalid,.pure-form textarea:focus:invalid,.pure-form select:focus:invalid{color:#b94a48;border-color:#e9322d}.pure-form input[type=file]:focus:invalid:focus,.pure-form input[type=radio]:focus:invalid:focus,.pure-form input[type=checkbox]:focus:invalid:focus{outline-color:#e9322d}.pure-form select{height:2.25em;border:1px solid #ccc;background-color:#fff}.pure-form select[multiple]{height:auto}.pure-form label{margin:.5em 0 .2em}.pure-form fieldset{margin:0;padding:.35em 0 .75em;border:0}.pure-form legend{display:block;width:100%;padding:.3em 0;margin-bottom:.3em;color:#333;border-bottom:1px solid #e5e5e5}.pure-form-stacked input[type=text],.pure-form-stacked input[type=password],.pure-form-stacked input[type=email],.pure-form-stacked input[type=url],.pure-form-stacked input[type=date],.pure-form-stacked input[type=month],.pure-form-stacked input[type=time],.pure-form-stacked input[type=datetime],.pure-form-stacked input[type=datetime-local],.pure-form-stacked input[type=week],.pure-form-stacked input[type=number],.pure-form-stacked input[type=search],.pure-form-stacked input[type=tel],.pure-form-stacked input[type=color],.pure-form-stacked input[type=file],.pure-form-stacked select,.pure-form-stacked label,.pure-form-stacked textarea{display:block;margin:.25em 0}.pure-form-stacked input:not([type]){display:block;margin:.25em 0}.pure-form-aligned input,.pure-form-aligned textarea,.pure-form-aligned select,.pure-form-aligned .pure-help-inline,.pure-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.pure-form-aligned textarea{vertical-align:top}.pure-form-aligned .pure-control-group{margin-bottom:.5em}.pure-form-aligned .pure-control-group label{text-align:right;display:inline-block;vertical-align:middle;width:10em;margin:0 1em 0 0}.pure-form-aligned .pure-controls{margin:1.5em 0 0 11em}.pure-form input.pure-input-rounded,.pure-form .pure-input-rounded{border-radius:2em;padding:.5em 1em}.pure-form .pure-group fieldset{margin-bottom:10px}.pure-form .pure-group input,.pure-form .pure-group textarea{display:block;padding:10px;margin:0 0 -1px;border-radius:0;position:relative;top:-1px}.pure-form .pure-group input:focus,.pure-form .pure-group textarea:focus{z-index:3}.pure-form .pure-group input:first-child,.pure-form .pure-group textarea:first-child{top:1px;border-radius:4px 4px 0 0;margin:0}.pure-form .pure-group input:first-child:last-child,.pure-form .pure-group textarea:first-child:last-child{top:1px;border-radius:4px;margin:0}.pure-form .pure-group input:last-child,.pure-form .pure-group textarea:last-child{top:-2px;border-radius:0 0 4px 4px;margin:0}.pure-form .pure-group button{margin:.35em 0}.pure-form .pure-input-1{width:100%}.pure-form .pure-input-2-3{width:66%}.pure-form .pure-input-1-2{width:50%}.pure-form .pure-input-1-3{width:33%}.pure-form .pure-input-1-4{width:25%}.pure-form .pure-help-inline,.pure-form-message-inline{display:inline-block;padding-left:.3em;color:#666;vertical-align:middle;font-size:.875em}.pure-form-message{display:block;color:#666;font-size:.875em}@media only screen and (max-width :480px){.pure-form button[type=submit]{margin:.7em 0 0}.pure-form input:not([type]),.pure-form input[type=text],.pure-form input[type=password],.pure-form input[type=email],.pure-form input[type=url],.pure-form input[type=date],.pure-form input[type=month],.pure-form input[type=time],.pure-form input[type=datetime],.pure-form input[type=datetime-local],.pure-form input[type=week],.pure-form input[type=number],.pure-form input[type=search],.pure-form input[type=tel],.pure-form input[type=color],.pure-form label{margin-bottom:.3em;display:block}.pure-group input:not([type]),.pure-group input[type=text],.pure-group input[type=password],.pure-group input[type=email],.pure-group input[type=url],.pure-group input[type=date],.pure-group input[type=month],.pure-group input[type=time],.pure-group input[type=datetime],.pure-group input[type=datetime-local],.pure-group input[type=week],.pure-group input[type=number],.pure-group input[type=search],.pure-group input[type=tel],.pure-group input[type=color]{margin-bottom:0}.pure-form-aligned .pure-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.pure-form-aligned .pure-controls{margin:1.5em 0 0}.pure-form .pure-help-inline,.pure-form-message-inline,.pure-form-message{display:block;font-size:.75em;padding:.2em 0 .8em}}.pure-menu{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.pure-menu-fixed{position:fixed;left:0;top:0;z-index:3}.pure-menu-list,.pure-menu-item{position:relative}.pure-menu-list{list-style:none;margin:0;padding:0}.pure-menu-item{padding:0;margin:0;height:100%}.pure-menu-link,.pure-menu-heading{display:block;text-decoration:none;white-space:nowrap}.pure-menu-horizontal{width:100%;white-space:nowrap}.pure-menu-horizontal .pure-menu-list{display:inline-block}.pure-menu-horizontal .pure-menu-item,.pure-menu-horizontal .pure-menu-heading,.pure-menu-horizontal .pure-menu-separator{display:inline-block;*display:inline;zoom:1;vertical-align:middle}.pure-menu-item .pure-menu-item{display:block}.pure-menu-children{display:none;position:absolute;left:100%;top:0;margin:0;padding:0;z-index:3}.pure-menu-horizontal .pure-menu-children{left:0;top:auto;width:inherit}.pure-menu-allow-hover:hover>.pure-menu-children,.pure-menu-active>.pure-menu-children{display:block;position:absolute}.pure-menu-has-children>.pure-menu-link:after{padding-left:.5em;content:"\25B8";font-size:small}.pure-menu-horizontal .pure-menu-has-children>.pure-menu-link:after{content:"\25BE"}.pure-menu-scrollable{overflow-y:scroll;overflow-x:hidden}.pure-menu-scrollable .pure-menu-list{display:block}.pure-menu-horizontal.pure-menu-scrollable .pure-menu-list{display:inline-block}.pure-menu-horizontal.pure-menu-scrollable{white-space:nowrap;overflow-y:hidden;overflow-x:auto;-ms-overflow-style:none;-webkit-overflow-scrolling:touch;padding:.5em 0}.pure-menu-horizontal.pure-menu-scrollable::-webkit-scrollbar{display:none}.pure-menu-separator{background-color:#ccc;height:1px;margin:.3em 0}.pure-menu-horizontal .pure-menu-separator{width:1px;height:1.3em;margin:0 .3em}.pure-menu-heading{text-transform:uppercase;color:#565d64}.pure-menu-link{color:#777}.pure-menu-children{background-color:#fff}.pure-menu-link,.pure-menu-disabled,.pure-menu-heading{padding:.5em 1em}.pure-menu-disabled{opacity:.5}.pure-menu-disabled .pure-menu-link:hover{background-color:transparent}.pure-menu-active>.pure-menu-link,.pure-menu-link:hover,.pure-menu-link:focus{background-color:#eee}.pure-menu-selected .pure-menu-link,.pure-menu-selected .pure-menu-link:visited{color:#000}.pure-table{border-collapse:collapse;border-spacing:0;empty-cells:show;border:1px solid #cbcbcb}.pure-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.pure-table td,.pure-table th{border-left:1px solid #cbcbcb;border-width:0 0 0 1px;font-size:inherit;margin:0;overflow:visible;padding:.5em 1em}.pure-table td:first-child,.pure-table th:first-child{border-left-width:0}.pure-table thead{background-color:#e0e0e0;color:#000;text-align:left;vertical-align:bottom}.pure-table td{background-color:transparent}.pure-table-odd td{background-color:#f2f2f2}.pure-table-striped tr:nth-child(2n-1) td{background-color:#f2f2f2}.pure-table-bordered td{border-bottom:1px solid #cbcbcb}.pure-table-bordered tbody>tr:last-child>td{border-bottom-width:0}.pure-table-horizontal td,.pure-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #cbcbcb}.pure-table-horizontal tbody>tr:last-child>td{border-bottom-width:0} diff --git a/dropwizard-example/src/main/resources/banner.txt b/dropwizard-example/src/main/resources/banner.txt new file mode 100644 index 00000000000..1cbdb95aa4e --- /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 00000000000..51d60153506 --- /dev/null +++ b/dropwizard-example/src/main/resources/com/example/helloworld/views/freemarker/person.ftl @@ -0,0 +1,11 @@ +<#-- @ftlvariable name="" type="com.example.views.PersonView" --> + + + + + + +

    Hello, ${person.fullName?html}!

    + You are an awesome ${person.jobTitle?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 00000000000..1084fe8431a --- /dev/null +++ b/dropwizard-example/src/main/resources/com/example/helloworld/views/mustache/person.mustache @@ -0,0 +1,9 @@ + + + + + +

    Hello, {{person.fullName}}!

    + You are an aweseome {{person.jobTitle}}! + + \ 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 00000000000..4ec4964cdcf --- /dev/null +++ b/dropwizard-example/src/main/resources/migrations.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + 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 00000000000..be7e5c9a750 --- /dev/null +++ b/dropwizard-example/src/main/resources/views/ftl/iso88591.ftl @@ -0,0 +1,10 @@ + + + +

    This is an example of a view containing ISO-8859-1 characters

    + +¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢ + + + + 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 00000000000..86d499e6d15 --- /dev/null +++ b/dropwizard-example/src/main/resources/views/ftl/utf8.ftl @@ -0,0 +1,9 @@ + + + +

    This is an example of a view containing UTF-8 characters

    + +€€€€€€€€€€€€€€€€€€ + + + \ 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 00000000000..be7e5c9a750 --- /dev/null +++ b/dropwizard-example/src/main/resources/views/mustache/iso88591.mustache @@ -0,0 +1,10 @@ + + + +

    This is an example of a view containing ISO-8859-1 characters

    + +¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢¢ + + + + 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 00000000000..86d499e6d15 --- /dev/null +++ b/dropwizard-example/src/main/resources/views/mustache/utf8.mustache @@ -0,0 +1,9 @@ + + + +

    This is an example of a view containing UTF-8 characters

    + +€€€€€€€€€€€€€€€€€€ + + + \ No newline at end of file diff --git a/dropwizard-example/src/test/java/com/example/helloworld/IntegrationTest.java b/dropwizard-example/src/test/java/com/example/helloworld/IntegrationTest.java new file mode 100644 index 00000000000..3a2e4b455bc --- /dev/null +++ b/dropwizard-example/src/test/java/com/example/helloworld/IntegrationTest.java @@ -0,0 +1,80 @@ +package com.example.helloworld; + +import com.example.helloworld.api.Saying; +import com.example.helloworld.core.Person; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class IntegrationTest { + + private static final String TMP_FILE = createTempFile(); + private static final String CONFIG_PATH = ResourceHelpers.resourceFilePath("test-example.yml"); + + @ClassRule + public static final DropwizardAppRule RULE = new DropwizardAppRule<>( + HelloWorldApplication.class, CONFIG_PATH, + ConfigOverride.config("database.url", "jdbc:h2:" + TMP_FILE)); + + private Client client; + + @BeforeClass + public static void migrateDb() throws Exception { + RULE.getApplication().run("db", "migrate", CONFIG_PATH); + } + + @Before + public void setUp() throws Exception { + client = ClientBuilder.newClient(); + } + + @After + public void tearDown() throws Exception { + client.close(); + } + + private static String createTempFile() { + try { + return File.createTempFile("test-example", null).getAbsolutePath(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + @Test + public void testHelloWorld() throws Exception { + final Optional name = Optional.of("Dr. IntegrationTest"); + final Saying saying = client.target("http://localhost:" + RULE.getLocalPort() + "/hello-world") + .queryParam("name", name.get()) + .request() + .get(Saying.class); + assertThat(saying.getContent()).isEqualTo(RULE.getConfiguration().buildTemplate().render(name)); + } + + @Test + public void testPostPerson() throws Exception { + final Person person = new Person("Dr. IntegrationTest", "Chief Wizard"); + final Person newPerson = client.target("http://localhost:" + RULE.getLocalPort() + "/people") + .request() + .post(Entity.entity(person, MediaType.APPLICATION_JSON_TYPE)) + .readEntity(Person.class); + assertThat(newPerson.getId()).isNotNull(); + assertThat(newPerson.getFullName()).isEqualTo(person.getFullName()); + assertThat(newPerson.getJobTitle()).isEqualTo(person.getJobTitle()); + } +} diff --git a/dropwizard-example/src/test/java/com/example/helloworld/resources/PeopleResourceTest.java b/dropwizard-example/src/test/java/com/example/helloworld/resources/PeopleResourceTest.java new file mode 100644 index 00000000000..7ef7356c11c --- /dev/null +++ b/dropwizard-example/src/test/java/com/example/helloworld/resources/PeopleResourceTest.java @@ -0,0 +1,80 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.core.Person; +import com.example.helloworld.db.PersonDAO; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.collect.ImmutableList; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.runners.MockitoJUnitRunner; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link PeopleResource}. + */ +@RunWith(MockitoJUnitRunner.class) +public class PeopleResourceTest { + private static final PersonDAO PERSON_DAO = mock(PersonDAO.class); + @ClassRule + public static final ResourceTestRule RESOURCES = ResourceTestRule.builder() + .addResource(new PeopleResource(PERSON_DAO)) + .build(); + @Captor + private ArgumentCaptor personCaptor; + private Person person; + + @Before + public void setUp() { + person = new Person(); + person.setFullName("Full Name"); + person.setJobTitle("Job Title"); + } + + @After + public void tearDown() { + reset(PERSON_DAO); + } + + @Test + public void createPerson() throws JsonProcessingException { + when(PERSON_DAO.create(any(Person.class))).thenReturn(person); + final Response response = RESOURCES.client().target("/people") + .request(MediaType.APPLICATION_JSON_TYPE) + .post(Entity.entity(person, MediaType.APPLICATION_JSON_TYPE)); + + assertThat(response.getStatusInfo()).isEqualTo(Response.Status.OK); + verify(PERSON_DAO).create(personCaptor.capture()); + assertThat(personCaptor.getValue()).isEqualTo(person); + } + + @Test + public void listPeople() throws Exception { + final ImmutableList people = ImmutableList.of(person); + when(PERSON_DAO.findAll()).thenReturn(people); + + final List response = RESOURCES.client().target("/people") + .request().get(new GenericType>() { + }); + + verify(PERSON_DAO).findAll(); + assertThat(response).containsAll(people); + } +} diff --git a/dropwizard-example/src/test/java/com/example/helloworld/resources/PersonResourceTest.java b/dropwizard-example/src/test/java/com/example/helloworld/resources/PersonResourceTest.java new file mode 100644 index 00000000000..3c918126ea1 --- /dev/null +++ b/dropwizard-example/src/test/java/com/example/helloworld/resources/PersonResourceTest.java @@ -0,0 +1,62 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.core.Person; +import com.example.helloworld.db.PersonDAO; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.core.Response; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link PersonResource}. + */ +public class PersonResourceTest { + private static final PersonDAO DAO = mock(PersonDAO.class); + @ClassRule + public static final ResourceTestRule RULE = ResourceTestRule.builder() + .addResource(new PersonResource(DAO)) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .build(); + private Person person; + + @Before + public void setup() { + person = new Person(); + person.setId(1L); + } + + @After + public void tearDown() { + reset(DAO); + } + + @Test + public void getPersonSuccess() { + when(DAO.findById(1L)).thenReturn(Optional.of(person)); + + Person found = RULE.getJerseyTest().target("/people/1").request().get(Person.class); + + assertThat(found.getId()).isEqualTo(person.getId()); + verify(DAO).findById(1L); + } + + @Test + public void getPersonNotFound() { + when(DAO.findById(2L)).thenReturn(Optional.empty()); + final Response response = RULE.getJerseyTest().target("/people/2").request().get(); + + assertThat(response.getStatusInfo().getStatusCode()).isEqualTo(Response.Status.NOT_FOUND.getStatusCode()); + verify(DAO).findById(2L); + } +} diff --git a/dropwizard-example/src/test/java/com/example/helloworld/resources/ProtectedClassResourceTest.java b/dropwizard-example/src/test/java/com/example/helloworld/resources/ProtectedClassResourceTest.java new file mode 100644 index 00000000000..57954aa3d84 --- /dev/null +++ b/dropwizard-example/src/test/java/com/example/helloworld/resources/ProtectedClassResourceTest.java @@ -0,0 +1,84 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.auth.ExampleAuthenticator; +import com.example.helloworld.auth.ExampleAuthorizer; +import com.example.helloworld.core.User; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.core.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public final class ProtectedClassResourceTest { + + private static final BasicCredentialAuthFilter BASIC_AUTH_HANDLER = + new BasicCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setPrefix("Basic") + .setRealm("SUPER SECRET STUFF") + .buildAuthFilter(); + + @ClassRule + public static final ResourceTestRule RULE = ResourceTestRule.builder() + .addProvider(RolesAllowedDynamicFeature.class) + .addProvider(new AuthDynamicFeature(BASIC_AUTH_HANDLER)) + .addProvider(new AuthValueFactoryProvider.Binder<>(User.class)) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addProvider(ProtectedClassResource.class) + .build(); + + @Test + public void testProtectedAdminEndpoint() { + String secret = RULE.getJerseyTest().target("/protected/admin").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Y2hpZWYtd2l6YXJkOnNlY3JldA==") + .get(String.class); + assertThat(secret).startsWith("Hey there, chief-wizard. It looks like you are an admin."); + } + + @Test + public void testProtectedBasicUserEndpoint() { + String secret = RULE.getJerseyTest().target("/protected").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0") + .get(String.class); + assertThat(secret).startsWith("Hey there, good-guy. You seem to be a basic user."); + } + + @Test + public void testProtectedBasicUserEndpointAsAdmin() { + String secret = RULE.getJerseyTest().target("/protected").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Y2hpZWYtd2l6YXJkOnNlY3JldA==") + .get(String.class); + assertThat(secret).startsWith("Hey there, chief-wizard. You seem to be a basic user."); + } + + @Test + public void testProtectedGuestEndpoint() { + String secret = RULE.getJerseyTest().target("/protected/guest").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Z3Vlc3Q6c2VjcmV0") + .get(String.class); + assertThat(secret).startsWith("Hey there, guest. You know the secret!"); + } + + @Test + public void testProtectedBasicUserEndpointPrincipalIsNotAuthorized403() { + try { + RULE.getJerseyTest().target("/protected").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Z3Vlc3Q6c2VjcmV0") + .get(String.class); + failBecauseExceptionWasNotThrown(ForbiddenException.class); + } catch (ForbiddenException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(403); + } + } + +} diff --git a/dropwizard-example/src/test/java/com/example/helloworld/resources/ProtectedResourceTest.java b/dropwizard-example/src/test/java/com/example/helloworld/resources/ProtectedResourceTest.java new file mode 100644 index 00000000000..e107778828b --- /dev/null +++ b/dropwizard-example/src/test/java/com/example/helloworld/resources/ProtectedResourceTest.java @@ -0,0 +1,96 @@ +package com.example.helloworld.resources; + +import com.example.helloworld.auth.ExampleAuthenticator; +import com.example.helloworld.auth.ExampleAuthorizer; +import com.example.helloworld.core.User; +import io.dropwizard.auth.AuthDynamicFeature; +import io.dropwizard.auth.AuthValueFactoryProvider; +import io.dropwizard.auth.basic.BasicCredentialAuthFilter; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.core.HttpHeaders; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class ProtectedResourceTest { + private static final BasicCredentialAuthFilter BASIC_AUTH_HANDLER = + new BasicCredentialAuthFilter.Builder() + .setAuthenticator(new ExampleAuthenticator()) + .setAuthorizer(new ExampleAuthorizer()) + .setPrefix("Basic") + .setRealm("SUPER SECRET STUFF") + .buildAuthFilter(); + + @ClassRule + public static final ResourceTestRule RULE = ResourceTestRule.builder() + .addProvider(RolesAllowedDynamicFeature.class) + .addProvider(new AuthDynamicFeature(BASIC_AUTH_HANDLER)) + .addProvider(new AuthValueFactoryProvider.Binder<>(User.class)) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addProvider(ProtectedResource.class) + .build(); + + @Test + public void testProtectedEndpoint() { + String secret = RULE.getJerseyTest().target("/protected").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0") + .get(String.class); + assertThat(secret).startsWith("Hey there, good-guy. You know the secret!"); + } + + @Test + public void testProtectedEndpointNoCredentials401() { + try { + RULE.getJerseyTest().target("/protected").request() + .get(String.class); + failBecauseExceptionWasNotThrown(NotAuthorizedException.class); + } catch (NotAuthorizedException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE)) + .containsOnly("Basic realm=\"SUPER SECRET STUFF\""); + } + + } + + @Test + public void testProtectedEndpointBadCredentials401() { + try { + RULE.getJerseyTest().target("/protected").request() + .header(HttpHeaders.AUTHORIZATION, "Basic c25lYWt5LWJhc3RhcmQ6YXNkZg==") + .get(String.class); + failBecauseExceptionWasNotThrown(NotAuthorizedException.class); + } catch (NotAuthorizedException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(401); + assertThat(e.getResponse().getHeaders().get(HttpHeaders.WWW_AUTHENTICATE)) + .containsOnly("Basic realm=\"SUPER SECRET STUFF\""); + } + + } + + @Test + public void testProtectedAdminEndpoint() { + String secret = RULE.getJerseyTest().target("/protected/admin").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Y2hpZWYtd2l6YXJkOnNlY3JldA==") + .get(String.class); + assertThat(secret).startsWith("Hey there, chief-wizard. It looks like you are an admin."); + } + + @Test + public void testProtectedAdminEndpointPrincipalIsNotAuthorized403() { + try { + RULE.getJerseyTest().target("/protected/admin").request() + .header(HttpHeaders.AUTHORIZATION, "Basic Z29vZC1ndXk6c2VjcmV0") + .get(String.class); + failBecauseExceptionWasNotThrown(ForbiddenException.class); + } catch (ForbiddenException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(403); + } + } +} diff --git a/dropwizard-example/src/test/resources/test-example.yml b/dropwizard-example/src/test/resources/test-example.yml new file mode 100644 index 00000000000..4632d2ab64d --- /dev/null +++ b/dropwizard-example/src/test/resources/test-example.yml @@ -0,0 +1,16 @@ +template: Hello, %s! +defaultName: Stranger + +database: + driverClass: org.h2.Driver + user: sa + password: sa + url: jdbc:h2:./target/test-example + +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 diff --git a/dropwizard-forms/pom.xml b/dropwizard-forms/pom.xml new file mode 100644 index 00000000000..f8e9dc14f49 --- /dev/null +++ b/dropwizard-forms/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-forms + jar + Dropwizard Multipart Form Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + org.glassfish.jersey.media + jersey-media-multipart + + + diff --git a/dropwizard-forms/src/main/java/io/dropwizard/forms/MultiPartBundle.java b/dropwizard-forms/src/main/java/io/dropwizard/forms/MultiPartBundle.java new file mode 100644 index 00000000000..89c13e8a678 --- /dev/null +++ b/dropwizard-forms/src/main/java/io/dropwizard/forms/MultiPartBundle.java @@ -0,0 +1,23 @@ +package io.dropwizard.forms; + +import io.dropwizard.Bundle; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import org.glassfish.jersey.media.multipart.MultiPartFeature; + +/** + * A {@link Bundle}, which enables the processing of multi-part form data by your application. + * + * @see org.glassfish.jersey.media.multipart.MultiPartFeature + * @see Jersey Multipart + */ +public class MultiPartBundle implements Bundle { + @Override + public void initialize(Bootstrap bootstrap) { + } + + @Override + public void run(Environment environment) { + environment.jersey().register(MultiPartFeature.class); + } +} diff --git a/dropwizard-forms/src/test/java/io/dropwizard/forms/MultiPartBundleTest.java b/dropwizard-forms/src/test/java/io/dropwizard/forms/MultiPartBundleTest.java new file mode 100644 index 00000000000..948a09699d2 --- /dev/null +++ b/dropwizard-forms/src/test/java/io/dropwizard/forms/MultiPartBundleTest.java @@ -0,0 +1,27 @@ +package io.dropwizard.forms; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.setup.Environment; +import org.glassfish.jersey.media.multipart.MultiPartFeature; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MultiPartBundleTest { + + @Test + public void testRun() throws Exception { + final Environment environment = new Environment( + "multipart-test", + Jackson.newObjectMapper(), + null, + new MetricRegistry(), + getClass().getClassLoader() + ); + + new MultiPartBundle().run(environment); + + assertThat(environment.jersey().getResourceConfig().getClasses()).contains(MultiPartFeature.class); + } +} \ No newline at end of file diff --git a/dropwizard-hibernate/pom.xml b/dropwizard-hibernate/pom.xml new file mode 100644 index 00000000000..b8f19644c82 --- /dev/null +++ b/dropwizard-hibernate/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-hibernate + Dropwizard Hibernate Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-db + + + com.fasterxml.jackson.datatype + jackson-datatype-hibernate5 + + + org.jadira.usertype + usertype.core + + + org.hibernate + hibernate-core + + + + io.dropwizard + dropwizard-testing + test + + + + org.hsqldb + hsqldb + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + test + + + 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 00000000000..7df09705ba5 --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/AbstractDAO.java @@ -0,0 +1,173 @@ +package io.dropwizard.hibernate; + +import io.dropwizard.util.Generics; +import org.hibernate.Criteria; +import org.hibernate.Hibernate; +import org.hibernate.HibernateException; +import org.hibernate.Query; +import org.hibernate.Session; +import org.hibernate.SessionFactory; + +import java.io.Serializable; +import java.util.List; + +import static java.util.Objects.requireNonNull; + +/** + * An abstract base class for Hibernate DAO classes. + * + * @param the class which this DAO manages + */ +public class AbstractDAO { + 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 = requireNonNull(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 }. + * + * @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(requireNonNull(queryName)); + } + + /** + * Returns the entity class managed by this DAO. + * + * @return the entity class managed by this DAO + */ + @SuppressWarnings("unchecked") + public Class getEntityClass() { + return (Class) 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) requireNonNull(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) requireNonNull(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 list(Criteria criteria) throws HibernateException { + return requireNonNull(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 list(Query query) throws HibernateException { + return requireNonNull(query).list(); + } + + /** + * Return the persistent instance of {@code } 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, requireNonNull(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). + *

    + * This operation cascades to associated instances if the association is mapped with + * cascade="save-update". + * + * @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(requireNonNull(entity)); + return entity; + } + + /** + * Force initialization of a proxy or persistent collection. + *

    + * 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 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 00000000000..4989aabf3f3 --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/HibernateBundle.java @@ -0,0 +1,97 @@ +package io.dropwizard.hibernate; + +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module; +import com.fasterxml.jackson.datatype.hibernate5.Hibernate5Module.Feature; +import com.google.common.collect.ImmutableList; +import io.dropwizard.Configuration; +import io.dropwizard.ConfiguredBundle; +import io.dropwizard.db.DatabaseConfiguration; +import io.dropwizard.db.PooledDataSourceFactory; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.dropwizard.util.Duration; +import org.hibernate.SessionFactory; + +public abstract class HibernateBundle implements ConfiguredBundle, DatabaseConfiguration { + public static final String DEFAULT_NAME = "hibernate"; + + private SessionFactory sessionFactory; + private boolean lazyLoadingEnabled = true; + + private final ImmutableList> entities; + private final SessionFactoryFactory sessionFactoryFactory; + + protected HibernateBundle(Class entity, Class... entities) { + this(ImmutableList.>builder().add(entity).add(entities).build(), + new SessionFactoryFactory()); + } + + protected HibernateBundle(ImmutableList> entities, + SessionFactoryFactory sessionFactoryFactory) { + this.entities = entities; + this.sessionFactoryFactory = sessionFactoryFactory; + } + + @Override + public final void initialize(Bootstrap bootstrap) { + bootstrap.getObjectMapper().registerModule(createHibernate5Module()); + } + + /** + * Override to configure the {@link Hibernate5Module}. + */ + protected Hibernate5Module createHibernate5Module() { + Hibernate5Module module = new Hibernate5Module(); + if (lazyLoadingEnabled) { + module.enable(Feature.FORCE_LAZY_LOADING); + } + return module; + } + + /** + * Override to configure the name of the bundle + * (It's used for the bundle health check and database pool metrics) + */ + protected String name() { + return DEFAULT_NAME; + } + + @Override + public final void run(T configuration, Environment environment) throws Exception { + final PooledDataSourceFactory dbConfig = getDataSourceFactory(configuration); + this.sessionFactory = sessionFactoryFactory.build(this, environment, dbConfig, entities, name()); + registerUnitOfWorkListerIfAbsent(environment).registerSessionFactory(name(), sessionFactory); + environment.healthChecks().register(name(), + new SessionFactoryHealthCheck( + environment.getHealthCheckExecutorService(), + dbConfig.getValidationQueryTimeout().orElse(Duration.seconds(5)), + sessionFactory, + dbConfig.getValidationQuery())); + } + + private UnitOfWorkApplicationListener registerUnitOfWorkListerIfAbsent(Environment environment) { + for (Object singleton : environment.jersey().getResourceConfig().getSingletons()) { + if (singleton instanceof UnitOfWorkApplicationListener) { + return (UnitOfWorkApplicationListener) singleton; + } + } + final UnitOfWorkApplicationListener listener = new UnitOfWorkApplicationListener(); + environment.jersey().register(listener); + return listener; + } + + public boolean isLazyLoadingEnabled() { + return lazyLoadingEnabled; + } + + public void setLazyLoadingEnabled(boolean lazyLoadingEnabled) { + this.lazyLoadingEnabled = lazyLoadingEnabled; + } + + 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 00000000000..cad5ed6400d --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/ScanningHibernateBundle.java @@ -0,0 +1,63 @@ +package io.dropwizard.hibernate; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableList.Builder; +import io.dropwizard.Configuration; +import org.glassfish.jersey.server.internal.scanning.AnnotationAcceptingListener; +import org.glassfish.jersey.server.internal.scanning.PackageNamesScanner; + +import javax.persistence.Entity; +import java.io.IOException; +import java.io.InputStream; + +/** + * Extension of HibernateBundle that scans given package for entities instead of giving them by hand. + */ +public abstract class ScanningHibernateBundle extends HibernateBundle { + /** + * @param pckg string with package containing Hibernate entities (classes annotated with Hibernate {@code @Entity} + * annotation) e. g. {@code com.codahale.fake.db.directory.entities} + */ + protected ScanningHibernateBundle(String pckg) { + this(pckg, new SessionFactoryFactory()); + } + + protected ScanningHibernateBundle(String pckg, SessionFactoryFactory sessionFactoryFactory) { + this(new String[]{pckg}, sessionFactoryFactory); + } + + protected ScanningHibernateBundle(String[] pckgs, SessionFactoryFactory sessionFactoryFactory) { + super(findEntityClassesFromDirectory(pckgs), sessionFactoryFactory); + } + + /** + * Method scanning given directory for classes containing Hibernate @Entity annotation + * + * @param pckgs string array with packages 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> findEntityClassesFromDirectory(String[] pckgs) { + @SuppressWarnings("unchecked") + final AnnotationAcceptingListener asl = new AnnotationAcceptingListener(Entity.class); + final PackageNamesScanner scanner = new PackageNamesScanner(pckgs, true); + + while (scanner.hasNext()) { + final String next = scanner.next(); + if (asl.accept(next)) { + try (final InputStream in = scanner.open()) { + asl.process(next, in); + } catch (IOException e) { + throw new RuntimeException("AnnotationAcceptingListener failed to process scanned resource: " + next); + } + } + } + + final Builder> builder = ImmutableList.builder(); + 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 00000000000..117363a840c --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryFactory.java @@ -0,0 +1,111 @@ +package io.dropwizard.hibernate; + +import io.dropwizard.db.ManagedDataSource; +import io.dropwizard.db.PooledDataSourceFactory; +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; +import java.util.TreeSet; + +public class SessionFactoryFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(SessionFactoryFactory.class); + private static final String DEFAULT_NAME = "hibernate"; + + public SessionFactory build(HibernateBundle bundle, + Environment environment, + PooledDataSourceFactory dbConfig, + List> entities) { + return build(bundle, environment, dbConfig, entities, DEFAULT_NAME); + } + + public SessionFactory build(HibernateBundle bundle, + Environment environment, + PooledDataSourceFactory dbConfig, + List> entities, + String name) { + final ManagedDataSource dataSource = dbConfig.build(environment.metrics(), name); + return build(bundle, environment, dbConfig, dataSource, entities); + } + + public SessionFactory build(HibernateBundle bundle, + Environment environment, + PooledDataSourceFactory dbConfig, + ManagedDataSource dataSource, + List> entities) { + 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 properties) { + final DatasourceConnectionProviderImpl connectionProvider = new DatasourceConnectionProviderImpl(); + connectionProvider.setDataSource(dataSource); + connectionProvider.configure(properties); + return connectionProvider; + } + + private SessionFactory buildSessionFactory(HibernateBundle bundle, + PooledDataSourceFactory dbConfig, + ConnectionProvider connectionProvider, + Map properties, + List> 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 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(configuration.getProperties()) + .build(); + + configure(configuration, registry); + + return configuration.buildSessionFactory(registry); + } + + protected void configure(Configuration configuration, ServiceRegistry registry) { + } + + private void addAnnotatedClasses(Configuration configuration, + Iterable> entities) { + final SortedSet entityClasses = new TreeSet<>(); + 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 00000000000..8dfc1e3d719 --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryHealthCheck.java @@ -0,0 +1,59 @@ +package io.dropwizard.hibernate; + +import com.codahale.metrics.health.HealthCheck; +import com.google.common.util.concurrent.MoreExecutors; +import io.dropwizard.db.TimeBoundHealthCheck; +import io.dropwizard.util.Duration; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; + +import java.util.concurrent.ExecutorService; + +public class SessionFactoryHealthCheck extends HealthCheck { + private final SessionFactory sessionFactory; + private final String validationQuery; + private final TimeBoundHealthCheck timeBoundHealthCheck; + + public SessionFactoryHealthCheck(SessionFactory sessionFactory, + String validationQuery) { + this(MoreExecutors.newDirectExecutorService(), Duration.seconds(0), sessionFactory, validationQuery); + } + + public SessionFactoryHealthCheck(ExecutorService executorService, + Duration duration, + SessionFactory sessionFactory, + String validationQuery) { + this.sessionFactory = sessionFactory; + this.validationQuery = validationQuery; + this.timeBoundHealthCheck = new TimeBoundHealthCheck(executorService, duration); + } + + + public SessionFactory getSessionFactory() { + return sessionFactory; + } + + public String getValidationQuery() { + return validationQuery; + } + + @Override + protected Result check() throws Exception { + return timeBoundHealthCheck.check(() -> { + try (Session session = sessionFactory.openSession()) { + final Transaction txn = session.beginTransaction(); + try { + session.createSQLQuery(validationQuery).list(); + txn.commit(); + } catch (Exception e) { + if (txn.getStatus().canRollback()) { + txn.rollback(); + } + throw e; + } + } + 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 00000000000..b9d71505d11 --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/SessionFactoryManager.java @@ -0,0 +1,32 @@ +package io.dropwizard.hibernate; + +import com.google.common.annotations.VisibleForTesting; +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; + } + + @VisibleForTesting + ManagedDataSource getDataSource() { + return dataSource; + } + + @Override + public void start() throws Exception { + dataSource.start(); + } + + @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 00000000000..50d5ee49e7d --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWork.java @@ -0,0 +1,59 @@ +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. + *

    To be used outside Jersey, one need to create a proxy of the component with the + * annotated method. methodMap = new HashMap<>(); + private Map sessionFactories = new HashMap<>(); + + public UnitOfWorkApplicationListener() { + } + + /** + * Construct an application event listener using the given name and session factory. + * + *

    + * When using this constructor, the {@link UnitOfWorkApplicationListener} + * should be added to a Jersey {@code ResourceConfig} as a singleton. + * + * @param name a name of a Hibernate bundle + * @param sessionFactory a {@link SessionFactory} + */ + public UnitOfWorkApplicationListener(String name, SessionFactory sessionFactory) { + registerSessionFactory(name, sessionFactory); + } + + /** + * Register a session factory with the given name. + * + * @param name a name of a Hibernate bundle + * @param sessionFactory a {@link SessionFactory} + */ + public void registerSessionFactory(String name, SessionFactory sessionFactory) { + sessionFactories.put(name, sessionFactory); + } + + private static class UnitOfWorkEventListener implements RequestEventListener { + private final Map methodMap; + private final UnitOfWorkAspect unitOfWorkAspect; + + UnitOfWorkEventListener(Map methodMap, + Map sessionFactories) { + this.methodMap = methodMap; + unitOfWorkAspect = new UnitOfWorkAspect(sessionFactories); + } + + @Override + public void onEvent(RequestEvent event) { + final RequestEvent.Type eventType = event.getType(); + if (eventType == RequestEvent.Type.RESOURCE_METHOD_START) { + UnitOfWork unitOfWork = methodMap.get(event.getUriInfo() + .getMatchedResourceMethod().getInvocable().getDefinitionMethod()); + unitOfWorkAspect.beforeStart(unitOfWork); + } else if (eventType == RequestEvent.Type.RESP_FILTERS_START) { + try { + unitOfWorkAspect.afterEnd(); + } catch (Exception e) { + throw new MappableException(e); + } + } else if (eventType == RequestEvent.Type.ON_EXCEPTION) { + unitOfWorkAspect.onError(); + } else if (eventType == RequestEvent.Type.FINISHED) { + unitOfWorkAspect.onFinish(); + } + } + } + + @Override + public void onEvent(ApplicationEvent event) { + if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) { + for (Resource resource : event.getResourceModel().getResources()) { + for (ResourceMethod method : resource.getAllMethods()) { + registerUnitOfWorkAnnotations(method); + } + + for (Resource childResource : resource.getChildResources()) { + for (ResourceMethod method : childResource.getAllMethods()) { + registerUnitOfWorkAnnotations(method); + } + } + } + } + } + + @Override + public RequestEventListener onRequest(RequestEvent event) { + return new UnitOfWorkEventListener(methodMap, sessionFactories); + } + + private void registerUnitOfWorkAnnotations(ResourceMethod method) { + UnitOfWork annotation = method.getInvocable().getDefinitionMethod().getAnnotation(UnitOfWork.class); + + if (annotation == null) { + annotation = method.getInvocable().getHandlingMethod().getAnnotation(UnitOfWork.class); + } + + if (annotation != null) { + this.methodMap.put(method.getInvocable().getDefinitionMethod(), annotation); + } + + } +} diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkAspect.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkAspect.java new file mode 100644 index 00000000000..0ea92b01291 --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkAspect.java @@ -0,0 +1,146 @@ +package io.dropwizard.hibernate; + +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.context.internal.ManagedSessionContext; + +import java.util.Map; + +/** + * An aspect providing operations around a method with the {@link UnitOfWork} annotation. + * It opens a Hibernate session and optionally a transaction. + *

    It should be created for every invocation of the method.

    + *

    Usage :

    + *
    + * {@code
    + *   UnitOfWorkProxyFactory unitOfWorkProxyFactory = ...
    + *   UnitOfWork unitOfWork = ...         // get annotation from method.
    + *
    + *   UnitOfWorkAspect aspect = unitOfWorkProxyFactory.newAspect();
    + *   try {
    + *     aspect.beforeStart(unitOfWork);
    + *     ...                               // perform business logic.
    + *     aspect.afterEnd();
    + *   } catch (Exception e) {
    + *     aspect.onError();
    + *     throw e;
    + *   } finally {
    + *     aspect.onFinish();
    + *   }
    + * }
    + * 
    + */ +public class UnitOfWorkAspect { + + private final Map sessionFactories; + + UnitOfWorkAspect(Map sessionFactories) { + this.sessionFactories = sessionFactories; + } + + // Context variables + private UnitOfWork unitOfWork; + private Session session; + private SessionFactory sessionFactory; + + public void beforeStart(UnitOfWork unitOfWork) { + if (unitOfWork == null) { + return; + } + this.unitOfWork = unitOfWork; + + sessionFactory = sessionFactories.get(unitOfWork.value()); + if (sessionFactory == null) { + // If the user didn't specify the name of a session factory, + // and we have only one registered, we can assume that it's the right one. + if (unitOfWork.value().equals(HibernateBundle.DEFAULT_NAME) && sessionFactories.size() == 1) { + sessionFactory = sessionFactories.values().iterator().next(); + } else { + throw new IllegalArgumentException("Unregistered Hibernate bundle: '" + unitOfWork.value() + "'"); + } + } + session = sessionFactory.openSession(); + try { + configureSession(); + ManagedSessionContext.bind(session); + beginTransaction(); + } catch (Throwable th) { + session.close(); + session = null; + ManagedSessionContext.unbind(sessionFactory); + throw th; + } + } + + public void afterEnd() { + if (session == null) { + return; + } + + try { + commitTransaction(); + } catch (Exception e) { + rollbackTransaction(); + throw e; + } + // We should not close the session to let the lazy loading work during serializing a response to the client. + // If the response successfully serialized, then the session will be closed by the `onFinish` method + } + + public void onError() { + if (session == null) { + return; + } + + try { + rollbackTransaction(); + } finally { + onFinish(); + } + } + + public void onFinish() { + try { + if (session != null) { + session.close(); + } + } finally { + session = null; + ManagedSessionContext.unbind(sessionFactory); + } + } + + private void configureSession() { + session.setDefaultReadOnly(unitOfWork.readOnly()); + session.setCacheMode(unitOfWork.cacheMode()); + session.setFlushMode(unitOfWork.flushMode()); + } + + private void beginTransaction() { + if (!unitOfWork.transactional()) { + return; + } + session.beginTransaction(); + } + + private void rollbackTransaction() { + if (!unitOfWork.transactional()) { + return; + } + final Transaction txn = session.getTransaction(); + if (txn != null && txn.getStatus().canRollback()) { + txn.rollback(); + } + } + + private void commitTransaction() { + if (!unitOfWork.transactional()) { + return; + } + final Transaction txn = session.getTransaction(); + if (txn != null && txn.getStatus().canRollback()) { + txn.commit(); + } + } +} diff --git a/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkAwareProxyFactory.java b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkAwareProxyFactory.java new file mode 100644 index 00000000000..66681cadf7b --- /dev/null +++ b/dropwizard-hibernate/src/main/java/io/dropwizard/hibernate/UnitOfWorkAwareProxyFactory.java @@ -0,0 +1,107 @@ +package io.dropwizard.hibernate; + +import com.google.common.collect.ImmutableMap; +import javassist.util.proxy.Proxy; +import javassist.util.proxy.ProxyFactory; +import org.hibernate.SessionFactory; + +import java.lang.reflect.InvocationTargetException; + +/** + * A factory for creating proxies for components that use Hibernate data access objects + * outside Jersey resources. + *

    A created proxy will be aware of the {@link UnitOfWork} annotation + * on the original class methods and will open a Hibernate session with a transaction + * around them.

    + */ +public class UnitOfWorkAwareProxyFactory { + + private final ImmutableMap sessionFactories; + + public UnitOfWorkAwareProxyFactory(String name, SessionFactory sessionFactory) { + sessionFactories = ImmutableMap.of(name, sessionFactory); + } + + public UnitOfWorkAwareProxyFactory(HibernateBundle... bundles) { + final ImmutableMap.Builder sessionFactoriesBuilder = ImmutableMap.builder(); + for (HibernateBundle bundle : bundles) { + sessionFactoriesBuilder.put(bundle.name(), bundle.getSessionFactory()); + } + sessionFactories = sessionFactoriesBuilder.build(); + } + + + /** + * Creates a new @UnitOfWork aware proxy of a class with the default constructor. + * + * @param clazz the specified class definition + * @param the type of the class + * @return a new proxy + */ + public T create(Class clazz) { + return create(clazz, new Class[]{}, new Object[]{}); + } + + /** + * Creates a new @UnitOfWork aware proxy of a class with an one-parameter constructor. + * + * @param clazz the specified class definition + * @param constructorParamType the type of the constructor parameter + * @param constructorArguments the argument passed to the constructor + * @param the type of the class + * @return a new proxy + */ + public T create(Class clazz, Class constructorParamType, Object constructorArguments) { + return create(clazz, new Class[]{constructorParamType}, new Object[]{constructorArguments}); + } + + /** + * Creates a new @UnitOfWork aware proxy of a class with a complex constructor. + * + * @param clazz the specified class definition + * @param constructorParamTypes the types of the constructor parameters + * @param constructorArguments the arguments passed to the constructor + * @param the type of the class + * @return a new proxy + */ + @SuppressWarnings("unchecked") + public T create(Class clazz, Class[] constructorParamTypes, Object[] constructorArguments) { + final ProxyFactory factory = new ProxyFactory(); + factory.setSuperclass(clazz); + + try { + final Proxy proxy = (Proxy) (constructorParamTypes.length == 0 ? + factory.createClass().getConstructor().newInstance() : + factory.create(constructorParamTypes, constructorArguments)); + proxy.setHandler((self, overridden, proceed, args) -> { + final UnitOfWork unitOfWork = overridden.getAnnotation(UnitOfWork.class); + final UnitOfWorkAspect unitOfWorkAspect = newAspect(); + try { + unitOfWorkAspect.beforeStart(unitOfWork); + Object result = proceed.invoke(self, args); + unitOfWorkAspect.afterEnd(); + return result; + } catch (InvocationTargetException e) { + unitOfWorkAspect.onError(); + throw e.getCause(); + } catch (Exception e) { + unitOfWorkAspect.onError(); + throw e; + } finally { + unitOfWorkAspect.onFinish(); + } + }); + return (T) proxy; + } catch (NoSuchMethodException | InstantiationException | IllegalAccessException | + InvocationTargetException e) { + throw new IllegalStateException("Unable to create a proxy for the class '" + clazz + "'", e); + } + } + + /** + * @return a new + */ + public UnitOfWorkAspect newAspect() { + return new UnitOfWorkAspect(sessionFactories); + } +} 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 00000000000..39a0ab4ee25 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/AbstractDAOTest.java @@ -0,0 +1,188 @@ +package io.dropwizard.hibernate; + +import com.google.common.collect.ImmutableList; +import org.hibernate.Criteria; +import org.hibernate.HibernateException; +import org.hibernate.Query; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +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.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AbstractDAOTest { + private static class MockDAO extends AbstractDAO { + 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 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 list(Criteria criteria) throws HibernateException { + return super.list(criteria); + } + + @Override + public List 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 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/DataExceptionMapper.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/DataExceptionMapper.java new file mode 100644 index 00000000000..cf74b40d622 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/DataExceptionMapper.java @@ -0,0 +1,26 @@ +package io.dropwizard.hibernate; + +import io.dropwizard.jersey.errors.ErrorMessage; +import org.hibernate.exception.DataException; +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; + +@Provider +public class DataExceptionMapper implements ExceptionMapper { + + private static final Logger LOGGER = LoggerFactory.getLogger(DataException.class); + + @Override + public Response toResponse(DataException e) { + LOGGER.error("Hibernate error", e); + String message = e.getCause().getMessage().contains("EMAIL") ? "Wrong email" : "Wrong input"; + + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorMessage(Response.Status.BAD_REQUEST.getStatusCode(), message)) + .build(); + } +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/Dog.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/Dog.java new file mode 100644 index 00000000000..c713021063f --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/Dog.java @@ -0,0 +1,41 @@ +package io.dropwizard.hibernate; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "dogs") +public class Dog { + @Id + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner") + private Person owner; + + @JsonProperty + public String getName() { + return name; + } + + @JsonProperty + public void setName(String name) { + this.name = name; + } + + @JsonProperty + public Person getOwner() { + return owner; + } + + @JsonProperty + public void setOwner(Person owner) { + this.owner = owner; + } +} 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 00000000000..3208c575a52 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/HibernateBundleTest.java @@ -0,0 +1,137 @@ +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.hibernate5.Hibernate5Module; +import com.google.common.collect.ImmutableList; +import io.dropwizard.Configuration; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jersey.DropwizardResourceConfig; +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.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyList; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class HibernateBundleTest { + private final DataSourceFactory dbConfig = new DataSourceFactory(); + private final ImmutableList> entities = ImmutableList.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 bundle = new HibernateBundle(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(jerseyEnvironment.getResourceConfig()).thenReturn(new DropwizardResourceConfig()); + + + when(factory.build(eq(bundle), + any(Environment.class), + any(DataSourceFactory.class), + anyList(), + eq("hibernate"))).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 captor = ArgumentCaptor.forClass(Module.class); + verify(objectMapperFactory).registerModule(captor.capture()); + + assertThat(captor.getValue()).isInstanceOf(Hibernate5Module.class); + } + + @Test + public void buildsASessionFactory() throws Exception { + bundle.run(configuration, environment); + + verify(factory).build(bundle, environment, dbConfig, entities, "hibernate"); + } + + @Test + public void registersATransactionalListener() throws Exception { + bundle.run(configuration, environment); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(UnitOfWorkApplicationListener.class); + verify(jerseyEnvironment).register(captor.capture()); + } + + @Test + public void registersASessionFactoryHealthCheck() throws Exception { + dbConfig.setValidationQuery("SELECT something"); + + bundle.run(configuration, environment); + + final ArgumentCaptor 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 + @SuppressWarnings("unchecked") + public void registersACustomNameOfHealthCheckAndDBPoolMetrics() throws Exception { + final HibernateBundle customBundle = new HibernateBundle(entities, factory) { + @Override + public DataSourceFactory getDataSourceFactory(Configuration configuration) { + return dbConfig; + } + + @Override + protected String name() { + return "custom-hibernate"; + } + }; + when(factory.build(eq(customBundle), + any(Environment.class), + any(DataSourceFactory.class), + anyList(), + eq("custom-hibernate"))).thenReturn(sessionFactory); + + customBundle.run(configuration, environment); + + final ArgumentCaptor captor = + ArgumentCaptor.forClass(SessionFactoryHealthCheck.class); + verify(healthChecks).register(eq("custom-hibernate"), captor.capture()); + } + + @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 00000000000..8a5ce053e9d --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/JerseyIntegrationTest.java @@ -0,0 +1,206 @@ +package io.dropwizard.hibernate; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.errors.ErrorMessage; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.setup.Environment; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +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.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class JerseyIntegrationTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + public static class PersonDAO extends AbstractDAO { + public PersonDAO(SessionFactory sessionFactory) { + super(sessionFactory); + } + + public Optional findByName(String name) { + return Optional.ofNullable(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 find(@PathParam("name") String name) { + return dao.findByName(name); + } + + @PUT + @UnitOfWork + public void save(Person person) { + dao.persist(person); + } + } + + private SessionFactory sessionFactory; + + @Override + @After + public void tearDown() throws Exception { + super.tearDown(); + + if (sessionFactory != null) { + sessionFactory.close(); + } + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + + 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() + "?hsqldb.translate_dti_types=false"); + dbConfig.setUser("sa"); + dbConfig.setDriverClass("org.hsqldb.jdbcDriver"); + dbConfig.setValidationQuery("SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS"); + + this.sessionFactory = factory.build(bundle, + environment, + dbConfig, + ImmutableList.of(Person.class)); + + try (Session session = sessionFactory.openSession()) { + session.createSQLQuery("DROP TABLE people IF EXISTS").executeUpdate(); + session.createSQLQuery( + "CREATE TABLE people (name varchar(100) primary key, email varchar(16), birthday timestamp with time zone)") + .executeUpdate(); + session.createSQLQuery( + "INSERT INTO people VALUES ('Coda', 'coda@example.com', '1979-01-02 00:22:00+0:00')") + .executeUpdate(); + } + + final DropwizardResourceConfig config = DropwizardResourceConfig.forTesting(new MetricRegistry()); + config.register(new UnitOfWorkApplicationListener("hr-db", sessionFactory)); + config.register(new PersonResource(new PersonDAO(sessionFactory))); + config.register(new JacksonMessageBodyProvider(Jackson.newObjectMapper())); + config.register(new DataExceptionMapper()); + + return config; + } + + @Override + protected void configureClient(ClientConfig config) { + config.register(new JacksonMessageBodyProvider(Jackson.newObjectMapper())); + } + + @Test + public void findsExistingData() throws Exception { + final Person coda = target("/people/Coda").request(MediaType.APPLICATION_JSON).get(Person.class); + + assertThat(coda.getName()) + .isEqualTo("Coda"); + + assertThat(coda.getEmail()) + .isEqualTo("coda@example.com"); + + assertThat(coda.getBirthday()) + .isEqualTo(new DateTime(1979, 1, 2, 0, 22, DateTimeZone.UTC)); + } + + @Test + public void doesNotFindMissingData() throws Exception { + try { + target("/people/Poof").request(MediaType.APPLICATION_JSON) + .get(Person.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(404); + } + } + + @Test + public void createsNewData() throws Exception { + final Person person = new Person(); + person.setName("Hank"); + person.setEmail("hank@example.com"); + person.setBirthday(new DateTime(1971, 3, 14, 19, 12, DateTimeZone.UTC)); + + target("/people/Hank").request().put(Entity.entity(person, MediaType.APPLICATION_JSON)); + + final Person hank = target("/people/Hank") + .request(MediaType.APPLICATION_JSON) + .get(Person.class); + + assertThat(hank.getName()) + .isEqualTo("Hank"); + + assertThat(hank.getEmail()) + .isEqualTo("hank@example.com"); + + assertThat(hank.getBirthday()) + .isEqualTo(person.getBirthday()); + } + + + @Test + public void testSqlExceptionIsHandled() throws Exception { + final Person person = new Person(); + person.setName("Jeff"); + person.setEmail("jeff.hammersmith@targetprocessinc.com"); + person.setBirthday(new DateTime(1984, 2, 11, 0, 0, DateTimeZone.UTC)); + + final Response response = target("/people/Jeff").request(). + put(Entity.entity(person, MediaType.APPLICATION_JSON)); + + assertThat(response.getStatusInfo()).isEqualTo(Response.Status.BAD_REQUEST); + assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(response.readEntity(ErrorMessage.class).getMessage()).isEqualTo("Wrong email"); + } +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/LazyLoadingTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/LazyLoadingTest.java new file mode 100644 index 00000000000..a463f0d18ef --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/LazyLoadingTest.java @@ -0,0 +1,210 @@ +package io.dropwizard.hibernate; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.db.PooledDataSourceFactory; +import io.dropwizard.jersey.errors.ErrorMessage; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; +import io.dropwizard.testing.ResourceHelpers; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.hibernate.FlushMode; +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.exception.ConstraintViolationException; +import org.junit.After; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.PUT; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import java.util.Optional; + +import static java.util.Objects.requireNonNull; +import static org.assertj.core.api.Assertions.assertThat; + +public class LazyLoadingTest { + + public static class TestConfiguration extends Configuration { + + DataSourceFactory dataSource = new DataSourceFactory(); + + TestConfiguration(@JsonProperty("dataSource") DataSourceFactory dataSource) { + this.dataSource = dataSource; + } + } + + public static class TestApplication extends io.dropwizard.Application { + final HibernateBundle hibernate = new HibernateBundle( + ImmutableList.of(Person.class, Dog.class), new SessionFactoryFactory()) { + @Override + public PooledDataSourceFactory getDataSourceFactory(TestConfiguration configuration) { + return configuration.dataSource; + } + }; + + @Override + public void initialize(Bootstrap bootstrap) { + bootstrap.addBundle(hibernate); + } + + @Override + public void run(TestConfiguration configuration, Environment environment) throws Exception { + + final SessionFactory sessionFactory = hibernate.getSessionFactory(); + initDatabase(sessionFactory); + + environment.jersey().register(new UnitOfWorkApplicationListener("hr-db", sessionFactory)); + environment.jersey().register(new DogResource(new DogDAO(sessionFactory))); + environment.jersey().register(new ConstraintViolationExceptionMapper()); + } + + private void initDatabase(SessionFactory sessionFactory) { + try (Session session = sessionFactory.openSession()) { + session.createSQLQuery( + "CREATE TABLE people (name varchar(100) primary key, email varchar(16), birthday timestamp with time zone)") + .executeUpdate(); + session.createSQLQuery( + "INSERT INTO people VALUES ('Coda', 'coda@example.com', '1979-01-02 00:22:00+0:00')") + .executeUpdate(); + session.createSQLQuery( + "CREATE TABLE dogs (name varchar(100) primary key, owner varchar(100), CONSTRAINT fk_owner FOREIGN KEY (owner) REFERENCES people(name))") + .executeUpdate(); + session.createSQLQuery( + "INSERT INTO dogs VALUES ('Raf', 'Coda')") + .executeUpdate(); + } + } + } + + public static class TestApplicationWithDisabledLazyLoading extends TestApplication { + @Override + public void initialize(Bootstrap bootstrap) { + hibernate.setLazyLoadingEnabled(false); + bootstrap.addBundle(hibernate); + } + } + + public static class DogDAO extends AbstractDAO { + DogDAO(SessionFactory sessionFactory) { + super(sessionFactory); + } + + Optional findByName(String name) { + return Optional.ofNullable(get(name)); + } + + Dog create(Dog dog) throws HibernateException { + currentSession().setFlushMode(FlushMode.COMMIT); + currentSession().save(requireNonNull(dog)); + return dog; + } + } + + @Path("/dogs/{name}") + @Produces(MediaType.APPLICATION_JSON) + public static class DogResource { + private final DogDAO dao; + + DogResource(DogDAO dao) { + this.dao = dao; + } + + @GET + @UnitOfWork(readOnly = true) + public Optional find(@PathParam("name") String name) { + return dao.findByName(name); + } + + @PUT + @UnitOfWork + public void create(Dog dog) { + dao.create(dog); + } + } + + public static class ConstraintViolationExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ConstraintViolationException e) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(new ErrorMessage(Response.Status.BAD_REQUEST.getStatusCode(), e.getCause().getMessage())) + .build(); + } + } + + private DropwizardTestSupport dropwizardTestSupport; + private Client client = new JerseyClientBuilder().build(); + + public void setup(Class> applicationClass) { + dropwizardTestSupport = new DropwizardTestSupport<>(applicationClass, ResourceHelpers.resourceFilePath("hibernate-integration-test.yaml"), + ConfigOverride.config("dataSource.url", "jdbc:hsqldb:mem:DbTest" + System.nanoTime() + "?hsqldb.translate_dti_types=false")); + dropwizardTestSupport.before(); + } + + @After + public void tearDown() { + dropwizardTestSupport.after(); + client.close(); + } + + private String getUrlPrefix() { + return "http://localhost:" + dropwizardTestSupport.getLocalPort(); + } + + @Test + public void serialisesLazyObjectWhenEnabled() throws Exception { + setup(TestApplication.class); + + final Dog raf = client.target(getUrlPrefix() + "/dogs/Raf").request(MediaType.APPLICATION_JSON).get(Dog.class); + + assertThat(raf.getName()) + .isEqualTo("Raf"); + + assertThat(raf.getOwner()) + .isNotNull(); + + assertThat(raf.getOwner().getName()) + .isEqualTo("Coda"); + } + + @Test + public void sendsNullWhenDisabled() throws Exception { + setup(TestApplicationWithDisabledLazyLoading.class); + + final Dog raf = client.target(getUrlPrefix() + "/dogs/Raf").request(MediaType.APPLICATION_JSON).get(Dog.class); + + assertThat(raf.getName()) + .isEqualTo("Raf"); + + assertThat(raf.getOwner()) + .isNull(); + } + + @Test + public void returnsErrorsWhenEnabled() throws Exception { + setup(TestApplication.class); + + final Dog raf = new Dog(); + raf.setName("Raf"); + + // Raf already exists so this should cause a primary key constraint violation + final Response response = client.target(getUrlPrefix() + "/dogs/Raf").request().put(Entity.entity(raf, MediaType.APPLICATION_JSON)); + assertThat(response.getStatusInfo()).isEqualTo(Response.Status.BAD_REQUEST); + assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(response.readEntity(ErrorMessage.class).getMessage()).contains("unique constraint", "table: DOGS"); + } +} 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 00000000000..478462b0a08 --- /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; + +@Entity +@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 00000000000..5a529b1b035 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/ScanningHibernateBundleTest.java @@ -0,0 +1,37 @@ +package io.dropwizard.hibernate; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class ScanningHibernateBundleTest { + + @Test + public void testFindEntityClassesFromDirectory() { + //given + String packageWithEntities = "io.dropwizard.hibernate.fake.entities.pckg"; + //when + ImmutableList> findEntityClassesFromDirectory = + ScanningHibernateBundle.findEntityClassesFromDirectory(new String[]{packageWithEntities}); + + //then + assertFalse(findEntityClassesFromDirectory.isEmpty()); + assertEquals(4, findEntityClassesFromDirectory.size()); + } + + @Test + public void testFindEntityClassesFromMultipleDirectories() { + //given + String packageWithEntities = "io.dropwizard.hibernate.fake.entities.pckg"; + String packageWithEntities2 = "io.dropwizard.hibernate.fake2.entities.pckg"; + //when + ImmutableList> findEntityClassesFromDirectory = + ScanningHibernateBundle.findEntityClassesFromDirectory(new String[]{packageWithEntities, packageWithEntities2}); + + //then + assertFalse(findEntityClassesFromDirectory.isEmpty()); + assertEquals(8, 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 00000000000..1f9a96d524b --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryFactoryTest.java @@ -0,0 +1,148 @@ +package io.dropwizard.hibernate; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.db.ManagedPooledDataSource; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.setup.Environment; +import org.hibernate.EmptyInterceptor; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.cfg.Configuration; +import org.hibernate.service.ServiceRegistry; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class SessionFactoryFactoryTest { + static { + BootstrapLogging.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 MetricRegistry metricRegistry = new MetricRegistry(); + + private DataSourceFactory config; + private SessionFactory sessionFactory; + + @Before + public void setUp() throws Exception { + when(environment.metrics()).thenReturn(metricRegistry); + when(environment.lifecycle()).thenReturn(lifecycleEnvironment); + + config = new DataSourceFactory(); + 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"); + + final ImmutableMap properties = ImmutableMap.of( + "hibernate.show_sql", "true", + "hibernate.dialect", "org.hibernate.dialect.HSQLDialect"); + config.setProperties(properties); + } + + @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 setsPoolName() { + build(); + + ArgumentCaptor sessionFactoryManager = ArgumentCaptor.forClass(SessionFactoryManager.class); + verify(lifecycleEnvironment).manage(sessionFactoryManager.capture()); + ManagedPooledDataSource dataSource = (ManagedPooledDataSource) sessionFactoryManager.getValue().getDataSource(); + assertThat(dataSource.getPool().getName()).isEqualTo("hibernate"); + } + + @Test + public void setsACustomPoolName() { + this.sessionFactory = factory.build(bundle, environment, config, + ImmutableList.of(Person.class), "custom-hibernate-db"); + + ArgumentCaptor sessionFactoryManager = ArgumentCaptor.forClass(SessionFactoryManager.class); + verify(lifecycleEnvironment).manage(sessionFactoryManager.capture()); + ManagedPooledDataSource dataSource = (ManagedPooledDataSource) sessionFactoryManager.getValue().getDataSource(); + assertThat(dataSource.getPool().getName()).isEqualTo("custom-hibernate-db"); + } + + @Test + public void buildsAWorkingSessionFactory() throws Exception { + build(); + + try (Session session = sessionFactory.openSession()) { + session.createSQLQuery("DROP TABLE people IF EXISTS").executeUpdate(); + session.createSQLQuery("CREATE TABLE people (name varchar(100) primary key, email varchar(100), birthday timestamp(0))").executeUpdate(); + session.createSQLQuery("INSERT INTO people VALUES ('Coda', 'coda@example.com', '1979-01-02 00:22:00')").executeUpdate(); + + final Person entity = session.get(Person.class, "Coda"); + + assertThat(entity.getName()) + .isEqualTo("Coda"); + + assertThat(entity.getEmail()) + .isEqualTo("coda@example.com"); + + assertThat(entity.getBirthday().toDateTime(DateTimeZone.UTC)) + .isEqualTo(new DateTime(1979, 1, 2, 0, 22, DateTimeZone.UTC)); + } + } + + @Test + public void configureRunsBeforeSessionFactoryCreation() { + final SessionFactoryFactory customFactory = new SessionFactoryFactory() { + @Override + protected void configure(Configuration configuration, ServiceRegistry registry) { + super.configure(configuration, registry); + configuration.setInterceptor(EmptyInterceptor.INSTANCE); + } + }; + sessionFactory = customFactory.build(bundle, + environment, + config, + ImmutableList.of(Person.class)); + + assertThat(sessionFactory.getSessionFactoryOptions().getInterceptor()).isSameAs(EmptyInterceptor.INSTANCE); + } + + private void build() { + this.sessionFactory = factory.build(bundle, + environment, + config, + ImmutableList.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 00000000000..635bd92b7ff --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryHealthCheckTest.java @@ -0,0 +1,88 @@ +package io.dropwizard.hibernate; + +import com.codahale.metrics.health.HealthCheck; +import org.hibernate.HibernateException; +import org.hibernate.SQLQuery; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.junit.Test; +import org.mockito.InOrder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.resource.transaction.spi.TransactionStatus.ACTIVE; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@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.getStatus()).thenReturn(ACTIVE); + + 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 00000000000..b295f5db09e --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/SessionFactoryManagerTest.java @@ -0,0 +1,35 @@ +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(); + } + + @Test + public void startsTheDataSourceOnStarting() throws Exception { + manager.start(); + + verify(dataSource).start(); + } +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkApplicationListenerTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkApplicationListenerTest.java new file mode 100644 index 00000000000..0b883d4f290 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkApplicationListenerTest.java @@ -0,0 +1,357 @@ +package io.dropwizard.hibernate; + +import org.glassfish.jersey.server.ExtendedUriInfo; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.model.ResourceModel; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.hibernate.CacheMode; +import org.hibernate.FlushMode; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.Transaction; +import org.hibernate.context.internal.ManagedSessionContext; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InOrder; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.resource.transaction.spi.TransactionStatus.ACTIVE; +import static org.hibernate.resource.transaction.spi.TransactionStatus.NOT_ACTIVE; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +@SuppressWarnings("HibernateResourceOpenedButNotSafelyClosed") +public class UnitOfWorkApplicationListenerTest { + private final SessionFactory sessionFactory = mock(SessionFactory.class); + private final SessionFactory analyticsSessionFactory = mock(SessionFactory.class); + private final UnitOfWorkApplicationListener listener = new UnitOfWorkApplicationListener(); + private final ApplicationEvent appEvent = mock(ApplicationEvent.class); + private final ExtendedUriInfo uriInfo = mock(ExtendedUriInfo.class); + + private final RequestEvent requestStartEvent = mock(RequestEvent.class); + private final RequestEvent requestMethodStartEvent = mock(RequestEvent.class); + private final RequestEvent responseFiltersStartEvent = mock(RequestEvent.class); + private final RequestEvent responseFinishedEvent = mock(RequestEvent.class); + private final RequestEvent requestMethodExceptionEvent = mock(RequestEvent.class); + private final Session session = mock(Session.class); + private final Session analyticsSession = mock(Session.class); + private final Transaction transaction = mock(Transaction.class); + private final Transaction analyticsTransaction = mock(Transaction.class); + + @SuppressWarnings("unchecked") + @Before + public void setUp() throws Exception { + listener.registerSessionFactory(HibernateBundle.DEFAULT_NAME, sessionFactory); + listener.registerSessionFactory("analytics", analyticsSessionFactory); + + when(sessionFactory.openSession()).thenReturn(session); + when(session.getSessionFactory()).thenReturn(sessionFactory); + when(session.beginTransaction()).thenReturn(transaction); + when(session.getTransaction()).thenReturn(transaction); + when(transaction.getStatus()).thenReturn(ACTIVE); + + when(analyticsSessionFactory.openSession()).thenReturn(analyticsSession); + when(analyticsSession.getSessionFactory()).thenReturn(analyticsSessionFactory); + when(analyticsSession.beginTransaction()).thenReturn(analyticsTransaction); + when(analyticsSession.getTransaction()).thenReturn(analyticsTransaction); + when(analyticsTransaction.getStatus()).thenReturn(ACTIVE); + + when(appEvent.getType()).thenReturn(ApplicationEvent.Type.INITIALIZATION_APP_FINISHED); + when(requestMethodStartEvent.getType()).thenReturn(RequestEvent.Type.RESOURCE_METHOD_START); + when(responseFinishedEvent.getType()).thenReturn(RequestEvent.Type.FINISHED); + when(requestMethodExceptionEvent.getType()).thenReturn(RequestEvent.Type.ON_EXCEPTION); + when(responseFiltersStartEvent.getType()).thenReturn(RequestEvent.Type.RESP_FILTERS_START); + when(requestMethodStartEvent.getUriInfo()).thenReturn(uriInfo); + when(responseFinishedEvent.getUriInfo()).thenReturn(uriInfo); + when(requestMethodExceptionEvent.getUriInfo()).thenReturn(uriInfo); + + prepareAppEvent("methodWithDefaultAnnotation"); + } + + @Test + public void opensAndClosesASession() throws Exception { + execute(); + + final InOrder inOrder = inOrder(sessionFactory, session); + inOrder.verify(sessionFactory).openSession(); + inOrder.verify(session).close(); + } + + @Test + public void bindsAndUnbindsTheSessionToTheManagedContext() throws Exception { + doAnswer(invocation -> { + assertThat(ManagedSessionContext.hasBind(sessionFactory)) + .isTrue(); + return null; + }).when(session).beginTransaction(); + + execute(); + + assertThat(ManagedSessionContext.hasBind(sessionFactory)).isFalse(); + } + + @Test + public void configuresTheSessionsReadOnlyDefault() throws Exception { + prepareAppEvent("methodWithReadOnlyAnnotation"); + + execute(); + + verify(session).setDefaultReadOnly(true); + } + + @Test + public void configuresTheSessionsCacheMode() throws Exception { + prepareAppEvent("methodWithCacheModeIgnoreAnnotation"); + + execute(); + + verify(session).setCacheMode(CacheMode.IGNORE); + } + + @Test + public void configuresTheSessionsFlushMode() throws Exception { + prepareAppEvent("methodWithFlushModeAlwaysAnnotation"); + + execute(); + + verify(session).setFlushMode(FlushMode.ALWAYS); + } + + @Test + public void doesNotBeginATransactionIfNotTransactional() throws Exception { + final String resourceMethodName = "methodWithTransactionalFalseAnnotation"; + prepareAppEvent(resourceMethodName); + + when(session.getTransaction()).thenReturn(null); + + execute(); + + verify(session, never()).beginTransaction(); + verifyZeroInteractions(transaction); + } + + @Test + public void detectsAnnotationOnHandlingMethod() throws NoSuchMethodException { + final String resourceMethodName = "handlingMethodAnnotated"; + prepareAppEvent(resourceMethodName); + + execute(); + + verify(session).setDefaultReadOnly(true); + } + + @Test + public void detectsAnnotationOnDefinitionMethod() throws NoSuchMethodException { + final String resourceMethodName = "definitionMethodAnnotated"; + prepareAppEvent(resourceMethodName); + + execute(); + + verify(session).setDefaultReadOnly(true); + } + + @Test + public void annotationOnDefinitionMethodOverridesHandlingMethod() throws NoSuchMethodException { + final String resourceMethodName = "bothMethodsAnnotated"; + prepareAppEvent(resourceMethodName); + + execute(); + + verify(session).setDefaultReadOnly(true); + } + + @Test + public void beginsAndCommitsATransactionIfTransactional() throws Exception { + execute(); + + final InOrder inOrder = inOrder(session, transaction); + inOrder.verify(session).beginTransaction(); + inOrder.verify(transaction).commit(); + inOrder.verify(session).close(); + } + + @Test + public void rollsBackTheTransactionOnException() throws Exception { + executeWithException(); + + final InOrder inOrder = inOrder(session, transaction); + inOrder.verify(session).beginTransaction(); + inOrder.verify(transaction).rollback(); + inOrder.verify(session).close(); + } + + @Test + public void doesNotCommitAnInactiveTransaction() throws Exception { + when(transaction.getStatus()).thenReturn(NOT_ACTIVE); + + execute(); + + verify(transaction, never()).commit(); + } + + @Test + public void doesNotCommitANullTransaction() throws Exception { + when(session.getTransaction()).thenReturn(null); + + execute(); + + verify(transaction, never()).commit(); + } + + @Test + public void doesNotRollbackAnInactiveTransaction() throws Exception { + when(transaction.getStatus()).thenReturn(NOT_ACTIVE); + + executeWithException(); + + verify(transaction, never()).rollback(); + } + + @Test + public void doesNotRollbackANullTransaction() throws Exception { + when(session.getTransaction()).thenReturn(null); + + executeWithException(); + + verify(transaction, never()).rollback(); + } + + @Test + public void beginsAndCommitsATransactionForAnalytics() throws Exception { + prepareAppEvent("methodWithUnitOfWorkOnAnalyticsDatabase"); + execute(); + + final InOrder inOrder = inOrder(analyticsSession, analyticsTransaction); + inOrder.verify(analyticsSession).beginTransaction(); + inOrder.verify(analyticsTransaction).commit(); + inOrder.verify(analyticsSession).close(); + } + + @Test + public void throwsExceptionOnNotRegisteredDatabase() throws Exception { + try { + prepareAppEvent("methodWithUnitOfWorkOnNotRegisteredDatabase"); + execute(); + Assert.fail(); + } catch (IllegalArgumentException e) { + Assert.assertEquals(e.getMessage(), "Unregistered Hibernate bundle: 'warehouse'"); + } + } + + private void prepareAppEvent(String resourceMethodName) throws NoSuchMethodException { + final Resource.Builder builder = Resource.builder(); + final MockResource mockResource = new MockResource(); + final Method handlingMethod = mockResource.getClass().getMethod(resourceMethodName); + + Method definitionMethod = handlingMethod; + Class interfaceClass = mockResource.getClass().getInterfaces()[0]; + if (methodDefinedOnInterface(resourceMethodName, interfaceClass.getMethods())) { + definitionMethod = interfaceClass.getMethod(resourceMethodName); + } + + final ResourceMethod resourceMethod = builder.addMethod() + .handlingMethod(handlingMethod) + .handledBy(mockResource, definitionMethod).build(); + final Resource resource = builder.build(); + final ResourceModel model = new ResourceModel.Builder(false).addResource(resource).build(); + + when(appEvent.getResourceModel()).thenReturn(model); + when(uriInfo.getMatchedResourceMethod()).thenReturn(resourceMethod); + } + + private static boolean methodDefinedOnInterface(String methodName, Method[] methods) { + for (Method method : methods) { + if (method.getName().equals(methodName)) { + return true; + } + } + return false; + } + + private void execute() { + listener.onEvent(appEvent); + RequestEventListener requestListener = listener.onRequest(requestStartEvent); + requestListener.onEvent(requestMethodStartEvent); + requestListener.onEvent(responseFiltersStartEvent); + requestListener.onEvent(responseFinishedEvent); + } + + private void executeWithException() { + listener.onEvent(appEvent); + RequestEventListener requestListener = listener.onRequest(requestStartEvent); + requestListener.onEvent(requestMethodStartEvent); + requestListener.onEvent(responseFiltersStartEvent); + requestListener.onEvent(requestMethodExceptionEvent); + requestListener.onEvent(responseFinishedEvent); + } + + public static class MockResource implements MockResourceInterface { + + @UnitOfWork(readOnly = false, cacheMode = CacheMode.NORMAL, transactional = true, flushMode = FlushMode.AUTO) + public void methodWithDefaultAnnotation() { + } + + @UnitOfWork(readOnly = true, cacheMode = CacheMode.NORMAL, transactional = true, flushMode = FlushMode.AUTO) + public void methodWithReadOnlyAnnotation() { + } + + @UnitOfWork(readOnly = false, cacheMode = CacheMode.IGNORE, transactional = true, flushMode = FlushMode.AUTO) + public void methodWithCacheModeIgnoreAnnotation() { + } + + @UnitOfWork(readOnly = false, cacheMode = CacheMode.NORMAL, transactional = true, flushMode = FlushMode.ALWAYS) + public void methodWithFlushModeAlwaysAnnotation() { + } + + @UnitOfWork(readOnly = false, cacheMode = CacheMode.NORMAL, transactional = false, flushMode = FlushMode.AUTO) + public void methodWithTransactionalFalseAnnotation() { + } + + @UnitOfWork(readOnly = true) + @Override + public void handlingMethodAnnotated() { + } + + @Override + public void definitionMethodAnnotated() { + } + + @UnitOfWork(readOnly = false) + @Override + public void bothMethodsAnnotated() { + + } + + @UnitOfWork("analytics") + public void methodWithUnitOfWorkOnAnalyticsDatabase() { + + } + + @UnitOfWork("warehouse") + public void methodWithUnitOfWorkOnNotRegisteredDatabase() { + + } + } + + public static interface MockResourceInterface { + + void handlingMethodAnnotated(); + + @UnitOfWork(readOnly = true) + void definitionMethodAnnotated(); + + @UnitOfWork(readOnly = true) + void bothMethodsAnnotated(); + } +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkAwareProxyFactoryTest.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkAwareProxyFactoryTest.java new file mode 100644 index 00000000000..539004f4f41 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/UnitOfWorkAwareProxyFactoryTest.java @@ -0,0 +1,146 @@ +package io.dropwizard.hibernate; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.setup.Environment; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class UnitOfWorkAwareProxyFactoryTest { + + static { + BootstrapLogging.bootstrap(); + } + + private SessionFactory sessionFactory; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + final HibernateBundle bundle = mock(HibernateBundle.class); + final Environment environment = mock(Environment.class); + when(environment.lifecycle()).thenReturn(mock(LifecycleEnvironment.class)); + when(environment.metrics()).thenReturn(new MetricRegistry()); + + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setUrl("jdbc:hsqldb:mem:unit-of-work-" + UUID.randomUUID().toString()); + dataSourceFactory.setUser("sa"); + dataSourceFactory.setDriverClass("org.hsqldb.jdbcDriver"); + dataSourceFactory.setValidationQuery("SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS"); + dataSourceFactory.setProperties(ImmutableMap.of("hibernate.dialect", "org.hibernate.dialect.HSQLDialect")); + dataSourceFactory.setInitialSize(1); + dataSourceFactory.setMinSize(1); + + sessionFactory = new SessionFactoryFactory() + .build(bundle, environment, dataSourceFactory, ImmutableList.of()); + try (Session session = sessionFactory.openSession()) { + session.createSQLQuery("create table user_sessions (token varchar(64) primary key, username varchar(16))") + .executeUpdate(); + session.createSQLQuery("insert into user_sessions values ('67ab89d', 'jeff_28')") + .executeUpdate(); + } + } + + @Test + public void testProxyWorks() throws Exception { + final SessionDao sessionDao = new SessionDao(sessionFactory); + final UnitOfWorkAwareProxyFactory unitOfWorkAwareProxyFactory = + new UnitOfWorkAwareProxyFactory("default", sessionFactory); + + final OAuthAuthenticator oAuthAuthenticator = unitOfWorkAwareProxyFactory + .create(OAuthAuthenticator.class, SessionDao.class, sessionDao); + assertThat(oAuthAuthenticator.authenticate("67ab89d")).isTrue(); + assertThat(oAuthAuthenticator.authenticate("bd1e23a")).isFalse(); + } + + @Test + public void testProxyWorksWithoutUnitOfWork() { + assertThat(new UnitOfWorkAwareProxyFactory("default", sessionFactory) + .create(PlainAuthenticator.class) + .authenticate("c82d11e")) + .isTrue(); + } + + @Test + public void testProxyHandlesErrors() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("Session cluster is down"); + + new UnitOfWorkAwareProxyFactory("default", sessionFactory) + .create(BrokenAuthenticator.class) + .authenticate("b812ae4"); + } + + @Test + public void testNewAspect() { + final UnitOfWorkAwareProxyFactory unitOfWorkAwareProxyFactory = + new UnitOfWorkAwareProxyFactory("default", sessionFactory); + + UnitOfWorkAspect aspect1 = unitOfWorkAwareProxyFactory.newAspect(); + UnitOfWorkAspect aspect2 = unitOfWorkAwareProxyFactory.newAspect(); + assertThat(aspect1).isNotSameAs(aspect2); + } + + static class SessionDao { + + private SessionFactory sessionFactory; + + public SessionDao(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + public boolean isExist(String token) { + return sessionFactory.getCurrentSession() + .createSQLQuery("select username from user_sessions where token=:token") + .setParameter("token", token) + .list() + .size() > 0; + } + + } + + static class OAuthAuthenticator { + + private SessionDao sessionDao; + + public OAuthAuthenticator(SessionDao sessionDao) { + this.sessionDao = sessionDao; + } + + @UnitOfWork + public boolean authenticate(String token) { + return sessionDao.isExist(token); + } + } + + static class PlainAuthenticator { + + public boolean authenticate(String token) { + return true; + } + } + + static class BrokenAuthenticator { + + @UnitOfWork + public boolean authenticate(String token) { + throw new IllegalStateException("Session cluster is down"); + } + } +} 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 00000000000..7d670e4f5b1 --- /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.assertj.core.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/pckg/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/FakeEntity1.java new file mode 100644 index 00000000000..37470c9be36 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake/entities/pckg/FakeEntity1.java @@ -0,0 +1,8 @@ +package io.dropwizard.hibernate.fake.entities.pckg; + +import javax.persistence.Entity; + +@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 00000000000..fbf8ed2b0be --- /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; + +@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 00000000000..0e0c0772029 --- /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; + +@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 00000000000..880a808b03b --- /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; + +@Entity +public class FakeEntity1 { + +} + diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/FakeEntity1.java new file mode 100644 index 00000000000..f4a7df26a92 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/FakeEntity1.java @@ -0,0 +1,8 @@ +package io.dropwizard.hibernate.fake2.entities.pckg; + +import javax.persistence.Entity; + +@Entity +public class FakeEntity1 { + +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/FakeEntity2.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/FakeEntity2.java new file mode 100644 index 00000000000..24574df61e2 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/FakeEntity2.java @@ -0,0 +1,8 @@ +package io.dropwizard.hibernate.fake2.entities.pckg; + +import javax.persistence.Entity; + +@Entity +public class FakeEntity2 { + +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/deep/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/deep/FakeEntity1.java new file mode 100644 index 00000000000..99307ca95c3 --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/deep/FakeEntity1.java @@ -0,0 +1,7 @@ +package io.dropwizard.hibernate.fake2.entities.pckg.deep; +import javax.persistence.Entity; + +@Entity +public class FakeEntity1 { + +} diff --git a/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/deep/deeper/FakeEntity1.java b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/deep/deeper/FakeEntity1.java new file mode 100644 index 00000000000..1c4a92f693c --- /dev/null +++ b/dropwizard-hibernate/src/test/java/io/dropwizard/hibernate/fake2/entities/pckg/deep/deeper/FakeEntity1.java @@ -0,0 +1,9 @@ +package io.dropwizard.hibernate.fake2.entities.pckg.deep.deeper; + +import javax.persistence.Entity; + +@Entity +public class FakeEntity1 { + +} + diff --git a/dropwizard-hibernate/src/test/resources/hibernate-integration-test.yaml b/dropwizard-hibernate/src/test/resources/hibernate-integration-test.yaml new file mode 100644 index 00000000000..3ba20d6caad --- /dev/null +++ b/dropwizard-hibernate/src/test/resources/hibernate-integration-test.yaml @@ -0,0 +1,15 @@ +server: + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 + requestLog: + appenders: [] +logging: + appenders: [] +dataSource: + user: "sa" + driverClass: "org.hsqldb.jdbcDriver" + validationQuery: "SELECT 1 FROM INFORMATION_SCHEMA.SYSTEM_USERS" 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-hibernate/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-http2/pom.xml b/dropwizard-http2/pom.xml new file mode 100644 index 00000000000..d5e8767f408 --- /dev/null +++ b/dropwizard-http2/pom.xml @@ -0,0 +1,292 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-http2 + Dropwizard HTTP/2 Support + + + + 8.1.3.v20150130 + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + -Xbootclasspath/p:/"${user.home}/.m2/repository/org/mortbay/jetty/alpn/alpn-boot/${alpn-boot.version}/alpn-boot-${alpn-boot.version}.jar" + -Duser.language=en -Duser.region=US + classes + 2 + true + + + + + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-jetty + + + + org.eclipse.jetty.http2 + http2-server + + + + org.eclipse.jetty.http2 + http2-client + test + + + + org.eclipse.jetty + jetty-client + test + + + + org.eclipse.jetty.http2 + http2-http-client-transport + test + + + + io.dropwizard + dropwizard-testing + test + + + + org.eclipse.jetty + jetty-alpn-server + + + + + org.mortbay.jetty.alpn + alpn-boot + ${alpn-boot.version} + test + + + + + + + jdk-1.8.0 + + 1.8.0 + + + 8.1.0.v20141016 + + + + jdk-1.8.0_05 + + 1.8.0_05 + + + 8.1.0.v20141016 + + + + jdk-1.8.0_11 + + 1.8.0_11 + + + 8.1.0.v20141016 + + + + jdk-1.8.0_20 + + 1.8.0_20 + + + 8.1.0.v20141016 + + + + jdk-1.8.0_25 + + 1.8.0_25 + + + 8.1.2.v20141202 + + + + jdk-1.8.0_31 + + 1.8.0_31 + + + 8.1.3.v20150130 + + + + jdk-1.8.0_40 + + 1.8.0_40 + + + 8.1.3.v20150130 + + + + jdk-1.8.0_45 + + 1.8.0_45 + + + 8.1.3.v20150130 + + + + jdk-1.8.0_51 + + 1.8.0_51 + + + 8.1.4.v20150727 + + + + jdk-1.8.0_60 + + 1.8.0_60 + + + 8.1.5.v20150921 + + + + jdk-1.8.0_65 + + 1.8.0_65 + + + 8.1.6.v20151105 + + + + jdk-1.8.0_66 + + 1.8.0_66 + + + 8.1.6.v20151105 + + + + jdk-1.8.0_71 + + 1.8.0_71 + + + 8.1.7.v20160121 + + + + jdk-1.8.0_72 + + 1.8.0_72 + + + 8.1.7.v20160121 + + + + jdk-1.8.0_73 + + 1.8.0_73 + + + 8.1.7.v20160121 + + + + jdk-1.8.0_74 + + 1.8.0_74 + + + 8.1.7.v20160121 + + + + jdk-1.8.0_77 + + 1.8.0_77 + + + 8.1.7.v20160121 + + + + jdk-1.8.0_91 + + 1.8.0_91 + + + 8.1.7.v20160121 + + + + jdk-1.8.0_92 + + 1.8.0_92 + + + 8.1.8.v20160420 + + + + jdk-1.8.0_101 + + 1.8.0_101 + + + 8.1.9.v20160720 + + + + jdk-1.8.0_102 + + 1.8.0_102 + + + 8.1.9.v20160720 + + + + diff --git a/dropwizard-http2/src/main/java/io/dropwizard/http2/Http2CConnectorFactory.java b/dropwizard-http2/src/main/java/io/dropwizard/http2/Http2CConnectorFactory.java new file mode 100644 index 00000000000..85b99356fd5 --- /dev/null +++ b/dropwizard-http2/src/main/java/io/dropwizard/http2/Http2CConnectorFactory.java @@ -0,0 +1,101 @@ +package io.dropwizard.http2; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.jetty.HttpConnectorFactory; +import io.dropwizard.jetty.HttpsConnectorFactory; +import io.dropwizard.jetty.Jetty93InstrumentedConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2CServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.ThreadPool; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +/** + * Builds HTTP/2 clear text (h2c) connectors. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code maxConcurrentStreams}<1024 + * The maximum number of concurrently open streams allowed on a single HTTP/2 connection. + * Larger values increase parallelism, but cost a memory commitment. + *
    {@code initialStreamRecvWindow}65535 + * The initial flow control window size for a new stream. Larger values may allow greater throughput, + * but also risk head of line blocking if TCP/IP flow control is triggered. + *
    + *

    + * For more configuration parameters, see {@link HttpsConnectorFactory}. + * @see HttpConnectorFactory + */ +@JsonTypeName("h2c") +public class Http2CConnectorFactory extends HttpConnectorFactory { + + @Min(100) + @Max(Integer.MAX_VALUE) + private int maxConcurrentStreams = 1024; + + @Min(1) + @Max(Integer.MAX_VALUE) + private int initialStreamRecvWindow = 65535; + + @JsonProperty + public int getMaxConcurrentStreams() { + return maxConcurrentStreams; + } + + @JsonProperty + public void setMaxConcurrentStreams(int maxConcurrentStreams) { + this.maxConcurrentStreams = maxConcurrentStreams; + } + + @JsonProperty + public int getInitialStreamRecvWindow() { + return initialStreamRecvWindow; + } + + @JsonProperty + public void setInitialStreamRecvWindow(int initialStreamRecvWindow) { + this.initialStreamRecvWindow = initialStreamRecvWindow; + } + + @Override + public Connector build(Server server, MetricRegistry metrics, String name, ThreadPool threadPool) { + + // Prepare connection factories for HTTP/2c + final HttpConfiguration httpConfig = buildHttpConfiguration(); + final HttpConnectionFactory http11 = buildHttpConnectionFactory(httpConfig); + final HTTP2ServerConnectionFactory http2c = new HTTP2CServerConnectionFactory(httpConfig); + http2c.setMaxConcurrentStreams(maxConcurrentStreams); + http2c.setInitialStreamRecvWindow(initialStreamRecvWindow); + + // The server connector should use HTTP/1.1 by default. It affords to the server to stay compatible + // with old clients. New clients which want to use HTTP/2, however, will make an HTTP/1.1 OPTIONS + // request with an Upgrade header with "h2c" value. The server supports HTTP/2 clear text connections, + // so it will return the predefined HTTP/2 preamble and the client and the server will switch to the + // new protocol. + return buildConnector(server, new ScheduledExecutorScheduler(), buildBufferPool(), name, threadPool, + new Jetty93InstrumentedConnectionFactory(http11, metrics.timer(httpConnections())), http2c); + } +} diff --git a/dropwizard-http2/src/main/java/io/dropwizard/http2/Http2ConnectorFactory.java b/dropwizard-http2/src/main/java/io/dropwizard/http2/Http2ConnectorFactory.java new file mode 100644 index 00000000000..047e7ca5c38 --- /dev/null +++ b/dropwizard-http2/src/main/java/io/dropwizard/http2/Http2ConnectorFactory.java @@ -0,0 +1,124 @@ +package io.dropwizard.http2; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.common.collect.ImmutableList; +import io.dropwizard.jetty.HttpsConnectorFactory; +import io.dropwizard.jetty.Jetty93InstrumentedConnectionFactory; +import org.eclipse.jetty.alpn.server.ALPNServerConnectionFactory; +import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.NegotiatingServerConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.ThreadPool; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; + +/** + * Builds HTTP/2 over TLS (h2) connectors. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code maxConcurrentStreams}<1024 + * The maximum number of concurrently open streams allowed on a single HTTP/2 connection. + * Larger values increase parallelism, but cost a memory commitment. + *
    {@code initialStreamRecvWindow}65535 + * The initial flow control window size for a new stream. Larger values may allow greater throughput, + * but also risk head of line blocking if TCP/IP flow control is triggered. + *
    + *

    + * For more configuration parameters, see {@link HttpsConnectorFactory}. + * + * @see HttpsConnectorFactory + */ +@JsonTypeName("h2") +public class Http2ConnectorFactory extends HttpsConnectorFactory { + + /** + * Supported protocols + */ + private static final String H2 = "h2"; + private static final String H2_17 = "h2-17"; + private static final String HTTP_1_1 = "http/1.1"; + + @Min(100) + @Max(Integer.MAX_VALUE) + private int maxConcurrentStreams = 1024; + + @Min(1) + @Max(Integer.MAX_VALUE) + private int initialStreamRecvWindow = 65535; + + @JsonProperty + public int getMaxConcurrentStreams() { + return maxConcurrentStreams; + } + + @JsonProperty + public void setMaxConcurrentStreams(int maxConcurrentStreams) { + this.maxConcurrentStreams = maxConcurrentStreams; + } + + @JsonProperty + public int getInitialStreamRecvWindow() { + return initialStreamRecvWindow; + } + + @JsonProperty + public void setInitialStreamRecvWindow(int initialStreamRecvWindow) { + this.initialStreamRecvWindow = initialStreamRecvWindow; + } + + @Override + public Connector build(Server server, MetricRegistry metrics, String name, ThreadPool threadPool) { + // HTTP/2 requires that a server MUST support TLSv1.2 and TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 cipher + // See http://http2.github.io/http2-spec/index.html#rfc.section.9.2.2 + setSupportedProtocols(ImmutableList.of("TLSv1.2")); + setSupportedCipherSuites(ImmutableList.of("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256")); + + // Setup connection factories + final HttpConfiguration httpConfig = buildHttpConfiguration(); + final HttpConnectionFactory http1 = buildHttpConnectionFactory(httpConfig); + final HTTP2ServerConnectionFactory http2 = new HTTP2ServerConnectionFactory(httpConfig); + http2.setMaxConcurrentStreams(maxConcurrentStreams); + http2.setInitialStreamRecvWindow(initialStreamRecvWindow); + + final NegotiatingServerConnectionFactory alpn = new ALPNServerConnectionFactory(H2, H2_17); + alpn.setDefaultProtocol(HTTP_1_1); // Speak HTTP 1.1 over TLS if negotiation fails + + final SslContextFactory sslContextFactory = buildSslContextFactory(); + sslContextFactory.addLifeCycleListener(logSslInfoOnStart(sslContextFactory)); + server.addBean(sslContextFactory); + + // We should use ALPN as a negotiation protocol. Old clients that don't support it will be served + // via HTTPS. New clients, however, that want to use HTTP/2 will use TLS with ALPN extension. + // If negotiation succeeds, the client and server switch to HTTP/2 protocol. + final SslConnectionFactory sslConnectionFactory = new SslConnectionFactory(sslContextFactory, "alpn"); + + return buildConnector(server, new ScheduledExecutorScheduler(), buildBufferPool(), name, threadPool, + new Jetty93InstrumentedConnectionFactory(sslConnectionFactory, metrics.timer(httpConnections())), + alpn, http2, http1); + } +} diff --git a/dropwizard-http2/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory b/dropwizard-http2/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory new file mode 100644 index 00000000000..0db89f7521a --- /dev/null +++ b/dropwizard-http2/src/main/resources/META-INF/services/io.dropwizard.jetty.ConnectorFactory @@ -0,0 +1,2 @@ +io.dropwizard.http2.Http2ConnectorFactory +io.dropwizard.http2.Http2CConnectorFactory diff --git a/dropwizard-http2/src/test/java/io/dropwizard/http2/AbstractHttp2Test.java b/dropwizard-http2/src/test/java/io/dropwizard/http2/AbstractHttp2Test.java new file mode 100644 index 00000000000..8594bda7473 --- /dev/null +++ b/dropwizard-http2/src/test/java/io/dropwizard/http2/AbstractHttp2Test.java @@ -0,0 +1,49 @@ +package io.dropwizard.http2; + +import com.google.common.base.Charsets; +import io.dropwizard.logging.BootstrapLogging; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.ContentResponse; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.http.HttpVersion; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Common code for HTTP/2 connector tests + */ +public class AbstractHttp2Test { + + static { + BootstrapLogging.bootstrap(); + } + + protected static void assertResponse(ContentResponse response) { + assertThat(response.getVersion()).isEqualTo(HttpVersion.HTTP_2); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContentAsString()).isEqualTo(FakeApplication.HELLO_WORLD); + } + + protected void performManyAsyncRequests(HttpClient client, String url) throws InterruptedException { + final int amount = 100; + final CountDownLatch latch = new CountDownLatch(amount); + for (int i = 0; i < amount; i++) { + client.newRequest(url) + .send(new BufferingResponseListener() { + @Override + public void onComplete(Result result) { + assertThat(result.getResponse().getVersion()).isEqualTo(HttpVersion.HTTP_2); + assertThat(result.getResponse().getStatus()).isEqualTo(200); + assertThat(getContentAsString(Charsets.UTF_8)).isEqualTo(FakeApplication.HELLO_WORLD); + latch.countDown(); + } + }); + } + + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + } +} diff --git a/dropwizard-http2/src/test/java/io/dropwizard/http2/FakeApplication.java b/dropwizard-http2/src/test/java/io/dropwizard/http2/FakeApplication.java new file mode 100644 index 00000000000..5ee90d88bb4 --- /dev/null +++ b/dropwizard-http2/src/test/java/io/dropwizard/http2/FakeApplication.java @@ -0,0 +1,37 @@ +package io.dropwizard.http2; + +import com.codahale.metrics.health.HealthCheck; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.setup.Environment; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +public class FakeApplication extends Application { + + public static final String HELLO_WORLD = "{\"hello\": \"World\"}"; + + @Override + public void run(Configuration configuration, Environment environment) throws Exception { + environment.jersey().register(new FakeResource()); + environment.healthChecks().register("fake-health-check", new HealthCheck() { + @Override + protected Result check() throws Exception { + return Result.healthy(); + } + }); + } + + @Path("/test") + @Produces(MediaType.APPLICATION_JSON) + public static class FakeResource { + + @GET + public String get() throws Exception { + return HELLO_WORLD; + } + } +} diff --git a/dropwizard-http2/src/test/java/io/dropwizard/http2/Http2CIntegrationTest.java b/dropwizard-http2/src/test/java/io/dropwizard/http2/Http2CIntegrationTest.java new file mode 100644 index 00000000000..656f9289215 --- /dev/null +++ b/dropwizard-http2/src/test/java/io/dropwizard/http2/Http2CIntegrationTest.java @@ -0,0 +1,67 @@ +package io.dropwizard.http2; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.Configuration; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.HTTP2ClientConnectionFactory; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Http2CIntegrationTest extends AbstractHttp2Test { + + + @Rule + public DropwizardAppRule appRule = new DropwizardAppRule<>( + FakeApplication.class, ResourceHelpers.resourceFilePath("test-http2c.yml")); + + private HttpClient client; + + @Before + public void setUp() throws Exception { + final HTTP2Client http2Client = new HTTP2Client(); + http2Client.setClientConnectionFactory(new HTTP2ClientConnectionFactory()); // No need for ALPN + client = new HttpClient(new HttpClientTransportOverHTTP2(http2Client), null); + client.start(); + } + + @After + public void tearDown() throws Exception { + client.stop(); + } + + @Test + public void testHttp11() { + final String hostname = "127.0.0.1"; + final int port = appRule.getLocalPort(); + final JerseyClient http11Client = new JerseyClientBuilder().build(); + final Response response = http11Client.target("http://" + hostname + ":" + port + "/api/test") + .request() + .get(); + assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(response.readEntity(String.class)).isEqualTo(FakeApplication.HELLO_WORLD); + http11Client.close(); + } + + @Test + public void testHttp2c() throws Exception { + assertResponse(client.GET("http://localhost:" + appRule.getLocalPort() + "/api/test")); + } + + @Test + public void testHttp2cManyRequests() throws Exception { + performManyAsyncRequests(client, "http://localhost:" + appRule.getLocalPort() + "/api/test"); + } +} diff --git a/dropwizard-http2/src/test/java/io/dropwizard/http2/Http2IntegrationTest.java b/dropwizard-http2/src/test/java/io/dropwizard/http2/Http2IntegrationTest.java new file mode 100644 index 00000000000..f85a2f54c48 --- /dev/null +++ b/dropwizard-http2/src/test/java/io/dropwizard/http2/Http2IntegrationTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.http2; + +import com.google.common.net.HttpHeaders; +import io.dropwizard.Configuration; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.ResourceHelpers; +import io.dropwizard.testing.junit.DropwizardAppRule; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.http2.client.HTTP2Client; +import org.eclipse.jetty.http2.client.http.HttpClientTransportOverHTTP2; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.glassfish.jersey.client.JerseyClient; +import org.glassfish.jersey.client.JerseyClientBuilder; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Http2IntegrationTest extends AbstractHttp2Test { + + @Rule + public final DropwizardAppRule appRule = new DropwizardAppRule<>( + FakeApplication.class, ResourceHelpers.resourceFilePath("test-http2.yml"), + Optional.of("tls_http2"), + ConfigOverride.config("tls_http2", "server.connector.keyStorePath", + ResourceHelpers.resourceFilePath("stores/http2_server.jks")), + ConfigOverride.config("tls_http2", "server.connector.trustStorePath", + ResourceHelpers.resourceFilePath("stores/http2_client.jts")) + ); + + private final SslContextFactory sslContextFactory = new SslContextFactory(); + private HttpClient client; + + @Before + public void setUp() throws Exception { + sslContextFactory.setTrustStorePath(ResourceHelpers.resourceFilePath("stores/http2_client.jts")); + sslContextFactory.setTrustStorePassword("http2_client"); + sslContextFactory.start(); + + client = new HttpClient(new HttpClientTransportOverHTTP2(new HTTP2Client()), sslContextFactory); + client.start(); + } + + @After + public void tearDown() throws Exception { + client.stop(); + } + + @Test + public void testHttp11() throws Exception { + final String hostname = "localhost"; + final int port = appRule.getLocalPort(); + final JerseyClient http11Client = new JerseyClientBuilder() + .sslContext(sslContextFactory.getSslContext()) + .build(); + final Response response = http11Client.target("https://" + hostname + ":" + port + "/api/test") + .request() + .get(); + assertThat(response.getHeaderString(HttpHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(response.readEntity(String.class)).isEqualTo(FakeApplication.HELLO_WORLD); + http11Client.close(); + } + + @Test + public void testHttp2() throws Exception { + assertResponse(client.GET("https://localhost:" + appRule.getLocalPort() + "/api/test")); + } + + @Test + public void testHttp2ManyRequests() throws Exception { + performManyAsyncRequests(client, "https://localhost:" + appRule.getLocalPort() + "/api/test"); + } +} diff --git a/dropwizard-http2/src/test/resources/stores/http2_client.jts b/dropwizard-http2/src/test/resources/stores/http2_client.jts new file mode 100644 index 00000000000..d6c0e7e50d4 Binary files /dev/null and b/dropwizard-http2/src/test/resources/stores/http2_client.jts differ diff --git a/dropwizard-http2/src/test/resources/stores/http2_server.crt b/dropwizard-http2/src/test/resources/stores/http2_server.crt new file mode 100644 index 00000000000..0ed89d0bd7c Binary files /dev/null and b/dropwizard-http2/src/test/resources/stores/http2_server.crt differ diff --git a/dropwizard-http2/src/test/resources/stores/http2_server.jks b/dropwizard-http2/src/test/resources/stores/http2_server.jks new file mode 100644 index 00000000000..b8fe44b933e Binary files /dev/null and b/dropwizard-http2/src/test/resources/stores/http2_server.jks differ diff --git a/dropwizard-http2/src/test/resources/test-http2.yml b/dropwizard-http2/src/test/resources/test-http2.yml new file mode 100644 index 00000000000..bc8607f8652 --- /dev/null +++ b/dropwizard-http2/src/test/resources/test-http2.yml @@ -0,0 +1,9 @@ +server: + type: simple + connector: + type: h2 + port: 0 + keyStorePassword: http2_server + validateCerts: false + applicationContextPath: /api + adminContextPath: /admin diff --git a/dropwizard-http2/src/test/resources/test-http2c.yml b/dropwizard-http2/src/test/resources/test-http2c.yml new file mode 100644 index 00000000000..a683fe3ad9b --- /dev/null +++ b/dropwizard-http2/src/test/resources/test-http2c.yml @@ -0,0 +1,8 @@ +server: + type: simple + connector: + type: h2c + port: 0 + applicationContextPath: /api + adminContextPath: /admin + diff --git a/dropwizard-jackson/pom.xml b/dropwizard-jackson/pom.xml new file mode 100644 index 00000000000..29bedfc08bd --- /dev/null +++ b/dropwizard-jackson/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-jackson + Dropwizard Jackson Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + com.google.guava + guava + + + io.dropwizard + dropwizard-util + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.datatype + jackson-datatype-guava + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + + + com.fasterxml.jackson.module + jackson-module-afterburner + + + com.fasterxml.jackson.datatype + jackson-datatype-joda + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/json/AnnotationSensitivePropertyNamingStrategy.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategy.java similarity index 70% rename from dropwizard/src/main/java/com/yammer/dropwizard/json/AnnotationSensitivePropertyNamingStrategy.java rename to dropwizard-jackson/src/main/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategy.java index 6ec6ba74c28..12a1b86f00a 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/json/AnnotationSensitivePropertyNamingStrategy.java +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/AnnotationSensitivePropertyNamingStrategy.java @@ -1,22 +1,25 @@ -package com.yammer.dropwizard.json; +package io.dropwizard.jackson; -import org.codehaus.jackson.map.MapperConfig; -import org.codehaus.jackson.map.PropertyNamingStrategy; -import org.codehaus.jackson.map.introspect.AnnotatedField; -import org.codehaus.jackson.map.introspect.AnnotatedMethod; -import org.codehaus.jackson.map.introspect.AnnotatedParameter; - -// TODO: 10/12/11 -- write tests for AnnotationSensitivePropertyNamingStrategy -// TODO: 10/12/11 -- write docs for AnnotationSensitivePropertyNamingStrategy +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.SnakeCaseStrategy}, and uses + * the default {@link PropertyNamingStrategy} otherwise. + */ public class AnnotationSensitivePropertyNamingStrategy extends PropertyNamingStrategy { - static final AnnotationSensitivePropertyNamingStrategy INSTANCE = - new AnnotationSensitivePropertyNamingStrategy(); - private final PropertyNamingStrategy snakeCase; + private static final long serialVersionUID = -1372862028366311230L; + + private final SnakeCaseStrategy snakeCase; public AnnotationSensitivePropertyNamingStrategy() { super(); - this.snakeCase = PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES; + this.snakeCase = new SnakeCaseStrategy(); } @Override 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 00000000000..f4fd77f108b --- /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 00000000000..94b7b283d37 --- /dev/null +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/DiscoverableSubtypeResolver.java @@ -0,0 +1,88 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.databind.jsontype.impl.StdSubtypeResolver; +import com.google.common.collect.ImmutableList; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +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 long serialVersionUID = 1L; + private static final Logger LOGGER = LoggerFactory.getLogger(DiscoverableSubtypeResolver.class); + + private final ImmutableList> discoveredSubtypes; + + public DiscoverableSubtypeResolver() { + this(Discoverable.class); + } + + public DiscoverableSubtypeResolver(Class rootKlass) { + final ImmutableList.Builder> subtypes = ImmutableList.builder(); + for (Class klass : discoverServices(rootKlass)) { + for (Class subtype : discoverServices(klass)) { + subtypes.add(subtype); + registerSubtypes(subtype); + } + } + this.discoveredSubtypes = subtypes.build(); + } + + public ImmutableList> getDiscoveredSubtypes() { + return discoveredSubtypes; + } + + protected ClassLoader getClassLoader() { + return this.getClass().getClassLoader(); + } + + protected List> discoverServices(Class klass) { + final List> serviceClasses = new ArrayList<>(); + 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 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, StandardCharsets.UTF_8); + BufferedReader reader = new BufferedReader(streamReader)) { + String line; + while ((line = reader.readLine()) != null) { + final Class loadedClass = loadClass(line); + if (loadedClass != null) { + serviceClasses.add(loadedClass); + } + } + } + } + } catch (IOException e) { + LOGGER.warn("Unable to load META-INF/services/{}", klass.getName(), e); + } + return serviceClasses; + } + + @Nullable + private Class loadClass(String line) { + try { + return getClassLoader().loadClass(line.trim()); + } catch (ClassNotFoundException e) { + LOGGER.info("Unable to load {}", line); + return null; + } + } +} 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 00000000000..e1a503bc3c7 --- /dev/null +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/FuzzyEnumModule.java @@ -0,0 +1,117 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.deser.Deserializers; +import com.fasterxml.jackson.databind.deser.std.EnumDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; +import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.google.common.base.CharMatcher; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * A module for deserializing enums that is more permissive than the default. + *

    + * This deserializer is more permissive in the following ways: + *

      + *
    • Whitespace is permitted but stripped from the input.
    • + *
    • Dashes and periods in the value are converted to underscores.
    • + *
    • Matching against the enum values is case insensitive.
    • + *
    + */ +public class FuzzyEnumModule extends Module { + private static class PermissiveEnumDeserializer extends StdScalarDeserializer> { + private static final long serialVersionUID = 1L; + + private final Enum[] constants; + private final List acceptedValues; + + @SuppressWarnings("unchecked") + protected PermissiveEnumDeserializer(Class> clazz) { + super(clazz); + this.constants = ((Class>) handledType()).getEnumConstants(); + this.acceptedValues = new ArrayList<>(); + 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('-', '_') + .replace('.', '_'); + for (Enum constant : constants) { + if (constant.name().equalsIgnoreCase(text)) { + return constant; + } + } + + //In some cases there are certain enums that don't follow the same patter across an enterprise. So this + //means that you have a mix of enums that use toString(), some use @JsonCreator, and some just use the + //standard constant name(). This block handles finding the proper enum by toString() + for (Enum constant : constants) { + if (constant.toString().equalsIgnoreCase(jp.getText())) { + 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 { + // If the user configured to use `toString` method to deserialize enums + if (config.hasDeserializationFeatures( + DeserializationFeature.READ_ENUMS_USING_TO_STRING.getMask())) { + return null; + } + + // If there is a JsonCreator annotation we should use that instead of the PermissiveEnumDeserializer + final Collection factoryMethods = desc.getFactoryMethods(); + if (factoryMethods != null) { + for (AnnotatedMethod am : factoryMethods) { + final JsonCreator creator = am.getAnnotation(JsonCreator.class); + if (creator != null) { + return EnumDeserializer.deserializerForCreator(config, type, am); + } + } + } + + return new PermissiveEnumDeserializer((Class>) 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 00000000000..7566fd25b04 --- /dev/null +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/GuavaExtrasModule.java @@ -0,0 +1,82 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.deser.Deserializers; +import com.fasterxml.jackson.databind.ser.Serializers; +import com.google.common.cache.CacheBuilderSpec; + +import java.io.IOException; + +public class GuavaExtrasModule extends Module { + private static class CacheBuilderSpecDeserializer extends JsonDeserializer { + @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 CacheBuilderSpecSerializer extends JsonSerializer { + @Override + public void serialize(CacheBuilderSpec value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException { + gen.writeString(value.toParsableString()); + } + } + + 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(); + } + + return super.findBeanDeserializer(type, config, beanDesc); + } + } + + private static class GuavaExtrasSerializers extends Serializers.Base { + @Override + public JsonSerializer findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) { + if (CacheBuilderSpec.class.isAssignableFrom(type.getRawClass())) { + return new CacheBuilderSpecSerializer(); + } + + return super.findSerializer(config, type, 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()); + context.addSerializers(new GuavaExtrasSerializers()); + } +} 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 00000000000..2df47750c7e --- /dev/null +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/Jackson.java @@ -0,0 +1,67 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.joda.JodaModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +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(); + + return configure(mapper); + } + + /** + * Creates a new {@link ObjectMapper} with a custom {@link com.fasterxml.jackson.core.JsonFactory} + * with Guava, Logback, and Joda Time support, as well as support for {@link JsonSnakeCase}. + * Also includes all {@link Discoverable} interface implementations. + * + * @param jsonFactory instance of {@link com.fasterxml.jackson.core.JsonFactory} to use + * for the created {@link com.fasterxml.jackson.databind.ObjectMapper} instance. + */ + public static ObjectMapper newObjectMapper(JsonFactory jsonFactory) { + final ObjectMapper mapper = new ObjectMapper(jsonFactory); + + return configure(mapper); + } + + /** + * Creates a new minimal {@link ObjectMapper} that will work with Dropwizard out of box. + *

    NOTE: Use it, if the default Dropwizard's {@link ObjectMapper}, created in + * {@link #newObjectMapper()}, is too aggressive for you.

    + */ + public static ObjectMapper newMinimalObjectMapper() { + return new ObjectMapper() + .registerModule(new GuavaModule()) + .registerModule(new LogbackModule()) + .setSubtypeResolver(new DiscoverableSubtypeResolver()); + } + + private static ObjectMapper configure(ObjectMapper mapper) { + 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.registerModules(new Jdk8Module()); + mapper.registerModules(new JavaTimeModule()); + mapper.setPropertyNamingStrategy(new AnnotationSensitivePropertyNamingStrategy()); + mapper.setSubtypeResolver(new DiscoverableSubtypeResolver()); + + return mapper; + } +} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/json/JsonSnakeCase.java b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/JsonSnakeCase.java similarity index 84% rename from dropwizard/src/main/java/com/yammer/dropwizard/json/JsonSnakeCase.java rename to dropwizard-jackson/src/main/java/io/dropwizard/jackson/JsonSnakeCase.java index b2cbd08643d..cf5397d79d6 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/json/JsonSnakeCase.java +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/JsonSnakeCase.java @@ -1,6 +1,6 @@ -package com.yammer.dropwizard.json; +package io.dropwizard.jackson; -import org.codehaus.jackson.annotate.JacksonAnnotation; +import com.fasterxml.jackson.annotation.JacksonAnnotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; 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 00000000000..389a5ff1e02 --- /dev/null +++ b/dropwizard-jackson/src/main/java/io/dropwizard/jackson/LogbackModule.java @@ -0,0 +1,88 @@ +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.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.SerializationConfig; +import com.fasterxml.jackson.databind.SerializerProvider; +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 { + @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 { + @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 00000000000..b95adb36123 --- /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.assertj.core.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 00000000000..0f7c3f4e5e8 --- /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.assertj.core.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 00000000000..990d4f594a9 --- /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; + +@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 00000000000..3d7622161c4 --- /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 00000000000..820dbe8bf6c --- /dev/null +++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/FuzzyEnumModuleTest.java @@ -0,0 +1,137 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.DeserializationFeature; +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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class FuzzyEnumModuleTest { + private final ObjectMapper mapper = new ObjectMapper(); + + private enum EnumWithLowercase { + lower_case_enum, + mixedCaseEnum + } + + private enum EnumWithCreator { + TEST; + + @JsonCreator + public static EnumWithCreator fromString(String value) { + return EnumWithCreator.TEST; + } + } + + private enum CurrencyCode { + USD("United States dollar"), + AUD("a_u_d"), + CAD("c-a-d"), + BLA("b.l.a"), + EUR("Euro"), + GBP("Pound sterling"); + + private final String description; + + CurrencyCode(String name) { + this.description = name; + } + + @Override + public String toString() { + return description; + } + } + + @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 mapsDottedEnums() throws Exception { + assertThat(mapper.readValue("\"REASON.UNKNOWN\"", ClientInfoStatus.class)) + .isEqualTo(ClientInfoStatus.REASON_UNKNOWN); + } + + @Test + public void mapsWhenEnumHasCreator() throws Exception { + assertThat(mapper.readValue("\"BLA\"", EnumWithCreator.class)) + .isEqualTo(EnumWithCreator.TEST); + } + + @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); + } + + @Test + public void readsEnumsUsingToString() throws Exception { + final ObjectMapper toStringEnumsMapper = mapper.copy() + .configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true); + assertThat(toStringEnumsMapper.readValue("\"Pound sterling\"", CurrencyCode.class)).isEqualTo(CurrencyCode.GBP); + } + + @Test + public void readsEnumsUsingToStringWithDeserializationFeatureOff() throws Exception { + assertThat(mapper.readValue("\"Pound sterling\"", CurrencyCode.class)).isEqualTo(CurrencyCode.GBP); + assertThat(mapper.readValue("\"a_u_d\"", CurrencyCode.class)).isEqualTo(CurrencyCode.AUD); + assertThat(mapper.readValue("\"c-a-d\"", CurrencyCode.class)).isEqualTo(CurrencyCode.CAD); + assertThat(mapper.readValue("\"b.l.a\"", CurrencyCode.class)).isEqualTo(CurrencyCode.BLA); + } +} 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 00000000000..4a5f8278206 --- /dev/null +++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/GuavaExtrasModuleTest.java @@ -0,0 +1,51 @@ +package io.dropwizard.jackson; + +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.assertj.core.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 canSerializeCacheBuilderSpecs() throws Exception { + assertThat(mapper.writeValueAsString(CacheBuilderSpec.disableCaching())) + .isEqualTo("\"maximumSize=0\""); + } + + @Test + public void canDeserializeAbsentOptions() throws Exception { + assertThat(mapper.readValue("null", Optional.class)) + .isEqualTo(Optional.absent()); + } + + @Test + public void canDeserializePresentOptions() throws Exception { + assertThat(mapper.readValue("\"woo\"", Optional.class)) + .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 00000000000..2889746bbbf --- /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; + +@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 00000000000..a2bb1ffe6e7 --- /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; + +@JsonTypeName("b") +public class ImplB implements ExampleSPI { +} diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/Issue1627.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/Issue1627.java new file mode 100644 index 00000000000..022161e3a58 --- /dev/null +++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/Issue1627.java @@ -0,0 +1,25 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.UUID; + +public class Issue1627 { + private final String string; + private final UUID uuid; + + public Issue1627(final String string, final UUID uuid) { + this.string = string; + this.uuid = uuid; + } + + @JsonProperty + public String getString() { + return this.string; + } + + @JsonProperty + public UUID getUuid() { + return this.uuid; + } +} diff --git a/dropwizard-jackson/src/test/java/io/dropwizard/jackson/JacksonTest.java b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/JacksonTest.java new file mode 100644 index 00000000000..1f7f509901d --- /dev/null +++ b/dropwizard-jackson/src/test/java/io/dropwizard/jackson/JacksonTest.java @@ -0,0 +1,53 @@ +package io.dropwizard.jackson; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JacksonTest { + @Test + public void objectMapperUsesGivenCustomJsonFactory() { + JsonFactory factory = Mockito.mock(JsonFactory.class); + + ObjectMapper mapper = Jackson.newObjectMapper(factory); + + assertThat(mapper.getFactory()).isSameAs(factory); + } + + @Test + public void objectMapperCanHandleNullInsteadOfCustomJsonFactory() { + ObjectMapper mapper = Jackson.newObjectMapper(null); + + assertThat(mapper.getFactory()).isNotNull(); + } + + @Test + public void objectMapperCanDeserializeJdk7Types() throws IOException { + final LogMetadata metadata = Jackson.newObjectMapper() + .readValue("{\"path\": \"/var/log/app/server.log\"}", LogMetadata.class); + assertThat(metadata).isNotNull(); + assertThat(metadata.path).isEqualTo(Paths.get("/var/log/app/server.log")); + } + + @Test + public void objectMapperSerializesNullValues() throws IOException { + final ObjectMapper mapper = Jackson.newObjectMapper(); + final Issue1627 pojo = new Issue1627(null, null); + final String json = "{\"string\":null,\"uuid\":null}"; + + assertThat(mapper.writeValueAsString(pojo)).isEqualTo(json); + } + + static class LogMetadata { + + public Path path; + } + +} 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 00000000000..9bcaa569d6f --- /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.assertj.core.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 00000000000..2d775f5d2f5 --- /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 00000000000..60e80f839fc --- /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 00000000000..9261c5a608e --- /dev/null +++ b/dropwizard-jdbi/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-jdbi + Dropwizard JDBI Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-db + + + org.jdbi + jdbi + + + io.dropwizard.metrics + metrics-jdbi + + + com.h2database + h2 + test + + + 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 00000000000..12ef10c16dc --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIFactory.java @@ -0,0 +1,157 @@ +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 io.dropwizard.db.ManagedDataSource; +import io.dropwizard.db.PooledDataSourceFactory; +import io.dropwizard.jdbi.args.GuavaOptionalArgumentFactory; +import io.dropwizard.jdbi.args.GuavaOptionalInstantArgumentFactory; +import io.dropwizard.jdbi.args.GuavaOptionalJodaTimeArgumentFactory; +import io.dropwizard.jdbi.args.GuavaOptionalLocalDateArgumentFactory; +import io.dropwizard.jdbi.args.GuavaOptionalLocalDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.GuavaOptionalOffsetTimeArgumentFactory; +import io.dropwizard.jdbi.args.GuavaOptionalZonedTimeArgumentFactory; +import io.dropwizard.jdbi.args.InstantArgumentFactory; +import io.dropwizard.jdbi.args.InstantMapper; +import io.dropwizard.jdbi.args.JodaDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.JodaDateTimeMapper; +import io.dropwizard.jdbi.args.LocalDateArgumentFactory; +import io.dropwizard.jdbi.args.LocalDateMapper; +import io.dropwizard.jdbi.args.LocalDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.LocalDateTimeMapper; +import io.dropwizard.jdbi.args.OffsetDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.OffsetDateTimeMapper; +import io.dropwizard.jdbi.args.OptionalArgumentFactory; +import io.dropwizard.jdbi.args.OptionalDoubleArgumentFactory; +import io.dropwizard.jdbi.args.OptionalDoubleMapper; +import io.dropwizard.jdbi.args.OptionalInstantArgumentFactory; +import io.dropwizard.jdbi.args.OptionalIntArgumentFactory; +import io.dropwizard.jdbi.args.OptionalIntMapper; +import io.dropwizard.jdbi.args.OptionalJodaTimeArgumentFactory; +import io.dropwizard.jdbi.args.OptionalLocalDateArgumentFactory; +import io.dropwizard.jdbi.args.OptionalLocalDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.OptionalLongArgumentFactory; +import io.dropwizard.jdbi.args.OptionalLongMapper; +import io.dropwizard.jdbi.args.OptionalOffsetDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.OptionalZonedDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.ZonedDateTimeArgumentFactory; +import io.dropwizard.jdbi.args.ZonedDateTimeMapper; +import io.dropwizard.jdbi.logging.LogbackLog; +import io.dropwizard.setup.Environment; +import io.dropwizard.util.Duration; +import org.skife.jdbi.v2.ColonPrefixNamedParamStatementRewriter; +import org.skife.jdbi.v2.DBI; +import org.slf4j.LoggerFactory; + +import java.util.Optional; +import java.util.TimeZone; + +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, + statementContext -> RAW_SQL); + } + } + + /** + * Get a time zone of a database + * + *

    Override this method to specify a time zone of a database + * to use in {@link io.dropwizard.jdbi.args.JodaDateTimeMapper} and + * {@link io.dropwizard.jdbi.args.JodaDateTimeArgument}

    + * + *

    It's needed for cases when the database operates in a different + * time zone then the application and it doesn't use the SQL type + * `TIMESTAMP WITH TIME ZONE`. It such cases information about the + * time zone should explicitly passed to the JDBC driver

    + * + * @return a time zone of a database + */ + protected Optional databaseTimeZone() { + return Optional.empty(); + } + + public DBI build(Environment environment, + PooledDataSourceFactory configuration, + String name) { + final ManagedDataSource dataSource = configuration.build(environment.metrics(), name); + return build(environment, configuration, dataSource, name); + } + + public DBI build(Environment environment, + PooledDataSourceFactory 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( + environment.getHealthCheckExecutorService(), + configuration.getValidationQueryTimeout().orElseGet(() -> Duration.seconds(5)), + 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 GuavaOptionalArgumentFactory(configuration.getDriverClass())); + dbi.registerArgumentFactory(new OptionalArgumentFactory(configuration.getDriverClass())); + dbi.registerArgumentFactory(new OptionalDoubleArgumentFactory()); + dbi.registerArgumentFactory(new OptionalIntArgumentFactory()); + dbi.registerArgumentFactory(new OptionalLongArgumentFactory()); + dbi.registerColumnMapper(new OptionalDoubleMapper()); + dbi.registerColumnMapper(new OptionalIntMapper()); + dbi.registerColumnMapper(new OptionalLongMapper()); + dbi.registerContainerFactory(new ImmutableListContainerFactory()); + dbi.registerContainerFactory(new ImmutableSetContainerFactory()); + dbi.registerContainerFactory(new GuavaOptionalContainerFactory()); + dbi.registerContainerFactory(new OptionalContainerFactory()); + + final Optional timeZone = databaseTimeZone(); + dbi.registerArgumentFactory(new JodaDateTimeArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new LocalDateArgumentFactory()); + dbi.registerArgumentFactory(new LocalDateTimeArgumentFactory()); + dbi.registerArgumentFactory(new InstantArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new OffsetDateTimeArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new ZonedDateTimeArgumentFactory(timeZone)); + + // Should be registered after GuavaOptionalArgumentFactory to be processed first + dbi.registerArgumentFactory(new GuavaOptionalJodaTimeArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new GuavaOptionalLocalDateArgumentFactory()); + dbi.registerArgumentFactory(new GuavaOptionalLocalDateTimeArgumentFactory()); + dbi.registerArgumentFactory(new GuavaOptionalInstantArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new GuavaOptionalOffsetTimeArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new GuavaOptionalZonedTimeArgumentFactory(timeZone)); + + // Should be registered after OptionalArgumentFactory to be processed first + dbi.registerArgumentFactory(new OptionalJodaTimeArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new OptionalLocalDateArgumentFactory()); + dbi.registerArgumentFactory(new OptionalLocalDateTimeArgumentFactory()); + dbi.registerArgumentFactory(new OptionalInstantArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new OptionalOffsetDateTimeArgumentFactory(timeZone)); + dbi.registerArgumentFactory(new OptionalZonedDateTimeArgumentFactory(timeZone)); + + dbi.registerColumnMapper(new JodaDateTimeMapper(timeZone)); + dbi.registerColumnMapper(new InstantMapper(timeZone)); + dbi.registerColumnMapper(new LocalDateMapper()); + dbi.registerColumnMapper(new LocalDateTimeMapper()); + dbi.registerColumnMapper(new OffsetDateTimeMapper()); + dbi.registerColumnMapper(new ZonedDateTimeMapper()); + + 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 00000000000..f22c45924e6 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/DBIHealthCheck.java @@ -0,0 +1,37 @@ +package io.dropwizard.jdbi; + +import com.codahale.metrics.health.HealthCheck; +import com.google.common.util.concurrent.MoreExecutors; +import io.dropwizard.db.TimeBoundHealthCheck; +import io.dropwizard.util.Duration; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +import java.util.concurrent.ExecutorService; + +public class DBIHealthCheck extends HealthCheck { + private final DBI dbi; + private final String validationQuery; + private final TimeBoundHealthCheck timeBoundHealthCheck; + + public DBIHealthCheck(ExecutorService executorService, Duration duration, DBI dbi, String validationQuery) { + this.dbi = dbi; + this.validationQuery = validationQuery; + this.timeBoundHealthCheck = new TimeBoundHealthCheck(executorService, duration); + } + + public DBIHealthCheck(DBI dbi, String validationQuery) { + this(MoreExecutors.newDirectExecutorService(), Duration.seconds(0), dbi, validationQuery); + } + + @Override + protected Result check() throws Exception { + return timeBoundHealthCheck.check(() -> { + try (Handle handle = dbi.open()) { + handle.execute(validationQuery); + return Result.healthy(); + } + }); + } + +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/GuavaOptionalContainerFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/GuavaOptionalContainerFactory.java new file mode 100755 index 00000000000..2cf4d37d0b3 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/GuavaOptionalContainerFactory.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 GuavaOptionalContainerFactory implements ContainerFactory> { + + @Override + public boolean accepts(Class type) { + return Optional.class.isAssignableFrom(type); + } + + @Override + public ContainerBuilder> newContainerBuilderFor(Class type) { + return new OptionalContainerBuilder(); + } + + private static class OptionalContainerBuilder implements ContainerBuilder> { + + private Optional optional = Optional.absent(); + + @Override + public ContainerBuilder> 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/ImmutableListContainerFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/ImmutableListContainerFactory.java new file mode 100644 index 00000000000..84796d33bf9 --- /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> { + @Override + public boolean accepts(Class type) { + return ImmutableList.class.isAssignableFrom(type); + } + + @Override + public ContainerBuilder> newContainerBuilderFor(Class type) { + return new ImmutableListContainerBuilder(); + } + + private static class ImmutableListContainerBuilder implements ContainerBuilder> { + private final ImmutableList.Builder builder = ImmutableList.builder(); + + @Override + public ContainerBuilder> 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 00000000000..1a857afbd28 --- /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> { + @Override + public boolean accepts(Class type) { + return ImmutableSet.class.isAssignableFrom(type); + } + + @Override + public ContainerBuilder> newContainerBuilderFor(Class type) { + return new ImmutableSetContainerBuilder(); + } + + + private static class ImmutableSetContainerBuilder implements ContainerBuilder> { + private final ImmutableSet.Builder builder = ImmutableSet.builder(); + + @Override + public ContainerBuilder> 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 00000000000..de670d9a35e --- /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; + + public 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 00000000000..8f6a584b362 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/OptionalContainerFactory.java @@ -0,0 +1,35 @@ +package io.dropwizard.jdbi; + +import org.skife.jdbi.v2.ContainerBuilder; +import org.skife.jdbi.v2.tweak.ContainerFactory; + +import java.util.Optional; + +public class OptionalContainerFactory implements ContainerFactory> { + + @Override + public boolean accepts(Class type) { + return Optional.class.isAssignableFrom(type); + } + + @Override + public ContainerBuilder> newContainerBuilderFor(Class type) { + return new OptionalContainerBuilder(); + } + + private static class OptionalContainerBuilder implements ContainerBuilder> { + + private Optional optional = Optional.empty(); + + @Override + public ContainerBuilder> add(Object it) { + optional = Optional.ofNullable(it); + return this; + } + + @Override + public Optional build() { + return optional; + } + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalArgumentFactory.java new file mode 100644 index 00000000000..463440d5724 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalArgumentFactory.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 GuavaOptionalArgumentFactory implements ArgumentFactory> { + 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 GuavaOptionalArgumentFactory(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 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/args/GuavaOptionalInstantArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalInstantArgumentFactory.java new file mode 100644 index 00000000000..0620257ab21 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalInstantArgumentFactory.java @@ -0,0 +1,44 @@ +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.time.Instant; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link Instant} arguments wrapped by Guava's {@link Optional}. + */ +public class GuavaOptionalInstantArgumentFactory implements ArgumentFactory> { + + private final java.util.Optional calendar; + + public GuavaOptionalInstantArgumentFactory() { + calendar = java.util.Optional.empty(); + } + + public GuavaOptionalInstantArgumentFactory(java.util.Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not Instant. + return optionalValue.isPresent() && optionalValue.get() instanceof Instant; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new InstantArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalJodaTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalJodaTimeArgumentFactory.java new file mode 100644 index 00000000000..8ca6b6105a0 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalJodaTimeArgumentFactory.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi.args; + +import com.google.common.base.Optional; +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; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for Joda's {@link DateTime} arguments wrapped by Guava's {@link Optional}. + */ +public class GuavaOptionalJodaTimeArgumentFactory implements ArgumentFactory> { + + private final java.util.Optional calendar; + + public GuavaOptionalJodaTimeArgumentFactory() { + calendar = java.util.Optional.empty(); + } + + public GuavaOptionalJodaTimeArgumentFactory(java.util.Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not DateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof DateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new JodaDateTimeArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalLocalDateArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalLocalDateArgumentFactory.java new file mode 100644 index 00000000000..a58308d66df --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalLocalDateArgumentFactory.java @@ -0,0 +1,30 @@ +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.time.LocalDate; + +/** + * An {@link ArgumentFactory} for {@link LocalDate} arguments wrapped by Guava's {@link Optional}. + */ +public class GuavaOptionalLocalDateArgumentFactory implements ArgumentFactory> { + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not LocalDate. + return optionalValue.isPresent() && optionalValue.get() instanceof LocalDate; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new LocalDateArgument(value.get()); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalLocalDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalLocalDateTimeArgumentFactory.java new file mode 100644 index 00000000000..44425144928 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalLocalDateTimeArgumentFactory.java @@ -0,0 +1,30 @@ +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.time.LocalDateTime; + +/** + * An {@link ArgumentFactory} for {@link LocalDateTime} arguments wrapped by Guava's {@link Optional}. + */ +public class GuavaOptionalLocalDateTimeArgumentFactory implements ArgumentFactory> { + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not LocalDateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof LocalDateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new LocalDateTimeArgument(value.get()); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalOffsetTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalOffsetTimeArgumentFactory.java new file mode 100644 index 00000000000..751586a0691 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalOffsetTimeArgumentFactory.java @@ -0,0 +1,44 @@ +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.time.OffsetDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link OffsetDateTime} arguments wrapped by Guava's {@link Optional}. + */ +public class GuavaOptionalOffsetTimeArgumentFactory implements ArgumentFactory> { + + private final java.util.Optional calendar; + + public GuavaOptionalOffsetTimeArgumentFactory() { + calendar = java.util.Optional.empty(); + } + + public GuavaOptionalOffsetTimeArgumentFactory(java.util.Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not OffsetDateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof OffsetDateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new OffsetDateTimeArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalZonedTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalZonedTimeArgumentFactory.java new file mode 100644 index 00000000000..69926c832af --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/GuavaOptionalZonedTimeArgumentFactory.java @@ -0,0 +1,44 @@ +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.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link ZonedDateTime} arguments wrapped by Guava's {@link Optional}. + */ +public class GuavaOptionalZonedTimeArgumentFactory implements ArgumentFactory> { + + private final java.util.Optional calendar; + + public GuavaOptionalZonedTimeArgumentFactory() { + calendar = java.util.Optional.empty(); + } + + public GuavaOptionalZonedTimeArgumentFactory(java.util.Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not ZonedDateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof ZonedDateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new ZonedDateTimeArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantArgument.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantArgument.java new file mode 100644 index 00000000000..a2257c3f9fd --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantArgument.java @@ -0,0 +1,41 @@ +package io.dropwizard.jdbi.args; + +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; +import java.time.Instant; +import java.util.Calendar; +import java.util.Optional; + +/** + * An {@link Argument} for {@link Instant} objects. + */ +public class InstantArgument implements Argument { + private final Instant instant; + private final Optional calendar; + + protected InstantArgument(final Instant instant, final Optional calendar) { + this.instant = instant; + this.calendar = calendar; + } + + @Override + public void apply(int position, PreparedStatement statement, StatementContext ctx) throws SQLException { + if (instant != null) { + if (calendar.isPresent()) { + // We need to make a clone, because Calendar is not thread-safe + // and some JDBC drivers mutate it during time calculations + final Calendar calendarClone = (Calendar) calendar.get().clone(); + statement.setTimestamp(position, Timestamp.from(instant), calendarClone); + } else { + statement.setTimestamp(position, Timestamp.from(instant)); + } + } else { + statement.setNull(position, Types.TIMESTAMP); + } + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantArgumentFactory.java new file mode 100644 index 00000000000..dc7adb75d11 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantArgumentFactory.java @@ -0,0 +1,43 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.Instant; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link Instant} arguments. + */ +public class InstantArgumentFactory implements ArgumentFactory { + /** + *

    {@link Calendar} for representing a database time zone.

    + * If a field is not represented in a database as + * {@code TIMESTAMP WITH TIME ZONE}, we need to set its time zone + * explicitly. Otherwise it will not be correctly represented in + * a time zone different from the time zone of the database. + */ + private final Optional calendar; + + public InstantArgumentFactory() { + this(Optional.empty()); + } + + public InstantArgumentFactory(final Optional tz) { + this.calendar = tz.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + return value instanceof Instant; + } + + @Override + public Argument build(Class expectedType, Instant value, StatementContext ctx) { + return new InstantArgument(value, calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantMapper.java new file mode 100644 index 00000000000..e8daf376348 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/InstantMapper.java @@ -0,0 +1,55 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * A {@link ResultColumnMapper} to map {@link Instant} objects. + */ +public class InstantMapper implements ResultColumnMapper { + /** + *

    {@link Calendar} for representing a database time zone.

    + * If a field is not represented in a database as + * {@code TIMESTAMP WITH TIME ZONE}, we need to set its time zone + * explicitly. Otherwise it will not be correctly represented in + * a time zone different from the time zone of the database. + */ + private final Optional calendar; + + public InstantMapper() { + this(Optional.empty()); + } + + public InstantMapper(final Optional tz) { + this.calendar = tz.map(GregorianCalendar::new); + } + + @Override + public Instant mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? + r.getTimestamp(columnNumber, cloneCalendar()) : + r.getTimestamp(columnNumber); + return timestamp == null ? null : timestamp.toInstant(); + } + + @Override + public Instant mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? + r.getTimestamp(columnLabel, cloneCalendar()) : + r.getTimestamp(columnLabel); + return timestamp == null ? null : timestamp.toInstant(); + } + + private Calendar cloneCalendar() { + return (Calendar) calendar.get().clone(); + } +} 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 00000000000..89cdc8d3cdb --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgument.java @@ -0,0 +1,44 @@ +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; +import java.util.Calendar; +import java.util.Optional; + +/** + * An {@link Argument} for Joda {@link DateTime} objects. + */ +public class JodaDateTimeArgument implements Argument { + + private final DateTime value; + private final Optional calendar; + + JodaDateTimeArgument(final DateTime value, final Optional calendar) { + this.value = value; + this.calendar = calendar; + } + + @Override + public void apply(final int position, + final PreparedStatement statement, + final StatementContext ctx) throws SQLException { + if (value != null) { + if (calendar.isPresent()) { + // We need to make a clone, because Calendar is not thread-safe + // and some JDBC drivers mutate it during time calculations + final Calendar calendarClone = (Calendar) calendar.get().clone(); + statement.setTimestamp(position, new Timestamp(value.getMillis()), calendarClone); + } else { + 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 00000000000..fcc8541518e --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentFactory.java @@ -0,0 +1,53 @@ +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; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for Joda {@link DateTime} arguments. + */ +public class JodaDateTimeArgumentFactory implements ArgumentFactory { + + /** + *

    {@link Calendar} for representing a database time zone.

    + * It's needed when an argument is not represented in a database + * as {@code TIMESTAMP WITH TIME ZONE}. In this case for correct + * representing of a timestamp an explicit cast to the database + * time zone is needed at the JDBC driver level. + */ + private final Optional calendar; + + public JodaDateTimeArgumentFactory() { + calendar = Optional.empty(); + } + + /** + * Create an argument factory with a custom time zone offset + * + * @param timeZone a time zone representing an offset + */ + public JodaDateTimeArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @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, calendar); + } +} 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 00000000000..77b133ee93e --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/JodaDateTimeMapper.java @@ -0,0 +1,69 @@ +package io.dropwizard.jdbi.args; + +import org.joda.time.DateTime; +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * A {@link ResultColumnMapper} to map Joda {@link DateTime} objects. + */ +public class JodaDateTimeMapper implements ResultColumnMapper { + + /** + *

    {@link Calendar} for representing a database time zone.

    + * If a field is not represented in a database as + * {@code TIMESTAMP WITH TIME ZONE}, we need to set its time zone + * explicitly. Otherwise it will not be correctly represented in + * a time zone different from the time zone of the database. + */ + private Optional calendar; + + public JodaDateTimeMapper() { + calendar = Optional.empty(); + } + + public JodaDateTimeMapper(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + /** + * Make a clone of a calendar. + *

    Despite the fact that {@link Calendar} is used only for + * representing a time zone, some JDBC drivers actually use it + * for time calculations,

    + *

    Also {@link Calendar} is not immutable, which makes it + * thread-unsafe. Therefore we need to make a copy to avoid + * state mutation problems.

    + * + * @return a clone of calendar, representing a database time zone + */ + private Calendar cloneCalendar() { + return (Calendar) calendar.get().clone(); + } + + @Override + public DateTime mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? r.getTimestamp(columnNumber, cloneCalendar()) : + r.getTimestamp(columnNumber); + if (timestamp == null) { + return null; + } + return new DateTime(timestamp.getTime()); } + + @Override + public DateTime mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? r.getTimestamp(columnLabel, cloneCalendar()) : + r.getTimestamp(columnLabel); + if (timestamp == null) { + return null; + } + return new DateTime(timestamp.getTime()); } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateArgument.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateArgument.java new file mode 100644 index 00000000000..a2d64af99a7 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateArgument.java @@ -0,0 +1,31 @@ +package io.dropwizard.jdbi.args; + +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; +import java.time.LocalDate; + +/** + * An {@link Argument} for {@link LocalDate} objects. + */ +public class LocalDateArgument implements Argument { + + private final LocalDate value; + + public LocalDateArgument(LocalDate value) { + this.value = value; + } + + @Override + public void apply(int position, PreparedStatement statement, StatementContext ctx) throws SQLException { + if (value != null) { + statement.setTimestamp(position, Timestamp.valueOf(value.atStartOfDay())); + } else { + statement.setNull(position, Types.TIMESTAMP); + } + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateArgumentFactory.java new file mode 100644 index 00000000000..9977bfa9daa --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateArgumentFactory.java @@ -0,0 +1,23 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.LocalDate; + +/** + * An {@link ArgumentFactory} for {@link LocalDate} arguments. + */ +public class LocalDateArgumentFactory implements ArgumentFactory { + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + return value instanceof LocalDate; + } + + @Override + public Argument build(Class expectedType, LocalDate value, StatementContext ctx) { + return new LocalDateArgument(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateMapper.java new file mode 100644 index 00000000000..c48a2b533d7 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateMapper.java @@ -0,0 +1,32 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDate; + +/** + * A {@link ResultColumnMapper} to map {@link LocalDate} objects. + */ +public class LocalDateMapper implements ResultColumnMapper { + @Override + public LocalDate mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final Timestamp timestamp = r.getTimestamp(columnLabel); + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime().toLocalDate(); + } + + @Override + public LocalDate mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final Timestamp timestamp = r.getTimestamp(columnNumber); + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime().toLocalDate(); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeArgument.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeArgument.java new file mode 100644 index 00000000000..7ea7fc7d1b9 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeArgument.java @@ -0,0 +1,33 @@ +package io.dropwizard.jdbi.args; + +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; +import java.time.LocalDateTime; + +/** + * An {@link Argument} for {@link LocalDateTime} objects. + */ +public class LocalDateTimeArgument implements Argument { + + private final LocalDateTime value; + + LocalDateTimeArgument(final LocalDateTime 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, Timestamp.valueOf(value)); + } else { + statement.setNull(position, Types.TIMESTAMP); + } + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeArgumentFactory.java new file mode 100644 index 00000000000..0154d64c868 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeArgumentFactory.java @@ -0,0 +1,27 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.LocalDateTime; + +/** + * An {@link ArgumentFactory} for {@link LocalDateTime} arguments. + */ +public class LocalDateTimeArgumentFactory implements ArgumentFactory { + + @Override + public boolean accepts(final Class expectedType, + final Object value, + final StatementContext ctx) { + return value instanceof LocalDateTime; + } + + @Override + public Argument build(final Class expectedType, + final LocalDateTime value, + final StatementContext ctx) { + return new LocalDateTimeArgument(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeMapper.java new file mode 100644 index 00000000000..5462f6adc5a --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/LocalDateTimeMapper.java @@ -0,0 +1,33 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +/** + * A {@link ResultColumnMapper} to map {@link LocalDateTime} objects. + */ +public class LocalDateTimeMapper implements ResultColumnMapper { + + @Override + public LocalDateTime mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final Timestamp timestamp = r.getTimestamp(columnLabel); + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime(); + } + + @Override + public LocalDateTime mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final Timestamp timestamp = r.getTimestamp(columnNumber); + if (timestamp == null) { + return null; + } + return timestamp.toLocalDateTime(); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeArgument.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeArgument.java new file mode 100644 index 00000000000..ff2e4549c79 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeArgument.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi.args; + +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; +import java.time.OffsetDateTime; +import java.util.Calendar; +import java.util.Optional; + +/** + * An {@link Argument} for {@link OffsetDateTime} objects. + */ +public class OffsetDateTimeArgument implements Argument { + + private final OffsetDateTime value; + private final Optional calendar; + + OffsetDateTimeArgument(final OffsetDateTime value, final Optional calendar) { + this.value = value; + this.calendar = calendar; + } + + @Override + public void apply(final int position, + final PreparedStatement statement, + final StatementContext ctx) throws SQLException { + if (value != null) { + if (calendar.isPresent()) { + // We need to make a clone, because Calendar is not thread-safe + // and some JDBC drivers mutate it during time calculations + final Calendar calendarClone = (Calendar) calendar.get().clone(); + statement.setTimestamp(position, new Timestamp(value.toInstant().toEpochMilli()), calendarClone); + } else { + statement.setTimestamp(position, new Timestamp(value.toInstant().toEpochMilli())); + } + } else { + statement.setNull(position, Types.TIMESTAMP); + } + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeArgumentFactory.java new file mode 100644 index 00000000000..db9ef6ad6b2 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeArgumentFactory.java @@ -0,0 +1,53 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.OffsetDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link OffsetDateTime} arguments. + */ +public class OffsetDateTimeArgumentFactory implements ArgumentFactory { + + /** + *

    {@link Calendar} for representing a database time zone.

    + * It's needed when an argument is not represented in a database + * as {@code TIMESTAMP WITH TIME ZONE}. In this case for correct + * representing of a timestamp an explicit cast to the database + * time zone is needed at the JDBC driver level. + */ + private final Optional calendar; + + public OffsetDateTimeArgumentFactory() { + calendar = Optional.empty(); + } + + /** + * Create an argument factory with a custom time zone offset + * + * @param timeZone a time zone representing an offset + */ + public OffsetDateTimeArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(final Class expectedType, + final Object value, + final StatementContext ctx) { + return value instanceof OffsetDateTime; + } + + @Override + public Argument build(final Class expectedType, + final OffsetDateTime value, + final StatementContext ctx) { + return new OffsetDateTimeArgument(value, calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeMapper.java new file mode 100644 index 00000000000..ae38ecd6672 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OffsetDateTimeMapper.java @@ -0,0 +1,75 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * A {@link ResultColumnMapper} to map {@link OffsetDateTime} objects. + */ +public class OffsetDateTimeMapper implements ResultColumnMapper { + + /** + *

    {@link Calendar} for representing a database time zone.

    + * If a field is not represented in a database as + * {@code TIMESTAMP WITH TIME ZONE}, we need to set its time zone + * explicitly. Otherwise it will not be correctly represented in + * a time zone different from the time zone of the database. + */ + private Optional calendar; + + public OffsetDateTimeMapper() { + calendar = Optional.empty(); + } + + public OffsetDateTimeMapper(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + /** + * Make a clone of a calendar. + *

    Despite the fact that {@link Calendar} is used only for + * representing a time zone, some JDBC drivers actually use it + * for time calculations,

    + *

    Also {@link Calendar} is not immutable, which makes it + * thread-unsafe. Therefore we need to make a copy to avoid + * state mutation problems.

    + * + * @return a clone of calendar, representing a database time zone + */ + private Calendar cloneCalendar() { + return (Calendar) calendar.get().clone(); + } + + @Override + public OffsetDateTime mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? r.getTimestamp(columnNumber, cloneCalendar()) : + r.getTimestamp(columnNumber); + return convertToOffsetDateTime(timestamp); + } + + @Override + public OffsetDateTime mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? r.getTimestamp(columnLabel, cloneCalendar()) : + r.getTimestamp(columnLabel); + return convertToOffsetDateTime(timestamp); + } + + private OffsetDateTime convertToOffsetDateTime(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + final Optional zoneId = calendar.flatMap(c -> Optional.of(c.getTimeZone().toZoneId())); + return OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp.getTime()), zoneId.orElse(ZoneId.systemDefault())); + } +} 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 00000000000..5e22140064a --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalArgumentFactory.java @@ -0,0 +1,73 @@ +package io.dropwizard.jdbi.args; + +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; +import java.util.Optional; + +public class OptionalArgumentFactory implements ArgumentFactory> { + private static class DefaultOptionalArgument implements Argument { + private final Optional value; + private final int nullType; + + private DefaultOptionalArgument(Optional value, int nullType) { + this.value = value; + this.nullType = nullType; + } + + private DefaultOptionalArgument(Optional value) { + this(value, Types.OTHER); + } + + @Override + public void apply(int position, + PreparedStatement statement, + StatementContext ctx) throws SQLException { + if (value.isPresent()) { + statement.setObject(position, value.get()); + } else { + statement.setNull(position, nullType); + } + } + } + + 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.orElse(null)); + } + } + + 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 value, StatementContext ctx) { + if ("com.microsoft.sqlserver.jdbc.SQLServerDriver".equals(jdbcDriver)) { + return new MsSqlOptionalArgument(value); + } else if ("oracle.jdbc.OracleDriver".equals(jdbcDriver)) { + return new DefaultOptionalArgument(value, Types.NULL); + } + return new DefaultOptionalArgument(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalDoubleArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalDoubleArgumentFactory.java new file mode 100644 index 00000000000..59ea1b901a5 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalDoubleArgumentFactory.java @@ -0,0 +1,41 @@ +package io.dropwizard.jdbi.args; + +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; +import java.util.OptionalDouble; + +public class OptionalDoubleArgumentFactory implements ArgumentFactory { + private static class DefaultOptionalArgument implements Argument { + private final OptionalDouble value; + + private DefaultOptionalArgument(OptionalDouble value) { + this.value = value; + } + + @Override + public void apply(int position, + PreparedStatement statement, + StatementContext ctx) throws SQLException { + if (value.isPresent()) { + statement.setDouble(position, value.getAsDouble()); + } else { + statement.setNull(position, Types.DOUBLE); + } + } + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + return value instanceof OptionalDouble; + } + + @Override + public Argument build(Class expectedType, OptionalDouble value, StatementContext ctx) { + return new DefaultOptionalArgument(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalDoubleMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalDoubleMapper.java new file mode 100644 index 00000000000..751fbf7d442 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalDoubleMapper.java @@ -0,0 +1,25 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.OptionalDouble; + +/** + * A {@link ResultColumnMapper} to map {@link OptionalDouble} objects. + */ +public class OptionalDoubleMapper implements ResultColumnMapper { + @Override + public OptionalDouble mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final double value = r.getDouble(columnNumber); + return r.wasNull() ? OptionalDouble.empty() : OptionalDouble.of(value); + } + + @Override + public OptionalDouble mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final double value = r.getDouble(columnLabel); + return r.wasNull() ? OptionalDouble.empty() : OptionalDouble.of(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalInstantArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalInstantArgumentFactory.java new file mode 100644 index 00000000000..e7e2a065b9c --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalInstantArgumentFactory.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.Instant; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link Instant} arguments wrapped by {@link Optional}. + */ +public class OptionalInstantArgumentFactory implements ArgumentFactory> { + + private final Optional calendar; + + public OptionalInstantArgumentFactory() { + calendar = Optional.empty(); + } + + public OptionalInstantArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not Instant. + return optionalValue.isPresent() && optionalValue.get() instanceof Instant; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new InstantArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalIntArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalIntArgumentFactory.java new file mode 100644 index 00000000000..5368580ad94 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalIntArgumentFactory.java @@ -0,0 +1,41 @@ +package io.dropwizard.jdbi.args; + +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; +import java.util.OptionalInt; + +public class OptionalIntArgumentFactory implements ArgumentFactory { + private static class DefaultOptionalArgument implements Argument { + private final OptionalInt value; + + private DefaultOptionalArgument(OptionalInt value) { + this.value = value; + } + + @Override + public void apply(int position, + PreparedStatement statement, + StatementContext ctx) throws SQLException { + if (value.isPresent()) { + statement.setInt(position, value.getAsInt()); + } else { + statement.setNull(position, Types.INTEGER); + } + } + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + return value instanceof OptionalInt; + } + + @Override + public Argument build(Class expectedType, OptionalInt value, StatementContext ctx) { + return new DefaultOptionalArgument(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalIntMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalIntMapper.java new file mode 100644 index 00000000000..eeb585495bf --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalIntMapper.java @@ -0,0 +1,25 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.OptionalInt; + +/** + * A {@link ResultColumnMapper} to map {@link OptionalInt} objects. + */ +public class OptionalIntMapper implements ResultColumnMapper { + @Override + public OptionalInt mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final int value = r.getInt(columnNumber); + return r.wasNull() ? OptionalInt.empty() : OptionalInt.of(value); + } + + @Override + public OptionalInt mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final int value = r.getInt(columnLabel); + return r.wasNull() ? OptionalInt.empty() : OptionalInt.of(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalJodaTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalJodaTimeArgumentFactory.java new file mode 100644 index 00000000000..ddd1bcf988d --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalJodaTimeArgumentFactory.java @@ -0,0 +1,44 @@ +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; + +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for Joda's {@link DateTime} arguments wrapped by {@link Optional}. + */ +public class OptionalJodaTimeArgumentFactory implements ArgumentFactory> { + + private final Optional calendar; + + public OptionalJodaTimeArgumentFactory() { + calendar = Optional.empty(); + } + + public OptionalJodaTimeArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not DateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof DateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new JodaDateTimeArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLocalDateArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLocalDateArgumentFactory.java new file mode 100644 index 00000000000..ec45882b78b --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLocalDateArgumentFactory.java @@ -0,0 +1,30 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.LocalDate; +import java.util.Optional; + +/** + * An {@link ArgumentFactory} for {@link LocalDate} arguments wrapped by {@link Optional}. + */ +public class OptionalLocalDateArgumentFactory implements ArgumentFactory> { + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not DateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof LocalDate; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new LocalDateArgument(value.get()); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLocalDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLocalDateTimeArgumentFactory.java new file mode 100644 index 00000000000..c240edc0969 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLocalDateTimeArgumentFactory.java @@ -0,0 +1,30 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * An {@link ArgumentFactory} for {@link LocalDateTime} arguments wrapped by {@link Optional}. + */ +public class OptionalLocalDateTimeArgumentFactory implements ArgumentFactory> { + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not LocalDateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof LocalDateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new LocalDateTimeArgument(value.get()); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLongArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLongArgumentFactory.java new file mode 100644 index 00000000000..fc46aec1da0 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLongArgumentFactory.java @@ -0,0 +1,41 @@ +package io.dropwizard.jdbi.args; + +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; +import java.util.OptionalLong; + +public class OptionalLongArgumentFactory implements ArgumentFactory { + private static class DefaultOptionalArgument implements Argument { + private final OptionalLong value; + + private DefaultOptionalArgument(OptionalLong value) { + this.value = value; + } + + @Override + public void apply(int position, + PreparedStatement statement, + StatementContext ctx) throws SQLException { + if (value.isPresent()) { + statement.setLong(position, value.getAsLong()); + } else { + statement.setNull(position, Types.BIGINT); + } + } + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + return value instanceof OptionalLong; + } + + @Override + public Argument build(Class expectedType, OptionalLong value, StatementContext ctx) { + return new DefaultOptionalArgument(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLongMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLongMapper.java new file mode 100644 index 00000000000..b2bea5426f7 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalLongMapper.java @@ -0,0 +1,25 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.OptionalLong; + +/** + * A {@link ResultColumnMapper} to map {@link OptionalLong} objects. + */ +public class OptionalLongMapper implements ResultColumnMapper { + @Override + public OptionalLong mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final long value = r.getLong(columnNumber); + return r.wasNull() ? OptionalLong.empty() : OptionalLong.of(value); + } + + @Override + public OptionalLong mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final long value = r.getLong(columnLabel); + return r.wasNull() ? OptionalLong.empty() : OptionalLong.of(value); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalOffsetDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalOffsetDateTimeArgumentFactory.java new file mode 100644 index 00000000000..ac313b4ca41 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalOffsetDateTimeArgumentFactory.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.OffsetDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link OffsetDateTime} arguments wrapped by {@link Optional}. + */ +public class OptionalOffsetDateTimeArgumentFactory implements ArgumentFactory> { + + private final Optional calendar; + + public OptionalOffsetDateTimeArgumentFactory() { + calendar = Optional.empty(); + } + + public OptionalOffsetDateTimeArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not OffsetDateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof OffsetDateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new OffsetDateTimeArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalZonedDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalZonedDateTimeArgumentFactory.java new file mode 100644 index 00000000000..060429e577f --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/OptionalZonedDateTimeArgumentFactory.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link ZonedDateTime} arguments wrapped by {@link Optional}. + */ +public class OptionalZonedDateTimeArgumentFactory implements ArgumentFactory> { + + private final Optional calendar; + + public OptionalZonedDateTimeArgumentFactory() { + calendar = Optional.empty(); + } + + public OptionalZonedDateTimeArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(Class expectedType, Object value, StatementContext ctx) { + if (value instanceof Optional) { + final Optional optionalValue = (Optional) value; + // Fall through to OptionalArgumentFactory if absent. + // Fall through to OptionalArgumentFactory if present, but not ZonedDateTime. + return optionalValue.isPresent() && optionalValue.get() instanceof ZonedDateTime; + } + return false; + } + + @Override + public Argument build(Class expectedType, Optional value, StatementContext ctx) { + // accepts guarantees that the value is present + return new ZonedDateTimeArgument(value.get(), calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeArgument.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeArgument.java new file mode 100644 index 00000000000..c2e8f279fd9 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeArgument.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi.args; + +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; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Optional; + +/** + * An {@link Argument} for {@link ZonedDateTime} objects. + */ +public class ZonedDateTimeArgument implements Argument { + + private final ZonedDateTime value; + private final Optional calendar; + + ZonedDateTimeArgument(final ZonedDateTime value, final Optional calendar) { + this.value = value; + this.calendar = calendar; + } + + @Override + public void apply(final int position, + final PreparedStatement statement, + final StatementContext ctx) throws SQLException { + if (value != null) { + if (calendar.isPresent()) { + // We need to make a clone, because Calendar is not thread-safe + // and some JDBC drivers mutate it during time calculations + final Calendar calendarClone = (Calendar) calendar.get().clone(); + statement.setTimestamp(position, new Timestamp(value.toInstant().toEpochMilli()), calendarClone); + } else { + statement.setTimestamp(position, new Timestamp(value.toInstant().toEpochMilli())); + } + } else { + statement.setNull(position, Types.TIMESTAMP); + } + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeArgumentFactory.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeArgumentFactory.java new file mode 100644 index 00000000000..47f3dee43e6 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeArgumentFactory.java @@ -0,0 +1,53 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.Argument; +import org.skife.jdbi.v2.tweak.ArgumentFactory; + +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * An {@link ArgumentFactory} for {@link ZonedDateTime} arguments. + */ +public class ZonedDateTimeArgumentFactory implements ArgumentFactory { + + /** + *

    {@link Calendar} for representing a database time zone.

    + * It's needed when an argument is not represented in a database + * as {@code TIMESTAMP WITH TIME ZONE}. In this case for correct + * representing of a timestamp an explicit cast to the database + * time zone is needed at the JDBC driver level. + */ + private final Optional calendar; + + public ZonedDateTimeArgumentFactory() { + calendar = Optional.empty(); + } + + /** + * Create an argument factory with a custom time zone offset + * + * @param timeZone a time zone representing an offset + */ + public ZonedDateTimeArgumentFactory(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + @Override + public boolean accepts(final Class expectedType, + final Object value, + final StatementContext ctx) { + return value instanceof ZonedDateTime; + } + + @Override + public Argument build(final Class expectedType, + final ZonedDateTime value, + final StatementContext ctx) { + return new ZonedDateTimeArgument(value, calendar); + } +} diff --git a/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeMapper.java b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeMapper.java new file mode 100644 index 00000000000..624100b654c --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/args/ZonedDateTimeMapper.java @@ -0,0 +1,75 @@ +package io.dropwizard.jdbi.args; + +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.tweak.ResultColumnMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +/** + * A {@link ResultColumnMapper} to map {@link ZonedDateTime} objects. + */ +public class ZonedDateTimeMapper implements ResultColumnMapper { + + /** + *

    {@link Calendar} for representing a database time zone.

    + * If a field is not represented in a database as + * {@code TIMESTAMP WITH TIME ZONE}, we need to set its time zone + * explicitly. Otherwise it will not be correctly represented in + * a time zone different from the time zone of the database. + */ + private Optional calendar; + + public ZonedDateTimeMapper() { + calendar = Optional.empty(); + } + + public ZonedDateTimeMapper(Optional timeZone) { + calendar = timeZone.map(GregorianCalendar::new); + } + + /** + * Make a clone of a calendar. + *

    Despite the fact that {@link Calendar} is used only for + * representing a time zone, some JDBC drivers actually use it + * for time calculations,

    + *

    Also {@link Calendar} is not immutable, which makes it + * thread-unsafe. Therefore we need to make a copy to avoid + * state mutation problems.

    + * + * @return a clone of calendar, representing a database time zone + */ + private Calendar cloneCalendar() { + return (Calendar) calendar.get().clone(); + } + + @Override + public ZonedDateTime mapColumn(ResultSet r, int columnNumber, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? r.getTimestamp(columnNumber, cloneCalendar()) : + r.getTimestamp(columnNumber); + return convertToZonedDateTime(timestamp); + } + + @Override + public ZonedDateTime mapColumn(ResultSet r, String columnLabel, StatementContext ctx) throws SQLException { + final Timestamp timestamp = calendar.isPresent() ? r.getTimestamp(columnLabel, cloneCalendar()) : + r.getTimestamp(columnLabel); + return convertToZonedDateTime(timestamp); + } + + private ZonedDateTime convertToZonedDateTime(Timestamp timestamp) { + if (timestamp == null) { + return null; + } + final Optional zoneId = calendar.flatMap(c -> Optional.of(c.getTimeZone().toZoneId())); + return ZonedDateTime.ofInstant(Instant.ofEpochMilli(timestamp.getTime()), zoneId.orElse(ZoneId.systemDefault())); + } +} 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 00000000000..dc9798a6c68 --- /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 00000000000..3e06ebb0a18 --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapper.java @@ -0,0 +1,35 @@ +package io.dropwizard.jdbi.jersey; + +import com.google.common.annotations.VisibleForTesting; +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. + */ +@Provider +public class LoggingDBIExceptionMapper extends LoggingExceptionMapper { + private static 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); + } + } + + @VisibleForTesting + static synchronized void setLogger(Logger newLogger) { + logger = newLogger; + } +} 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 00000000000..504c190e4eb --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapper.java @@ -0,0 +1,30 @@ +package io.dropwizard.jdbi.jersey; + +import com.google.common.annotations.VisibleForTesting; +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 + */ +@Provider +public class LoggingSQLExceptionMapper extends LoggingExceptionMapper { + private static 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); + } + } + + @VisibleForTesting + static synchronized void setLogger(Logger newLogger) { + logger = newLogger; + } +} 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 00000000000..376d1dfe55e --- /dev/null +++ b/dropwizard-jdbi/src/main/java/io/dropwizard/jdbi/logging/LogbackLog.java @@ -0,0 +1,51 @@ +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/DBIHealthCheckTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/DBIHealthCheckTest.java new file mode 100644 index 00000000000..435ad253fc9 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/DBIHealthCheckTest.java @@ -0,0 +1,44 @@ +package io.dropwizard.jdbi; + +import com.codahale.metrics.health.HealthCheck; +import io.dropwizard.util.Duration; +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class DBIHealthCheckTest { + + @Test + public void testItTimesOutProperly() throws Exception { + String validationQuery = "select 1"; + DBI dbi = mock(DBI.class); + Handle handle = mock(Handle.class); + when(dbi.open()).thenReturn(handle); + Mockito.doAnswer(invocation -> { + try { + TimeUnit.SECONDS.sleep(10); + } catch (Exception ignored) { + } + return null; + }).when(handle).execute(validationQuery); + + ExecutorService executorService = Executors.newSingleThreadExecutor(); + DBIHealthCheck dbiHealthCheck = new DBIHealthCheck(executorService, + Duration.milliseconds(5), + dbi, + validationQuery); + HealthCheck.Result result = dbiHealthCheck.check(); + executorService.shutdown(); + assertThat("is unhealthy", false, is(result.isHealthy())); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/GuavaJDBITest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/GuavaJDBITest.java new file mode 100755 index 00000000000..6efd0a04c87 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/GuavaJDBITest.java @@ -0,0 +1,183 @@ +package io.dropwizard.jdbi; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.health.HealthCheckRegistry; +import com.google.common.base.Optional; +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.BootstrapLogging; +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.StringColumnMapper; + +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class GuavaJDBITest { + private final DataSourceFactory hsqlConfig = new DataSourceFactory(); + + { + BootstrapLogging.bootstrap(); + hsqlConfig.setUrl("jdbc:h2:mem:GuavaJDBITest-" + 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 = new ArrayList<>(); + 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); + when(environment.getHealthCheckExecutorService()).thenReturn(Executors.newSingleThreadExecutor()); + + this.dbi = factory.build(environment, hsqlConfig, "hsql"); + final ArgumentCaptor 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@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@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@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 names = handle.createQuery("SELECT name FROM people WHERE age < ?") + .bind(0, 50) + .map(StringColumnMapper.INSTANCE); + assertThat(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 GuavaPersonDAO dao = dbi.open(GuavaPersonDAO.class); + + assertThat(dao.findByName(Optional.of("Coda Hale"))) + .isEqualTo("Coda Hale"); + } + + @Test + public void sqlObjectsCanReturnImmutableLists() throws Exception { + final GuavaPersonDAO dao = dbi.open(GuavaPersonDAO.class); + + assertThat(dao.findAllNames()) + .containsOnly("Coda Hale", "Kris Gale", "Old Guy", "Alice Example"); + } + + @Test + public void sqlObjectsCanReturnImmutableSets() throws Exception { + final GuavaPersonDAO dao = dbi.open(GuavaPersonDAO.class); + + assertThat(dao.findAllUniqueNames()) + .containsOnly("Coda Hale", "Kris Gale", "Old Guy", "Alice Example"); + } + + @Test + public void sqlObjectsCanReturnOptional() throws Exception { + final GuavaPersonDAO dao = dbi.open(GuavaPersonDAO.class); + + final Optional found = dao.findByEmail("chale@yammer-inc.com"); + assertThat(found).isNotNull(); + assertThat(found.isPresent()).isTrue(); + assertThat(found.get()).isEqualTo("Coda Hale"); + + + final Optional missing = dao.findByEmail("cemalettin.koc@gmail.com"); + assertThat(missing).isNotNull(); + assertThat(missing.isPresent()).isFalse(); + assertThat(missing.orNull()).isNull(); + } + + @Test + public void sqlObjectsCanReturnJodaDateTime() throws Exception { + final GuavaPersonDAO dao = dbi.open(GuavaPersonDAO.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@example.org"); + assertThat(notFound).isNull(); + + final Optional absentDateTime = dao.getCreatedAtByName("Alice Example"); + assertThat(absentDateTime).isNotNull(); + assertThat(absentDateTime.isPresent()).isFalse(); + + final Optional presentDateTime = dao.getCreatedAtByName("Coda Hale"); + assertThat(presentDateTime).isNotNull(); + assertThat(presentDateTime.isPresent()).isTrue(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/GuavaPersonDAO.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/GuavaPersonDAO.java new file mode 100755 index 00000000000..db67d71d7b7 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/GuavaPersonDAO.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 GuavaPersonDAO { + @SqlQuery("SELECT name FROM people WHERE name = :name") + String findByName(@Bind("name") Optional name); + + @SqlQuery("SELECT name FROM people ORDER BY name ASC") + ImmutableList findAllNames(); + + @SqlQuery("SELECT DISTINCT name FROM people") + ImmutableSet findAllUniqueNames(); + + @SqlQuery("SELECT name FROM people WHERE email = :email ") + @SingleValueResult(String.class) + Optional findByEmail(@Bind("email") String email); + + @SqlQuery("SELECT created_at FROM people WHERE created_at > :from ORDER BY created_at DESC LIMIT 1") + DateTime getLatestCreatedAt(@Bind("from") DateTime from); + + @SqlQuery("SELECT created_at FROM people WHERE name = :name") + @SingleValueResult(DateTime.class) + Optional getCreatedAtByName(@Bind("name") String name); + + @SqlQuery("SELECT created_at FROM people WHERE email = :email") + DateTime getCreatedAtByEmail(@Bind("email") String email); +} 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 00000000000..6d8f3ece019 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/JDBITest.java @@ -0,0 +1,183 @@ +package io.dropwizard.jdbi; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.health.HealthCheckRegistry; +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.BootstrapLogging; +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.StringColumnMapper; + +import java.sql.Timestamp; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.Executors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class JDBITest { + private final DataSourceFactory hsqlConfig = new DataSourceFactory(); + + { + BootstrapLogging.bootstrap(); + hsqlConfig.setUrl("jdbc:h2:mem:JDBITest-" + 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 = new ArrayList<>(); + 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); + when(environment.getHealthCheckExecutorService()).thenReturn(Executors.newSingleThreadExecutor()); + + this.dbi = factory.build(environment, hsqlConfig, "hsql"); + final ArgumentCaptor 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@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@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@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 names = handle.createQuery("SELECT name FROM people WHERE age < ?") + .bind(0, 50) + .map(StringColumnMapper.INSTANCE); + assertThat(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 found = dao.findByEmail("chale@yammer-inc.com"); + assertThat(found).isNotNull(); + assertThat(found.isPresent()).isTrue(); + assertThat(found.get()).isEqualTo("Coda Hale"); + + + final Optional missing = dao.findByEmail("cemalettin.koc@gmail.com"); + assertThat(missing).isNotNull(); + assertThat(missing.isPresent()).isFalse(); + assertThat(missing.orElse(null)).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@example.org"); + assertThat(notFound).isNull(); + + final Optional absentDateTime = dao.getCreatedAtByName("Alice Example"); + assertThat(absentDateTime).isNotNull(); + assertThat(absentDateTime.isPresent()).isFalse(); + + final Optional 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 00000000000..b51c220840b --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/PersonDAO.java @@ -0,0 +1,35 @@ +package io.dropwizard.jdbi; + +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; + +import java.util.Optional; + +public interface PersonDAO { + @SqlQuery("SELECT name FROM people WHERE name = :name") + String findByName(@Bind("name") Optional name); + + @SqlQuery("SELECT name FROM people ORDER BY name ASC") + ImmutableList findAllNames(); + + @SqlQuery("SELECT DISTINCT name FROM people") + ImmutableSet findAllUniqueNames(); + + @SqlQuery("SELECT name FROM people WHERE email = :email ") + @SingleValueResult(String.class) + Optional findByEmail(@Bind("email") String email); + + @SqlQuery("SELECT created_at FROM people WHERE created_at > :from ORDER BY created_at DESC LIMIT 1") + DateTime getLatestCreatedAt(@Bind("from") DateTime from); + + @SqlQuery("SELECT created_at FROM people WHERE name = :name") + @SingleValueResult(DateTime.class) + Optional getCreatedAtByName(@Bind("name") String name); + + @SqlQuery("SELECT created_at FROM people WHERE email = :email") + DateTime getCreatedAtByEmail(@Bind("email") String email); +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/InstantArgumentTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/InstantArgumentTest.java new file mode 100644 index 00000000000..eb952cab82e --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/InstantArgumentTest.java @@ -0,0 +1,53 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.StatementContext; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Optional; +import java.util.TimeZone; + +public class InstantArgumentTest { + + private final PreparedStatement statement = Mockito.mock(PreparedStatement.class); + private final StatementContext context = Mockito.mock(StatementContext.class); + + @Test + public void apply() throws Exception { + ZonedDateTime zonedDateTime = ZonedDateTime.parse("2012-12-21T00:00:00.000Z"); + ZonedDateTime expected = zonedDateTime.withZoneSameInstant(ZoneId.systemDefault()); + + new InstantArgument(zonedDateTime.toInstant(), Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.from(expected.toInstant())); + } + + @Test + public void apply_ValueIsNull() throws Exception { + new InstantArgument(null, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setNull(1, Types.TIMESTAMP); + } + + @Test + public void applyCalendar() throws Exception { + final ZoneId systemDefault = ZoneId.systemDefault(); + + // this test only asserts that a calendar was passed in. Not that the JDBC driver + // will do the right thing and adjust the time. + final ZonedDateTime zonedDateTime = ZonedDateTime.parse("2012-12-21T00:00:00.000Z"); + final ZonedDateTime expected = zonedDateTime.withZoneSameInstant(systemDefault); + final Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone(systemDefault)); + + new InstantArgument(zonedDateTime.toInstant(), Optional.of(calendar)).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.from(expected.toInstant()), calendar); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/InstantMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/InstantMapperTest.java new file mode 100644 index 00000000000..87b9c77f284 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/InstantMapperTest.java @@ -0,0 +1,58 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class InstantMapperTest { + + private final ResultSet resultSet = Mockito.mock(ResultSet.class); + + @Test + public void mapColumnByName() throws Exception { + ZonedDateTime expected = ZonedDateTime.parse("2012-12-21T00:00:00.000Z"); + ZonedDateTime stored = expected.withZoneSameInstant(ZoneId.systemDefault()); + when(resultSet.getTimestamp("instant")).thenReturn(Timestamp.from(stored.toInstant())); + + Instant actual = new InstantMapper().mapColumn(resultSet, "instant", null); + + assertThat(actual).isEqualTo(expected.toInstant()); + } + + @Test + public void mapColumnByName_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp("instant")).thenReturn(null); + + Instant actual = new InstantMapper().mapColumn(resultSet, "instant", null); + + assertThat(actual).isNull(); + } + + @Test + public void mapColumnByIndex() throws Exception { + ZonedDateTime expected = ZonedDateTime.parse("2012-12-21T00:00:00.000Z"); + ZonedDateTime stored = expected.withZoneSameInstant(ZoneId.systemDefault()); + when(resultSet.getTimestamp(1)).thenReturn(Timestamp.from(stored.toInstant())); + + Instant actual = new InstantMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isEqualTo(expected.toInstant()); + } + + @Test + public void mapColumnByIndex_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(null); + + Instant actual = new InstantMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isNull(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentTest.java new file mode 100644 index 00000000000..51f68e83592 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/JodaDateTimeArgumentTest.java @@ -0,0 +1,33 @@ +package io.dropwizard.jdbi.args; + +import org.joda.time.DateTime; +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.StatementContext; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.Optional; + +public class JodaDateTimeArgumentTest { + + private final PreparedStatement statement = Mockito.mock(PreparedStatement.class); + private final StatementContext context = Mockito.mock(StatementContext.class); + + @Test + public void apply() throws Exception { + DateTime dateTime = DateTime.parse("2007-12-03T10:15:30.375"); + + new JodaDateTimeArgument(dateTime, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.valueOf("2007-12-03 10:15:30.375")); + } + + @Test + public void apply_ValueIsNull() throws Exception { + new JodaDateTimeArgument(null, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setNull(1, Types.TIMESTAMP); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/JodaDateTimeMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/JodaDateTimeMapperTest.java new file mode 100644 index 00000000000..49b8f9b5801 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/JodaDateTimeMapperTest.java @@ -0,0 +1,52 @@ +package io.dropwizard.jdbi.args; + +import org.joda.time.DateTime; +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.Timestamp; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class JodaDateTimeMapperTest { + + private final ResultSet resultSet = Mockito.mock(ResultSet.class); + + @Test + public void mapColumnByName() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(Timestamp.valueOf("2007-12-03 10:15:30.375")); + + DateTime actual = new JodaDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isEqualTo(DateTime.parse("2007-12-03T10:15:30.375")); + } + + @Test + public void mapColumnByName_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(null); + + DateTime actual = new JodaDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isNull(); + } + + @Test + public void mapColumnByIndex() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(Timestamp.valueOf("2007-12-03 10:15:30.375")); + + DateTime actual = new JodaDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isEqualTo(DateTime.parse("2007-12-03T10:15:30.375")); + } + + @Test + public void mapColumnByIndex_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(null); + + DateTime actual = new JodaDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isNull(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateArgumentTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateArgumentTest.java new file mode 100644 index 00000000000..49bc5537a4d --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateArgumentTest.java @@ -0,0 +1,32 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.StatementContext; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDate; + +public class LocalDateArgumentTest { + + private final PreparedStatement statement = Mockito.mock(PreparedStatement.class); + private final StatementContext context = Mockito.mock(StatementContext.class); + + @Test + public void apply() throws Exception { + LocalDate localDate = LocalDate.parse("2007-12-03"); + + new LocalDateArgument(localDate).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.valueOf("2007-12-03 00:00:00.000")); + } + + @Test + public void apply_ValueIsNull() throws Exception { + new LocalDateArgument(null).apply(1, statement, context); + + Mockito.verify(statement).setNull(1, Types.TIMESTAMP); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateMapperTest.java new file mode 100644 index 00000000000..0c3090bc637 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateMapperTest.java @@ -0,0 +1,52 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class LocalDateMapperTest { + + private final ResultSet resultSet = Mockito.mock(ResultSet.class); + + @Test + public void mapColumnByName() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(Timestamp.valueOf("2007-12-03 00:00:00.000")); + + LocalDate actual = new LocalDateMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isEqualTo(LocalDate.parse("2007-12-03")); + } + + @Test + public void mapColumnByName_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(null); + + LocalDate actual = new LocalDateMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isNull(); + } + + @Test + public void mapColumnByIndex() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(Timestamp.valueOf("2007-12-03 00:00:00.000")); + + LocalDate actual = new LocalDateMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isEqualTo(LocalDate.parse("2007-12-03")); + } + + @Test + public void mapColumnByIndex_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(null); + + LocalDate actual = new LocalDateMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isNull(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateTimeArgumentTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateTimeArgumentTest.java new file mode 100644 index 00000000000..de8cf3bdc8c --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateTimeArgumentTest.java @@ -0,0 +1,32 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.StatementContext; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.LocalDateTime; + +public class LocalDateTimeArgumentTest { + + private final PreparedStatement statement = Mockito.mock(PreparedStatement.class); + private final StatementContext context = Mockito.mock(StatementContext.class); + + @Test + public void apply() throws Exception { + LocalDateTime localDateTime = LocalDateTime.parse("2007-12-03T10:15:30.375"); + + new LocalDateTimeArgument(localDateTime).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.valueOf("2007-12-03 10:15:30.375")); + } + + @Test + public void apply_ValueIsNull() throws Exception { + new LocalDateTimeArgument(null).apply(1, statement, context); + + Mockito.verify(statement).setNull(1, Types.TIMESTAMP); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateTimeMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateTimeMapperTest.java new file mode 100644 index 00000000000..7d9607f11cc --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/LocalDateTimeMapperTest.java @@ -0,0 +1,52 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class LocalDateTimeMapperTest { + + private final ResultSet resultSet = Mockito.mock(ResultSet.class); + + @Test + public void mapColumnByName() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(Timestamp.valueOf("2007-12-03 10:15:30.375")); + + LocalDateTime actual = new LocalDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isEqualTo(LocalDateTime.parse("2007-12-03T10:15:30.375")); + } + + @Test + public void mapColumnByName_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(null); + + LocalDateTime actual = new LocalDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isNull(); + } + + @Test + public void mapColumnByIndex() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(Timestamp.valueOf("2007-12-03 10:15:30.375")); + + LocalDateTime actual = new LocalDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isEqualTo(LocalDateTime.parse("2007-12-03T10:15:30.375")); + } + + @Test + public void mapColumnByIndex_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(null); + + LocalDateTime actual = new LocalDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isNull(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OffsetDateTimeArgumentTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OffsetDateTimeArgumentTest.java new file mode 100644 index 00000000000..7f139654307 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OffsetDateTimeArgumentTest.java @@ -0,0 +1,36 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.StatementContext; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Optional; + +public class OffsetDateTimeArgumentTest { + + private final PreparedStatement statement = Mockito.mock(PreparedStatement.class); + private final StatementContext context = Mockito.mock(StatementContext.class); + + @Test + public void apply() throws Exception { + final Instant now = OffsetDateTime.now().toInstant(); + final OffsetDateTime dateTime = OffsetDateTime.ofInstant(now, ZoneId.systemDefault()); + + new OffsetDateTimeArgument(dateTime, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.from(now)); + } + + @Test + public void apply_ValueIsNull() throws Exception { + new OffsetDateTimeArgument(null, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setNull(1, Types.TIMESTAMP); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OffsetDateTimeMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OffsetDateTimeMapperTest.java new file mode 100644 index 00000000000..71fd76eee05 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OffsetDateTimeMapperTest.java @@ -0,0 +1,58 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneId; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class OffsetDateTimeMapperTest { + + private final ResultSet resultSet = Mockito.mock(ResultSet.class); + + @Test + public void mapColumnByName() throws Exception { + final Instant now = OffsetDateTime.now().toInstant(); + + when(resultSet.getTimestamp("name")).thenReturn(Timestamp.from(now)); + + OffsetDateTime actual = new OffsetDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isEqualTo(OffsetDateTime.ofInstant(now, ZoneId.systemDefault())); + } + + @Test + public void mapColumnByName_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(null); + + OffsetDateTime actual = new OffsetDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isNull(); + } + + @Test + public void mapColumnByIndex() throws Exception { + final Instant now = OffsetDateTime.now().toInstant(); + + when(resultSet.getTimestamp(1)).thenReturn(Timestamp.from(now)); + + OffsetDateTime actual = new OffsetDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isEqualTo(OffsetDateTime.ofInstant(now, ZoneId.systemDefault())); + } + + @Test + public void mapColumnByIndex_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(null); + + OffsetDateTime actual = new OffsetDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isNull(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalDoubleTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalDoubleTest.java new file mode 100644 index 00000000000..031ba7a186b --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalDoubleTest.java @@ -0,0 +1,63 @@ +package io.dropwizard.jdbi.args; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; + +import java.io.IOException; +import java.util.OptionalDouble; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalDoubleTest { + private final Environment env = new Environment("test-optional-double", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TestDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-double-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE test (id INT PRIMARY KEY, optional DOUBLE)"); + } + dao = dbi.onDemand(TestDao.class); + } + + @Test + public void testPresent() { + dao.insert(1, OptionalDouble.of(123.456D)); + + assertThat(dao.findOptionalDoubleById(1).getAsDouble()).isEqualTo(123.456D); + } + + @Test + public void testAbsent() { + dao.insert(2, OptionalDouble.empty()); + + assertThat(dao.findOptionalDoubleById(2).isPresent()).isFalse(); + } + + interface TestDao { + + @SqlUpdate("INSERT INTO test(id, optional) VALUES (:id, :optional)") + void insert(@Bind("id") int id, @Bind("optional") OptionalDouble optional); + + @SqlQuery("SELECT optional FROM test WHERE id = :id") + OptionalDouble findOptionalDoubleById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalIntTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalIntTest.java new file mode 100644 index 00000000000..234979df058 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalIntTest.java @@ -0,0 +1,63 @@ +package io.dropwizard.jdbi.args; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; + +import java.io.IOException; +import java.util.OptionalInt; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalIntTest { + private final Environment env = new Environment("test-optional-int", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TestDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-int-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE test (id INT PRIMARY KEY, optional INT)"); + } + dao = dbi.onDemand(TestDao.class); + } + + @Test + public void testPresent() { + dao.insert(1, OptionalInt.of(42)); + + assertThat(dao.findOptionalIntById(1).getAsInt()).isEqualTo(42); + } + + @Test + public void testAbsent() { + dao.insert(2, OptionalInt.empty()); + + assertThat(dao.findOptionalIntById(2).isPresent()).isFalse(); + } + + interface TestDao { + + @SqlUpdate("INSERT INTO test(id, optional) VALUES (:id, :optional)") + void insert(@Bind("id") int id, @Bind("optional") OptionalInt optional); + + @SqlQuery("SELECT optional FROM test WHERE id = :id") + OptionalInt findOptionalIntById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalLongTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalLongTest.java new file mode 100644 index 00000000000..fdd8d1164b2 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/OptionalLongTest.java @@ -0,0 +1,63 @@ +package io.dropwizard.jdbi.args; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; + +import java.io.IOException; +import java.util.OptionalLong; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalLongTest { + private final Environment env = new Environment("test-optional-long", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TestDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-long-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE test (id INT PRIMARY KEY, optional BIGINT)"); + } + dao = dbi.onDemand(TestDao.class); + } + + @Test + public void testPresent() { + dao.insert(1, OptionalLong.of(42L)); + + assertThat(dao.findOptionalLongById(1).getAsLong()).isEqualTo(42); + } + + @Test + public void testAbsent() { + dao.insert(2, OptionalLong.empty()); + + assertThat(dao.findOptionalLongById(2).isPresent()).isFalse(); + } + + interface TestDao { + + @SqlUpdate("INSERT INTO test(id, optional) VALUES (:id, :optional)") + void insert(@Bind("id") int id, @Bind("optional") OptionalLong optional); + + @SqlQuery("SELECT optional FROM test WHERE id = :id") + OptionalLong findOptionalLongById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/ZonedDateTimeArgumentTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/ZonedDateTimeArgumentTest.java new file mode 100644 index 00000000000..81cf53afc49 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/ZonedDateTimeArgumentTest.java @@ -0,0 +1,34 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; +import org.skife.jdbi.v2.StatementContext; + +import java.sql.PreparedStatement; +import java.sql.Timestamp; +import java.sql.Types; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Optional; + +public class ZonedDateTimeArgumentTest { + + private final PreparedStatement statement = Mockito.mock(PreparedStatement.class); + private final StatementContext context = Mockito.mock(StatementContext.class); + + @Test + public void apply() throws Exception { + ZonedDateTime dateTime = ZonedDateTime.of(2007, 12, 3, 10, 15, 30, 375_000_000, ZoneId.systemDefault()); + + new ZonedDateTimeArgument(dateTime, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setTimestamp(1, Timestamp.valueOf("2007-12-03 10:15:30.375")); + } + + @Test + public void apply_ValueIsNull() throws Exception { + new ZonedDateTimeArgument(null, Optional.empty()).apply(1, statement, context); + + Mockito.verify(statement).setNull(1, Types.TIMESTAMP); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/ZonedDateTimeMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/ZonedDateTimeMapperTest.java new file mode 100644 index 00000000000..227c34a6919 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/args/ZonedDateTimeMapperTest.java @@ -0,0 +1,53 @@ +package io.dropwizard.jdbi.args; + +import org.junit.Test; +import org.mockito.Mockito; + +import java.sql.ResultSet; +import java.sql.Timestamp; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +public class ZonedDateTimeMapperTest { + + private final ResultSet resultSet = Mockito.mock(ResultSet.class); + + @Test + public void mapColumnByName() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(Timestamp.valueOf("2007-12-03 10:15:30.375")); + + ZonedDateTime actual = new ZonedDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isEqualTo(ZonedDateTime.of(2007, 12, 3, 10, 15, 30, 375_000_000, ZoneId.systemDefault())); + } + + @Test + public void mapColumnByName_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp("name")).thenReturn(null); + + ZonedDateTime actual = new ZonedDateTimeMapper().mapColumn(resultSet, "name", null); + + assertThat(actual).isNull(); + } + + @Test + public void mapColumnByIndex() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(Timestamp.valueOf("2007-12-03 10:15:30.375")); + + ZonedDateTime actual = new ZonedDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isEqualTo(ZonedDateTime.of(2007, 12, 3, 10, 15, 30, 375_000_000, ZoneId.systemDefault())); + } + + @Test + public void mapColumnByIndex_TimestampIsNull() throws Exception { + when(resultSet.getTimestamp(1)).thenReturn(null); + + ZonedDateTime actual = new ZonedDateTimeMapper().mapColumn(resultSet, 1, null); + + assertThat(actual).isNull(); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/bundles/DBIExceptionsBundleTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/bundles/DBIExceptionsBundleTest.java new file mode 100644 index 00000000000..23f686e2f23 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/bundles/DBIExceptionsBundleTest.java @@ -0,0 +1,28 @@ +package io.dropwizard.jdbi.bundles; + +import io.dropwizard.jdbi.jersey.LoggingDBIExceptionMapper; +import io.dropwizard.jdbi.jersey.LoggingSQLExceptionMapper; +import io.dropwizard.jersey.setup.JerseyEnvironment; +import io.dropwizard.setup.Environment; +import org.junit.Test; + +import static org.mockito.Mockito.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class DBIExceptionsBundleTest { + + @Test + public void test() { + Environment environment = mock(Environment.class); + JerseyEnvironment jerseyEnvironment = mock(JerseyEnvironment.class); + when(environment.jersey()).thenReturn(jerseyEnvironment); + + new DBIExceptionsBundle().run(environment); + + verify(jerseyEnvironment, times(1)).register(isA(LoggingSQLExceptionMapper.class)); + verify(jerseyEnvironment, times(1)).register(isA(LoggingDBIExceptionMapper.class)); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapperTest.java new file mode 100644 index 00000000000..ce88fb662a2 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/jersey/LoggingDBIExceptionMapperTest.java @@ -0,0 +1,51 @@ +package io.dropwizard.jdbi.jersey; + +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.StatementContext; +import org.skife.jdbi.v2.exceptions.DBIException; +import org.skife.jdbi.v2.exceptions.NoResultsException; +import org.skife.jdbi.v2.exceptions.TransactionFailedException; +import org.slf4j.Logger; + +import java.sql.SQLException; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class LoggingDBIExceptionMapperTest { + + LoggingDBIExceptionMapper dbiExceptionMapper; + Logger logger; + + @Before + public void setUp() throws Exception { + logger = mock(Logger.class); + dbiExceptionMapper = new LoggingDBIExceptionMapper(); + LoggingDBIExceptionMapper.setLogger(logger); + } + + @Test + public void testSqlExceptionIsCause() throws Exception { + StatementContext statementContext = mock(StatementContext.class); + RuntimeException runtimeException = new RuntimeException("DB is down"); + SQLException sqlException = new SQLException("DB error", runtimeException); + DBIException dbiException = new NoResultsException("Unable get a result set", sqlException, statementContext); + + dbiExceptionMapper.logException(9812, dbiException); + + verify(logger).error("Error handling a request: 0000000000002654", sqlException); + verify(logger).error("Error handling a request: 0000000000002654", runtimeException); + verify(logger, never()).error("Error handling a request: 0000000000002654", dbiException); + } + + @Test + public void testPlainDBIException() throws Exception { + DBIException dbiException = new TransactionFailedException("Transaction failed for unknown reason"); + + dbiExceptionMapper.logException(9812, dbiException); + + verify(logger).error("Error handling a request: 0000000000002654", dbiException); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapperTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapperTest.java new file mode 100644 index 00000000000..57cb6b51a4c --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/jersey/LoggingSQLExceptionMapperTest.java @@ -0,0 +1,26 @@ +package io.dropwizard.jdbi.jersey; + +import org.junit.Test; +import org.slf4j.Logger; + +import java.sql.SQLException; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class LoggingSQLExceptionMapperTest { + + @Test + public void testLogException() throws Exception { + Logger logger = mock(Logger.class); + LoggingSQLExceptionMapper sqlExceptionMapper = new LoggingSQLExceptionMapper(); + LoggingSQLExceptionMapper.setLogger(logger); + + RuntimeException runtimeException = new RuntimeException("DB is down"); + SQLException sqlException = new SQLException("DB error", runtimeException); + sqlExceptionMapper.logException(4981, sqlException); + + verify(logger).error("Error handling a request: 0000000000001375", sqlException); + verify(logger).error("Error handling a request: 0000000000001375", runtimeException); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/DBIClient.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/DBIClient.java new file mode 100644 index 00000000000..abf137b8134 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/DBIClient.java @@ -0,0 +1,74 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.eclipse.jetty.util.component.LifeCycle; +import org.junit.rules.ExternalResource; +import org.skife.jdbi.v2.DBI; + +import java.util.List; +import java.util.Optional; +import java.util.TimeZone; + +/** + * Configured JDBI client for the database + */ +public class DBIClient extends ExternalResource { + + private final TimeZone dbTimeZone; + + private DBI dbi; + private List managedObjects; + + public DBIClient(TimeZone dbTimeZone) { + this.dbTimeZone = dbTimeZone; + } + + public DBI getDbi() { + return dbi; + } + + @Override + protected void before() throws Throwable { + final Environment environment = new Environment("test", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), + getClass().getClassLoader()); + + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:tcp://localhost/fldb"); + dataSourceFactory.setUser("sa"); + dataSourceFactory.setPassword(""); + + // Set the time zone of the database + final DBIFactory dbiFactory = new DBIFactory() { + @Override + protected Optional databaseTimeZone() { + return Optional.of(dbTimeZone); + } + }; + dbi = dbiFactory.build(environment, dataSourceFactory, "test-jdbi-time-zones"); + + // Start the DB pool + managedObjects = environment.lifecycle().getManagedObjects(); + for (LifeCycle managedObject : managedObjects) { + managedObject.start(); + } + } + + @Override + protected void after() { + // Shutdown the DB pool + try { + for (LifeCycle managedObject : managedObjects) { + managedObject.stop(); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/DatabaseInTimeZone.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/DatabaseInTimeZone.java new file mode 100644 index 00000000000..6bd9ee88827 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/DatabaseInTimeZone.java @@ -0,0 +1,65 @@ +package io.dropwizard.jdbi.timestamps; + +import org.h2.tools.Server; +import org.junit.rules.ExternalResource; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; + +/** + * Run an instance of the H2 database in an another time zone + */ +public class DatabaseInTimeZone extends ExternalResource { + + private final TemporaryFolder temporaryFolder; + private final TimeZone timeZone; + + private Process process; + + public DatabaseInTimeZone(TemporaryFolder temporaryFolder, TimeZone timeZone) { + this.temporaryFolder = temporaryFolder; + this.timeZone = timeZone; + } + + @Override + protected void before() throws Throwable { + String java = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java"; + File h2jar = new File(Server.class.getProtectionDomain().getCodeSource().getLocation().toURI()); + String vmArguments = "-Duser.timezone=" + timeZone.getID(); + + ProcessBuilder pb = new ProcessBuilder(java, vmArguments, "-cp", h2jar.getAbsolutePath(), Server.class.getName(), + "-tcp", "-baseDir", temporaryFolder.newFolder().getAbsolutePath()); + process = pb.start(); + } + + @Override + protected void after() { + try { + // Graceful shutdown of the database + Server.shutdownTcpServer("tcp://localhost:9092", "", true, false); + boolean exited = waitFor(process, 1, TimeUnit.SECONDS); + if (!exited) { + process.destroy(); + } + } catch (Exception e) { + throw new IllegalStateException("Unable shutdown DB", e); + } + } + + private static boolean waitFor(Process process, long timeout, TimeUnit unit) throws InterruptedException { + long startTime = System.nanoTime(); + while (true) { + try { + process.exitValue(); + return true; + } catch (IllegalThreadStateException ex) { + Thread.sleep(100); + } + if (System.nanoTime() - startTime > unit.toNanos(timeout)) { + return false; + } + } + } +} \ No newline at end of file diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalDateTimeTest.java new file mode 100644 index 00000000000..2f5c7d6aac0 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalDateTimeTest.java @@ -0,0 +1,78 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalDateTimeTest { + + private final Environment env = new Environment("test-guava-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:guava-date-time-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final DateTime startDate = DateTime.now(); + final DateTime endDate = startDate.plusDays(1); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.absent()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), DateTime.now(), Optional.absent(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") DateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalInstantTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalInstantTest.java new file mode 100644 index 00000000000..b9eb74037c2 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalInstantTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalInstantTest { + private final Environment env = new Environment("test-guava-instant", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:guava-instant-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final Instant startDate = Instant.now(); + final Instant endDate = startDate.plus(1L, ChronoUnit.DAYS); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.absent()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), Instant.now(), + Optional.absent(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") Instant startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalLocalDateTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalLocalDateTest.java new file mode 100644 index 00000000000..ce1e4923c3e --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalLocalDateTest.java @@ -0,0 +1,78 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalLocalDateTest { + private final Environment env = new Environment("test-guava-local-date", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:guava-local-date-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final LocalDate startDate = LocalDate.now(); + final LocalDate endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.absent()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), LocalDate.now(), + Optional.absent(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") LocalDate startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalLocalDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalLocalDateTimeTest.java new file mode 100644 index 00000000000..2d5f6d969a7 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalLocalDateTimeTest.java @@ -0,0 +1,78 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalLocalDateTimeTest { + private final Environment env = new Environment("test-guava-local-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:guava-local-date-time-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final LocalDateTime startDate = LocalDateTime.now(); + final LocalDateTime endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.absent()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), LocalDateTime.now(), + Optional.absent(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") LocalDateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalOffsetDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalOffsetDateTimeTest.java new file mode 100644 index 00000000000..2d847ac6685 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalOffsetDateTimeTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.OffsetDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalOffsetDateTimeTest { + + private final Environment env = new Environment("test-guava-offset-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:guava-offset-date-time-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final OffsetDateTime startDate = OffsetDateTime.now(); + final OffsetDateTime endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.absent()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), OffsetDateTime.now(), + Optional.absent(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") OffsetDateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalZonedDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalZonedDateTimeTest.java new file mode 100644 index 00000000000..0d2f43b0e39 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/GuavaOptionalZonedDateTimeTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalZonedDateTimeTest { + + private final Environment env = new Environment("test-guava-zoned-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:guava-zoned-date-time-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final ZonedDateTime startDate = ZonedDateTime.now(); + final ZonedDateTime endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.absent()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), ZonedDateTime.now(), + Optional.absent(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") ZonedDateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/JodaDateTimeSqlTimestampTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/JodaDateTimeSqlTimestampTest.java new file mode 100644 index 00000000000..54db3dce357 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/JodaDateTimeSqlTimestampTest.java @@ -0,0 +1,134 @@ +package io.dropwizard.jdbi.timestamps; + +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.TestRule; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; + +import java.util.Random; +import java.util.TimeZone; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for handling translation between DateTime to SQL TIMESTAMP + * in a different time zone + */ +public class JodaDateTimeSqlTimestampTest { + + private static final DateTimeFormatter ISO_FMT = ISODateTimeFormat.dateTimeNoMillis(); + + private static TemporaryFolder temporaryFolder; + private static DatabaseInTimeZone databaseInTimeZone; + private static DateTimeZone dbTimeZone; + private static DBIClient dbiClient; + @ClassRule + public static TestRule chain; + + static { + boolean done = false; + while (!done) { + try { + final TimeZone timeZone = getRandomTimeZone(); + dbTimeZone = DateTimeZone.forTimeZone(timeZone); + temporaryFolder = new TemporaryFolder(); + databaseInTimeZone = new DatabaseInTimeZone(temporaryFolder, timeZone); + dbiClient = new DBIClient(timeZone); + chain = RuleChain.outerRule(temporaryFolder) + .around(databaseInTimeZone) + .around(dbiClient); + done = true; + } catch (IllegalArgumentException e) { + if (!e.getMessage().contains("is not recognised")) { + throw e; + } + } + } + } + + private Handle handle; + private FlightDao flightDao; + + + private static TimeZone getRandomTimeZone() { + String[] ids = TimeZone.getAvailableIDs(); + return TimeZone.getTimeZone(ids[new Random().nextInt(ids.length)]); + } + + @Before + public void setUp() throws Exception { + handle = dbiClient.getDbi().open(); + handle.execute("CREATE TABLE flights (" + + " flight_id VARCHAR(5) PRIMARY KEY," + + " departure_airport VARCHAR(3) NOT NULL," + + " arrival_airport VARCHAR(3) NOT NULL," + + " departure_time TIMESTAMP NOT NULL," + + " arrival_time TIMESTAMP NOT NULL" + + ")"); + flightDao = handle.attach(FlightDao.class); + } + + @After + public void tearDown() throws Exception { + handle.execute("DROP TABLE flights"); + handle.close(); + } + + @Test + public void testInsertTimestamp() { + final DateTime departureTime = ISO_FMT.parseDateTime("2015-04-01T06:00:00-05:00"); + final DateTime arrivalTime = ISO_FMT.parseDateTime("2015-04-01T21:00:00+02:00"); + final int result = flightDao.insert("C1671", "ORD", "DUS", departureTime, arrivalTime); + assertThat(result).isGreaterThan(0); + + final Integer serverDepartureHour = (Integer) handle.select( + "SELECT EXTRACT(HOUR FROM departure_time) departure_hour " + + "FROM flights WHERE flight_id=?", "C1671").get(0).get("departure_hour"); + final Integer serverArrivalHour = (Integer) handle.select( + "SELECT EXTRACT(HOUR FROM arrival_time) arrival_hour " + + "FROM flights WHERE flight_id=?", "C1671").get(0).get("arrival_hour"); + + assertThat(serverDepartureHour).isEqualTo(departureTime.withZone(dbTimeZone).getHourOfDay()); + assertThat(serverArrivalHour).isEqualTo(arrivalTime.withZone(dbTimeZone).getHourOfDay()); + } + + @Test + public void testReadTimestamp() { + int result = handle.insert( + "INSERT INTO flights(flight_id, departure_airport, arrival_airport, departure_time, arrival_time) " + + "VALUES ('C1671','ORD','DUS','2015-04-01T06:00:00-05:00','2015-04-01T21:00:00+02:00')"); + assertThat(result).isGreaterThan(0); + + final DateTime departureTime = flightDao.getDepartureTime("C1671"); + final DateTime arrivalTime = flightDao.getArrivalTime("C1671"); + + assertThat(departureTime).isEqualTo(ISO_FMT.parseDateTime("2015-04-01T06:00:00-05:00")); + assertThat(arrivalTime).isEqualTo(ISO_FMT.parseDateTime("2015-04-01T21:00:00+02:00")); + } + + public interface FlightDao { + + @SqlUpdate("INSERT INTO flights(flight_id, departure_airport, arrival_airport,departure_time, arrival_time) " + + "VALUES (:flight_id, :departure_airport, :arrival_airport, :departure_time, :arrival_time)") + int insert(@Bind("flight_id") String flightId, @Bind("departure_airport") String departureAirport, + @Bind("arrival_airport") String arrivalAirport, + @Bind("departure_time") DateTime departureTime, @Bind("arrival_time") DateTime arrivalTime); + + @SqlQuery("SELECT arrival_time FROM flights WHERE flight_id=:flight_id") + DateTime getArrivalTime(@Bind("flight_id") String flightId); + + @SqlQuery("SELECT departure_time FROM flights WHERE flight_id=:flight_id") + DateTime getDepartureTime(@Bind("flight_id") String flightId); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalDateTimeTest.java new file mode 100644 index 00000000000..4635a625be3 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalDateTimeTest.java @@ -0,0 +1,80 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.joda.time.DateTime; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalDateTimeTest { + + private final Environment env = new Environment("test-optional-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:date-time-optional-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final DateTime startDate = DateTime.now(); + final DateTime endDate = startDate.plusDays(1); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.empty()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), DateTime.now(), + Optional.empty(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") DateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalInstantTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalInstantTest.java new file mode 100644 index 00000000000..88654406941 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalInstantTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalInstantTest { + private final Environment env = new Environment("test-optional-instant", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-instant-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final Instant startDate = Instant.now(); + final Instant endDate = startDate.plus(1L, ChronoUnit.DAYS); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.empty()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), Instant.now(), + Optional.empty(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") Instant startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalLocalDateTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalLocalDateTest.java new file mode 100644 index 00000000000..039e4b10c68 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalLocalDateTest.java @@ -0,0 +1,78 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.LocalDate; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalLocalDateTest { + private final Environment env = new Environment("test-optional-local-date", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-local-date-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final LocalDate startDate = LocalDate.now(); + final LocalDate endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.empty()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), LocalDate.now(), + Optional.empty(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") LocalDate startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalLocalDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalLocalDateTimeTest.java new file mode 100644 index 00000000000..d5c625ac88d --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalLocalDateTimeTest.java @@ -0,0 +1,78 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalLocalDateTimeTest { + private final Environment env = new Environment("test-optional-local-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-local-date-time" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final LocalDateTime startDate = LocalDateTime.now(); + final LocalDateTime endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.empty()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), LocalDateTime.now(), + Optional.empty(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") LocalDateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalOffsetDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalOffsetDateTimeTest.java new file mode 100644 index 00000000000..9b4db54321b --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalOffsetDateTimeTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.OffsetDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalOffsetDateTimeTest { + + private final Environment env = new Environment("test-optional-offset-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-offset-date-time-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final OffsetDateTime startDate = OffsetDateTime.now(); + final OffsetDateTime endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.empty()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), OffsetDateTime.now(), + Optional.empty(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") OffsetDateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} diff --git a/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalZonedDateTimeTest.java b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalZonedDateTimeTest.java new file mode 100644 index 00000000000..c5778e37368 --- /dev/null +++ b/dropwizard-jdbi/src/test/java/io/dropwizard/jdbi/timestamps/OptionalZonedDateTimeTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.jdbi.timestamps; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jdbi.DBIFactory; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; +import org.skife.jdbi.v2.sqlobject.Bind; +import org.skife.jdbi.v2.sqlobject.SqlQuery; +import org.skife.jdbi.v2.sqlobject.SqlUpdate; +import org.skife.jdbi.v2.sqlobject.customizers.SingleValueResult; + +import java.io.IOException; +import java.time.ZonedDateTime; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalZonedDateTimeTest { + + private final Environment env = new Environment("test-optional-zoned-date-time", Jackson.newObjectMapper(), + Validators.newValidator(), new MetricRegistry(), null); + + private TaskDao dao; + + @Before + public void setupTests() throws IOException { + final DataSourceFactory dataSourceFactory = new DataSourceFactory(); + dataSourceFactory.setDriverClass("org.h2.Driver"); + dataSourceFactory.setUrl("jdbc:h2:mem:optional-zoned-date-time-" + System.currentTimeMillis() + "?user=sa"); + dataSourceFactory.setInitialSize(1); + final DBI dbi = new DBIFactory().build(env, dataSourceFactory, "test"); + try (Handle h = dbi.open()) { + h.execute("CREATE TABLE tasks (" + + "id INT PRIMARY KEY, " + + "assignee VARCHAR(255) NOT NULL, " + + "start_date TIMESTAMP, " + + "end_date TIMESTAMP, " + + "comments VARCHAR(1024) " + + ")"); + } + dao = dbi.onDemand(TaskDao.class); + } + + @Test + public void testPresent() { + final ZonedDateTime startDate = ZonedDateTime.now(); + final ZonedDateTime endDate = startDate.plusDays(1L); + dao.insert(1, Optional.of("John Hughes"), startDate, Optional.of(endDate), Optional.empty()); + + assertThat(dao.findEndDateById(1).get()).isEqualTo(endDate); + } + + @Test + public void testAbsent() { + dao.insert(2, Optional.of("Kate Johansen"), ZonedDateTime.now(), + Optional.empty(), Optional.of("To be done")); + + assertThat(dao.findEndDateById(2).isPresent()).isFalse(); + } + + interface TaskDao { + + @SqlUpdate("INSERT INTO tasks(id, assignee, start_date, end_date, comments) " + + "VALUES (:id, :assignee, :start_date, :end_date, :comments)") + void insert(@Bind("id") int id, @Bind("assignee") Optional assignee, + @Bind("start_date") ZonedDateTime startDate, @Bind("end_date") Optional endDate, + @Bind("comments") Optional comments); + + @SqlQuery("SELECT end_date FROM tasks WHERE id = :id") + @SingleValueResult + Optional findEndDateById(@Bind("id") int id); + } +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-jdbi/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-jersey/pom.xml b/dropwizard-jersey/pom.xml new file mode 100644 index 00000000000..73c7d721916 --- /dev/null +++ b/dropwizard-jersey/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-jersey + Dropwizard Jersey Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-jackson + + + io.dropwizard + dropwizard-validation + + + io.dropwizard + dropwizard-logging + + + org.glassfish.jersey.core + jersey-server + + + org.glassfish.jersey.ext + jersey-metainf-services + + + org.glassfish.jersey.ext + jersey-bean-validation + + + io.dropwizard.metrics + metrics-jersey2 + + + com.fasterxml.jackson.jaxrs + jackson-jaxrs-json-provider + + + org.glassfish.jersey.containers + jersey-container-servlet + + + org.glassfish.jersey.test-framework + jersey-test-framework-core + test + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-webapp + + + org.eclipse.jetty + jetty-continuation + + + org.apache.commons + commons-lang3 + + + 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 00000000000..7c31f29c22a --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/DropwizardResourceConfig.java @@ -0,0 +1,254 @@ +package io.dropwizard.jersey; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jersey2.InstrumentedResourceMethodApplicationListener; +import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.classmate.TypeResolver; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ComparisonChain; +import io.dropwizard.jersey.caching.CacheControlledResponseFeature; +import io.dropwizard.jersey.params.NonEmptyStringParamFeature; +import io.dropwizard.jersey.sessions.SessionFactoryProvider; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.server.model.Resource; +import org.glassfish.jersey.server.model.ResourceMethod; +import org.glassfish.jersey.server.monitoring.ApplicationEvent; +import org.glassfish.jersey.server.monitoring.ApplicationEventListener; +import org.glassfish.jersey.server.monitoring.RequestEvent; +import org.glassfish.jersey.server.monitoring.RequestEventListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.Path; +import javax.ws.rs.ext.Provider; +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Set; +import java.util.TreeSet; + +public class DropwizardResourceConfig extends ResourceConfig { + private static final Logger LOGGER = LoggerFactory.getLogger(DropwizardResourceConfig.class); + private static final String NEWLINE = String.format("%n"); + private static final TypeResolver TYPE_RESOLVER = new TypeResolver(); + + private String urlPattern = "/*"; + + public DropwizardResourceConfig(MetricRegistry metricRegistry) { + this(false, metricRegistry); + } + + public DropwizardResourceConfig() { + this(true, null); + } + + public DropwizardResourceConfig(boolean testOnly, MetricRegistry metricRegistry) { + super(); + + if (metricRegistry == null) { + metricRegistry = new MetricRegistry(); + } + + property(ServerProperties.WADL_FEATURE_DISABLE, Boolean.TRUE); + if (!testOnly) { + // create a subclass to pin it to Throwable + register(new ComponentLoggingListener(this)); + } + + register(new InstrumentedResourceMethodApplicationListener(metricRegistry)); + register(CacheControlledResponseFeature.class); + register(io.dropwizard.jersey.guava.OptionalMessageBodyWriter.class); + register(io.dropwizard.jersey.guava.OptionalParamFeature.class); + register(io.dropwizard.jersey.optional.OptionalMessageBodyWriter.class); + register(io.dropwizard.jersey.optional.OptionalDoubleMessageBodyWriter.class); + register(io.dropwizard.jersey.optional.OptionalIntMessageBodyWriter.class); + register(io.dropwizard.jersey.optional.OptionalLongMessageBodyWriter.class); + register(io.dropwizard.jersey.optional.OptionalParamFeature.class); + register(NonEmptyStringParamFeature.class); + register(new SessionFactoryProvider.Binder()); + } + + public static DropwizardResourceConfig forTesting(MetricRegistry metricRegistry) { + return new DropwizardResourceConfig(true, metricRegistry); + } + + public void logComponents() { + LOGGER.debug("resources = {}", canonicalNamesByAnnotation(Path.class)); + LOGGER.debug("providers = {}", canonicalNamesByAnnotation(Provider.class)); + LOGGER.info(getEndpointsInfo()); + } + + public String getUrlPattern() { + return urlPattern; + } + + public void setUrlPattern(String urlPattern) { + this.urlPattern = urlPattern; + } + + /** + * Combines types of getClasses() and getSingletons in one Set. + * + * @return all registered types + */ + @VisibleForTesting + Set> allClasses() { + final Set> allClasses = new HashSet<>(getClasses()); + for (Object singleton : getSingletons()) { + allClasses.add(singleton.getClass()); + } + return allClasses; + } + + private Set canonicalNamesByAnnotation(final Class annotation) { + final Set result = new HashSet<>(); + for (Class clazz : getClasses()) { + if (clazz.isAnnotationPresent(annotation)) { + result.add(clazz.getCanonicalName()); + } + } + return result; + } + + public String getEndpointsInfo() { + final StringBuilder msg = new StringBuilder(1024); + final Set endpointLogLines = new TreeSet<>(new EndpointComparator()); + + msg.append("The following paths were found for the configured resources:"); + msg.append(NEWLINE).append(NEWLINE); + + final Set> allResources = new HashSet<>(); + for (Class clazz : allClasses()) { + if (!clazz.isInterface() && Resource.from(clazz) != null) { + allResources.add(clazz); + } + } + + for (Class klass : allResources) { + new EndpointLogger(urlPattern, klass).populate(endpointLogLines); + } + + if (!endpointLogLines.isEmpty()) { + for (EndpointLogLine line : endpointLogLines) { + msg.append(line).append(NEWLINE); + } + } else { + msg.append(" NONE").append(NEWLINE); + } + + return msg.toString(); + } + + + /** + * Takes care of recursively creating all registered endpoints and providing them as Collection of lines to log + * on application start. + */ + private static class EndpointLogger { + private final String rootPath; + private final Class klass; + + EndpointLogger(String urlPattern, Class klass) { + this.rootPath = urlPattern.endsWith("/*") ? urlPattern.substring(0, urlPattern.length() - 1) : urlPattern; + this.klass = klass; + } + + public void populate(Set endpointLogLines) { + populate(this.rootPath, klass, false, endpointLogLines); + } + + private void populate(String basePath, Class klass, boolean isLocator, + Set endpointLogLines) { + populate(basePath, klass, isLocator, Resource.from(klass), endpointLogLines); + } + + private void populate(String basePath, Class klass, boolean isLocator, Resource resource, + Set endpointLogLines) { + if (!isLocator) { + basePath = normalizePath(basePath, resource.getPath()); + } + + for (ResourceMethod method : resource.getResourceMethods()) { + endpointLogLines.add(new EndpointLogLine(method.getHttpMethod(), basePath, klass)); + } + + for (Resource childResource : resource.getChildResources()) { + for (ResourceMethod method : childResource.getAllMethods()) { + if (method.getType() == ResourceMethod.JaxrsType.RESOURCE_METHOD) { + final String path = normalizePath(basePath, childResource.getPath()); + endpointLogLines.add(new EndpointLogLine(method.getHttpMethod(), path, klass)); + } else if (method.getType() == ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR) { + final String path = normalizePath(basePath, childResource.getPath()); + final ResolvedType responseType = TYPE_RESOLVER + .resolve(method.getInvocable().getResponseType()); + final Class erasedType = !responseType.getTypeBindings().isEmpty() ? + responseType.getTypeBindings().getBoundType(0).getErasedType() : + responseType.getErasedType(); + populate(path, erasedType, true, endpointLogLines); + } + } + } + } + + private static String normalizePath(String basePath, String path) { + if (path == null) { + return basePath; + } + if (basePath.endsWith("/")) { + return path.startsWith("/") ? basePath + path.substring(1) : basePath + path; + } + return path.startsWith("/") ? basePath + path : basePath + "/" + path; + } + } + + private static class EndpointLogLine { + private final String httpMethod; + private final String basePath; + private final Class klass; + + EndpointLogLine(String httpMethod, String basePath, Class klass) { + this.basePath = basePath; + this.klass = klass; + this.httpMethod = httpMethod; + } + + @Override + public String toString() { + return String.format(" %-7s %s (%s)", httpMethod, basePath, klass.getCanonicalName()); + } + } + + private static class EndpointComparator implements Comparator, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public int compare(EndpointLogLine endpointA, EndpointLogLine endpointB) { + return ComparisonChain.start() + .compare(endpointA.basePath, endpointB.basePath) + .compare(endpointA.httpMethod, endpointB.httpMethod) + .result(); + } + } + + private static class ComponentLoggingListener implements ApplicationEventListener { + private final DropwizardResourceConfig config; + + ComponentLoggingListener(DropwizardResourceConfig config) { + this.config = config; + } + + @Override + public void onEvent(ApplicationEvent event) { + if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) { + config.logComponents(); + } + } + + @Override + public RequestEventListener onRequest(RequestEvent requestEvent) { + return null; + } + } +} 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 00000000000..1fe5d8a4180 --- /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; + +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@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 00000000000..429252572d2 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControl.java @@ -0,0 +1,196 @@ +package io.dropwizard.jersey.caching; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.concurrent.TimeUnit; + +/** + * An annotation which adds a constant {@code Cache-Control} header to the response produced by + * the annotated method. + */ +@Documented +@Target({ElementType.TYPE, ElementType.METHOD}) +@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. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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. + * + *

    From the HTTPbis spec:

    + *
    + * The proxy-revalidate response directive has the same meaning as the must-revalidate + * response directive, except that it does not apply to private caches. + *
    + * + * @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()}. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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()}. + * + *

    From the HTTPbis spec:

    + *
    + * 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. + *
    + * + * @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/CacheControlledResponseFeature.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControlledResponseFeature.java new file mode 100644 index 00000000000..091c035560a --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/caching/CacheControlledResponseFeature.java @@ -0,0 +1,62 @@ +package io.dropwizard.jersey.caching; + +import org.glassfish.jersey.server.model.AnnotatedMethod; + +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerResponseContext; +import javax.ws.rs.container.ContainerResponseFilter; +import javax.ws.rs.container.DynamicFeature; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@Provider +public class CacheControlledResponseFeature implements DynamicFeature { + + @Override + public void configure(final ResourceInfo resourceInfo, final FeatureContext configuration) { + final AnnotatedMethod am = new AnnotatedMethod(resourceInfo.getResourceMethod()); + + // check to see if it has cache control annotation + final CacheControl cc = am.getAnnotation(CacheControl.class); + if (cc != null) { + configuration.register(new CacheControlledResponseFilter(cc)); + } + } + + private static class CacheControlledResponseFilter implements ContainerResponseFilter { + private static final int ONE_YEAR_IN_SECONDS = (int) TimeUnit.DAYS.toSeconds(365); + private String cacheResponseHeader; + + CacheControlledResponseFilter(CacheControl control) { + 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); + } + + cacheResponseHeader = cacheControl.toString(); + } + + @Override + public void filter(ContainerRequestContext requestContext, + ContainerResponseContext responseContext) throws IOException { + if (!cacheResponseHeader.isEmpty()) { + responseContext.getHeaders().add(HttpHeaders.CACHE_CONTROL, cacheResponseHeader); + } + + } + + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/EarlyEofExceptionMapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/EarlyEofExceptionMapper.java new file mode 100644 index 00000000000..dc11964e323 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/EarlyEofExceptionMapper.java @@ -0,0 +1,29 @@ +package io.dropwizard.jersey.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}. +*/ +@Provider +public class EarlyEofExceptionMapper implements ExceptionMapper { + + 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(); + } +} 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 00000000000..20383247336 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/ErrorMessage.java @@ -0,0 +1,72 @@ +package io.dropwizard.jersey.errors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; + +import java.util.Objects; + +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ErrorMessage { + private final int code; + private final String message; + private final String details; + + public ErrorMessage(String message) { + this(500, message); + } + + public ErrorMessage(int code, String message) { + this(code, message, null); + } + + @JsonCreator + public ErrorMessage(@JsonProperty("code") int code, @JsonProperty("message") String message, + @JsonProperty("details") String details) { + this.code = code; + this.message = message; + this.details = details; + } + + @JsonProperty("code") + public Integer getCode() { + return code; + } + + @JsonProperty("message") + public String getMessage() { + return message; + } + + @JsonProperty("details") + public String getDetails() { + return details; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + final ErrorMessage other = (ErrorMessage) obj; + return Objects.equals(code, other.code) + && Objects.equals(message, other.message) + && Objects.equals(details, other.details); + } + + @Override + public int hashCode() { + return Objects.hash(code, message, details); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("code", code) + .add("message", message).add("details", details).toString(); + } +} 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 00000000000..8ab95f757a2 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/errors/LoggingExceptionMapper.java @@ -0,0 +1,64 @@ +package io.dropwizard.jersey.errors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.concurrent.ThreadLocalRandom; + +@Provider +public abstract class LoggingExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(LoggingExceptionMapper.class); + + @Override + public Response toResponse(E exception) { + final int status; + final ErrorMessage errorMessage; + + if (exception instanceof WebApplicationException) { + final Response response = ((WebApplicationException) exception).getResponse(); + Response.Status.Family family = response.getStatusInfo().getFamily(); + if (family.equals(Response.Status.Family.REDIRECTION)) { + return response; + } + if (family.equals(Response.Status.Family.SERVER_ERROR)) { + logException(exception); + } + status = response.getStatus(); + errorMessage = new ErrorMessage(status, exception.getLocalizedMessage()); + } else { + final long id = logException(exception); + status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + errorMessage = new ErrorMessage(formatErrorMessage(id, exception)); + } + + return Response.status(status) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorMessage) + .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 long logException(E exception) { + final long id = ThreadLocalRandom.current().nextLong(); + logException(id, exception); + return 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 00000000000..c632495a1d6 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/filter/AllowedMethodsFilter.java @@ -0,0 +1,61 @@ +package io.dropwizard.jersey.filter; + +import com.google.common.collect.ImmutableSet; +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 javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +public class AllowedMethodsFilter implements Filter { + + public static final String ALLOWED_METHODS_PARAM = "allowedMethods"; + public static final Set DEFAULT_ALLOWED_METHODS = ImmutableSet.of( + "GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH" + ); + + private static final Logger LOG = LoggerFactory.getLogger(AllowedMethodsFilter.class); + + private Set allowedMethods = new HashSet<>(); + + @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/OptionalMessageBodyWriter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalMessageBodyWriter.java new file mode 100644 index 00000000000..50d635c099c --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalMessageBodyWriter.java @@ -0,0 +1,62 @@ +package io.dropwizard.jersey.guava; + +import com.google.common.base.Optional; +import org.glassfish.jersey.message.MessageBodyWorkers; + +import javax.inject.Inject; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +@Provider +@Produces(MediaType.WILDCARD) +public class OptionalMessageBodyWriter implements MessageBodyWriter> { + + @Inject + private javax.inject.Provider mbw; + + // Jersey ignores this + @Override + public long getSize(Optional entity, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType) { + return 0; + } + + @Override + public boolean isWriteable(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType) { + return Optional.class.isAssignableFrom(type); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void writeTo(Optional entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) + throws IOException { + if (!entity.isPresent()) { + throw new NotFoundException(); + } + + final ParameterizedType actualGenericType = (ParameterizedType) genericType; + final Type actualGenericTypeArgument = actualGenericType.getActualTypeArguments()[0]; + final MessageBodyWriter writer = mbw.get().getMessageBodyWriter(entity.get().getClass(), + actualGenericTypeArgument, annotations, mediaType); + writer.writeTo(entity.get(), entity.get().getClass(), + actualGenericTypeArgument, + annotations, mediaType, httpHeaders, entityStream); + } + +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamBinder.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamBinder.java new file mode 100644 index 00000000000..b7b018f7af5 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamBinder.java @@ -0,0 +1,14 @@ +package io.dropwizard.jersey.guava; + +import org.glassfish.hk2.utilities.binding.AbstractBinder; + +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverterProvider; + +final class OptionalParamBinder extends AbstractBinder { + @Override + protected void configure() { + // Param converter providers + bind(OptionalParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamConverterProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamConverterProvider.java new file mode 100644 index 00000000000..6b873cbe7d5 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamConverterProvider.java @@ -0,0 +1,70 @@ +package io.dropwizard.jersey.guava; + +import com.google.common.base.Optional; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.ClassTypePair; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; + +@Singleton +public class OptionalParamConverterProvider implements ParamConverterProvider { + private final ServiceLocator locator; + + @Inject + public OptionalParamConverterProvider(final ServiceLocator locator) { + this.locator = locator; + } + + /** + * {@inheritDoc} + */ + @Override + public ParamConverter getConverter(final Class rawType, final Type genericType, final Annotation[] annotations) { + if (Optional.class.equals(rawType)) { + final List ctps = ReflectionHelper.getTypeArgumentAndClass(genericType); + final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null; + + if (ctp == null || ctp.rawClass() == String.class) { + return new ParamConverter() { + @Override + public T fromString(final String value) { + return rawType.cast(Optional.fromNullable(value)); + } + + @Override + public String toString(final T value) { + return value.toString(); + } + }; + } + + for (ParamConverterProvider provider : Providers.getProviders(locator, ParamConverterProvider.class)) { + final ParamConverter converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations); + if (converter != null) { + return new ParamConverter() { + @Override + public T fromString(final String value) { + final Object convertedValue = value == null ? null : converter.fromString(value); + return rawType.cast(Optional.fromNullable(convertedValue)); + } + + @Override + public String toString(final T value) { + return value.toString(); + } + }; + } + } + } + + return null; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamFeature.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamFeature.java new file mode 100644 index 00000000000..787df2f44ef --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/guava/OptionalParamFeature.java @@ -0,0 +1,12 @@ +package io.dropwizard.jersey.guava; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +public class OptionalParamFeature implements Feature { + @Override + public boolean configure(final FeatureContext context) { + context.register(new OptionalParamBinder()); + return true; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/gzip/ConfiguredGZipEncoder.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/gzip/ConfiguredGZipEncoder.java new file mode 100644 index 00000000000..2ee21cbe25c --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/gzip/ConfiguredGZipEncoder.java @@ -0,0 +1,50 @@ +package io.dropwizard.jersey.gzip; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.client.ClientRequestFilter; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.WriterInterceptor; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.IOException; +import java.util.zip.GZIPOutputStream; + +/** + * GZIP encoding support. Writer interceptor that encodes the output if + * {@link HttpHeaders#CONTENT_ENCODING Content-Encoding header} value equals + * to {@code gzip} or {@code x-gzip}. + * + * If so configured, it will encode the output even if the {@code gzip} and {@code x-gzip} + * {@link HttpHeaders#CONTENT_ENCODING Content-Encoding header} is missing, and insert a value + * of {@code gzip} for that header. + * + */ +@Provider +@Priority(Priorities.ENTITY_CODER) +public class ConfiguredGZipEncoder implements WriterInterceptor, ClientRequestFilter { + private boolean forceEncoding = false; + + public ConfiguredGZipEncoder(boolean forceEncoding) { + this.forceEncoding = forceEncoding; + } + + @Override + public void filter(ClientRequestContext context) throws IOException { + if (context.hasEntity() && context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING) == null && this.forceEncoding) { + context.getHeaders().add(HttpHeaders.CONTENT_ENCODING, "gzip"); + } + } + + @Override + public final void aroundWriteTo(WriterInterceptorContext context) throws IOException { + final String contentEncoding = (String) context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING); + if ((contentEncoding != null) && + (contentEncoding.equals("gzip") || contentEncoding.equals("x-gzip"))) { + context.setOutputStream(new GZIPOutputStream(context.getOutputStream())); + } + context.proceed(); + } + +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/gzip/GZipDecoder.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/gzip/GZipDecoder.java new file mode 100644 index 00000000000..feb64b2e330 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/gzip/GZipDecoder.java @@ -0,0 +1,40 @@ +package io.dropwizard.jersey.gzip; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.ext.Provider; +import javax.ws.rs.ext.ReaderInterceptor; +import javax.ws.rs.ext.ReaderInterceptorContext; +import java.io.IOException; +import java.util.zip.GZIPInputStream; + +/** + * GZIP encoding support. Reader interceptor that decodes the input if + * {@link HttpHeaders#CONTENT_ENCODING Content-Encoding header} value equals + * to {@code gzip} or {@code x-gzip}. + * + * We're using this instead of Jersey's built in {@link org.glassfish.jersey.message.GZipEncoder} + * because that unconditionally encodes on writing, whereas dropwizard-client + * needs the encoding to be configurable. See {@link ConfiguredGZipEncoder} + * + */ +@Provider +@Priority(Priorities.ENTITY_CODER) +public class GZipDecoder implements ReaderInterceptor { + + @Override + public Object aroundReadFrom(ReaderInterceptorContext context) throws IOException { + if (!context.getHeaders().containsKey(HttpHeaders.ACCEPT_ENCODING)) { + context.getHeaders().add(HttpHeaders.ACCEPT_ENCODING, "gzip"); + } + + final String contentEncoding = context.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING); + if (contentEncoding != null && + (contentEncoding.equals("gzip") || contentEncoding.equals("x-gzip"))) { + context.setInputStream(new GZIPInputStream(context.getInputStream())); + } + return context.proceed(); + } + +} 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 100644 index 00000000000..1fadb8f24f0 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProvider.java @@ -0,0 +1,74 @@ +package io.dropwizard.jersey.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnoreType; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.jaxrs.json.JacksonJaxbJsonProvider; + +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; + +/** + * A Jersey provider which enables using Jackson to parse request entities into objects and generate + * response entities from objects. + *

    + * (Essentially, extends {@link JacksonJaxbJsonProvider} with support for {@link JsonIgnoreType}.) + */ +public class JacksonMessageBodyProvider extends JacksonJaxbJsonProvider { + private final ObjectMapper mapper; + + public JacksonMessageBodyProvider(ObjectMapper mapper) { + 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 boolean isWriteable(Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType) { + return isProvidable(type) && super.isWriteable(type, genericType, annotations, mediaType); + } + + @Override + public Object readFrom(Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + InputStream entityStream) throws IOException { + try { + return super.readFrom(type, genericType, annotations, mediaType, httpHeaders, entityStream); + } catch (IOException e) { + if (e instanceof JsonProcessingException) { + throw e; + } + + // Deserializing malformatted URLs, for instance, will result in an IOException so + // wrap in an exception we can handle + throw JsonMappingException.fromUnexpectedIOE(e); + } + } + + 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 00000000000..dba7874a425 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapper.java @@ -0,0 +1,80 @@ +package io.dropwizard.jersey.jackson; + +import com.fasterxml.jackson.core.JsonGenerationException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.PropertyBindingException; +import com.google.common.base.Throwables; +import io.dropwizard.jersey.errors.ErrorMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class JsonProcessingExceptionMapper implements ExceptionMapper { + private static final Logger LOGGER = LoggerFactory.getLogger(JsonProcessingExceptionMapper.class); + private final boolean showDetails; + + public JsonProcessingExceptionMapper() { + this(false); + } + + public JsonProcessingExceptionMapper(boolean showDetails) { + this.showDetails = showDetails; + } + + @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, or it is not known how to serialize the type it's + * a server error and we should inform the developer. + */ + if (exception instanceof JsonMappingException) { + final JsonMappingException ex = (JsonMappingException) exception; + final Throwable cause = Throwables.getRootCause(ex); + + // Exceptions that denote an error on the client side + final boolean clientCause = cause instanceof InvalidFormatException || + cause instanceof PropertyBindingException; + + // Until completely foolproof mechanism can be worked out in coordination + // with Jackson on how to communicate client vs server fault, compare + // start of message with known server faults. + final boolean beanError = cause.getMessage().startsWith("No suitable constructor found") || + cause.getMessage().startsWith("Can not construct instance") || + cause.getMessage().startsWith("No serializer found for class"); + + if (beanError && !clientCause) { + LOGGER.error("Unable to serialize or deserialize the specific type", exception); + return Response.serverError().build(); + } + } + + /* + * Otherwise, it's those pesky users. + */ + LOGGER.debug("Unable to process JSON", exception); + final ErrorMessage errorMessage = new ErrorMessage(Response.Status.BAD_REQUEST.getStatusCode(), + "Unable to process JSON", showDetails ? message : null); + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.APPLICATION_JSON_TYPE) + .entity(errorMessage) + .build(); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalDateParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalDateParam.java new file mode 100644 index 00000000000..195a7e78fd1 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalDateParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.LocalDate; + +/** + * A parameter encapsulating date values. All non-parsable values will return a {@code 400 Bad + * Request} response. + * + * @see LocalDate + */ +public class LocalDateParam extends AbstractParam { + public LocalDateParam(final String input) { + super(input); + } + + @Override + protected LocalDate parse(final String input) throws Exception { + return LocalDate.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalDateTimeParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalDateTimeParam.java new file mode 100644 index 00000000000..712898ffc33 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalDateTimeParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.LocalDateTime; + +/** + * A parameter encapsulating date/time values. All non-parsable values will return a {@code 400 Bad + * Request} response. + * + * @see LocalDateTime + */ +public class LocalDateTimeParam extends AbstractParam { + public LocalDateTimeParam(final String input) { + super(input); + } + + @Override + protected LocalDateTime parse(final String input) throws Exception { + return LocalDateTime.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalTimeParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalTimeParam.java new file mode 100644 index 00000000000..8f2896a1049 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/LocalTimeParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.LocalTime; + +/** + * A parameter encapsulating time values. All non-parsable values will return a {@code 400 Bad + * Request} response. + * + * @see LocalTime + */ +public class LocalTimeParam extends AbstractParam { + public LocalTimeParam(final String input) { + super(input); + } + + @Override + protected LocalTime parse(final String input) throws Exception { + return LocalTime.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/OffsetDateTimeParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/OffsetDateTimeParam.java new file mode 100644 index 00000000000..380dee0c48b --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/OffsetDateTimeParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.OffsetDateTime; + +/** + * A parameter encapsulating date/time values containing an offset from UTC. + * All non-parsable values will return a {@code 400 Bad Request} response. + * + * @see OffsetDateTime + */ +public class OffsetDateTimeParam extends AbstractParam { + public OffsetDateTimeParam(final String input) { + super(input); + } + + @Override + protected OffsetDateTime parse(final String input) throws Exception { + return OffsetDateTime.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/YearMonthParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/YearMonthParam.java new file mode 100644 index 00000000000..0af0ef37c44 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/YearMonthParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.YearMonth; + +/** + * A parameter encapsulating year and month values. All non-parsable values will return a {@code 400 Bad + * Request} response. + * + * @see YearMonth + */ +public class YearMonthParam extends AbstractParam { + public YearMonthParam(final String input) { + super(input); + } + + @Override + protected YearMonth parse(final String input) throws Exception { + return YearMonth.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/YearParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/YearParam.java new file mode 100644 index 00000000000..7c92dc12c6a --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/YearParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.Year; + +/** + * A parameter encapsulating year values. All non-parsable values will return a {@code 400 Bad + * Request} response. + * + * @see java.time.YearMonth + */ +public class YearParam extends AbstractParam { + public YearParam(final String input) { + super(input); + } + + @Override + protected Year parse(final String input) throws Exception { + return Year.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/ZoneIdParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/ZoneIdParam.java new file mode 100644 index 00000000000..821633b236a --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/ZoneIdParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.ZoneId; + +/** + * A parameter encapsulating time-zone IDs, such as Europe/Paris. + * All non-parsable values will return a {@code 400 Bad Request} response. + * + * @see ZoneId + */ +public class ZoneIdParam extends AbstractParam { + public ZoneIdParam(final String input) { + super(input); + } + + @Override + protected ZoneId parse(final String input) throws Exception { + return ZoneId.of(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/ZonedDateTimeParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/ZonedDateTimeParam.java new file mode 100644 index 00000000000..b90d618b85b --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/jsr310/ZonedDateTimeParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.jsr310; + +import io.dropwizard.jersey.params.AbstractParam; + +import java.time.ZonedDateTime; + +/** + * A parameter encapsulating date/time values containing timezone information. + * All non-parsable values will return a {@code 400 Bad Request} response. + * + * @see ZonedDateTime + */ +public class ZonedDateTimeParam extends AbstractParam { + public ZonedDateTimeParam(final String input) { + super(input); + } + + @Override + protected ZonedDateTime parse(final String input) throws Exception { + return ZonedDateTime.parse(input); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalDoubleMessageBodyWriter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalDoubleMessageBodyWriter.java new file mode 100644 index 00000000000..45d5434becc --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalDoubleMessageBodyWriter.java @@ -0,0 +1,45 @@ +package io.dropwizard.jersey.optional; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.OptionalDouble; + +@Provider +@Produces(MediaType.WILDCARD) +public class OptionalDoubleMessageBodyWriter implements MessageBodyWriter { + // Jersey ignores this + @Override + public long getSize(OptionalDouble entity, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return OptionalDouble.class.isAssignableFrom(type); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void writeTo(OptionalDouble entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException { + if (!entity.isPresent()) { + throw new NotFoundException(); + } + + entityStream.write(Double.toString(entity.getAsDouble()).getBytes(StandardCharsets.US_ASCII)); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalDoubleParamConverterProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalDoubleParamConverterProvider.java new file mode 100644 index 00000000000..e6dadd2a186 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalDoubleParamConverterProvider.java @@ -0,0 +1,46 @@ +package io.dropwizard.jersey.optional; + +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.OptionalDouble; + +import static com.google.common.base.Preconditions.checkArgument; + +@Singleton +public class OptionalDoubleParamConverterProvider implements ParamConverterProvider { + private final OptionalDoubleParamConverter paramConverter = new OptionalDoubleParamConverter(); + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public ParamConverter getConverter(final Class rawType, final Type genericType, + final Annotation[] annotations) { + return OptionalDouble.class.equals(rawType) ? (ParamConverter) paramConverter : null; + } + + public static class OptionalDoubleParamConverter implements ParamConverter { + @Override + public OptionalDouble fromString(final String value) { + if (value == null) { + return OptionalDouble.empty(); + } + + try { + return OptionalDouble.of(Double.parseDouble(value)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public String toString(final OptionalDouble value) { + checkArgument(value != null); + return value.isPresent() ? Double.toString(value.getAsDouble()) : ""; + } + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalIntMessageBodyWriter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalIntMessageBodyWriter.java new file mode 100644 index 00000000000..7f694bcb704 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalIntMessageBodyWriter.java @@ -0,0 +1,45 @@ +package io.dropwizard.jersey.optional; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.OptionalInt; + +@Provider +@Produces(MediaType.WILDCARD) +public class OptionalIntMessageBodyWriter implements MessageBodyWriter { + // Jersey ignores this + @Override + public long getSize(OptionalInt entity, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return OptionalInt.class.isAssignableFrom(type); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void writeTo(OptionalInt entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException { + if (!entity.isPresent()) { + throw new NotFoundException(); + } + + entityStream.write(Integer.toString(entity.getAsInt()).getBytes(StandardCharsets.US_ASCII)); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalIntParamConverterProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalIntParamConverterProvider.java new file mode 100644 index 00000000000..03fe9f971af --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalIntParamConverterProvider.java @@ -0,0 +1,46 @@ +package io.dropwizard.jersey.optional; + +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.OptionalInt; + +import static com.google.common.base.Preconditions.checkArgument; + +@Singleton +public class OptionalIntParamConverterProvider implements ParamConverterProvider { + private final OptionalIntParamConverter paramConverter = new OptionalIntParamConverter(); + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public ParamConverter getConverter(final Class rawType, final Type genericType, + final Annotation[] annotations) { + return OptionalInt.class.equals(rawType) ? (ParamConverter) paramConverter : null; + } + + public static class OptionalIntParamConverter implements ParamConverter { + @Override + public OptionalInt fromString(final String value) { + if (value == null) { + return OptionalInt.empty(); + } + + try { + return OptionalInt.of(Integer.parseInt(value)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public String toString(final OptionalInt value) { + checkArgument(value != null); + return value.isPresent() ? Integer.toString(value.getAsInt()) : ""; + } + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalLongMessageBodyWriter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalLongMessageBodyWriter.java new file mode 100644 index 00000000000..3618145630c --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalLongMessageBodyWriter.java @@ -0,0 +1,44 @@ +package io.dropwizard.jersey.optional; + +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.OptionalLong; + +@Provider +@Produces(MediaType.WILDCARD) +public class OptionalLongMessageBodyWriter implements MessageBodyWriter { + // Jersey ignores this + @Override + public long getSize(OptionalLong entity, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return OptionalLong.class.isAssignableFrom(type); + } + + @Override + public void writeTo(OptionalLong entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) throws IOException { + if (!entity.isPresent()) { + throw new NotFoundException(); + } + + entityStream.write(Long.toString(entity.getAsLong()).getBytes(StandardCharsets.US_ASCII)); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalLongParamConverterProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalLongParamConverterProvider.java new file mode 100644 index 00000000000..1dc6ff23866 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalLongParamConverterProvider.java @@ -0,0 +1,46 @@ +package io.dropwizard.jersey.optional; + +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.OptionalLong; + +import static com.google.common.base.Preconditions.checkArgument; + +@Singleton +public class OptionalLongParamConverterProvider implements ParamConverterProvider { + private OptionalLongParamConverter paramConverter = new OptionalLongParamConverter(); + + /** + * {@inheritDoc} + */ + @Override + @SuppressWarnings("unchecked") + public ParamConverter getConverter(final Class rawType, final Type genericType, + final Annotation[] annotations) { + return OptionalLong.class.equals(rawType) ? (ParamConverter) paramConverter : null; + } + + public static class OptionalLongParamConverter implements ParamConverter { + @Override + public OptionalLong fromString(final String value) { + if (value == null) { + return OptionalLong.empty(); + } + + try { + return OptionalLong.of(Long.parseLong(value)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public String toString(final OptionalLong value) { + checkArgument(value != null); + return value.isPresent() ? Long.toString(value.getAsLong()) : ""; + } + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalMessageBodyWriter.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalMessageBodyWriter.java new file mode 100644 index 00000000000..569286ba08b --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalMessageBodyWriter.java @@ -0,0 +1,62 @@ +package io.dropwizard.jersey.optional; + +import org.glassfish.jersey.message.MessageBodyWorkers; + +import javax.inject.Inject; +import javax.ws.rs.NotFoundException; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Optional; + +@Provider +@Produces(MediaType.WILDCARD) +public class OptionalMessageBodyWriter implements MessageBodyWriter> { + + @Inject + private javax.inject.Provider mbw; + + // Jersey ignores this + @Override + public long getSize(Optional entity, Class type, Type genericType, + Annotation[] annotations, MediaType mediaType) { + return 0; + } + + @Override + public boolean isWriteable(Class type, Type genericType, + Annotation[] annotations, MediaType mediaType) { + return Optional.class.isAssignableFrom(type); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public void writeTo(Optional entity, + Class type, + Type genericType, + Annotation[] annotations, + MediaType mediaType, + MultivaluedMap httpHeaders, + OutputStream entityStream) + throws IOException { + if (!entity.isPresent()) { + throw new NotFoundException(); + } + + final Type innerGenericType = (genericType instanceof ParameterizedType) ? + ((ParameterizedType) genericType).getActualTypeArguments()[0] : entity.get().getClass(); + + final MessageBodyWriter writer = mbw.get().getMessageBodyWriter(entity.get().getClass(), + innerGenericType, annotations, mediaType); + writer.writeTo(entity.get(), entity.get().getClass(), + innerGenericType, annotations, mediaType, httpHeaders, entityStream); + } + +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamBinder.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamBinder.java new file mode 100644 index 00000000000..8171f32a665 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamBinder.java @@ -0,0 +1,17 @@ +package io.dropwizard.jersey.optional; + +import org.glassfish.hk2.utilities.binding.AbstractBinder; + +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverterProvider; + +final class OptionalParamBinder extends AbstractBinder { + @Override + protected void configure() { + // Param converter providers + bind(OptionalParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class); + bind(OptionalDoubleParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class); + bind(OptionalIntParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class); + bind(OptionalLongParamConverterProvider.class).to(ParamConverterProvider.class).in(Singleton.class); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamConverterProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamConverterProvider.java new file mode 100644 index 00000000000..80e095e87df --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamConverterProvider.java @@ -0,0 +1,70 @@ +package io.dropwizard.jersey.optional; + +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.jersey.internal.inject.Providers; +import org.glassfish.jersey.internal.util.ReflectionHelper; +import org.glassfish.jersey.internal.util.collection.ClassTypePair; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Optional; + +@Singleton +public class OptionalParamConverterProvider implements ParamConverterProvider { + private final ServiceLocator locator; + + @Inject + public OptionalParamConverterProvider(final ServiceLocator locator) { + this.locator = locator; + } + + /** + * {@inheritDoc} + */ + @Override + public ParamConverter getConverter(final Class rawType, final Type genericType, + final Annotation[] annotations) { + if (Optional.class.equals(rawType)) { + final List ctps = ReflectionHelper.getTypeArgumentAndClass(genericType); + final ClassTypePair ctp = (ctps.size() == 1) ? ctps.get(0) : null; + + if (ctp == null || ctp.rawClass() == String.class) { + return new ParamConverter() { + @Override + public T fromString(final String value) { + return rawType.cast(Optional.ofNullable(value)); + } + + @Override + public String toString(final T value) { + return value.toString(); + } + }; + } + + for (ParamConverterProvider provider : Providers.getProviders(locator, ParamConverterProvider.class)) { + final ParamConverter converter = provider.getConverter(ctp.rawClass(), ctp.type(), annotations); + if (converter != null) { + return new ParamConverter() { + @Override + public T fromString(final String value) { + return rawType.cast(Optional.ofNullable(value).map(s -> converter.fromString(value))); + } + + @Override + public String toString(final T value) { + return value.toString(); + } + }; + } + } + } + + return null; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamFeature.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamFeature.java new file mode 100644 index 00000000000..391036eb45b --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/optional/OptionalParamFeature.java @@ -0,0 +1,12 @@ +package io.dropwizard.jersey.optional; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +public class OptionalParamFeature implements Feature { + @Override + public boolean configure(final FeatureContext context) { + context.register(new OptionalParamBinder()); + return true; + } +} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/AbstractParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/AbstractParam.java similarity index 72% rename from dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/AbstractParam.java rename to dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/AbstractParam.java index a92f956d361..bda79c7f0b5 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/AbstractParam.java +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/AbstractParam.java @@ -1,6 +1,11 @@ -package com.yammer.dropwizard.jersey.params; +package io.dropwizard.jersey.params; + +import io.dropwizard.jersey.errors.ErrorMessage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; @@ -10,6 +15,7 @@ * @param the type of value wrapped by the parameter */ public abstract class AbstractParam { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractParam.class); private final T value; /** @@ -31,28 +37,39 @@ protected AbstractParam(String input) { * 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)}. + * {@link #errorMessage(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) { + LOGGER.debug("Invalid input received: {}", input); return Response.status(getErrorStatus()) - .entity(errorMessage(input, e)) + .entity(new ErrorMessage(getErrorStatus().getStatusCode(), + errorMessage(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.APPLICATION_JSON_TYPE; + } + /** * Given a string representation which was unable to be parsed and the exception thrown, produce - * a plain text entity to be sent to the client. + * 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()); + protected String errorMessage(Exception e) { + return String.format("Invalid parameter: %s", e.getMessage()); } /** @@ -86,8 +103,12 @@ public T get() { @Override public boolean equals(Object obj) { - if (this == obj) { return true; } - if ((obj == null) || (getClass() != obj.getClass())) { return false; } + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } final AbstractParam that = (AbstractParam) obj; return value.equals(that.value); } diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/BooleanParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/BooleanParam.java similarity index 82% rename from dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/BooleanParam.java rename to dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/BooleanParam.java index 8dfc6246ac9..6fa0e8f8df8 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/BooleanParam.java +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/BooleanParam.java @@ -1,4 +1,4 @@ -package com.yammer.dropwizard.jersey.params; +package io.dropwizard.jersey.params; /** * A parameter encapsulating boolean values. If the query parameter value is {@code "true"}, @@ -12,8 +12,8 @@ public BooleanParam(String input) { } @Override - protected String errorMessage(String input, Exception e) { - return '"' + input + "\" must be \"true\" or \"false\"."; + protected String errorMessage(Exception e) { + return "Parameter must be \"true\" or \"false\"."; } @Override 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 00000000000..12ba3fc7d0e --- /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 { + public DateTimeParam(String input) { + super(input); + } + + @Override + protected DateTime parse(String input) throws Exception { + return new DateTime(input, DateTimeZone.UTC); + } +} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/IntParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/IntParam.java similarity index 70% rename from dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/IntParam.java rename to dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/IntParam.java index cb0faaf2f28..79ff5afd5f0 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/IntParam.java +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/IntParam.java @@ -1,4 +1,4 @@ -package com.yammer.dropwizard.jersey.params; +package io.dropwizard.jersey.params; /** * A parameter encapsulating integer values. All non-decimal values will return a @@ -10,8 +10,8 @@ public IntParam(String input) { } @Override - protected String errorMessage(String input, Exception e) { - return '"' + input + "\" is not a number."; + protected String errorMessage(Exception e) { + return "Parameter is not a number."; } @Override diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LocalDateParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LocalDateParam.java new file mode 100644 index 00000000000..caef80cadc0 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LocalDateParam.java @@ -0,0 +1,18 @@ +package io.dropwizard.jersey.params; + +import org.joda.time.LocalDate; + +/** + * A parameter encapsulating local date values. All non-parsable values will return a {@code 400 Bad + * Request} response. + */ +public class LocalDateParam extends AbstractParam { + public LocalDateParam(String input) { + super(input); + } + + @Override + protected LocalDate parse(String input) throws Exception { + return new LocalDate(input); + } +} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/LongParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LongParam.java similarity index 69% rename from dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/LongParam.java rename to dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LongParam.java index 814eaaf6686..b77e26af760 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/jersey/params/LongParam.java +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/LongParam.java @@ -1,4 +1,4 @@ -package com.yammer.dropwizard.jersey.params; +package io.dropwizard.jersey.params; /** * A parameter encapsulating long values. All non-decimal values will return a {@code 400 Bad @@ -10,8 +10,8 @@ public LongParam(String input) { } @Override - protected String errorMessage(String input, Exception e) { - return '"' + input + "\" is not a number."; + protected String errorMessage(Exception e) { + return "Parameter is not a number."; } @Override diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/NonEmptyStringParam.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/NonEmptyStringParam.java new file mode 100644 index 00000000000..914fa0b2f03 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/NonEmptyStringParam.java @@ -0,0 +1,22 @@ +package io.dropwizard.jersey.params; + +import com.google.common.base.Strings; + +import java.util.Optional; + +/** + * A parameter encapsulating optional string values with the condition that empty string inputs are + * interpreted as being absent. This class is useful when it is desired for empty parameters to be + * synonymous with absent parameters instead of empty parameters evaluating to + * {@code Optional.of("")}. + */ +public class NonEmptyStringParam extends AbstractParam> { + public NonEmptyStringParam(String input) { + super(input); + } + + @Override + protected Optional parse(String input) throws Exception { + return Optional.ofNullable(Strings.emptyToNull(input)); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/NonEmptyStringParamFeature.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/NonEmptyStringParamFeature.java new file mode 100644 index 00000000000..3e56baabd42 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/params/NonEmptyStringParamFeature.java @@ -0,0 +1,39 @@ +package io.dropwizard.jersey.params; + +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Optional; + +/** + * A class for describing how Jersey should serialize a {@link NonEmptyStringParam}. If the + * parameter was not detected in the response, instead of the resulting value being null, it will + * evaluate to {@link Optional#empty()} + */ +public class NonEmptyStringParamFeature implements Feature { + @Override + public boolean configure(final FeatureContext context) { + context.register(new ParamConverterProvider() { + @Override + public ParamConverter getConverter(final Class rawType, + final Type genericType, + final Annotation[] annotations) { + return (rawType != NonEmptyStringParam.class) ? null : new ParamConverter() { + @Override + public T fromString(final String value) { + return rawType.cast(new NonEmptyStringParam(value)); + } + + @Override + public String toString(final T value) { + return value == null ? null : value.toString(); + } + }; + } + }); + return true; + } +} 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 00000000000..a15bf26e05d --- /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 { + + public UUIDParam(String input) { + super(input); + } + + @Override + protected String errorMessage(Exception e) { + return "Parameter 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 00000000000..8309f03c10b --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Flash.java @@ -0,0 +1,27 @@ +package io.dropwizard.jersey.sessions; + +import javax.servlet.http.HttpSession; +import java.util.Optional; + +public class Flash { + 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 get() { + return Optional.ofNullable(value); + } + + public void set(T value) { + session.setAttribute(ATTRIBUTE, value); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/FlashFactory.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/FlashFactory.java new file mode 100644 index 00000000000..a577791fec5 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/FlashFactory.java @@ -0,0 +1,32 @@ +package io.dropwizard.jersey.sessions; + +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.ws.rs.core.Context; + +public final class FlashFactory extends AbstractContainerRequestValueFactory> { + @Context + private HttpServletRequest request; + private boolean doNotCreate; + + public FlashFactory(boolean doNotCreate) { + this.doNotCreate = doNotCreate; + } + + @Override + @SuppressWarnings("rawtypes") + public Flash provide() { + if (request == null) { + return null; + } + + final HttpSession session = request.getSession(!this.doNotCreate); + if (session != null) { + return new Flash(session); + } + + return null; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/HttpSessionFactory.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/HttpSessionFactory.java new file mode 100644 index 00000000000..df635b2d06a --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/HttpSessionFactory.java @@ -0,0 +1,26 @@ +package io.dropwizard.jersey.sessions; + +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.ws.rs.core.Context; + +public final class HttpSessionFactory extends AbstractContainerRequestValueFactory { + @Context + private HttpServletRequest request; + private boolean doNotCreate; + + public HttpSessionFactory(boolean doNotCreate) { + this.doNotCreate = doNotCreate; + } + + @Override + public HttpSession provide() { + if (request == null) { + return null; + } + + return request.getSession(!doNotCreate); + } +} 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 00000000000..81914d23f6b --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/Session.java @@ -0,0 +1,14 @@ +package io.dropwizard.jersey.sessions; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.PARAMETER, ElementType.FIELD}) +public @interface Session { + boolean doNotCreate() default false; +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/SessionFactoryProvider.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/SessionFactoryProvider.java new file mode 100644 index 00000000000..1bdcf44622c --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/sessions/SessionFactoryProvider.java @@ -0,0 +1,62 @@ +package io.dropwizard.jersey.sessions; + +import org.glassfish.hk2.api.Factory; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.servlet.http.HttpSession; + +@Singleton +public class SessionFactoryProvider extends AbstractValueFactoryProvider { + + @Inject + public SessionFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, + final ServiceLocator injector) { + super(extractorProvider, injector, Parameter.Source.UNKNOWN); + } + + @Override + protected Factory createValueFactory(final Parameter parameter) { + final Class classType = parameter.getRawType(); + + final Session sessionAnnotation = parameter.getAnnotation(Session.class); + if (sessionAnnotation == null) { + return null; + } + + if (classType.isAssignableFrom(HttpSession.class)) { + return new HttpSessionFactory(sessionAnnotation.doNotCreate()); + } else if (classType.isAssignableFrom(Flash.class)) { + return new FlashFactory(sessionAnnotation.doNotCreate()); + } else { + return null; + } + } + + public static class SessionInjectionResolver extends ParamInjectionResolver { + public SessionInjectionResolver() { + super(SessionFactoryProvider.class); + } + } + + public static class Binder extends AbstractBinder { + @Override + protected void configure() { + bind(SessionFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(SessionInjectionResolver.class).to( + new TypeLiteral>() { + } + ).in(Singleton.class); + } + } +} + 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 00000000000..09a7fd95dec --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyContainerHolder.java @@ -0,0 +1,19 @@ +package io.dropwizard.jersey.setup; + +import javax.servlet.Servlet; + +public class JerseyContainerHolder { + private Servlet container; + + public JerseyContainerHolder(Servlet container) { + this.container = container; + } + + public Servlet getContainer() { + return container; + } + + public void setContainer(Servlet 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 00000000000..1a556581466 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyEnvironment.java @@ -0,0 +1,119 @@ +package io.dropwizard.jersey.setup; + +import com.google.common.base.Function; +import io.dropwizard.jersey.DropwizardResourceConfig; +import org.glassfish.jersey.server.ResourceConfig; + +import javax.annotation.Nullable; +import javax.servlet.Servlet; + +import static java.util.Objects.requireNonNull; + +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 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.register(requireNonNull(component)); + } + + /** + * Adds the given class as a Jersey component.

    N.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.register(requireNonNull(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.packages(requireNonNull(packages)); + } + + /** + * Enables the Jersey feature with the given name. + * + * @param featureName the name of the feature to be enabled + * @see org.glassfish.jersey.server.ResourceConfig + */ + public void enable(String featureName) { + config.property(requireNonNull(featureName), Boolean.TRUE); + } + + /** + * Disables the Jersey feature with the given name. + * + * @param featureName the name of the feature to be disabled + * @see org.glassfish.jersey.server.ResourceConfig + */ + public void disable(String featureName) { + config.property(requireNonNull(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 org.glassfish.jersey.server.ResourceConfig + */ + public void property(String name, @Nullable Object value) { + config.property(requireNonNull(name), value); + } + + /** + * Gets the given Jersey property. + * + * @param name the name of the Jersey property + * @see org.glassfish.jersey.server.ResourceConfig + */ + @SuppressWarnings("unchecked") + public T getProperty(String name) { + return (T) config.getProperties().get(name); + } + + public String getUrlPattern() { + return config.getUrlPattern(); + } + + public void setUrlPattern(String urlPattern) { + String normalizedUrlPattern = urlPattern; + if (!normalizedUrlPattern.endsWith("*") && !normalizedUrlPattern.endsWith("/")) { + normalizedUrlPattern += "/"; + } + if (!normalizedUrlPattern.endsWith("*")) { + normalizedUrlPattern += "*"; + } + config.setUrlPattern(normalizedUrlPattern); + } + + public DropwizardResourceConfig getResourceConfig() { + return config; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyServletContainer.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyServletContainer.java new file mode 100644 index 00000000000..f03f78bc34f --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/setup/JerseyServletContainer.java @@ -0,0 +1,27 @@ +package io.dropwizard.jersey.setup; + +import io.dropwizard.jersey.DropwizardResourceConfig; +import org.glassfish.jersey.servlet.ServletContainer; + +/** + * Extends {@link ServletContainer} to provide consumers of dropwizard-jersey + * a means of obtaining a container without directly depending on Jersey. + */ +public class JerseyServletContainer extends ServletContainer { + + private static final long serialVersionUID = -3747494819983708680L; + + /** + * Create Jersey Servlet container. + */ + public JerseyServletContainer() { + } + + /** + * Create Jersey Servlet container. + * @param resourceConfig container configuration. + */ + public JerseyServletContainer(DropwizardResourceConfig resourceConfig) { + super(resourceConfig); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ConstraintMessage.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ConstraintMessage.java new file mode 100644 index 00000000000..b0300d6576a --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ConstraintMessage.java @@ -0,0 +1,225 @@ +package io.dropwizard.jersey.validation; + +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Iterables; +import io.dropwizard.validation.ValidationMethod; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.glassfish.jersey.server.model.Invocable; +import org.glassfish.jersey.server.model.Parameter; + +import javax.validation.ConstraintViolation; +import javax.validation.ElementKind; +import javax.validation.Path; +import javax.validation.metadata.ConstraintDescriptor; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +public class ConstraintMessage { + + private static final Cache>, String> MESSAGES_CACHE = + CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(); + + private ConstraintMessage() { + } + + /** + * Gets the human friendly location of where the violation was raised. + */ + public static String getMessage(ConstraintViolation v, Invocable invocable) { + final Pair> of = + Pair.of(v.getPropertyPath(), v.getConstraintDescriptor()); + final String cachedMessage = MESSAGES_CACHE.getIfPresent(of); + if (cachedMessage == null) { + final String message = calculateMessage(v, invocable); + MESSAGES_CACHE.put(of, message); + return message; + } + return cachedMessage; + } + + private static String calculateMessage(ConstraintViolation v, Invocable invocable) { + final Optional returnValueName = getMethodReturnValueName(v); + if (returnValueName.isPresent()) { + final String name = isValidationMethod(v) ? + StringUtils.substringBeforeLast(returnValueName.get(), ".") : returnValueName.get(); + return name + " " + v.getMessage(); + } + + // Take the message specified in a ValidationMethod annotation if it + // is what caused the violation + if (isValidationMethod(v)) { + return v.getMessage(); + } + + final Optional entity = isRequestEntity(v, invocable); + if (entity.isPresent()) { + // A present entity means that the request body failed validation but + // if the request entity is simple (eg. byte[], String, etc), the entity + // string will be empty, so prepend a message about the request body + final String prefix = Strings.isNullOrEmpty(entity.get()) ? "The request body" : entity.get(); + return prefix + " " + v.getMessage(); + } + + // Check if the violation occurred on a *Param annotation and if so, + // return a human friendly error (eg. "Query param xxx may not be null") + final Optional memberName = getMemberName(v, invocable); + if (memberName.isPresent()) { + return memberName.get() + " " + v.getMessage(); + } + + return v.getPropertyPath() + " " + v.getMessage(); + } + + /** + * Determines if constraint violation occurred in the request entity. If it did, return a client + * friendly string representation of where the error occurred (eg. "patient.name") + */ + public static Optional isRequestEntity(ConstraintViolation violation, Invocable invocable) { + final Path.Node parent = Iterables.get(violation.getPropertyPath(), 1, null); + if (parent == null) { + return Optional.empty(); + } + final List parameters = invocable.getParameters(); + + switch (parent.getKind()) { + case PARAMETER: + final Parameter param = parameters.get(parent.as(Path.ParameterNode.class).getParameterIndex()); + if (param.getSource().equals(Parameter.Source.UNKNOWN)) { + return Optional.of(Joiner.on('.').join(Iterables.skip(violation.getPropertyPath(), 2))); + } + default: + break; + } + + return Optional.empty(); + } + + /** + * Gets a method parameter (or a parameter field) name, if the violation raised in it. + */ + private static Optional getMemberName(ConstraintViolation violation, Invocable invocable) { + final int size = Iterables.size(violation.getPropertyPath()); + if (size < 2) { + return Optional.empty(); + } + + final Path.Node parent = Iterables.get(violation.getPropertyPath(), size - 2); + final Path.Node member = Iterables.getLast(violation.getPropertyPath()); + switch (parent.getKind()) { + case PARAMETER: + // Constraint violation most likely failed with a BeanParam + final List parameters = invocable.getParameters(); + final Parameter param = parameters.get(parent.as(Path.ParameterNode.class).getParameterIndex()); + + // Extract the failing *Param annotation inside the Bean Param + if (param.getSource().equals(Parameter.Source.BEAN_PARAM)) { + final Field field = FieldUtils.getField(param.getRawType(), member.getName(), true); + return getMemberName(field.getDeclaredAnnotations()); + } + + return Optional.empty(); + case METHOD: + // Constraint violation occurred directly on a function + // parameter annotated with *Param + final Method method = invocable.getHandlingMethod(); + final int paramIndex = member.as(Path.ParameterNode.class).getParameterIndex(); + return getMemberName(method.getParameterAnnotations()[paramIndex]); + default: + return Optional.empty(); + } + } + + /** + * Gets the method return value name, if the violation is raised in it + */ + private static Optional getMethodReturnValueName(ConstraintViolation violation) { + int returnValueNames = -1; + + final StringBuilder result = new StringBuilder("server response"); + for (Path.Node node : violation.getPropertyPath()) { + if (node.getKind().equals(ElementKind.RETURN_VALUE)) { + returnValueNames = 0; + } else if (returnValueNames >= 0) { + result.append(returnValueNames++ == 0 ? " " : ".").append(node); + } + } + + return returnValueNames >= 0 ? Optional.of(result.toString()) : Optional.empty(); + } + + /** + * Derives member's name and type from it's annotations + */ + private static Optional getMemberName(Annotation[] memberAnnotations) { + for (Annotation a : memberAnnotations) { + if (a instanceof QueryParam) { + return Optional.of("query param " + ((QueryParam) a).value()); + } else if (a instanceof PathParam) { + return Optional.of("path param " + ((PathParam) a).value()); + } else if (a instanceof HeaderParam) { + return Optional.of("header " + ((HeaderParam) a).value()); + } else if (a instanceof CookieParam) { + return Optional.of("cookie " + ((CookieParam) a).value()); + } else if (a instanceof FormParam) { + return Optional.of("form field " + ((FormParam) a).value()); + } else if (a instanceof Context) { + return Optional.of("context"); + } else if (a instanceof MatrixParam) { + return Optional.of("matrix param " + ((MatrixParam) a).value()); + } + } + + return Optional.empty(); + } + + private static boolean isValidationMethod(ConstraintViolation v) { + return v.getConstraintDescriptor().getAnnotation() instanceof ValidationMethod; + } + + /** + * Given a set of constraint violations and a Jersey {@link Invocable} where the constraint + * occurred, determine the HTTP Status code for the response. A return value violation is an + * internal server error, an invalid request body is unprocessable entity, and any params that + * are invalid means a bad request + */ + public static > int determineStatus(Set violations, Invocable invocable) { + if (violations.size() > 0) { + final ConstraintViolation violation = violations.iterator().next(); + for (Path.Node node : violation.getPropertyPath()) { + switch (node.getKind()) { + case RETURN_VALUE: + return 500; + case PARAMETER: + // Now determine if the parameter is the request entity + final int index = node.as(Path.ParameterNode.class).getParameterIndex(); + final Parameter parameter = invocable.getParameters().get(index); + return parameter.getSource().equals(Parameter.Source.UNKNOWN) ? 422 : 400; + default: + continue; + } + } + } + + // This shouldn't hit, but if it does, we'll return a unprocessable entity + return 422; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/DropwizardConfiguredValidator.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/DropwizardConfiguredValidator.java new file mode 100644 index 00000000000..36a8bfece40 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/DropwizardConfiguredValidator.java @@ -0,0 +1,139 @@ +package io.dropwizard.jersey.validation; + +import com.google.common.collect.ImmutableList; +import io.dropwizard.validation.ConstraintViolations; +import io.dropwizard.validation.Validated; +import org.glassfish.jersey.server.internal.inject.ConfiguredValidator; +import org.glassfish.jersey.server.model.Invocable; +import org.glassfish.jersey.server.model.Parameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Validator; +import javax.validation.executable.ExecutableValidator; +import javax.validation.groups.Default; +import javax.validation.metadata.BeanDescriptor; +import javax.ws.rs.WebApplicationException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public class DropwizardConfiguredValidator implements ConfiguredValidator { + private static final Logger LOGGER = LoggerFactory.getLogger(DropwizardConfiguredValidator.class); + + private final Validator validator; + + public DropwizardConfiguredValidator(Validator validator) { + this.validator = requireNonNull(validator); + } + + @Override + public void validateResourceAndInputParams(Object resource, final Invocable invocable, Object[] objects) + throws ConstraintViolationException { + final Class[] groups = getGroup(invocable); + final Set> violations = new HashSet<>(); + final BeanDescriptor beanDescriptor = getConstraintsForClass(resource.getClass()); + + if (beanDescriptor.isBeanConstrained()) { + violations.addAll(validate(resource, groups)); + } + + violations.addAll(forExecutables().validateParameters(resource, invocable.getHandlingMethod(), objects, groups)); + if (!violations.isEmpty()) { + throw new JerseyViolationException(violations, invocable); + } + } + + /** + * If the request entity is annotated with {@link Validated} then run + * validations in the specified constraint group else validate with the + * {@link Default} group + */ + private Class[] getGroup(Invocable invocable) { + final ImmutableList.Builder[]> builder = ImmutableList.builder(); + for (Parameter parameter : invocable.getParameters()) { + if (parameter.isAnnotationPresent(Validated.class)) { + builder.add(parameter.getAnnotation(Validated.class).value()); + } + } + + final ImmutableList[]> groups = builder.build(); + switch (groups.size()) { + // No parameters were annotated with Validated, so validate under the default group + case 0: return new Class[] {Default.class}; + + // A single parameter was annotated with Validated, so use their group + case 1: return groups.get(0); + + // Multiple parameters were annotated with Validated, so we must check if + // all groups are equal to each other, if not, throw an exception because + // the validator is unable to handle parameters validated under different + // groups. If the parameters have the same group, we can grab the first + // group. + default: + for (int i = 0; i < groups.size(); i++) { + for (int j = i; j < groups.size(); j++) { + if (!Arrays.deepEquals(groups.get(i), groups.get(j))) { + throw new WebApplicationException("Parameters must have the same validation groups in " + + invocable.getHandlingMethod().getName(), 500); + } + } + } + return groups.get(0); + } + } + + @Override + public void validateResult(Object resource, Invocable invocable, Object returnValue) + throws ConstraintViolationException { + // If the Validated annotation is on a method, then validate the response with + // the specified constraint group. + final Class[] groups; + if (invocable.getHandlingMethod().isAnnotationPresent(Validated.class)) { + groups = invocable.getHandlingMethod().getAnnotation(Validated.class).value(); + } else { + groups = new Class[]{Default.class}; + } + + final Set> violations = + forExecutables().validateReturnValue(resource, invocable.getHandlingMethod(), returnValue, groups); + if (!violations.isEmpty()) { + LOGGER.trace("Response validation failed: {}", ConstraintViolations.copyOf(violations)); + throw new JerseyViolationException(violations, invocable); + } + } + + @Override + public Set> validate(T t, Class... classes) { + return validator.validate(t, classes); + } + + @Override + public Set> validateProperty(T t, String s, Class... classes) { + return validator.validateProperty(t, s, classes); + } + + @Override + public Set> validateValue(Class aClass, String s, Object o, Class... classes) { + return validator.validateValue(aClass, s, o, classes); + } + + @Override + public BeanDescriptor getConstraintsForClass(Class aClass) { + return validator.getConstraintsForClass(aClass); + } + + @Override + public T unwrap(Class aClass) { + return validator.unwrap(aClass); + } + + @Override + public ExecutableValidator forExecutables() { + return validator.forExecutables(); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/HibernateValidationFeature.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/HibernateValidationFeature.java new file mode 100644 index 00000000000..9861b7b03b1 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/HibernateValidationFeature.java @@ -0,0 +1,32 @@ +package io.dropwizard.jersey.validation; + +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.ConfiguredValidator; + +import javax.validation.Validator; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; + +/** + * Register a Dropwizard configured {@link Validator} with Jersey, so that Jersey doesn't use its + * default, which doesn't have our configurations applied. + */ +public class HibernateValidationFeature implements Feature { + private final Validator validator; + + public HibernateValidationFeature(Validator validator) { + this.validator = validator; + } + + @Override + public boolean configure(FeatureContext context) { + context.register(new AbstractBinder() { + @Override + protected void configure() { + bind(new DropwizardConfiguredValidator(validator)).to(ConfiguredValidator.class); + } + }); + + return true; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/JerseyViolationException.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/JerseyViolationException.java new file mode 100644 index 00000000000..974deccf7f0 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/JerseyViolationException.java @@ -0,0 +1,24 @@ +package io.dropwizard.jersey.validation; + +import org.glassfish.jersey.server.model.Invocable; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.Set; + +/** + * A {@link ConstraintViolationException} that occurs while Jersey is + * validating constraints on a resource endpoint. + */ +public class JerseyViolationException extends ConstraintViolationException { + private final Invocable invocable; + + public JerseyViolationException(Set> constraintViolations, Invocable invocable) { + super(constraintViolations); + this.invocable = invocable; + } + + public Invocable getInvocable() { + return invocable; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/JerseyViolationExceptionMapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/JerseyViolationExceptionMapper.java new file mode 100644 index 00000000000..733a7dbeae1 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/JerseyViolationExceptionMapper.java @@ -0,0 +1,27 @@ +package io.dropwizard.jersey.validation; + +import com.google.common.collect.FluentIterable; +import com.google.common.collect.ImmutableList; +import org.glassfish.jersey.server.model.Invocable; + +import javax.validation.ConstraintViolation; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import java.util.Set; + +@Provider +public class JerseyViolationExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(final JerseyViolationException exception) { + final Set> violations = exception.getConstraintViolations(); + final Invocable invocable = exception.getInvocable(); + final ImmutableList errors = FluentIterable.from(exception.getConstraintViolations()) + .transform(violation -> ConstraintMessage.getMessage(violation, invocable)).toList(); + + final int status = ConstraintMessage.determineStatus(violations, invocable); + return Response.status(status) + .entity(new ValidationErrorMessage(errors)) + .build(); + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/NonEmptyStringParamUnwrapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/NonEmptyStringParamUnwrapper.java new file mode 100644 index 00000000000..d2092ddd6a8 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/NonEmptyStringParamUnwrapper.java @@ -0,0 +1,23 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.jersey.params.NonEmptyStringParam; +import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper; + +import java.lang.reflect.Type; + +/** + * Let's the validator know that when validating a {@link NonEmptyStringParam} to validate the + * underlying value. This class is needed, temporarily, while Hibernate is not able to unwrap nested + * classes . + */ +public class NonEmptyStringParamUnwrapper extends ValidatedValueUnwrapper { + @Override + public Object handleValidatedValue(final NonEmptyStringParam nonEmptyStringParam) { + return nonEmptyStringParam.get().orElse(null); + } + + @Override + public Type getValidatedValueType(final Type type) { + return String.class; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ParamValidatorUnwrapper.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ParamValidatorUnwrapper.java new file mode 100644 index 00000000000..573668d7680 --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ParamValidatorUnwrapper.java @@ -0,0 +1,28 @@ +package io.dropwizard.jersey.validation; + + +import com.fasterxml.classmate.TypeResolver; +import io.dropwizard.jersey.params.AbstractParam; +import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper; + +import java.lang.reflect.Type; + +/** + * Let's the validator know that when validating a class that is an {@link AbstractParam} to + * validate the underlying value. + */ +public class ParamValidatorUnwrapper extends ValidatedValueUnwrapper> { + private final TypeResolver resolver = new TypeResolver(); + + @Override + public Object handleValidatedValue(final AbstractParam abstractParam) { + return abstractParam == null ? null : abstractParam.get(); + } + + @Override + public Type getValidatedValueType(final Type type) { + return resolver.resolve(type) + .typeParametersFor(AbstractParam.class).get(0) + .getErasedType(); + } +} 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 00000000000..30ec5712acc --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/ValidationErrorMessage.java @@ -0,0 +1,19 @@ +package io.dropwizard.jersey.validation; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; + +public class ValidationErrorMessage { + private final ImmutableList errors; + + @JsonCreator + public ValidationErrorMessage(@JsonProperty("errors") ImmutableList errors) { + this.errors = errors; + } + + @JsonProperty + public ImmutableList getErrors() { + return errors; + } +} diff --git a/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/Validators.java b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/Validators.java new file mode 100644 index 00000000000..5549bc2b00d --- /dev/null +++ b/dropwizard-jersey/src/main/java/io/dropwizard/jersey/validation/Validators.java @@ -0,0 +1,38 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.validation.BaseValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; + +import javax.validation.Validator; +import javax.validation.ValidatorFactory; + +/** + * A utility class for Hibernate. + */ +public class Validators { + private Validators() { /* singleton */ } + + /** + * Creates a new {@link Validator} based on {@link #newValidatorFactory()} + */ + public static Validator newValidator() { + return newValidatorFactory().getValidator(); + } + + /** + * Creates a new {@link ValidatorFactory} based on {@link #newConfiguration()} + */ + public static ValidatorFactory newValidatorFactory() { + return newConfiguration().buildValidatorFactory(); + } + + /** + * Creates a new {@link HibernateValidatorConfiguration} with all the custom {@link + * org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper} registered. + */ + public static HibernateValidatorConfiguration newConfiguration() { + return BaseValidator.newConfiguration() + .addValidatedValueHandler(new NonEmptyStringParamUnwrapper()) + .addValidatedValueHandler(new ParamValidatorUnwrapper()); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/AsyncServletTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/AsyncServletTest.java new file mode 100644 index 00000000000..e7520a2d5a8 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/AsyncServletTest.java @@ -0,0 +1,35 @@ +package io.dropwizard.jersey; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.dummy.DummyResource; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AsyncServletTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(DummyResource.class); + } + + @Test + public void testAsyncResponse() { + final Response response = target("/async").request(MediaType.TEXT_PLAIN_TYPE).get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(String.class)).isEqualTo("foobar"); + } +} 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 00000000000..32b61cdae02 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/DropwizardResourceConfigTest.java @@ -0,0 +1,220 @@ +package io.dropwizard.jersey; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.dummy.DummyResource; +import io.dropwizard.logging.BootstrapLogging; +import org.junit.Before; +import org.junit.Test; + +import javax.ws.rs.DELETE; +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 static org.assertj.core.api.Assertions.assertThat; + +public class DropwizardResourceConfigTest { + static { + BootstrapLogging.bootstrap(); + } + + private DropwizardResourceConfig rc; + + @Before + public void setUp() { + rc = DropwizardResourceConfig.forTesting(new MetricRegistry()); + } + + @Test + public void findsResourceClassInPackage() { + rc.packages(DummyResource.class.getPackage().getName()); + + assertThat(rc.getClasses()).contains(DummyResource.class); + } + + @Test + public void findsResourceClassesInPackageAndSubpackage() { + rc.packages(getClass().getPackage().getName()); + + assertThat(rc.getClasses()).contains( + DummyResource.class, + TestResource.class, + ResourceInterface.class); + } + + @Test + public void combinesAlRegisteredClasses() { + rc.register(new TestResource()); + rc.registerClasses(ResourceInterface.class, ImplementingResource.class); + + assertThat(rc.allClasses()).contains( + TestResource.class, + ResourceInterface.class, + ImplementingResource.class + ); + } + + @Test + public void combinesAlRegisteredClassesPathOnMethodLevel() { + rc.register(new TestResource()); + rc.register(new ResourcePathOnMethodLevel()); + + assertThat(rc.allClasses()).contains( + TestResource.class, + ResourcePathOnMethodLevel.class + ); + + assertThat(rc.getEndpointsInfo()) + .contains("GET /bar (io.dropwizard.jersey.DropwizardResourceConfigTest.ResourcePathOnMethodLevel)") + .contains("GET /dummy (io.dropwizard.jersey.DropwizardResourceConfigTest.TestResource)"); + } + + @Test + public void logsNoInterfaces() { + rc.packages(getClass().getPackage().getName()); + + assertThat(rc.getEndpointsInfo()).doesNotContain("io.dropwizard.jersey.DropwizardResourceConfigTest.ResourceInterface"); + } + + @Test + public void logsNoEndpointsWhenNoResourcesAreRegistered() { + assertThat(rc.getEndpointsInfo()).contains(" NONE"); + } + + @Test + public void logsEndpoints() { + rc.register(TestResource.class); + rc.register(ImplementingResource.class); + + assertThat(rc.getEndpointsInfo()) + .contains("GET /dummy (io.dropwizard.jersey.DropwizardResourceConfigTest.TestResource)") + .contains("GET /another (io.dropwizard.jersey.DropwizardResourceConfigTest.ImplementingResource)"); + } + + @Test + public void logsEndpointsSorted() { + rc.register(DummyResource.class); + rc.register(TestResource2.class); + rc.register(TestResource.class); + rc.register(ImplementingResource.class); + + final String expectedLog = String.format( + "The following paths were found for the configured resources:%n" + + "%n" + + " GET / (io.dropwizard.jersey.dummy.DummyResource)%n" + + " GET /another (io.dropwizard.jersey.DropwizardResourceConfigTest.ImplementingResource)%n" + + " GET /async (io.dropwizard.jersey.dummy.DummyResource)%n" + + " DELETE /dummy (io.dropwizard.jersey.DropwizardResourceConfigTest.TestResource2)%n" + + " GET /dummy (io.dropwizard.jersey.DropwizardResourceConfigTest.TestResource)%n" + + " POST /dummy (io.dropwizard.jersey.DropwizardResourceConfigTest.TestResource2)%n"); + assertThat(rc.getEndpointsInfo()).isEqualTo(expectedLog); + } + + @Test + public void logsNestedEndpoints() { + rc.register(WrapperResource.class); + + assertThat(rc.getEndpointsInfo()) + .contains(" GET /wrapper/bar (io.dropwizard.jersey.DropwizardResourceConfigTest.ResourcePathOnMethodLevel)") + .contains(" GET /locator/bar (io.dropwizard.jersey.DropwizardResourceConfigTest.ResourcePathOnMethodLevel)"); + } + + @Test + public void duplicatePathsTest() { + rc.register(TestDuplicateResource.class); + final String expectedLog = String.format("The following paths were found for the configured resources:%n" + "%n" + + " GET /anotherMe (io.dropwizard.jersey.DropwizardResourceConfigTest.TestDuplicateResource)%n" + + " GET /callme (io.dropwizard.jersey.DropwizardResourceConfigTest.TestDuplicateResource)%n"); + + assertThat(rc.getEndpointsInfo()).contains(expectedLog); + assertThat(rc.getEndpointsInfo()).containsOnlyOnce(" GET /callme (io.dropwizard.jersey.DropwizardResourceConfigTest.TestDuplicateResource)"); + } + + @Path("/dummy") + public static class TestResource { + @GET + public String foo() { + return "bar"; + } + } + + @Path("/dummy") + public static class TestResource2 { + @POST + public String fooPost() { + return "bar"; + } + + @DELETE + public String fooDelete() { + return "bar"; + } + } + + @Path("/") + public static class TestDuplicateResource { + + @GET + @Path("callme") + @Produces(MediaType.APPLICATION_JSON) + public String fooGet() { + return "bar"; + } + + @GET + @Path("callme") + @Produces(MediaType.TEXT_HTML) + public String fooGet2() { + return "bar2"; + } + + @GET + @Path("callme") + @Produces(MediaType.APPLICATION_XML) + public String fooGet3() { + return "bar3"; + } + + @GET + @Path("anotherMe") + public String fooGet4() { + return "bar4"; + } + + } + + @Path("/another") + public static interface ResourceInterface { + @GET + public String bar(); + } + + @Path("/") + public static class WrapperResource { + @Path("wrapper") + public ResourcePathOnMethodLevel getNested() { + return new ResourcePathOnMethodLevel(); + } + + @Path("locator") + public Class getNested2() { + return ResourcePathOnMethodLevel.class; + } + } + + public static class ResourcePathOnMethodLevel { + @GET @Path("/bar") + public String bar() { + return ""; + } + } + + public static class ImplementingResource implements ResourceInterface { + @Override + public String bar() { + return ""; + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/JerseyContentTypeTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/JerseyContentTypeTest.java new file mode 100644 index 00000000000..dfa5cc4a247 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/JerseyContentTypeTest.java @@ -0,0 +1,43 @@ +package io.dropwizard.jersey; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.dummy.DummyResource; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JerseyContentTypeTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(DummyResource.class); + } + + @Test + public void testValidContentType() { + final Response response = target("/").request(MediaType.TEXT_PLAIN_TYPE).get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(String.class)).isEqualTo("bar"); + } + + @Test + public void testInvalidContentType() { + final Response response = target("/").request("foo").get(); + + assertThat(response.getStatus()).isEqualTo(406); + assertThat(response.hasEntity()).isEqualTo(false); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/MyMessage.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/MyMessage.java new file mode 100644 index 00000000000..a8e266f7870 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/MyMessage.java @@ -0,0 +1,13 @@ +package io.dropwizard.jersey; + +public class MyMessage { + private final String message; + + public MyMessage(final String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/MyMessageParamConverterProvider.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/MyMessageParamConverterProvider.java new file mode 100644 index 00000000000..8229f7be650 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/MyMessageParamConverterProvider.java @@ -0,0 +1,33 @@ +package io.dropwizard.jersey; + +import javax.ws.rs.ext.ParamConverter; +import javax.ws.rs.ext.ParamConverterProvider; +import javax.ws.rs.ext.Provider; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +@Provider +public class MyMessageParamConverterProvider implements ParamConverterProvider { + + @Override + @SuppressWarnings("unchecked") + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (genericType.equals(MyMessage.class)) { + return (ParamConverter) new MyMessageParamConverter(); + } + return null; + } + + private static class MyMessageParamConverter implements ParamConverter { + + @Override + public MyMessage fromString(String value) { + return new MyMessage(value); + } + + @Override + public String toString(MyMessage value) { + return value.getMessage(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CacheControlledResponseFeatureTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CacheControlledResponseFeatureTest.java new file mode 100644 index 00000000000..232c887e837 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/caching/CacheControlledResponseFeatureTest.java @@ -0,0 +1,101 @@ +package io.dropwizard.jersey.caching; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.core.Application; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CacheControlledResponseFeatureTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + ResourceConfig rc = DropwizardResourceConfig.forTesting(new MetricRegistry()); + rc = rc.register(CachingResource.class); + return rc; + } + + @Test + public void immutableResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/immutable").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("no-transform, max-age=31536000"); + } + + @Test + public void privateResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/private").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("private, no-transform"); + } + + @Test + public void maxAgeResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/max-age").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("no-transform, max-age=1123200"); + } + + @Test + public void noCacheResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/no-cache").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("no-cache, no-transform"); + } + + @Test + public void noStoreResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/no-store").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("no-store, no-transform"); + } + + @Test + public void noTransformResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/no-transform").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .isNull(); + } + + @Test + public void mustRevalidateResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/must-revalidate").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("no-transform, must-revalidate"); + } + + @Test + public void proxyRevalidateResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/proxy-revalidate").request().get(); + + assertThat(response.getHeaders().get(HttpHeaders.CACHE_CONTROL)) + .containsOnly("no-transform, proxy-revalidate"); + } + + @Test + public void sharedMaxAgeResponsesHaveCacheControlHeaders() throws Exception { + final Response response = target("/caching/shared-max-age").request().get(); + + 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 00000000000..3b2fdb44000 --- /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; + +@Path("/caching/") +@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 00000000000..0c1aa4be8bc --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/dummy/DummyResource.java @@ -0,0 +1,23 @@ +package io.dropwizard.jersey.dummy; + +import org.glassfish.jersey.server.ManagedAsync; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.container.AsyncResponse; +import javax.ws.rs.container.Suspended; + +@Path("/") +public class DummyResource { + @GET + public String foo() { + return "bar"; + } + + @GET + @Path("/async") + @ManagedAsync + public void async(@Suspended final AsyncResponse ar) { + ar.resume("foobar"); + } +} 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 00000000000..479b78023b1 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/DefaultJacksonMessageBodyProvider.java @@ -0,0 +1,13 @@ +package io.dropwizard.jersey.errors; + +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; + +import javax.ws.rs.ext.Provider; + +@Provider +public class DefaultJacksonMessageBodyProvider extends JacksonMessageBodyProvider { + public DefaultJacksonMessageBodyProvider() { + super(Jackson.newObjectMapper()); + } +} 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 00000000000..3ef7106412e --- /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; + +@Provider +public class DefaultLoggingExceptionMapper extends LoggingExceptionMapper { +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/EarlyEofExceptionMapperTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/EarlyEofExceptionMapperTest.java new file mode 100644 index 00000000000..2e3a52c2011 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/EarlyEofExceptionMapperTest.java @@ -0,0 +1,18 @@ +package io.dropwizard.jersey.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-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 00000000000..8b3c4d29194 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/ExceptionResource.java @@ -0,0 +1,48 @@ +package io.dropwizard.jersey.errors; + +import com.fasterxml.jackson.databind.JsonMappingException; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; + +@Path("/exception/") +@Produces(MediaType.APPLICATION_JSON) +public class ExceptionResource { + @GET + public String show() throws IOException { + throw new IOException("WHAT"); + } + + @GET + @Path("json-mapping-exception") + public void jsonMappingException() throws JsonMappingException { + throw new JsonMappingException(new StringReader(""), "BOOM"); + } + + @GET + @Path("web-application-exception") + public void webApplicationException() throws WebApplicationException { + throw new WebApplicationException("KAPOW", Response.Status.BAD_REQUEST); + } + + @GET + @Path("web-application-exception-with-redirect") + public void webApplicationExceptionWithRedirect() throws WebApplicationException { + URI redirectPath = UriBuilder.fromPath("/exception/redirect-target").build(); + throw new WebApplicationException(Response.seeOther(redirectPath).build()); + } + + @GET + @Path("redirect-target") + public Response redirectTarget() { + return Response.ok().entity("{\"status\":\"OK\"}").build(); + } +} 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 00000000000..f7cce045c19 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/errors/LoggingExceptionMapperTest.java @@ -0,0 +1,80 @@ +package io.dropwizard.jersey.errors; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class LoggingExceptionMapperTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(DefaultLoggingExceptionMapper.class) + .register(DefaultJacksonMessageBodyProvider.class) + .register(ExceptionResource.class); + } + + @Test + public void returnsAnErrorMessage() throws Exception { + try { + target("/exception/").request(MediaType.APPLICATION_JSON).get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + final Response response = e.getResponse(); + + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.readEntity(String.class)).startsWith("{\"code\":500,\"message\":" + + "\"There was an error processing your request. It has been logged (ID "); + } + } + + @Test + public void handlesJsonMappingException() throws Exception { + try { + target("/exception/json-mapping-exception").request(MediaType.APPLICATION_JSON).get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + final Response response = e.getResponse(); + + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.readEntity(String.class)).startsWith("{\"code\":500,\"message\":" + + "\"There was an error processing your request. It has been logged (ID "); + } + } + + @Test + public void formatsWebApplicationException() throws Exception { + try { + target("/exception/web-application-exception").request(MediaType.APPLICATION_JSON).get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + final Response response = e.getResponse(); + + assertThat(response.getStatus()).isEqualTo(400); + assertThat(response.readEntity(String.class)).isEqualTo("{\"code\":400,\"message\":\"KAPOW\"}"); + } + } + + @Test + public void handlesRedirectInWebApplicationException() { + String responseText = target("/exception/web-application-exception-with-redirect") + .request(MediaType.APPLICATION_JSON) + .get(String.class); + assertThat(responseText).isEqualTo("{\"status\":\"OK\"}"); + } +} 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 00000000000..c623c223142 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/AllowedMethodsFilterTest.java @@ -0,0 +1,138 @@ +package io.dropwizard.jersey.filter; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; +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 javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class AllowedMethodsFilterTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + private static final int DISALLOWED_STATUS_CODE = Response.Status.METHOD_NOT_ALLOWED.getStatusCode(); + private static final int OK_STATUS_CODE = Response.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 setUpFilter() { + filter.init(config); + } + + + @Override + protected TestContainerFactory getTestContainerFactory() + throws TestContainerException { + return new GrizzlyWebTestContainerFactory(); + } + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + final ResourceConfig rc = DropwizardResourceConfig.forTesting(new MetricRegistry()); + + final Map filterParams = ImmutableMap.of( + AllowedMethodsFilter.ALLOWED_METHODS_PARAM, "GET,POST"); + + return ServletDeploymentContext.builder(rc) + .addFilter(AllowedMethodsFilter.class, "allowedMethodsFilter", filterParams) + .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, DropwizardResourceConfig.class.getName()) + .initParam(ServerProperties.PROVIDER_CLASSNAMES, DummyResource.class.getName()) + .build(); + } + + private int getResponseStatusForRequestMethod(String method, boolean includeEntity) { + final Response resourceResponse = includeEntity + ? target("/ping").request().method(method, Entity.entity("", MediaType.TEXT_PLAIN)) + : target("/ping").request().method(method); + + try { + return resourceResponse.getStatus(); + } finally { + resourceResponse.close(); + } + } + + @Test + public void testGetRequestAllowed() { + assertEquals(OK_STATUS_CODE, getResponseStatusForRequestMethod("GET", false)); + } + + @Test + public void testPostRequestAllowed() { + assertEquals(OK_STATUS_CODE, getResponseStatusForRequestMethod("POST", true)); + } + + @Test + public void testPutRequestBlocked() { + assertEquals(DISALLOWED_STATUS_CODE, getResponseStatusForRequestMethod("PUT", true)); + } + + @Test + public void testDeleteRequestBlocked() { + assertEquals(DISALLOWED_STATUS_CODE, getResponseStatusForRequestMethod("DELETE", false)); + } + + @Test + public void testTraceRequestBlocked() { + assertEquals(DISALLOWED_STATUS_CODE, getResponseStatusForRequestMethod("TRACE", false)); + } + + @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 00000000000..b164cc27e61 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/filter/DummyResource.java @@ -0,0 +1,38 @@ +package io.dropwizard.jersey.filter; + +import io.dropwizard.jersey.PATCH; + +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.core.Response; + +@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/OptionalCookieParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalCookieParamResourceTest.java new file mode 100644 index 00000000000..9fc4b78123a --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalCookieParamResourceTest.java @@ -0,0 +1,109 @@ +package io.dropwizard.jersey.guava; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.CookieParam; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalCookieParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalCookieParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() { + String defaultMessage = "Default Message"; + String response = target("/optional/message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsBlank() { + String response = target("/optional/message").request().cookie("message", "").get(String.class); + assertThat(response).isEqualTo(""); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() { + String customMessage = "Custom Message"; + String response = target("/optional/message").request().cookie("message", customMessage).get(String.class); + assertThat(response).isEqualTo(customMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() { + String defaultMessage = "My Default Message"; + String response = target("/optional/my-message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() { + String myMessage = "My Message"; + String response = target("/optional/my-message").request().cookie("mymessage", myMessage).get(String.class); + assertThat(response).isEqualTo(myMessage); + } + + @Test(expected = BadRequestException.class) + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() { + String invalidUUID = "invalid-uuid"; + target("/optional/uuid").request().cookie("uuid", invalidUUID).get(String.class); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() { + String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + String response = target("/optional/uuid").request().get(String.class); + assertThat(response).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() { + String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + String response = target("/optional/uuid").request().cookie("uuid", uuid).get(String.class); + assertThat(response).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalCookieParamResource { + @GET + @Path("/message") + public String getMessage(@CookieParam("message") Optional message) { + return message.or("Default Message"); + } + + @GET + @Path("/my-message") + public String getMyMessage(@CookieParam("mymessage") Optional myMessage) { + return myMessage.or(new MyMessage("My Default Message")).getMessage(); + } + + @GET + @Path("/uuid") + public String getUUID(@CookieParam("uuid") Optional uuid) { + return uuid.or(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalFormParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalFormParamResourceTest.java new file mode 100644 index 00000000000..f791efd34d0 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalFormParamResourceTest.java @@ -0,0 +1,128 @@ +package io.dropwizard.jersey.guava; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.Response; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalFormParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalFormParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() throws IOException { + final String defaultMessage = "Default Message"; + final Response response = target("/optional/message").request().post(Entity.form(new MultivaluedStringMap())); + + assertThat(response.readEntity(String.class)).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageBlank() throws IOException { + final Form form = new Form("message", ""); + final Response response = target("/optional/message").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(""); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() throws IOException { + final String customMessage = "Custom Message"; + final Form form = new Form("message", customMessage); + final Response response = target("/optional/message").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(customMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() throws IOException { + final String defaultMessage = "My Default Message"; + final Response response = target("/optional/my-message").request().post(Entity.form(new MultivaluedStringMap())); + + assertThat(response.readEntity(String.class)).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() throws IOException { + final String myMessage = "My Message"; + final Form form = new Form("mymessage", myMessage); + final Response response = target("/optional/my-message").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(myMessage); + } + + @Test + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() throws IOException { + final String invalidUUID = "invalid-uuid"; + final Form form = new Form("uuid", invalidUUID); + final Response response = target("/optional/uuid").request().post(Entity.form(form)); + + assertThat(response.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode()); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() throws IOException { + final String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + final Response response = target("/optional/uuid").request().post(Entity.form(new MultivaluedStringMap())); + + assertThat(response.readEntity(String.class)).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() throws IOException { + final String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + final Form form = new Form("uuid", uuid); + final Response response = target("/optional/uuid").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalFormParamResource { + + @POST + @Path("/message") + public String getMessage(@FormParam("message") Optional message) { + return message.or("Default Message"); + } + + @POST + @Path("/my-message") + public String getMyMessage(@FormParam("mymessage") Optional myMessage) { + return myMessage.or(new MyMessage("My Default Message")).getMessage(); + } + + @POST + @Path("/uuid") + public String getUUID(@FormParam("uuid") Optional uuid) { + return uuid.or(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalHeaderParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalHeaderParamResourceTest.java new file mode 100644 index 00000000000..d740016d8ef --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalHeaderParamResourceTest.java @@ -0,0 +1,109 @@ +package io.dropwizard.jersey.guava; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalHeaderParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalHeaderParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() { + String defaultMessage = "Default Message"; + String response = target("/optional/message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsBlank() { + String response = target("/optional/message").request().header("message", "").get(String.class); + assertThat(response).isEqualTo(""); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() { + String customMessage = "Custom Message"; + String response = target("/optional/message").request().header("message", customMessage).get(String.class); + assertThat(response).isEqualTo(customMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() { + String defaultMessage = "My Default Message"; + String response = target("/optional/my-message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() { + String myMessage = "My Message"; + String response = target("/optional/my-message").request().header("mymessage", myMessage).get(String.class); + assertThat(response).isEqualTo(myMessage); + } + + @Test(expected = BadRequestException.class) + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() { + String invalidUUID = "invalid-uuid"; + target("/optional/uuid").request().header("uuid", invalidUUID).get(String.class); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() { + String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + String response = target("/optional/uuid").request().get(String.class); + assertThat(response).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() { + String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + String response = target("/optional/uuid").request().header("uuid", uuid).get(String.class); + assertThat(response).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalHeaderParamResource { + @GET + @Path("/message") + public String getMessage(@HeaderParam("message") Optional message) { + return message.or("Default Message"); + } + + @GET + @Path("/my-message") + public String getMyMessage(@HeaderParam("mymessage") Optional myMessage) { + return myMessage.or(new MyMessage("My Default Message")).getMessage(); + } + + @GET + @Path("/uuid") + public String getUUID(@HeaderParam("uuid") Optional uuid) { + return uuid.or(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalMessageBodyWriterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalMessageBodyWriterTest.java new file mode 100644 index 00000000000..a6f39a59673 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalMessageBodyWriterTest.java @@ -0,0 +1,68 @@ +package io.dropwizard.jersey.guava; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class OptionalMessageBodyWriterTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalReturnResource.class); + } + + @Test + public void presentOptionalsReturnTheirValue() throws Exception { + assertThat(target("/optional-return/") + .queryParam("id", "woo").request() + .get(String.class)) + .isEqualTo("woo"); + } + + @Test + public void absentOptionalsThrowANotFound() throws Exception { + try { + target("/optional-return/").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(404); + } + } + + @Path("/optional-return/") + @Produces(MediaType.TEXT_PLAIN) + public static class OptionalReturnResource { + @GET + public Optional showWithQueryParam(@QueryParam("id") String id) { + return Optional.fromNullable(id); + } + + @POST + public Optional showWithFormParam(@FormParam("id") String id) { + return Optional.fromNullable(id); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalQueryParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalQueryParamResourceTest.java new file mode 100644 index 00000000000..ee2fb5a1efd --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/guava/OptionalQueryParamResourceTest.java @@ -0,0 +1,143 @@ +package io.dropwizard.jersey.guava; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.base.Optional; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Application; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalQueryParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalQueryParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() { + String defaultMessage = "Default Message"; + String response = target("/optional/message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() { + String customMessage = "Custom Message"; + String response = target("/optional/message").queryParam("message", customMessage).request().get(String.class); + assertThat(response).isEqualTo(customMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsBlank() { + String response = target("/optional/message").queryParam("message", "").request().get(String.class); + assertThat(response).isEqualTo(""); + } + + @Test + public void shouldReturnDecodedMessageWhenEncodedMessageIsPresent() { + String encodedMessage = "Custom%20Message"; + String decodedMessage = "Custom Message"; + String response = target("/optional/message").queryParam("message", encodedMessage).request().get(String.class); + assertThat(response).isEqualTo(decodedMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() { + String defaultMessage = "My Default Message"; + String response = target("/optional/my-message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() { + String myMessage = "My Message"; + String response = target("/optional/my-message").queryParam("mymessage", myMessage).request().get(String.class); + assertThat(response).isEqualTo(myMessage); + } + + @Test(expected = BadRequestException.class) + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() { + String invalidUUID = "invalid-uuid"; + target("/optional/uuid").queryParam("uuid", invalidUUID).request().get(String.class); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() { + String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + String response = target("/optional/uuid").request().get(String.class); + assertThat(response).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() { + String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + String response = target("/optional/uuid").queryParam("uuid", uuid).request().get(String.class); + assertThat(response).isEqualTo(uuid); + } + + @Test + public void shouldReturnDefaultValueWhenValueIsAbsent() { + String response = target("/optional/value").request().get(String.class); + assertThat(response).isEqualTo("42"); + } + + @Test + public void shouldReturnDefaultValueWhenEmptyValueIsGiven() { + String response = target("/optional/value").queryParam("value", "").request().get(String.class); + assertThat(response).isEqualTo("42"); + } + + @Test + public void shouldReturnValueWhenValueIsPresent() { + String value = "123456"; + String response = target("/optional/value").queryParam("value", value).request().get(String.class); + assertThat(response).isEqualTo(value); + } + + @Path("/optional") + public static class OptionalQueryParamResource { + + @GET + @Path("/message") + public String getMessage(@QueryParam("message") Optional message) { + return message.or("Default Message"); + } + + @GET + @Path("/my-message") + public String getMyMessage(@QueryParam("mymessage") Optional myMessage) { + return myMessage.or(new MyMessage("My Default Message")).getMessage(); + } + + @GET + @Path("/uuid") + public String getUUID(@QueryParam("uuid") Optional uuid) { + return uuid.or(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + + @GET + @Path("/value") + public String getValue(@QueryParam("value") Optional value) { + return value.or(42).toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/gzip/ConfiguredGZipEncoderTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/gzip/ConfiguredGZipEncoderTest.java new file mode 100644 index 00000000000..2de901d9152 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/gzip/ConfiguredGZipEncoderTest.java @@ -0,0 +1,194 @@ +package io.dropwizard.jersey.gzip; + +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.ClientRequestContext; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.WriterInterceptorContext; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.zip.GZIPOutputStream; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + + +public class ConfiguredGZipEncoderTest { + @Test + public void gzipParametersSpec() throws IOException { + ClientRequestContext context = mock(ClientRequestContext.class); + MultivaluedMap headers = new MultivaluedHashMap<>(); + when(context.getHeaders()).thenReturn(headers); + headers.put(HttpHeaders.CONTENT_ENCODING, null); + when(context.hasEntity()).thenReturn(true); + + new ConfiguredGZipEncoder(true).filter(context); + + assertThat(headers.getFirst(HttpHeaders.CONTENT_ENCODING).toString(), is("gzip")); + } + + @Test + public void aroundWriteToSpec() throws IOException, WebApplicationException { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, "gzip"); + WriterInterceptorContextMock context = new WriterInterceptorContextMock(headers); + new ConfiguredGZipEncoder(true).aroundWriteTo(context); + assertThat(context.getOutputStream(), is(instanceOf(GZIPOutputStream.class))); + assertThat(context.isProceedCalled(), is(true)); + } + @Test + public void aroundWriteToSpecX_GZip() throws IOException, WebApplicationException { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, "x-gzip"); + WriterInterceptorContextMock context = new WriterInterceptorContextMock(headers); + new ConfiguredGZipEncoder(true).aroundWriteTo(context); + assertThat(context.getOutputStream(), is(instanceOf(GZIPOutputStream.class))); + assertThat(context.isProceedCalled(), is(true)); + } + @Test + public void otherEncodingWillNotAroundWrite() throws IOException, WebApplicationException { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, "someOtherEnc"); + WriterInterceptorContextMock context = new WriterInterceptorContextMock(headers); + new ConfiguredGZipEncoder(true).aroundWriteTo(context); + assertThat(context.getOutputStream(), is(not(instanceOf(GZIPOutputStream.class)))); + assertThat(context.isProceedCalled(), is(true)); + } + @Test + public void noEncodingwillNotAroundWrite() throws IOException, WebApplicationException { + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.CONTENT_ENCODING, null); + WriterInterceptorContextMock context = new WriterInterceptorContextMock(headers); + new ConfiguredGZipEncoder(true).aroundWriteTo(context); + assertThat(context.getOutputStream(), is(not(instanceOf(GZIPOutputStream.class)))); + assertThat(context.isProceedCalled(), is(true)); + } + + + @Test(expected = NullPointerException.class) + public void contextMayNotBeNull() throws IOException { + ClientRequestContext context = null; + new ConfiguredGZipEncoder(false).filter(context); + } + + + private class WriterInterceptorContextMock implements WriterInterceptorContext { + private final MultivaluedMap headers; + private OutputStream os = new OutputStream() { + @Override + public void write(int i) throws IOException { + //void + } + }; + private boolean proceedCalled = false; + + public WriterInterceptorContextMock(MultivaluedMap headers) { + this.headers = headers; + } + + @Override + public void proceed() throws IOException, WebApplicationException { + proceedCalled = true; + } + + @Override + public Object getEntity() { + return null; + } + + @Override + public void setEntity(Object entity) { + + } + + @Override + public OutputStream getOutputStream() { + return os; + } + + @Override + public void setOutputStream(OutputStream os) { + this.os = os; + } + + @Override + public MultivaluedMap getHeaders() { + return headers; + } + + @Override + public Object getProperty(String name) { + return null; + } + + @Override + public Collection getPropertyNames() { + return null; + } + + @Override + public void setProperty(String name, Object object) { + + } + + @Override + public void removeProperty(String name) { + + } + + @Override + public Annotation[] getAnnotations() { + return new Annotation[0]; + } + + @Override + public void setAnnotations(Annotation[] annotations) { + + } + + @Override + public Class getType() { + return null; + } + + @Override + public void setType(Class type) { + + } + + @Override + public Type getGenericType() { + return null; + } + + @Override + public void setGenericType(Type genericType) { + + } + + @Override + public MediaType getMediaType() { + return null; + } + + @Override + public void setMediaType(MediaType mediaType) { + + } + + public boolean isProceedCalled() { + return proceedCalled; + } + } +} 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 00000000000..3701090f516 --- /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 messages; + + public BrokenRepresentation(List messages) { + this.messages = messages; + } + + @JsonProperty + public List getMessages() { + return messages; + } + + @JsonProperty + public void setMessages(List 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 00000000000..fff0d02a875 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/DefaultJacksonMessageBodyProvider.java @@ -0,0 +1,12 @@ +package io.dropwizard.jersey.jackson; + +import io.dropwizard.jackson.Jackson; + +import javax.ws.rs.ext.Provider; + +@Provider +public class DefaultJacksonMessageBodyProvider extends JacksonMessageBodyProvider { + public DefaultJacksonMessageBodyProvider() { + super(Jackson.newObjectMapper()); + } +} 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 00000000000..dc43a9736f5 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JacksonMessageBodyProviderTest.java @@ -0,0 +1,299 @@ +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.reflect.TypeToken; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.validation.Validated; +import org.hibernate.validator.constraints.NotEmpty; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.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; + +@SuppressWarnings({"serial", "unchecked"}) +public class JacksonMessageBodyProviderTest { + private static final Annotation[] NONE = new Annotation[0]; + + public static class Example { + @Min(0) + @JsonProperty + public int id; + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Example other = (Example) obj; + return Objects.equals(this.id, other.id); + } + } + + public static class ListExample { + @NotEmpty + @Valid + @JsonProperty + public List examples; + } + + public interface Partial1 { + } + + public interface Partial2 { + } + + public static class PartialExample { + @Min(value = 0, groups = Partial1.class) + @JsonProperty + public int id; + + @NotNull(groups = Partial2.class) + @JsonProperty + public 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); + + @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(StandardCharsets.UTF_8)); + final Class klass = Example.class; + + final Object obj = provider.readFrom((Class) klass, + Example.class, + NONE, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + 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(StandardCharsets.UTF_8)); + final Class klass = PartialExample.class; + + final Object obj = provider.readFrom((Class) klass, + PartialExample.class, + new Annotation[]{valid}, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + 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(StandardCharsets.UTF_8)); + final Class klass = PartialExample.class; + + final Object obj = provider.readFrom((Class) klass, + PartialExample.class, + new Annotation[]{valid}, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + entity); + + assertThat(obj) + .isInstanceOf(PartialExample.class); + + assertThat(((PartialExample) obj).id) + .isEqualTo(1); + } + + @Test + public void throwsAJsonProcessingExceptionForMalformedRequestEntities() throws Exception { + final ByteArrayInputStream entity = new ByteArrayInputStream("{\"id\":-1d".getBytes(StandardCharsets.UTF_8)); + + try { + final Class klass = Example.class; + provider.readFrom((Class) klass, + Example.class, + NONE, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + 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 MultivaluedHashMap<>(), + output); + + assertThat(output.toString()) + .isEqualTo("{\"id\":500}"); + } + + @Test + public void returnsValidatedCollectionRequestEntities() throws Exception { + testValidatedCollectionType(Collection.class, + new TypeToken>() { + }.getType()); + } + + @Test + public void returnsValidatedSetRequestEntities() throws Exception { + testValidatedCollectionType(Set.class, + new TypeToken>() { + }.getType()); + } + + @Test + public void returnsValidatedListRequestEntities() throws Exception { + testValidatedCollectionType(List.class, + new TypeToken>() { + }.getType()); + } + + 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(StandardCharsets.UTF_8)); + + final Object obj = provider.readFrom((Class) klass, + type, + new Annotation[]{valid}, + MediaType.APPLICATION_JSON_TYPE, + new MultivaluedHashMap<>(), + entity); + + assertThat(obj) + .isInstanceOf(klass); + + Iterator iterator = ((Iterable) obj).iterator(); + assertThat(iterator.next().id).isEqualTo(1); + assertThat(iterator.next().id).isEqualTo(2); + } + +} 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 00000000000..21accb9d2dd --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonProcessingExceptionMapperTest.java @@ -0,0 +1,134 @@ +package io.dropwizard.jersey.jackson; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonProcessingExceptionMapperTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .packages("io.dropwizard.jersey.jackson"); + } + + @Override + protected void configureClient(ClientConfig config) { + final ObjectMapper mapper = new ObjectMapper(); + final JacksonMessageBodyProvider provider = new JacksonMessageBodyProvider(mapper); + config.register(provider); + } + + @Test + public void returnsA500ForNonDeserializableRepresentationClasses() throws Exception { + Response response = target("/json/broken").request(MediaType.APPLICATION_JSON) + .post(Entity.entity(new BrokenRepresentation(ImmutableList.of("whee")), MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(500); + } + + @Test + public void returnsA500ForListNonDeserializableRepresentationClasses() throws Exception { + final ImmutableList ent = + ImmutableList.of(new BrokenRepresentation(ImmutableList.of()), + new BrokenRepresentation(ImmutableList.of("whoo"))); + + Response response = target("/json/brokenList").request(MediaType.APPLICATION_JSON) + .post(Entity.entity(ent, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(500); + } + + @Test + public void returnsA500ForNonSerializableRepresentationClassesOutbound() throws Exception { + Response response = target("/json/brokenOutbound").request(MediaType.APPLICATION_JSON).get(); + assertThat(response.getStatus()).isEqualTo(500); + } + + @Test + public void returnsA500ForAbstractEntity() throws Exception { + Response response = target("/json/interface").request(MediaType.APPLICATION_JSON) + .post(Entity.entity("\"hello\"", MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(500); + } + + @Test + public void returnsA500ForAbstractEntities() throws Exception { + Response response = target("/json/interfaceList").request(MediaType.APPLICATION_JSON) + .post(Entity.entity("[\"hello\"]", MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(500); + } + + @Test + public void returnsA400ForMalformedInputCausingIoException() throws Exception { + assertEndpointReturns400("url", "\"no-scheme.com\""); + } + + @Test + public void returnsA400ForListWrongInputType() throws Exception { + assertEndpointReturns400("urlList", "\"no-scheme.com\""); + } + + @Test + public void returnsA400ForMalformedListInputCausingIoException() throws Exception { + assertEndpointReturns400("urlList", "[\"no-scheme.com\"]"); + } + + @Test + public void returnsA400ForNonDeserializableRequestEntities() throws Exception { + assertEndpointReturns400("ok", new UnknownRepresentation(100)); + } + + @Test + public void returnsA400ForWrongInputType() throws Exception { + assertEndpointReturns400("ok", "false"); + } + + @Test + public void returnsA400ForInvalidFormatRequestEntities() throws Exception { + assertEndpointReturns400("ok", "{\"message\": \"a\", \"date\": \"2016-01-01\"}"); + } + + @Test + public void returnsA400ForInvalidFormatRequestEntitiesWrapped() throws Exception { + assertEndpointReturns400("ok", "{\"message\": \"1\", \"date\": \"a\"}"); + } + + @Test + public void returnsA400ForInvalidFormatRequestEntitiesArray() throws Exception { + assertEndpointReturns400("ok", "{\"message\": \"1\", \"date\": [1,1,1,1]}"); + } + + @Test + public void returnsA400ForSemanticInvalidDate() throws Exception { + assertEndpointReturns400("ok", "{\"message\": \"1\", \"date\": [-1,-1,-1]}"); + } + + private void assertEndpointReturns400(String endpoint, T entity) { + Response response = target(String.format("/json/%s", endpoint)) + .request(MediaType.APPLICATION_JSON) + .post(Entity.entity(entity, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(400); + + JsonNode errorMessage = response.readEntity(JsonNode.class); + assertThat(errorMessage.get("code").asInt()).isEqualTo(400); + assertThat(errorMessage.get("message").asText()).isEqualTo("Unable to process JSON"); + assertThat(errorMessage.has("details")).isFalse(); + } +} 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 00000000000..3d34ddd04a1 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/JsonResource.java @@ -0,0 +1,64 @@ +package io.dropwizard.jersey.jackson; + +import com.google.common.collect.ImmutableList; + +import javax.ws.rs.Consumes; +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.net.URL; +import java.util.List; + +@Path("/json/") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class JsonResource { + @POST + @Path("/broken") + public void broken(BrokenRepresentation rep) { + System.out.println(rep); + } + + @GET + @Path("/brokenOutbound") + public NonBeanImplementation brokenOutbound() { + return new NonBeanImplementation(); + } + + @POST + @Path("/ok") + public List ok(OkRepresentation rep) { + return ImmutableList.of(rep.getMessage()); + } + + @POST + @Path("/brokenList") + public List ok(List rep) { + return ImmutableList.of(rep.size()); + } + + @POST + @Path("/url") + public void url(URL url) { + } + + @POST + @Path("/urlList") + public void urlList(List url) { + } + + @POST + @Path("/interface") + public void face(IInterface inter) { + } + + @POST + @Path("/interfaceList") + public void face(List inter) { + } + + private interface IInterface { + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/NonBeanImplementation.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/NonBeanImplementation.java new file mode 100644 index 00000000000..cba28450359 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/NonBeanImplementation.java @@ -0,0 +1,12 @@ +package io.dropwizard.jersey.jackson; + +/** + * Jackson has less insight on how to serialize/deserialize an object that + * doesn't adhere to the Bean spec. This class needs additional annotations in + * order to be properly serialized/deserialized. + */ +public class NonBeanImplementation { + public Integer val() { + return 1; + } +} 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 00000000000..d18b80dca64 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/OkRepresentation.java @@ -0,0 +1,30 @@ +package io.dropwizard.jersey.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.time.LocalDate; + +public class OkRepresentation { + private Integer message; + private LocalDate date; + + @JsonProperty + public Integer getMessage() { + return message; + } + + @JsonProperty + public LocalDate getDate() { + return date; + } + + @JsonProperty + public void setMessage(Integer message) { + this.message = message; + } + + @JsonProperty + public void setDate(LocalDate date) { + this.date = date; + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/UnknownRepresentation.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/UnknownRepresentation.java new file mode 100644 index 00000000000..f64c63d5b43 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jackson/UnknownRepresentation.java @@ -0,0 +1,21 @@ +package io.dropwizard.jersey.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class UnknownRepresentation { + private int bork; + + public UnknownRepresentation(int bork) { + this.bork = bork; + } + + @JsonProperty + public int getBork() { + return bork; + } + + @JsonProperty + public void setBork(int bork) { + this.bork = bork; + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalDateParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalDateParamTest.java new file mode 100644 index 00000000000..d5b845edf53 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalDateParamTest.java @@ -0,0 +1,17 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LocalDateParamTest { + @Test + public void parsesDateTimes() throws Exception { + final LocalDateParam param = new LocalDateParam("2012-11-19"); + + assertThat(param.get()) + .isEqualTo(LocalDate.of(2012, 11, 19)); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalDateTimeParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalDateTimeParamTest.java new file mode 100644 index 00000000000..d95fd84f55b --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalDateTimeParamTest.java @@ -0,0 +1,17 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LocalDateTimeParamTest { + @Test + public void parsesDateTimes() throws Exception { + final LocalDateTimeParam param = new LocalDateTimeParam("2012-11-19T13:37"); + + assertThat(param.get()) + .isEqualTo(LocalDateTime.of(2012, 11, 19, 13, 37)); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalTimeParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalTimeParamTest.java new file mode 100644 index 00000000000..a102a788f8a --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/LocalTimeParamTest.java @@ -0,0 +1,17 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.LocalTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LocalTimeParamTest { + @Test + public void parsesDateTimes() throws Exception { + final LocalTimeParam param = new LocalTimeParam("12:34:56"); + + assertThat(param.get()) + .isEqualTo(LocalTime.of(12, 34, 56)); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/OffsetDateTimeParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/OffsetDateTimeParamTest.java new file mode 100644 index 00000000000..8f760a29fe5 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/OffsetDateTimeParamTest.java @@ -0,0 +1,18 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OffsetDateTimeParamTest { + @Test + public void parsesDateTimes() throws Exception { + final OffsetDateTimeParam param = new OffsetDateTimeParam("2012-11-19T13:37+01:00"); + + assertThat(param.get()) + .isEqualTo(OffsetDateTime.of(2012, 11, 19, 13, 37, 0, 0, ZoneOffset.ofHours(1))); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/YearMonthParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/YearMonthParamTest.java new file mode 100644 index 00000000000..b040dfd740f --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/YearMonthParamTest.java @@ -0,0 +1,18 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.Month; +import java.time.YearMonth; + +import static org.assertj.core.api.Assertions.assertThat; + +public class YearMonthParamTest { + @Test + public void parsesDateTimes() throws Exception { + final YearMonthParam param = new YearMonthParam("2012-11"); + + assertThat(param.get()) + .isEqualTo(YearMonth.of(2012, Month.NOVEMBER)); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/YearParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/YearParamTest.java new file mode 100644 index 00000000000..ece5c464f2f --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/YearParamTest.java @@ -0,0 +1,17 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.Year; + +import static org.assertj.core.api.Assertions.assertThat; + +public class YearParamTest { + @Test + public void parsesDateTimes() throws Exception { + final YearParam param = new YearParam("2012"); + + assertThat(param.get()) + .isEqualTo(Year.of(2012)); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/ZoneIdParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/ZoneIdParamTest.java new file mode 100644 index 00000000000..aee85592292 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/ZoneIdParamTest.java @@ -0,0 +1,17 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.ZoneId; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ZoneIdParamTest { + @Test + public void parsesDateTimes() throws Exception { + final ZoneIdParam param = new ZoneIdParam("Europe/Berlin"); + + assertThat(param.get()) + .isEqualTo(ZoneId.of("Europe/Berlin")); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/ZonedDateTimeParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/ZonedDateTimeParamTest.java new file mode 100644 index 00000000000..96749e58bd5 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/jsr310/ZonedDateTimeParamTest.java @@ -0,0 +1,18 @@ +package io.dropwizard.jersey.jsr310; + +import org.junit.Test; + +import java.time.ZoneId; +import java.time.ZonedDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ZonedDateTimeParamTest { + @Test + public void parsesDateTimes() throws Exception { + final ZonedDateTimeParam param = new ZonedDateTimeParam("2012-11-19T13:37+01:00[Europe/Berlin]"); + + assertThat(param.get()) + .isEqualTo(ZonedDateTime.of(2012, 11, 19, 13, 37, 0, 0, ZoneId.of("Europe/Berlin"))); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalCookieParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalCookieParamResourceTest.java new file mode 100644 index 00000000000..c0d35d4da09 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalCookieParamResourceTest.java @@ -0,0 +1,109 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.CookieParam; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalCookieParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalCookieParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() { + String defaultMessage = "Default Message"; + String response = target("/optional/message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsBlank() { + String response = target("/optional/message").request().cookie("message", "").get(String.class); + assertThat(response).isEqualTo(""); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() { + String customMessage = "Custom Message"; + String response = target("/optional/message").request().cookie("message", customMessage).get(String.class); + assertThat(response).isEqualTo(customMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() { + String defaultMessage = "My Default Message"; + String response = target("/optional/my-message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() { + String myMessage = "My Message"; + String response = target("/optional/my-message").request().cookie("mymessage", myMessage).get(String.class); + assertThat(response).isEqualTo(myMessage); + } + + @Test(expected = BadRequestException.class) + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() { + String invalidUUID = "invalid-uuid"; + target("/optional/uuid").request().cookie("uuid", invalidUUID).get(String.class); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() { + String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + String response = target("/optional/uuid").request().get(String.class); + assertThat(response).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() { + String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + String response = target("/optional/uuid").request().cookie("uuid", uuid).get(String.class); + assertThat(response).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalCookieParamResource { + @GET + @Path("/message") + public String getMessage(@CookieParam("message") Optional message) { + return message.orElse("Default Message"); + } + + @GET + @Path("/my-message") + public String getMyMessage(@CookieParam("mymessage") Optional myMessage) { + return myMessage.orElse(new MyMessage("My Default Message")).getMessage(); + } + + @GET + @Path("/uuid") + public String getUUID(@CookieParam("uuid") Optional uuid) { + return uuid.orElse(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalDoubleMessageBodyWriterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalDoubleMessageBodyWriterTest.java new file mode 100644 index 00000000000..84b1473ba23 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalDoubleMessageBodyWriterTest.java @@ -0,0 +1,82 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.OptionalDouble; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class OptionalDoubleMessageBodyWriterTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalDoubleReturnResource.class); + } + + @Test + public void presentOptionalsReturnTheirValue() throws Exception { + assertThat(target("optional-return") + .queryParam("id", "1").request() + .get(Double.class)) + .isEqualTo(1); + } + + @Test + public void presentOptionalsReturnTheirValueWithResponse() throws Exception { + assertThat(target("optional-return/response-wrapped") + .queryParam("id", "1").request() + .get(Double.class)) + .isEqualTo(1); + } + + @Test + public void absentOptionalsThrowANotFound() throws Exception { + try { + target("optional-return").request().get(Double.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(404); + } + } + + @Path("optional-return") + @Produces(MediaType.TEXT_PLAIN) + public static class OptionalDoubleReturnResource { + @GET + public OptionalDouble showWithQueryParam(@QueryParam("id") OptionalDouble id) { + return id; + } + + @POST + public OptionalDouble showWithFormParam(@FormParam("id") OptionalDouble id) { + return id; + } + + @Path("response-wrapped") + @GET + public Response showWithQueryParamResponse(@QueryParam("id") OptionalDouble id) { + return Response.ok(id).build(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalFormParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalFormParamResourceTest.java new file mode 100644 index 00000000000..71b6b7c90f6 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalFormParamResourceTest.java @@ -0,0 +1,128 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.internal.util.collection.MultivaluedStringMap; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalFormParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalFormParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() throws IOException { + final String defaultMessage = "Default Message"; + final Response response = target("/optional/message").request().post(Entity.form(new MultivaluedStringMap())); + + assertThat(response.readEntity(String.class)).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageBlank() throws IOException { + final Form form = new Form("message", ""); + final Response response = target("/optional/message").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(""); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() throws IOException { + final String customMessage = "Custom Message"; + final Form form = new Form("message", customMessage); + final Response response = target("/optional/message").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(customMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() throws IOException { + final String defaultMessage = "My Default Message"; + final Response response = target("/optional/my-message").request().post(Entity.form(new MultivaluedStringMap())); + + assertThat(response.readEntity(String.class)).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() throws IOException { + final String myMessage = "My Message"; + final Form form = new Form("mymessage", myMessage); + final Response response = target("/optional/my-message").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(myMessage); + } + + @Test + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() throws IOException { + final String invalidUUID = "invalid-uuid"; + final Form form = new Form("uuid", invalidUUID); + final Response response = target("/optional/uuid").request().post(Entity.form(form)); + + assertThat(response.getStatus()).isEqualTo(Response.Status.BAD_REQUEST.getStatusCode()); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() throws IOException { + final String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + final Response response = target("/optional/uuid").request().post(Entity.form(new MultivaluedStringMap())); + + assertThat(response.readEntity(String.class)).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() throws IOException { + final String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + final Form form = new Form("uuid", uuid); + final Response response = target("/optional/uuid").request().post(Entity.form(form)); + + assertThat(response.readEntity(String.class)).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalFormParamResource { + + @POST + @Path("/message") + public String getMessage(@FormParam("message") Optional message) { + return message.orElse("Default Message"); + } + + @POST + @Path("/my-message") + public String getMyMessage(@FormParam("mymessage") Optional myMessage) { + return myMessage.orElse(new MyMessage("My Default Message")).getMessage(); + } + + @POST + @Path("/uuid") + public String getUUID(@FormParam("uuid") Optional uuid) { + return uuid.orElse(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalHeaderParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalHeaderParamResourceTest.java new file mode 100644 index 00000000000..1eb03e9dad8 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalHeaderParamResourceTest.java @@ -0,0 +1,109 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.Path; +import javax.ws.rs.core.Application; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalHeaderParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalHeaderParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() { + String defaultMessage = "Default Message"; + String response = target("/optional/message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsBlank() { + String response = target("/optional/message").request().header("message", "").get(String.class); + assertThat(response).isEqualTo(""); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() { + String customMessage = "Custom Message"; + String response = target("/optional/message").request().header("message", customMessage).get(String.class); + assertThat(response).isEqualTo(customMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() { + String defaultMessage = "My Default Message"; + String response = target("/optional/my-message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() { + String myMessage = "My Message"; + String response = target("/optional/my-message").request().header("mymessage", myMessage).get(String.class); + assertThat(response).isEqualTo(myMessage); + } + + @Test(expected = BadRequestException.class) + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() { + String invalidUUID = "invalid-uuid"; + target("/optional/uuid").request().header("uuid", invalidUUID).get(String.class); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() { + String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + String response = target("/optional/uuid").request().get(String.class); + assertThat(response).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() { + String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + String response = target("/optional/uuid").request().header("uuid", uuid).get(String.class); + assertThat(response).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalHeaderParamResource { + @GET + @Path("/message") + public String getMessage(@HeaderParam("message") Optional message) { + return message.orElse("Default Message"); + } + + @GET + @Path("/my-message") + public String getMyMessage(@HeaderParam("mymessage") Optional myMessage) { + return myMessage.orElse(new MyMessage("My Default Message")).getMessage(); + } + + @GET + @Path("/uuid") + public String getUUID(@HeaderParam("uuid") Optional uuid) { + return uuid.orElse(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalIntMessageBodyWriterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalIntMessageBodyWriterTest.java new file mode 100644 index 00000000000..3c21c1b0bde --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalIntMessageBodyWriterTest.java @@ -0,0 +1,82 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.OptionalInt; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class OptionalIntMessageBodyWriterTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalIntReturnResource.class); + } + + @Test + public void presentOptionalsReturnTheirValue() throws Exception { + assertThat(target("optional-return") + .queryParam("id", "1").request() + .get(Integer.class)) + .isEqualTo(1); + } + + @Test + public void presentOptionalsReturnTheirValueWithResponse() throws Exception { + assertThat(target("optional-return/response-wrapped") + .queryParam("id", "1").request() + .get(Integer.class)) + .isEqualTo(1); + } + + @Test + public void absentOptionalsThrowANotFound() throws Exception { + try { + target("optional-return").request().get(Integer.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(404); + } + } + + @Path("optional-return") + @Produces(MediaType.TEXT_PLAIN) + public static class OptionalIntReturnResource { + @GET + public OptionalInt showWithQueryParam(@QueryParam("id") OptionalInt id) { + return id; + } + + @POST + public OptionalInt showWithFormParam(@FormParam("id") OptionalInt id) { + return id; + } + + @Path("response-wrapped") + @GET + public Response showWithQueryParamResponse(@QueryParam("id") OptionalInt id) { + return Response.ok(id).build(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalLongMessageBodyWriterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalLongMessageBodyWriterTest.java new file mode 100644 index 00000000000..daecc6f06d0 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalLongMessageBodyWriterTest.java @@ -0,0 +1,82 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.OptionalLong; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class OptionalLongMessageBodyWriterTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalLongReturnResource.class); + } + + @Test + public void presentOptionalsReturnTheirValue() throws Exception { + assertThat(target("optional-return") + .queryParam("id", "1").request() + .get(Long.class)) + .isEqualTo(1L); + } + + @Test + public void presentOptionalsReturnTheirValueWithResponse() throws Exception { + assertThat(target("optional-return/response-wrapped") + .queryParam("id", "1").request() + .get(Long.class)) + .isEqualTo(1L); + } + + @Test + public void absentOptionalsThrowANotFound() throws Exception { + try { + target("optional-return").request().get(Long.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()).isEqualTo(404); + } + } + + @Path("optional-return") + @Produces(MediaType.TEXT_PLAIN) + public static class OptionalLongReturnResource { + @GET + public OptionalLong showWithQueryParam(@QueryParam("id") OptionalLong id) { + return id; + } + + @POST + public OptionalLong showWithFormParam(@FormParam("id") OptionalLong id) { + return id; + } + + @Path("response-wrapped") + @GET + public Response showWithQueryParamResponse(@QueryParam("id") OptionalLong id) { + return Response.ok(id).build(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalMessageBodyWriterTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalMessageBodyWriterTest.java new file mode 100644 index 00000000000..5adcdd40937 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalMessageBodyWriterTest.java @@ -0,0 +1,83 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class OptionalMessageBodyWriterTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalReturnResource.class); + } + + @Test + public void presentOptionalsReturnTheirValue() throws Exception { + assertThat(target("optional-return") + .queryParam("id", "woo").request() + .get(String.class)) + .isEqualTo("woo"); + } + + @Test + public void presentOptionalsReturnTheirValueWithResponse() throws Exception { + assertThat(target("optional-return/response-wrapped") + .queryParam("id", "woo").request() + .get(String.class)) + .isEqualTo("woo"); + } + + @Test + public void absentOptionalsThrowANotFound() throws Exception { + try { + target("optional-return").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(404); + } + } + + @Path("optional-return") + @Produces(MediaType.TEXT_PLAIN) + public static class OptionalReturnResource { + @GET + public Optional showWithQueryParam(@QueryParam("id") String id) { + return Optional.ofNullable(id); + } + + @POST + public Optional showWithFormParam(@FormParam("id") String id) { + return Optional.ofNullable(id); + } + + @Path("response-wrapped") + @GET + public Response showWithQueryParamResponse(@QueryParam("id") String id) { + return Response.ok(Optional.ofNullable(id)).build(); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalQueryParamResourceTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalQueryParamResourceTest.java new file mode 100644 index 00000000000..86820003d30 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/optional/OptionalQueryParamResourceTest.java @@ -0,0 +1,118 @@ +package io.dropwizard.jersey.optional; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.MyMessage; +import io.dropwizard.jersey.MyMessageParamConverterProvider; +import io.dropwizard.jersey.params.UUIDParam; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.BadRequestException; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Application; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalQueryParamResourceTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(OptionalQueryParamResource.class) + .register(MyMessageParamConverterProvider.class); + } + + @Test + public void shouldReturnDefaultMessageWhenMessageIsNotPresent() { + String defaultMessage = "Default Message"; + String response = target("/optional/message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsPresent() { + String customMessage = "Custom Message"; + String response = target("/optional/message").queryParam("message", customMessage).request().get(String.class); + assertThat(response).isEqualTo(customMessage); + } + + @Test + public void shouldReturnMessageWhenMessageIsBlank() { + String response = target("/optional/message").queryParam("message", "").request().get(String.class); + assertThat(response).isEqualTo(""); + } + + @Test + public void shouldReturnDecodedMessageWhenEncodedMessageIsPresent() { + String encodedMessage = "Custom%20Message"; + String decodedMessage = "Custom Message"; + String response = target("/optional/message").queryParam("message", encodedMessage).request().get(String.class); + assertThat(response).isEqualTo(decodedMessage); + } + + @Test + public void shouldReturnDefaultMessageWhenMyMessageIsNotPresent() { + String defaultMessage = "My Default Message"; + String response = target("/optional/my-message").request().get(String.class); + assertThat(response).isEqualTo(defaultMessage); + } + + @Test + public void shouldReturnMyMessageWhenMyMessageIsPresent() { + String myMessage = "My Message"; + String response = target("/optional/my-message").queryParam("mymessage", myMessage).request().get(String.class); + assertThat(response).isEqualTo(myMessage); + } + + @Test(expected = BadRequestException.class) + public void shouldThrowBadRequestExceptionWhenInvalidUUIDIsPresent() { + String invalidUUID = "invalid-uuid"; + target("/optional/uuid").queryParam("uuid", invalidUUID).request().get(String.class); + } + + @Test + public void shouldReturnDefaultUUIDWhenUUIDIsNotPresent() { + String defaultUUID = "d5672fa8-326b-40f6-bf71-d9dacf44bcdc"; + String response = target("/optional/uuid").request().get(String.class); + assertThat(response).isEqualTo(defaultUUID); + } + + @Test + public void shouldReturnUUIDWhenValidUUIDIsPresent() { + String uuid = "fd94b00d-bd50-46b3-b42f-905a9c9e7d78"; + String response = target("/optional/uuid").queryParam("uuid", uuid).request().get(String.class); + assertThat(response).isEqualTo(uuid); + } + + @Path("/optional") + public static class OptionalQueryParamResource { + + @GET + @Path("/message") + public String getMessage(@QueryParam("message") Optional message) { + return message.orElse("Default Message"); + } + + @GET + @Path("/my-message") + public String getMyMessage(@QueryParam("mymessage") Optional myMessage) { + return myMessage.orElse(new MyMessage("My Default Message")).getMessage(); + } + + @GET + @Path("/uuid") + public String getUUID(@QueryParam("uuid") Optional uuid) { + return uuid.orElse(new UUIDParam("d5672fa8-326b-40f6-bf71-d9dacf44bcdc")).get().toString(); + } + } +} diff --git a/dropwizard/src/test/java/com/yammer/dropwizard/jersey/params/tests/BooleanParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/BooleanParamTest.java similarity index 50% rename from dropwizard/src/test/java/com/yammer/dropwizard/jersey/params/tests/BooleanParamTest.java rename to dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/BooleanParamTest.java index 5b11cd0e5cb..a8a89657a8d 100644 --- a/dropwizard/src/test/java/com/yammer/dropwizard/jersey/params/tests/BooleanParamTest.java +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/BooleanParamTest.java @@ -1,46 +1,45 @@ -package com.yammer.dropwizard.jersey.params.tests; +package io.dropwizard.jersey.params; -import com.yammer.dropwizard.jersey.params.BooleanParam; +import io.dropwizard.jersey.errors.ErrorMessage; import org.junit.Test; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.fail; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; public class BooleanParamTest { @Test public void trueReturnsTrue() throws Exception { final BooleanParam param = new BooleanParam("true"); - assertThat(param.get(), - is(true)); + assertThat(param.get()) + .isTrue(); } @Test public void uppercaseTrueReturnsTrue() throws Exception { final BooleanParam param = new BooleanParam("TRUE"); - assertThat(param.get(), - is(true)); + assertThat(param.get()) + .isTrue(); } @Test public void falseReturnsFalse() throws Exception { final BooleanParam param = new BooleanParam("false"); - - assertThat(param.get(), - is(false)); + + assertThat(param.get()) + .isFalse(); } @Test public void uppercaseFalseReturnsFalse() throws Exception { final BooleanParam param = new BooleanParam("FALSE"); - - assertThat(param.get(), - is(false)); + + assertThat(param.get()) + .isFalse(); } @Test @@ -48,15 +47,17 @@ public void uppercaseFalseReturnsFalse() throws Exception { public void nullThrowsAnException() throws Exception { try { new BooleanParam(null); - fail("expected a WebApplicationException, but none was thrown"); + failBecauseExceptionWasNotThrown(WebApplicationException.class); } catch (WebApplicationException e) { final Response response = e.getResponse(); - - assertThat(response.getStatus(), - is(400)); - - assertThat((String) response.getEntity(), - is("\"null\" must be \"true\" or \"false\"")); + + assertThat(response.getStatus()) + .isEqualTo(400); + + ErrorMessage entity = (ErrorMessage) response.getEntity(); + assertThat(entity.getCode()).isEqualTo(400); + assertThat(entity.getMessage()) + .isEqualTo("Parameter must be \"true\" or \"false\"."); } } @@ -65,15 +66,17 @@ public void nullThrowsAnException() throws Exception { public void nonBooleanValuesThrowAnException() throws Exception { try { new BooleanParam("foo"); - fail("expected a WebApplicationException, but none was thrown"); + failBecauseExceptionWasNotThrown(WebApplicationException.class); } catch (WebApplicationException e) { final Response response = e.getResponse(); - assertThat(response.getStatus(), - is(400)); + assertThat(response.getStatus()) + .isEqualTo(400); - assertThat((String) response.getEntity(), - is("\"foo\" must be \"true\" or \"false\".")); + ErrorMessage entity = (ErrorMessage) response.getEntity(); + assertThat(entity.getCode()).isEqualTo(400); + assertThat(entity.getMessage()) + .isEqualTo("Parameter 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 00000000000..9ccda09a7a3 --- /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.assertj.core.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 00000000000..633b9e4e17f --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/IntParamTest.java @@ -0,0 +1,39 @@ +package io.dropwizard.jersey.params; + +import io.dropwizard.jersey.errors.ErrorMessage; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.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); + + ErrorMessage entity = (ErrorMessage) response.getEntity(); + assertThat(entity.getCode()).isEqualTo(400); + assertThat(entity.getMessage()) + .isEqualTo("Parameter is not a number."); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LocalDateParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LocalDateParamTest.java new file mode 100644 index 00000000000..58ce5df5a93 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LocalDateParamTest.java @@ -0,0 +1,16 @@ +package io.dropwizard.jersey.params; + +import org.joda.time.LocalDate; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LocalDateParamTest { + @Test + public void parsesLocalDates() throws Exception { + final LocalDateParam param = new LocalDateParam("2012-11-20"); + + assertThat(param.get()) + .isEqualTo(new LocalDate(2012, 11, 20)); + } +} 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 00000000000..b1e5071e51e --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/LongParamTest.java @@ -0,0 +1,39 @@ +package io.dropwizard.jersey.params; + +import io.dropwizard.jersey.errors.ErrorMessage; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.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); + + ErrorMessage entity = (ErrorMessage) response.getEntity(); + assertThat(entity.getCode()).isEqualTo(400); + assertThat(entity.getMessage()) + .isEqualTo("Parameter is not a number."); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/NonEmptyStringParamProviderTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/NonEmptyStringParamProviderTest.java new file mode 100644 index 00000000000..5d1048ae2d7 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/NonEmptyStringParamProviderTest.java @@ -0,0 +1,62 @@ +package io.dropwizard.jersey.params; + + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Application; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NonEmptyStringParamProviderTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(NonEmptyStringParamResource.class); + } + + @Test + public void shouldReturnDefaultMessageWhenNonExistent() { + String response = target("/non-empty/string").request().get(String.class); + assertThat(response).isEqualTo("Hello"); + } + + @Test + public void shouldReturnDefaultMessageWhenEmptyString() { + String response = target("/non-empty/string").queryParam("message", "").request().get(String.class); + assertThat(response).isEqualTo("Hello"); + } + + @Test + public void shouldReturnDefaultMessageWhenNull() { + String response = target("/non-empty/string").queryParam("message").request().get(String.class); + assertThat(response).isEqualTo("Hello"); + } + + @Test + public void shouldReturnMessageWhenSpecified() { + String response = target("/non-empty/string").queryParam("message", "Goodbye").request().get(String.class); + assertThat(response).isEqualTo("Goodbye"); + } + + @Path("/non-empty") + public static class NonEmptyStringParamResource { + @GET + @Path("/string") + public String getMessage(@QueryParam("message") NonEmptyStringParam message) { + return message.get().orElse("Hello"); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/NonEmptyStringParamTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/NonEmptyStringParamTest.java new file mode 100644 index 00000000000..ee9b2a0fff4 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/NonEmptyStringParamTest.java @@ -0,0 +1,27 @@ +package io.dropwizard.jersey.params; + +import org.junit.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NonEmptyStringParamTest { + @Test + public void aBlankStringIsAnAbsentString() throws Exception { + final NonEmptyStringParam param = new NonEmptyStringParam(""); + assertThat(param.get()).isEqualTo(Optional.empty()); + } + + @Test + public void aNullStringIsAnAbsentString() throws Exception { + final NonEmptyStringParam param = new NonEmptyStringParam(null); + assertThat(param.get()).isEqualTo(Optional.empty()); + } + + @Test + public void aStringWithContentIsItself() throws Exception { + final NonEmptyStringParam param = new NonEmptyStringParam("hello"); + assertThat(param.get()).isEqualTo(Optional.of("hello")); + } +} 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 00000000000..9e4883c4451 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/params/UUIDParamTest.java @@ -0,0 +1,43 @@ +package io.dropwizard.jersey.params; + +import io.dropwizard.jersey.errors.ErrorMessage; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.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); + + ErrorMessage entity = (ErrorMessage) response.getEntity(); + assertThat(entity.getCode()).isEqualTo(400); + assertThat(entity.getMessage()) + .isEqualTo("Parameter is not a UUID."); + } + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashFactoryTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashFactoryTest.java new file mode 100644 index 00000000000..f227f4c2d09 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashFactoryTest.java @@ -0,0 +1,76 @@ +package io.dropwizard.jersey.sessions; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FlashFactoryTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected TestContainerFactory getTestContainerFactory() + throws TestContainerException { + return new GrizzlyWebTestContainerFactory(); + } + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + final ResourceConfig rc = DropwizardResourceConfig.forTesting(new MetricRegistry()); + + return ServletDeploymentContext.builder(rc) + .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, DropwizardResourceConfig.class.getName()) + .initParam(ServerProperties.PROVIDER_CLASSNAMES, FlashResource.class.getName()) + .build(); + } + + @Test + public void passesInHttpSessions() throws Exception { + Response firstResponse = target("/flash").request(MediaType.TEXT_PLAIN) + .post(Entity.entity("Mr. Peeps", MediaType.TEXT_PLAIN)); + + final Map cookies = firstResponse.getCookies(); + firstResponse.close(); + + Invocation.Builder builder = target("/flash").request().accept(MediaType.TEXT_PLAIN); + + for (NewCookie cookie : cookies.values()) { + builder = builder.cookie(cookie); + } + + final String secondResponse = builder.get(String.class); + assertThat(secondResponse).isEqualTo("Mr. Peeps"); + + Invocation.Builder anotherBuilder = target("/flash").request().accept(MediaType.TEXT_PLAIN); + + for (NewCookie cookie : cookies.values()) { + anotherBuilder = 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 00000000000..9e68d3fa272 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/FlashResource.java @@ -0,0 +1,26 @@ +package io.dropwizard.jersey.sessions; + +import javax.ws.rs.Consumes; +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.Objects; + +@Path("/flash/") +@Consumes(MediaType.TEXT_PLAIN) +@Produces(MediaType.TEXT_PLAIN) +public class FlashResource { + + @POST + public void setName(@Session Flash flash, + String name) { + flash.set(name); + } + + @GET + public String getName(@Session Flash flash) { + return Objects.toString(flash.get().orElse(null)); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/HttpSessionFactoryTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/HttpSessionFactoryTest.java new file mode 100644 index 00000000000..687080fa1a5 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/HttpSessionFactoryTest.java @@ -0,0 +1,65 @@ +package io.dropwizard.jersey.sessions; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.server.ServerProperties; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.TestProperties; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerException; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class HttpSessionFactoryTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected TestContainerFactory getTestContainerFactory() + throws TestContainerException { + return new GrizzlyWebTestContainerFactory(); + } + + + @Override + protected DeploymentContext configureDeployment() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + final ResourceConfig rc = DropwizardResourceConfig.forTesting(new MetricRegistry()); + return ServletDeploymentContext.builder(rc) + .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, DropwizardResourceConfig.class.getName()) + .initParam(ServerProperties.PROVIDER_CLASSNAMES, SessionResource.class.getName()) + .build(); + } + + @Test + public void passesInHttpSessions() throws Exception { + Response firstResponse = target("/session/").request(MediaType.TEXT_PLAIN) + .post(Entity.entity("Mr. Peeps", MediaType.TEXT_PLAIN)); + + final Map cookies = firstResponse.getCookies(); + firstResponse.close(); + + Invocation.Builder builder = target("/session/").request().accept(MediaType.TEXT_PLAIN); + + for (NewCookie cookie : cookies.values()) { + builder.cookie(cookie); + } + + assertThat(builder.get(String.class)).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 00000000000..7829b4a543b --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/sessions/SessionResource.java @@ -0,0 +1,27 @@ +package io.dropwizard.jersey.sessions; + +import javax.servlet.http.HttpSession; +import javax.ws.rs.Consumes; +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.Objects; + +@Path("/session/") +@Consumes(MediaType.TEXT_PLAIN) +@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/setup/JerseyEnvironmentTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/setup/JerseyEnvironmentTest.java new file mode 100644 index 00000000000..cf2dbfc4f0d --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/setup/JerseyEnvironmentTest.java @@ -0,0 +1,36 @@ +package io.dropwizard.jersey.setup; + +import io.dropwizard.jersey.DropwizardResourceConfig; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class JerseyEnvironmentTest { + + private final JerseyContainerHolder holder = mock(JerseyContainerHolder.class); + private final DropwizardResourceConfig config = new DropwizardResourceConfig(); + private final JerseyEnvironment jerseyEnvironment = new JerseyEnvironment(holder, config); + + @Test + public void urlPatternEndsWithSlashStar() { + assertPatternEndsWithSlashStar("/missing/slash/star"); + } + + @Test + public void urlPatternEndsWithStar() { + assertPatternEndsWithSlashStar("/missing/star/"); + } + + @Test + public void urlPatternSuffixNoop() { + String slashStarPath = "/slash/star/*"; + jerseyEnvironment.setUrlPattern(slashStarPath); + assertThat(jerseyEnvironment.getUrlPattern()).isEqualTo(slashStarPath); + } + + private void assertPatternEndsWithSlashStar(String jerseyRootPath) { + jerseyEnvironment.setUrlPattern(jerseyRootPath); + assertThat(jerseyEnvironment.getUrlPattern()).endsWith("/*"); + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/BeanParameter.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/BeanParameter.java new file mode 100644 index 00000000000..e8c6891f8b7 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/BeanParameter.java @@ -0,0 +1,21 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.validation.ValidationMethod; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.ws.rs.QueryParam; + +public class BeanParameter { + @QueryParam("name") + @NotEmpty + private String name; + + public String getName() { + return name; + } + + @ValidationMethod(message = "name must be Coda") + public boolean isCoda() { + return "Coda".equals(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 00000000000..15ea738a790 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ConstraintViolationExceptionMapperTest.java @@ -0,0 +1,551 @@ +package io.dropwizard.jersey.validation; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.Example; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.ListExample; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.PartialExample; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assume.assumeThat; + +public class ConstraintViolationExceptionMapperTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + private static final Locale DEFAULT_LOCALE = Locale.getDefault(); + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .packages("io.dropwizard.jersey.validation") + .register(new HibernateValidationFeature(Validators.newValidator())); + } + + @BeforeClass + public static void init() { + // Set default locale to English because some tests assert localized error messages + Locale.setDefault(Locale.ENGLISH); + } + + @AfterClass + public static void shutdown() { + Locale.setDefault(DEFAULT_LOCALE); + } + + @Test + public void postInvalidEntityIs422() throws Exception { + assumeThat(Locale.getDefault().getLanguage(), is("en")); + + final Response response = target("/valid/foo").request(MediaType.APPLICATION_JSON) + .post(Entity.entity("{}", MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)).isEqualTo("{\"errors\":[\"name may not be empty\"]}"); + } + + @Test + public void postNullEntityIs422() throws Exception { + final Response response = target("/valid/foo").request(MediaType.APPLICATION_JSON) + .post(Entity.entity(null, MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(422); + + String ret = "{\"errors\":[\"The request body may not be null\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void postInvalidatedEntityIs422() throws Exception { + assumeThat(Locale.getDefault().getLanguage(), is("en")); + + final Response response = target("/valid/fooValidated").request(MediaType.APPLICATION_JSON) + .post(Entity.entity("{}", MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)).isEqualTo("{\"errors\":[\"name may not be empty\"]}"); + } + + @Test + public void returnInvalidEntityIs500() throws Exception { + assumeThat(Locale.getDefault().getLanguage(), is("en")); + + final Response response = target("/valid/foo").request(MediaType.APPLICATION_JSON) + .post(Entity.entity("{ \"name\": \"Coda\" }", MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"server response name may not be empty\"]}"); + } + + @Test + public void returnInvalidatedEntityIs500() throws Exception { + assumeThat(Locale.getDefault().getLanguage(), is("en")); + + final Response response = target("/valid/fooValidated").request(MediaType.APPLICATION_JSON) + .post(Entity.entity("{ \"name\": \"Coda\" }", MediaType.APPLICATION_JSON)); + assertThat(response.getStatus()).isEqualTo(500); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"server response name may not be empty\"]}"); + } + + @Test + public void getInvalidReturnIs500() throws Exception { + // return value is too long and so will fail validation + final Response response = target("/valid/bar") + .queryParam("name", "dropwizard").request().get(); + assertThat(response.getStatus()).isEqualTo(500); + + String ret = "{\"errors\":[\"server response length must be between 0 and 3\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidQueryParamsIs400() throws Exception { + // query parameter is too short and so will fail validation + final Response response = target("/valid/bar") + .queryParam("name", "hi").request().get(); + + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"query param name length must be between 3 and 2147483647\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + + // Send another request to trigger reflection cache + final Response cache = target("/valid/bar") + .queryParam("name", "hi").request().get(); + assertThat(cache.getStatus()).isEqualTo(400); + assertThat(cache.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void cacheIsForParamNamesOnly() throws Exception { + // query parameter must not be null, and must be at least 3 + final Response response = target("/valid/fhqwhgads") + .queryParam("num", 2).request().get(); + + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"query param num must be greater than or equal to 3\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + + // Send another request to trigger reflection cache. This one is invalid in a different way + // and should get a different message. + final Response cache = target("/valid/fhqwhgads").request().get(); + assertThat(cache.getStatus()).isEqualTo(400); + ret = "{\"errors\":[\"query param num may not be null\"]}"; + assertThat(cache.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void postInvalidPrimitiveIs422() throws Exception { + // query parameter is too short and so will fail validation + final Response response = target("/valid/simpleEntity") + .request().post(Entity.json("hi")); + + assertThat(response.getStatus()).isEqualTo(422); + + String ret = "{\"errors\":[\"The request body length must be between 3 and 5\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidCustomTypeIs400() throws Exception { + // query parameter is too short and so will fail validation + final Response response = target("/valid/barter") + .queryParam("name", "hi").request().get(); + + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"query param name length must be between 3 and 2147483647\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidBeanParamsIs400() throws Exception { + // bean parameter is too short and so will fail validation + final Response response = target("/valid/zoo") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("\"name must be Coda\"") + .containsOnlyOnce("\"query param name may not be empty\""); + } + + @Test + public void getInvalidSubBeanParamsIs400() throws Exception { + final Response response = target("/valid/sub-zoo") + .queryParam("address", "42 Wallaby Way") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("query param name may not be empty") + .containsOnlyOnce("name must be Coda"); + } + + @Test + public void getGroupSubBeanParamsIs400() throws Exception { + final Response response = target("/valid/sub-group-zoo") + .queryParam("address", "42 WALLABY WAY") + .queryParam("name", "Coda") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("[\"address must not be uppercase\"]"); + } + + @Test + public void postValidGroupsIs400() throws Exception { + final Response response = target("/valid/sub-valid-group-zoo") + .queryParam("address", "42 WALLABY WAY") + .queryParam("name", "Coda") + .request() + .post(Entity.json("{}")); + assertThat(response.getStatus()).isEqualTo(400); + + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("[\"address must not be uppercase\"]"); + } + + @Test + public void getInvalidatedBeanParamsIs400() throws Exception { + // bean parameter is too short and so will fail validation + final Response response = target("/valid/zoo2") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("\"name must be Coda\"") + .containsOnlyOnce("\"query param name may not be empty\""); + } + + @Test + public void getInvalidHeaderParamsIs400() throws Exception { + final Response response = target("/valid/head") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"header cheese may not be empty\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidCookieParamsIs400() throws Exception { + final Response response = target("/valid/cooks") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"cookie user_id may not be empty\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidPathParamsIs400() throws Exception { + final Response response = target("/valid/goods/11") + .request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"path param id not a well-formed email address\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidFormParamsIs400() throws Exception { + final Response response = target("/valid/form") + .request().post(Entity.form(new Form())); + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"form field username may not be empty\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void postInvalidMethodClassIs422() throws Exception { + final Response response = target("/valid/nothing") + .request().post(Entity.entity("{}", MediaType.APPLICATION_JSON_TYPE)); + assertThat(response.getStatus()).isEqualTo(422); + + String ret = "{\"errors\":[\"must have a false thing\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidNestedReturnIs500() throws Exception { + final Response response = target("/valid/nested").request().get(); + assertThat(response.getStatus()).isEqualTo(500); + + String ret = "{\"errors\":[\"server response representation.name may not be empty\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidNested2ReturnIs500() throws Exception { + final Response response = target("/valid/nested2").request().get(); + assertThat(response.getStatus()).isEqualTo(500); + + String ret = "{\"errors\":[\"server response example must have a false thing\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidContextIs400() throws Exception { + final Response response = target("/valid/context").request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"context may not be null\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void getInvalidMatrixParamIs400() throws Exception { + final Response response = target("/valid/matrix") + .matrixParam("bob", "").request().get(); + assertThat(response.getStatus()).isEqualTo(400); + + String ret = "{\"errors\":[\"matrix param bob may not be empty\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + } + + @Test + public void functionWithSameNameReturnDifferentErrors() throws Exception { + // This test is to make sure that functions with the same name and + // number of parameters (but different parameter types), don't return + // the same validation error due to any caching effects + final Response response = target("/valid/head") + .request().get(); + + String ret = "{\"errors\":[\"header cheese may not be empty\"]}"; + assertThat(response.readEntity(String.class)).isEqualTo(ret); + + final Response response2 = target("/valid/headCopy") + .request().get(); + String ret2 = "{\"errors\":[\"query param cheese may not be null\"]}"; + assertThat(response2.readEntity(String.class)).isEqualTo(ret2); + } + + @Test + public void paramsCanBeValidatedWhenNull() { + assertThat(target("/valid/nullable-int-param") + .request().get().readEntity(String.class)).isEqualTo("I was null"); + } + + @Test + public void paramsCanBeUnwrappedAndValidated() { + assertThat(target("/valid/nullable-int-param").queryParam("num", 4) + .request().get().readEntity(String.class)) + .containsOnlyOnce("[\"query param num must be less than or equal to 3\"]"); + } + + @Test + public void returnPartialValidatedRequestEntities() { + final Response response = target("/valid/validatedPartialExample") + .request().post(Entity.json("{\"id\":1}")); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(PartialExample.class).id) + .isEqualTo(1); + } + + @Test + public void invalidEntityExceptionForPartialValidatedRequestEntities() { + final Response response = target("/valid/validatedPartialExampleBoth") + .request().post(Entity.json("{\"id\":1}")); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"text may not be null\"]}"); + } + + @Test + public void returnPartialBothValidatedRequestEntities() { + final Response response = target("/valid/validatedPartialExampleBoth") + .request().post(Entity.json("{\"id\":1,\"text\":\"hello Cemo\"}")); + + assertThat(response.getStatus()).isEqualTo(200); + + PartialExample ex = response.readEntity(PartialExample.class); + assertThat(ex.id).isEqualTo(1); + assertThat(ex.text).isEqualTo("hello Cemo"); + } + + @Test + public void invalidEntityExceptionForInvalidRequestEntities() { + final Response response = target("/valid/validExample") + .request().post(Entity.json("{\"id\":-1}")); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"id must be greater than or equal to 0\"]}"); + } + + @Test + public void returnRequestEntities() { + final Response response = target("/valid/validExample") + .request().post(Entity.json("{\"id\":1}")); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(Example.class).id) + .isEqualTo(1); + } + + @Test + public void returnRequestArrayEntities() { + final Response response = target("/valid/validExampleArray") + .request().post(Entity.json("[{\"id\":1}, {\"id\":2}]")); + + final Example ex1 = new Example(); + final Example ex2 = new Example(); + ex1.id = 1; + ex2.id = 2; + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(Example[].class)) + .containsExactly(ex1, ex2); + } + + @Test + public void invalidRequestCollectionEntities() { + final Response response = target("/valid/validExampleCollection") + .request().post(Entity.json("[{\"id\":-1}, {\"id\":-2}]")); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .contains("id must be greater than or equal to 0", + "id must be greater than or equal to 0"); + } + + @Test + public void invalidRequestSingleCollectionEntities() { + final Response response = target("/valid/validExampleCollection") + .request().post(Entity.json("[{\"id\":1}, {\"id\":-2}]")); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("id must be greater than or equal to 0"); + } + + @Test + public void returnRequestCollectionEntities() { + final Response response = target("/valid/validExampleCollection") + .request().post(Entity.json("[{\"id\":1}, {\"id\":2}]")); + + assertThat(response.getStatus()).isEqualTo(200); + final Collection example = + response.readEntity(new GenericType>() { + }); + + Example ex1 = new Example(); + Example ex2 = new Example(); + ex1.id = 1; + ex2.id = 2; + + assertThat(example).containsOnly(ex1, ex2); + } + + @Test + public void invalidRequestSetEntities() { + final Response response = target("/valid/validExampleSet") + .request().post(Entity.json("[{\"id\":1}, {\"id\":-2}]")); + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("id must be greater than or equal to 0"); + } + + @Test + public void invalidRequestListEntities() { + final Response response = target("/valid/validExampleList") + .request().post(Entity.json("[{\"id\":-1}, {\"id\":-2}]")); + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"id must be greater than or equal to 0\"," + + "\"id must be greater than or equal to 0\"]}"); + } + + @Test + public void throwsAConstraintViolationExceptionForEmptyRequestEntities() { + final Response response = target("/valid/validExample") + .request().post(Entity.json(null)); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"The request body may not be null\"]}"); + } + + @Test + public void returnsValidatedMapRequestEntities() { + final Response response = target("/valid/validExampleMap") + .request().post(Entity.json("{\"one\": {\"id\":1}, \"two\": {\"id\":2}}")); + + assertThat(response.getStatus()).isEqualTo(200); + + Map map = response.readEntity(new GenericType>() { + }); + assertThat(map.get("one").id).isEqualTo(1); + assertThat(map.get("two").id).isEqualTo(2); + } + + @Test + public void invalidMapRequestEntities() { + final Response response = target("/valid/validExampleMap") + .request().post(Entity.json("{\"one\": {\"id\":-1}, \"two\": {\"id\":-2}}")); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .isEqualTo("{\"errors\":[\"id must be greater than or equal to 0\"," + + "\"id must be greater than or equal to 0\"]}"); + } + + @Test + public void returnsValidatedEmbeddedListEntities() { + final Response response = target("/valid/validExampleEmbeddedList") + .request().post(Entity.json("[ {\"examples\": [ {\"id\":1 } ] } ]")); + + assertThat(response.getStatus()).isEqualTo(200); + List res = response.readEntity(new GenericType>() { + }); + assertThat(res).hasSize(1); + assertThat(res.get(0).examples).hasSize(1); + assertThat(res.get(0).examples.get(0).id).isEqualTo(1); + } + + @Test + public void invalidEmbeddedListEntities() { + final Response response = target("/valid/validExampleEmbeddedList") + .request().post(Entity.json("[ {\"examples\": [ {\"id\":1 } ] }, { } ]")); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("examples may not be empty"); + } + + @Test + public void testInvalidFieldQueryParam() { + final Response response = target("/valid/bar") + .queryParam("sort", "foo") + .request() + .get(); + + assertThat(response.getStatus()).isEqualTo(422); + assertThat(response.readEntity(String.class)) + .containsOnlyOnce("sortParam must match \\\"^(asc|desc)$\\\""); + } +} 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 00000000000..16d30136abf --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/DefaultJacksonMessageBodyProvider.java @@ -0,0 +1,14 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; + +import javax.ws.rs.ext.Provider; + +@Provider +public class DefaultJacksonMessageBodyProvider extends JacksonMessageBodyProvider { + public DefaultJacksonMessageBodyProvider() { + super(Jackson.newObjectMapper()); + } +} + diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/FailingExample.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/FailingExample.java new file mode 100644 index 00000000000..3e9361ecc1f --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/FailingExample.java @@ -0,0 +1,10 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.validation.ValidationMethod; + +public class FailingExample { + @ValidationMethod(message = "must have a false thing") + public boolean isFail() { + return false; + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ParamValidatorUnwrapperTest.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ParamValidatorUnwrapperTest.java new file mode 100644 index 00000000000..bb979f14557 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ParamValidatorUnwrapperTest.java @@ -0,0 +1,52 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.jersey.params.IntParam; +import io.dropwizard.jersey.params.NonEmptyStringParam; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.valuehandling.UnwrapValidatedValue; +import org.junit.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import javax.validation.constraints.Min; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ParamValidatorUnwrapperTest { + + public static class Example { + @Min(3) + @UnwrapValidatedValue + public IntParam inter = new IntParam("4"); + + @Length(max = 3) + @UnwrapValidatedValue + public NonEmptyStringParam name = new NonEmptyStringParam("a"); + } + + private final Validator validator = Validators.newValidator(); + + @Test + public void succeedsWithAllGoodData() { + final Example example = new Example(); + final Set> validate = validator.validate(example); + assertThat(validate).isEmpty(); + } + + @Test + public void failsWithInvalidIntParam() { + final Example example = new Example(); + example.inter = new IntParam("2"); + final Set> validate = validator.validate(example); + assertThat(validate).hasSize(1); + } + + @Test + public void failsWithInvalidNonEmptyStringParam() { + final Example example = new Example(); + example.name = new NonEmptyStringParam("hello"); + final Set> validate = validator.validate(example); + assertThat(validate).hasSize(1); + } +} \ No newline at end of file diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/SubBeanParameter.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/SubBeanParameter.java new file mode 100644 index 00000000000..07e1daaad4a --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/SubBeanParameter.java @@ -0,0 +1,26 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest; +import io.dropwizard.validation.ValidationMethod; +import org.assertj.core.util.Strings; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.ws.rs.QueryParam; +import java.util.Locale; + +public class SubBeanParameter extends BeanParameter { + @QueryParam("address") + @NotEmpty + private String address; + + + @ValidationMethod(message = "address must not be uppercase", + groups = JacksonMessageBodyProviderTest.Partial1.class) + public boolean isAddressNotUppercase() { + return Strings.isNullOrEmpty(address) || (!address.toUpperCase(Locale.US).equals(address)); + } + + public String getAddress() { + return address; + } +} 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 00000000000..2628c202e24 --- /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 00000000000..86def480216 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/ValidatingResource.java @@ -0,0 +1,250 @@ +package io.dropwizard.jersey.validation; + +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.Example; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.ListExample; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.Partial1; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.Partial2; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProviderTest.PartialExample; +import io.dropwizard.jersey.params.IntParam; +import io.dropwizard.jersey.params.NonEmptyStringParam; +import io.dropwizard.validation.Validated; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.Length; +import org.hibernate.validator.constraints.NotEmpty; +import org.hibernate.validator.valuehandling.UnwrapValidatedValue; + +import javax.servlet.ServletContext; +import javax.validation.Valid; +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.ws.rs.BeanParam; +import javax.ws.rs.Consumes; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.MatrixParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Path("/valid/") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class ValidatingResource { + + @QueryParam("sort") + @Pattern(regexp = "^(asc|desc)$") + private String sortParam; + + @POST + @Path("foo") + @Valid + public ValidRepresentation blah(@NotNull @Valid ValidRepresentation representation, @QueryParam("somethingelse") String xer) { + return new ValidRepresentation(); + } + + @POST + @Path("fooValidated") + @Validated + @Valid + public ValidRepresentation blahValidated(@Validated @Valid ValidRepresentation representation) { + return new ValidRepresentation(); + } + + @POST + @Path("simpleEntity") + public String simpleEntity(@Length(min = 3, max = 5) String name) { + return name; + } + + @GET + @Path("bar") + @Length(max = 3) + public String blaze(@QueryParam("name") @Length(min = 3) String name) { + return name; + } + + @GET + @Path("barter") + public String isnt(@QueryParam("name") @Length(min = 3) @UnwrapValidatedValue NonEmptyStringParam name) { + return name.get().orElse(null); + } + + @POST + @Path("validatedPartialExampleBoth") + public PartialExample validatedPartialExampleBoth( + @Validated({Partial1.class, Partial2.class}) @Valid PartialExample obj) { + return obj; + } + + @POST + @Path("validExample") + public Example validExample(@NotNull @Valid Example obj) { + return obj; + } + + @POST + @Path("validExampleArray") + public Example[] validExample(@Valid Example[] obj) { + return obj; + } + + @POST + @Path("validExampleCollection") + public Collection validExample(@Valid Collection obj) { + return obj; + } + + @POST + @Path("validExampleMap") + public Map validExample(@Valid Map obj) { + return obj; + } + + @POST + @Path("validExampleSet") + public Set validExample(@Valid Set obj) { + return obj; + } + + @POST + @Path("validExampleList") + public List validExample(@Valid List obj) { + return obj; + } + + @POST + @Path("validatedPartialExample") + public PartialExample validatedPartialExample( + @Validated({Partial1.class}) @Valid PartialExample obj) { + return obj; + } + + @POST + @Path("validExampleEmbeddedList") + public List validExampleEmbedded(@Valid List obj) { + return obj; + } + + @GET + @Path("fhqwhgads") + public String everybody(@QueryParam("num") @Min(3L) @NotNull Long param) { + return param.toString(); + } + + @GET + @Path("zoo") + public String blazer(@Valid @BeanParam BeanParameter params) { + return params.getName(); + } + + @GET + @Path("sub-zoo") + public String subBlazer(@Valid @BeanParam SubBeanParameter params) { + return params.getName() + " " + params.getAddress(); + } + + @GET + @Path("sub-group-zoo") + public String subGroupBlazer(@Valid @Validated(Partial1.class) @BeanParam SubBeanParameter params) { + return params.getName() + " " + params.getAddress(); + } + + @POST + @Path("sub-valid-group-zoo") + public String subValidGroupBlazer( + @Valid @Validated(Partial1.class) @BeanParam SubBeanParameter params, + @Valid @Validated(Partial1.class) ValidRepresentation entity) { + return params.getName() + " " + params.getAddress() + " " + entity.getName(); + } + + @GET + @Path("zoo2") + public String blazerValidated(@Validated @Valid @BeanParam BeanParameter params) { + return params.getName(); + } + + @GET + @Path("head") + public String heads(@HeaderParam("cheese") @NotEmpty String secretSauce) { + return secretSauce; + } + + @GET + @Path("headCopy") + public String heads(@QueryParam("cheese") @NotNull @UnwrapValidatedValue(false) IntParam secretSauce) { + return secretSauce.get().toString(); + } + + @GET + @Path("nullable-int-param") + public String nullableIntParam(@QueryParam("num") @Max(3) IntParam secretSauce) { + return secretSauce == null ? "I was null" : secretSauce.get().toString(); + } + + @GET + @Path("cooks") + public String cooks(@CookieParam("user_id") @NotEmpty String secretSauce) { + return secretSauce; + } + + @GET + @Path("goods/{id}") + public String pather(@PathParam("id") @Email String is) { + return is; + } + + @POST + @Path("form") + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public String form(@FormParam("username") @NotEmpty String secretSauce) { + return secretSauce; + } + + @GET + @Path("nested") + @Valid + public WrappedValidRepresentation nested() { + WrappedValidRepresentation result = new WrappedValidRepresentation(); + result.setRepresentation(new ValidRepresentation()); + return result; + } + + @GET + @Path("nested2") + @Valid + public WrappedFailingExample nested2() { + WrappedFailingExample result = new WrappedFailingExample(); + result.setExample(new FailingExample()); + return result; + } + + @GET + @Path("context") + public String contextual(@Valid @Context @NotNull ServletContext con) { + return "A"; + } + + @GET + @Path("matrix") + public String matrixParam(@MatrixParam("bob") @NotEmpty String param) { + return param; + } + + @POST + @Path("nothing") + public FailingExample valmeth(@Valid FailingExample exam) { + return exam; + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/WrappedFailingExample.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/WrappedFailingExample.java new file mode 100644 index 00000000000..d08f4d08148 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/WrappedFailingExample.java @@ -0,0 +1,20 @@ +package io.dropwizard.jersey.validation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; + +public class WrappedFailingExample { + @Valid + private FailingExample example; + + @JsonProperty + public FailingExample getExample() { + return example; + } + + @JsonProperty + public void setExample(FailingExample example) { + this.example = example; + } +} diff --git a/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/WrappedValidRepresentation.java b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/WrappedValidRepresentation.java new file mode 100644 index 00000000000..58f344ffb63 --- /dev/null +++ b/dropwizard-jersey/src/test/java/io/dropwizard/jersey/validation/WrappedValidRepresentation.java @@ -0,0 +1,20 @@ +package io.dropwizard.jersey.validation; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import javax.validation.Valid; + +public class WrappedValidRepresentation { + @Valid + private ValidRepresentation representation; + + @JsonProperty + public ValidRepresentation getRepresentation() { + return representation; + } + + @JsonProperty + public void setRepresentation(ValidRepresentation representation) { + this.representation = representation; + } +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-jersey/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-jetty/pom.xml b/dropwizard-jetty/pom.xml new file mode 100644 index 00000000000..84724f6f5d4 --- /dev/null +++ b/dropwizard-jetty/pom.xml @@ -0,0 +1,82 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-jetty + Dropwizard Jetty Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-logging + + + + javax.servlet + javax.servlet-api + provided + + + io.dropwizard.metrics + metrics-jetty9 + + + org.eclipse.jetty + jetty-server + + + org.eclipse.jetty + jetty-servlet + + + org.eclipse.jetty + jetty-servlets + + + org.eclipse.jetty + jetty-http + + + + + org.eclipse.jetty + jetty-http + tests + test + + + org.eclipse.jetty + jetty-servlet + tests + test + + + + io.dropwizard + dropwizard-configuration + test + + + diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/BiDiGzipHandler.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/BiDiGzipHandler.java new file mode 100644 index 00000000000..3985e03cbf5 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/BiDiGzipHandler.java @@ -0,0 +1,281 @@ +package io.dropwizard.jetty; + +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.gzip.GzipHandler; + +import javax.servlet.ReadListener; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Enumeration; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +/** + * An extension of {@link GzipHandler} which decompresses gzip- and deflate-encoded request + * entities. + */ +public class BiDiGzipHandler extends GzipHandler { + + private final ThreadLocal localInflater = new ThreadLocal<>(); + + /** + * Size of the buffer for decompressing requests + */ + private int inputBufferSize = 8192; + + /** + * Whether inflating (decompressing) of deflate-encoded requests + * should be performed in the GZIP-compatible mode + */ + private boolean inflateNoWrap = true; + + public boolean isInflateNoWrap() { + return inflateNoWrap; + } + + public void setInflateNoWrap(boolean inflateNoWrap) { + this.inflateNoWrap = inflateNoWrap; + } + + public BiDiGzipHandler() { + } + + public void setInputBufferSize(int inputBufferSize) { + this.inputBufferSize = inputBufferSize; + } + + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException { + final String encoding = request.getHeader(HttpHeader.CONTENT_ENCODING.asString()); + if (GZIP.equalsIgnoreCase(encoding)) { + super.handle(target, baseRequest, wrapGzippedRequest(removeContentEncodingHeader(request)), response); + } else if (DEFLATE.equalsIgnoreCase(encoding)) { + super.handle(target, baseRequest, wrapDeflatedRequest(removeContentEncodingHeader(request)), response); + } else { + super.handle(target, baseRequest, request, response); + } + } + + private Inflater buildInflater() { + final Inflater inflater = localInflater.get(); + if (inflater != null) { + // The request could fail in the middle of decompressing, so potentially we can get + // a broken inflater in the thread local storage. That's why we need to clear the storage. + localInflater.set(null); + + // Reuse the inflater from the thread local storage + inflater.reset(); + return inflater; + } else { + return new Inflater(inflateNoWrap); + } + } + + private WrappedServletRequest wrapDeflatedRequest(HttpServletRequest request) throws IOException { + final Inflater inflater = buildInflater(); + final InflaterInputStream input = new InflaterInputStream(request.getInputStream(), inflater, inputBufferSize) { + @Override + public void close() throws IOException { + super.close(); + localInflater.set(inflater); + } + }; + return new WrappedServletRequest(request, input); + } + + private WrappedServletRequest wrapGzippedRequest(HttpServletRequest request) throws IOException { + return new WrappedServletRequest(request, new GZIPInputStream(request.getInputStream(), inputBufferSize)); + } + + private HttpServletRequest removeContentEncodingHeader(final HttpServletRequest request) { + return new RemoveHttpHeaderWrapper(request, HttpHeader.CONTENT_ENCODING.asString()); + } + + 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 StandardCharsets.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); + } + + @Override + public boolean isFinished() { + try { + return input.available() == 0; + } catch (IOException ignored) { + } + return true; + } + + @Override + public boolean isReady() { + try { + return input.available() > 0; + } catch (IOException ignored) { + } + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException(); + } + } + + private static class RemoveHttpHeaderWrapper extends HttpServletRequestWrapper { + private final String headerName; + + RemoveHttpHeaderWrapper(final HttpServletRequest request, final String headerName) { + super(request); + this.headerName = headerName; + } + + /** + * The default behavior of this method is to return + * getIntHeader(String name) on the wrapped request object. + * + * @param name a String specifying the name of a request header + */ + @Override + public int getIntHeader(final String name) { + if (headerName.equalsIgnoreCase(name)) { + return -1; + } else { + return super.getIntHeader(name); + } + } + + /** + * The default behavior of this method is to return getHeaders(String name) + * on the wrapped request object. + * + * @param name a String specifying the name of a request header + */ + @Override + public Enumeration getHeaders(final String name) { + if (headerName.equalsIgnoreCase(name)) { + return Collections.emptyEnumeration(); + } else { + return super.getHeaders(name); + } + } + + /** + * The default behavior of this method is to return getHeader(String name) + * on the wrapped request object. + * + * @param name a String specifying the name of a request header + */ + @Override + public String getHeader(final String name) { + if (headerName.equalsIgnoreCase(name)) { + return null; + } else { + return super.getHeader(name); + } + } + + /** + * The default behavior of this method is to return getDateHeader(String name) + * on the wrapped request object. + * + * @param name a String specifying the name of a request header + */ + @Override + public long getDateHeader(final String name) { + if (headerName.equalsIgnoreCase(name)) { + return -1L; + } else { + return super.getDateHeader(name); + } + } + } +} 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 00000000000..27c9e93a35b --- /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. + */ +@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 00000000000..19f3deeec11 --- /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 handlers; + + public ContextRoutingHandler(Map handlers) { + this.handlers = new ArrayTernaryTrie<>(false); + for (Map.Entry 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/GzipHandlerFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/GzipHandlerFactory.java new file mode 100644 index 00000000000..d02c818bbf3 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/GzipHandlerFactory.java @@ -0,0 +1,206 @@ +package io.dropwizard.jetty; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.Iterables; +import io.dropwizard.util.Size; +import org.eclipse.jetty.server.Handler; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.HashSet; +import java.util.Set; +import java.util.zip.Deflater; + +import static java.util.Objects.requireNonNull; + +/** + * Builds GZIP filters. + * + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code enabled}trueIf true, all requests with `gzip` or `deflate` in the `Accept-Encoding` header will have their + * response entities compressed and requests with `gzip` or `deflate` in the `Content-Encoding` + * header will have their request entities decompressed.
    {@code minimumEntitySize}256 bytesAll response entities under this size are not compressed.
    {@code bufferSize}8KiBThe size of the buffer to use when compressing.
    {@code excludedUserAgentPatterns}(none)The set of user agent patterns to exclude from compression.
    {@code compressedMimeTypes}(Jetty's default)The list of mime types to compress. The default is all types apart the + * commonly known image, video, audio and compressed types.
    {@code includedMethods}(Jetty's default)The list list of HTTP methods to compress. The default is to compress + * only GET responses.
    {@code deflateCompressionLevel}-1The compression level used for ZLIB deflation(compression).
    {@code gzipCompatibleInflation}trueIf true, then ZLIB inflation(decompression) will be performed in the GZIP-compatible mode.
    + */ +public class GzipHandlerFactory { + + private boolean enabled = true; + + @NotNull + private Size minimumEntitySize = Size.bytes(256); + + @NotNull + private Size bufferSize = Size.kilobytes(8); + + // By default compress responses for all user-agents + private Set excludedUserAgentPatterns = new HashSet<>(); + private Set compressedMimeTypes; + private Set includedMethods; + + @Min(Deflater.DEFAULT_COMPRESSION) + @Max(Deflater.BEST_COMPRESSION) + private int deflateCompressionLevel = Deflater.DEFAULT_COMPRESSION; + + private boolean gzipCompatibleInflation = true; + + private boolean syncFlush = false; + + @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 = requireNonNull(size); + } + + @JsonProperty + public Size getBufferSize() { + return bufferSize; + } + + @JsonProperty + public void setBufferSize(Size size) { + this.bufferSize = requireNonNull(size); + } + + @JsonProperty + public Set getCompressedMimeTypes() { + return compressedMimeTypes; + } + + @JsonProperty + public void setCompressedMimeTypes(Set mimeTypes) { + this.compressedMimeTypes = mimeTypes; + } + + @JsonProperty + public int getDeflateCompressionLevel() { + return deflateCompressionLevel; + } + + @JsonProperty + public void setDeflateCompressionLevel(int level) { + this.deflateCompressionLevel = level; + } + + @JsonProperty + public boolean isGzipCompatibleInflation() { + return gzipCompatibleInflation; + } + + @JsonProperty + public void setGzipCompatibleInflation(boolean gzipCompatibleInflation) { + this.gzipCompatibleInflation = gzipCompatibleInflation; + } + + public Set getExcludedUserAgentPatterns() { + return excludedUserAgentPatterns; + } + + public void setExcludedUserAgentPatterns(Set excludedUserAgentPatterns) { + this.excludedUserAgentPatterns = excludedUserAgentPatterns; + } + + @JsonProperty + public Set getIncludedMethods() { + return includedMethods; + } + + @JsonProperty + public void setIncludedMethods(Set methods) { + this.includedMethods = methods; + } + + @JsonProperty + public boolean isSyncFlush() { + return syncFlush; + } + + @JsonProperty + public void setSyncFlush(boolean syncFlush) { + this.syncFlush = syncFlush; + } + + public BiDiGzipHandler build(Handler handler) { + final BiDiGzipHandler gzipHandler = new BiDiGzipHandler(); + gzipHandler.setHandler(handler); + gzipHandler.setMinGzipSize((int) minimumEntitySize.toBytes()); + gzipHandler.setInputBufferSize((int) bufferSize.toBytes()); + gzipHandler.setCompressionLevel(deflateCompressionLevel); + gzipHandler.setSyncFlush(syncFlush); + + if (compressedMimeTypes != null) { + gzipHandler.setIncludedMimeTypes(Iterables.toArray(compressedMimeTypes, String.class)); + } + + if (includedMethods != null) { + gzipHandler.setIncludedMethods(Iterables.toArray(includedMethods, String.class)); + } + + gzipHandler.setExcludedAgentPatterns(Iterables.toArray(excludedUserAgentPatterns, String.class)); + gzipHandler.setInflateNoWrap(gzipCompatibleInflation); + + return gzipHandler; + } +} 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 00000000000..fdd4b3fb469 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpConnectorFactory.java @@ -0,0 +1,538 @@ +package io.dropwizard.jetty; + +import com.codahale.metrics.MetricRegistry; +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.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ForwardedRequestCustomizer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +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. + * + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code port}8080The TCP/IP port on which to listen for incoming connections.
    {@code bindHost}(none)The hostname to bind to.
    {@code inheritChannel}false + * Whether this connector uses a channel inherited from the JVM. + * Use it with Server::Starter, + * to launch an instance of Jetty on demand. + *
    {@code headerCacheSize}512 bytesThe size of the header field cache.
    {@code 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. + *
    {@code 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. + *
    {@code 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. + *
    {@code inputBufferSize}8KiBThe size of the per-connection input buffer.
    {@code idleTimeout}30 seconds + * 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. + *

    + * The max idle time is applied: + *

      + *
    • When waiting for a new message to be received on a connection
    • + *
    • 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. + *

    {@code minBufferPoolSize}64 bytesThe minimum size of the buffer pool.
    {@code bufferPoolIncrement}1KiBThe increment by which the buffer pool should be increased.
    {@code maxBufferPoolSize}64KiBThe maximum size of the buffer pool.
    {@code acceptorThreads}half the # of CPUsThe number of worker threads dedicated to accepting connections.
    {@code selectorThreads}the # of CPUsThe number of worker threads dedicated to sending and receiving data.
    {@code acceptQueueSize}(OS default)The size of the TCP/IP accept queue for the listening socket.
    {@code reuseAddress}trueWhether or not {@code SO_REUSEADDR} is enabled on the listening socket.
    {@code soLingerTime}(disabled)Enable/disable {@code SO_LINGER} with the specified linger time.
    {@code useServerHeader}falseWhether or not to add the {@code Server} header to each response.
    {@code useDateHeader}trueWhether or not to add the {@code Date} header to each response.
    {@code useForwardedHeaders}true + * Whether or not to look at {@code X-Forwarded-*} headers added by proxies. See + * {@link ForwardedRequestCustomizer} for details. + *
    + */ +@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; + + private boolean inheritChannel = false; + + @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 boolean isInheritChannel() { + return inheritChannel; + } + + @JsonProperty + public void setInheritChannel(boolean inheritChannel) { + this.inheritChannel = inheritChannel; + } + + @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(); + + return buildConnector(server, scheduler, bufferPool, name, threadPool, + new Jetty93InstrumentedConnectionFactory(httpConnectionFactory, + metrics.timer(httpConnections()))); + } + + /** + * Get name of the timer that tracks incoming HTTP connections + */ + protected String httpConnections() { + return name(HttpConnectionFactory.class, bindHost, Integer.toString(port), "connections"); + } + + 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); + connector.setInheritChannel(inheritChannel); + if (acceptQueueSize != null) { + connector.setAcceptQueueSize(acceptQueueSize); + } else { + // if we do not set the acceptQueueSize, when jetty + // creates the ServerSocket, it uses the default backlog of 50, and + // not the value from the OS. Therefore we set to the value + // obtained from NetUtil, which will attempt to read the value from the OS. + // somaxconn setting + connector.setAcceptQueueSize(NetUtil.getTcpBacklog()); + } + + 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 00000000000..1f2d6e20af3 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/HttpsConnectorFactory.java @@ -0,0 +1,715 @@ +package io.dropwizard.jetty; + +import com.codahale.metrics.MetricRegistry; +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 io.dropwizard.validation.ValidationMethod; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.LifeCycle; +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 javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import java.io.File; +import java.net.URI; +import java.security.KeyStore; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Builds HTTPS connectors (HTTP over TLS/SSL). + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code keyStorePath}REQUIRED + * The path to the Java key store which contains the host certificate and private key. + *
    {@code keyStorePassword}REQUIRED + * The password used to access the key store. + *
    {@code keyStoreType}{@code JKS} + * The type of key store (usually {@code JKS}, {@code PKCS12}, {@code JCEKS}, + * {@code Windows-MY}, or {@code Windows-ROOT}). + *
    {@code keyStoreProvider}(none) + * The JCE provider to use to access the key store. + *
    {@code trustStorePath}(none) + * The path to the Java key store which contains the CA certificates used to establish + * trust. + *
    {@code trustStorePassword}(none)The password used to access the trust store.
    {@code trustStoreType}{@code JKS} + * The type of trust store (usually {@code JKS}, {@code PKCS12}, {@code JCEKS}, + * {@code Windows-MY}, or {@code Windows-ROOT}). + *
    {@code trustStoreProvider}(none) + * The JCE provider to use to access the trust store. + *
    {@code keyManagerPassword}(none)The password, if any, for the key manager.
    {@code needClientAuth}(none)Whether or not client authentication is required.
    {@code wantClientAuth}(none)Whether or not client authentication is requested.
    {@code certAlias}(none)The alias of the certificate to use.
    {@code crlPath}(none)The path to the file which contains the Certificate Revocation List.
    {@code enableCRLDP}falseWhether or not CRL Distribution Points (CRLDP) support is enabled.
    {@code enableOCSP}falseWhether or not On-Line Certificate Status Protocol (OCSP) support is enabled.
    {@code maxCertPathLength}(unlimited)The maximum certification path length.
    {@code ocspResponderUrl}(none)The location of the OCSP responder.
    {@code jceProvider}(none)The name of the JCE provider to use for cryptographic support.
    {@code validateCerts}true + * Whether or not to validate TLS certificates before starting. If enabled, Dropwizard + * will refuse to start with expired or otherwise invalid certificates. + *
    {@code validatePeers}trueWhether or not to validate TLS peer certificates.
    {@code supportedProtocols}(none) + * A list of protocols (e.g., {@code SSLv3}, {@code TLSv1}) which are supported. All + * other protocols will be refused. + *
    {@code excludedProtocols}(none) + * A list of protocols (e.g., {@code SSLv3}, {@code TLSv1}) which are excluded. These + * protocols will be refused. + *
    {@code supportedCipherSuites}(none) + * 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 + *
    {@code excludedCipherSuites}(none) + * 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. + *
    {@code allowRenegotiation}trueWhether or not TLS renegotiation is allowed.
    {@code endpointIdentificationAlgorithm}(none) + * Which endpoint identification algorithm, if any, to use during the TLS handshake. + *
    + *

    + * For more configuration parameters, see {@link HttpConnectorFactory}. + * + * @see HttpConnectorFactory + */ +@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 supportedProtocols; + private List excludedProtocols; + private List supportedCipherSuites; + private List 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 getSupportedProtocols() { + return supportedProtocols; + } + + @JsonProperty + public void setSupportedProtocols(List supportedProtocols) { + this.supportedProtocols = supportedProtocols; + } + + @JsonProperty + public List getExcludedProtocols() { + return excludedProtocols; + } + + @JsonProperty + public void setExcludedProtocols(List excludedProtocols) { + this.excludedProtocols = excludedProtocols; + } + + @JsonProperty + public List getSupportedCipherSuites() { + return supportedCipherSuites; + } + + @JsonProperty + public List getExcludedCipherSuites() { + return excludedCipherSuites; + } + + @JsonProperty + public void setExcludedCipherSuites(List excludedCipherSuites) { + this.excludedCipherSuites = excludedCipherSuites; + } + + @JsonProperty + public void setSupportedCipherSuites(List 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) { + final HttpConfiguration httpConfig = buildHttpConfiguration(); + + final HttpConnectionFactory httpConnectionFactory = buildHttpConnectionFactory(httpConfig); + + final SslContextFactory sslContextFactory = buildSslContextFactory(); + sslContextFactory.addLifeCycleListener(logSslInfoOnStart(sslContextFactory)); + + server.addBean(sslContextFactory); + + final SslConnectionFactory sslConnectionFactory = + new SslConnectionFactory(sslContextFactory, HttpVersion.HTTP_1_1.toString()); + + final Scheduler scheduler = new ScheduledExecutorScheduler(); + + final ByteBufferPool bufferPool = buildBufferPool(); + + return buildConnector(server, scheduler, bufferPool, name, threadPool, + new Jetty93InstrumentedConnectionFactory( + sslConnectionFactory, + metrics.timer(httpConnections())), + httpConnectionFactory); + } + + @Override + protected HttpConfiguration buildHttpConfiguration() { + final HttpConfiguration config = super.buildHttpConfiguration(); + config.setSecureScheme("https"); + config.setSecurePort(getPort()); + config.addCustomizer(new SecureRequestCustomizer()); + return config; + } + + /** Register a listener that waits until the ssl context factory has started. Once it has + * started we can grab the fully initialized context so we can log the parameters. + */ + protected AbstractLifeCycle.AbstractLifeCycleListener logSslInfoOnStart(final SslContextFactory sslContextFactory) { + return new AbstractLifeCycle.AbstractLifeCycleListener() { + @Override + public void lifeCycleStarted(LifeCycle event) { + logSupportedParameters(sslContextFactory.getSslContext()); + } + }; + } + + private void logSupportedParameters(SSLContext context) { + if (LOGGED.compareAndSet(false, true)) { + 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)); + + if (getSupportedProtocols() != null) { + LOGGER.info("Configured protocols: {}", getSupportedProtocols()); + } + + if (getExcludedProtocols() != null) { + LOGGER.info("Excluded protocols: {}", getExcludedProtocols()); + } + + if (getSupportedCipherSuites() != null) { + LOGGER.info("Configured cipher suites: {}", getSupportedCipherSuites()); + } + + if (getExcludedCipherSuites() != null) { + LOGGER.info("Excluded cipher suites: {}", getExcludedCipherSuites()); + } + } + } + + protected SslContextFactory buildSslContextFactory() { + final SslContextFactory factory = new SslContextFactory(); + if (keyStorePath != null) { + factory.setKeyStorePath(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); + + factory.setValidateCerts(validateCerts); + factory.setValidatePeerCerts(validatePeers); + + if (supportedProtocols != null) { + factory.setIncludeProtocols(Iterables.toArray(supportedProtocols, String.class)); + } + + if (excludedProtocols != null) { + factory.setExcludeProtocols(Iterables.toArray(excludedProtocols, 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/Jetty93InstrumentedConnectionFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/Jetty93InstrumentedConnectionFactory.java new file mode 100644 index 00000000000..1458122da17 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/Jetty93InstrumentedConnectionFactory.java @@ -0,0 +1,63 @@ +package io.dropwizard.jetty; + +import com.codahale.metrics.Timer; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.util.component.ContainerLifeCycle; + +import java.util.List; + +/** + * A version {@link com.codahale.metrics.jetty9.InstrumentedConnectionFactory}, which supports Jetty 9.3 API. + * NOTE: This class could be replaced, when dropwizard-metrics-jetty9 will support Jetty 9.3. + */ +public class Jetty93InstrumentedConnectionFactory extends ContainerLifeCycle implements ConnectionFactory { + + private final ConnectionFactory connectionFactory; + private final Timer timer; + + public Jetty93InstrumentedConnectionFactory(ConnectionFactory connectionFactory, Timer timer) { + this.connectionFactory = connectionFactory; + this.timer = timer; + addBean(connectionFactory); + } + + public ConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + public Timer getTimer() { + return timer; + } + + @Override + public String getProtocol() { + return connectionFactory.getProtocol(); + } + + @Override + public List getProtocols() { + return connectionFactory.getProtocols(); + } + + @Override + public Connection newConnection(Connector connector, EndPoint endPoint) { + final Connection connection = connectionFactory.newConnection(connector, endPoint); + connection.addListener(new Connection.Listener() { + private Timer.Context context; + + @Override + public void onOpened(Connection connection) { + this.context = timer.time(); + } + + @Override + public void onClosed(Connection connection) { + context.stop(); + } + }); + return connection; + } +} diff --git a/dropwizard-jetty/src/main/java/io/dropwizard/jetty/LocalIpFilter.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/LocalIpFilter.java new file mode 100644 index 00000000000..a3825397c2e --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/LocalIpFilter.java @@ -0,0 +1,29 @@ +/** + * Copyright 2013-2014 The Apache Software Foundation (Curator Project) + * + * The Apache Software Foundation licenses this file to you under the Apache + * License, version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.dropwizard.jetty; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; + +/** + * @see https://github.com/apache/curator/blob/master/curator-x-discovery/src/main/java/org/apache/curator/x/discovery/LocalIpFilter.java + */ +public interface LocalIpFilter { + + public boolean use(NetworkInterface networkInterface, + InetAddress address) throws SocketException; +} \ No newline at end of file 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 00000000000..1e23b70b1fe --- /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/NetUtil.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/NetUtil.java new file mode 100644 index 00000000000..337b609a154 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/NetUtil.java @@ -0,0 +1,155 @@ +/* + * Copyright 2012 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.dropwizard.jetty; + +import com.google.common.io.Files; +import java.io.File; +import java.io.IOException; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicReference; + +/** + * This class is taken from the Netty project, and all credit goes to them. + * It has been modified, to remove dependencies on other classes, and to convert to methods, rather than a + * static value. + * + * The {@link #getAllLocalIPs()} method was taken from the Apache Curator project + * which is also under the Apache 2.0 license. + */ +public class NetUtil { + public static final int DEFAULT_TCP_BACKLOG_WINDOWS = 200; + public static final int DEFAULT_TCP_BACKLOG_LINUX = 128; + public static final String TCP_BACKLOG_SETTING_LOCATION = "/proc/sys/net/core/somaxconn"; + + private static final AtomicReference localIpFilter = new AtomicReference((nif, adr) -> + (adr != null) && !adr.isLoopbackAddress() && (nif.isPointToPoint() || !adr.isLinkLocalAddress()) + ); + + /** + * The SOMAXCONN value of the current machine. If failed to get the value, {@code 200} is used as a + * default value for Windows or {@code 128} for others. + */ + public static int getTcpBacklog() { + return getTcpBacklog(getDefaultTcpBacklog()); + } + + /** + * The SOMAXCONN value of the current machine. If failed to get the value, defaultBacklog argument is + * used + */ + public static int getTcpBacklog(int tcpBacklog) { + // Taken from netty. + + // As a SecurityManager may prevent reading the somaxconn file we wrap this in a privileged block. + // + // See https://github.com/netty/netty/issues/3680 + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Integer run() { + // Determine the default somaxconn (server socket backlog) value of the platform. + // The known defaults: + // - Windows NT Server 4.0+: 200 + // - Linux and Mac OS X: 128 + try { + String setting = Files.toString(new File(TCP_BACKLOG_SETTING_LOCATION), StandardCharsets.UTF_8); + return Integer.parseInt(setting.trim()); + } catch (SecurityException | IOException | NumberFormatException | NullPointerException e) { + return tcpBacklog; + } + } + }); + + } + + public static boolean isWindows() { + final boolean windows = System.getProperty("os.name", "").toLowerCase(Locale.US).contains("win"); + return windows; + } + + public static int getDefaultTcpBacklog() { + return isWindows() ? DEFAULT_TCP_BACKLOG_WINDOWS : DEFAULT_TCP_BACKLOG_LINUX; + } + + /** + * Replace the default local ip filter used by {@link #getAllLocalIPs()} + * + * @param newLocalIpFilter the new local ip filter + */ + public static void setLocalIpFilter(LocalIpFilter newLocalIpFilter) { + localIpFilter.set(newLocalIpFilter); + } + + /** + * Return the current local ip filter used by {@link #getAllLocalIPs()} + * + * @return ip filter + */ + public static LocalIpFilter getLocalIpFilter() { + return localIpFilter.get(); + } + + /** + * based on http://pastebin.com/5X073pUc + *

    + * + * Returns all available IP addresses. + *

    + * In error case or if no network connection is established, we return + * an empty list here. + *

    + * Loopback addresses are excluded - so 127.0.0.1 will not be never + * returned. + *

    + * The "primary" IP might not be the first one in the returned list. + * + * @return Returns all IP addresses (can be an empty list in error case + * or if network connection is missing). + * @see getAllLocalIPs + * @see ServiceInstanceBuilder.java + * @throws SocketException errors + */ + public static Collection getAllLocalIPs() throws SocketException { + final List listAdr = new ArrayList<>(); + final Enumeration nifs = NetworkInterface.getNetworkInterfaces(); + if (nifs == null) { + return listAdr; + } + + while (nifs.hasMoreElements()) { + final NetworkInterface nif = nifs.nextElement(); + // We ignore subinterfaces - as not yet needed. + + final Enumeration adrs = nif.getInetAddresses(); + while (adrs.hasMoreElements()) { + final InetAddress adr = adrs.nextElement(); + if (localIpFilter.get().use(nif, adr)) { + listAdr.add(adr); + } + } + } + return listAdr; + } +} 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 00000000000..91401a7bf43 --- /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 synchronized 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, null); + } + try { + servlet.service(request, response); + } finally { + baseRequest.setAsyncSupported(asyncSupported, null); + } + } +} 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 00000000000..4281ec6af7d --- /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 { + private final Connector connector; + private final Handler handler; + + private Entry(Connector connector, Handler handler) { + this.connector = connector; + this.handler = handler; + } + } + + private final Entry[] entries; + + public RoutingHandler(Map handlers) { + this.entries = new Entry[handlers.size()]; + int i = 0; + for (Map.Entry 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/ServerPushFilterFactory.java b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ServerPushFilterFactory.java new file mode 100644 index 00000000000..438b1034ac8 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/ServerPushFilterFactory.java @@ -0,0 +1,155 @@ +package io.dropwizard.jetty; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Joiner; +import io.dropwizard.util.Duration; +import io.dropwizard.validation.MinDuration; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlets.PushCacheFilter; + +import javax.annotation.Nullable; +import javax.servlet.DispatcherType; +import javax.validation.constraints.Min; +import java.util.EnumSet; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * A factory for building HTTP/2 {@link PushCacheFilter}, + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code enabled}false + * If true, the filter will organize resources as primary resources (those referenced by the + * Referer header) and secondary resources (those that have the Referer header). + * Secondary resources that have been requested within a time window from the request of the + * primary resource will be associated with the it. The next time a client will + * request the primary resource, the server will send to the client the secondary resources + * along with the primary in a single response. + *
    {@code associatePeriod}4 seconds + * The time window within which a request for a secondary resource will be associated to a + * primary resource. + *
    {@code maxAssociations}16 + * The maximum number of secondary resources that may be associated to a primary resource. + *
    {@code refererHosts}All hosts + * The list of referrer hosts for which the server push technology is supported. + *
    {@code refererPorts}All ports + * The list of referrer ports for which the server push technology is supported. + *
    + */ +public class ServerPushFilterFactory { + + private static final Joiner COMMA_JOINER = Joiner.on(","); + + private boolean enabled = false; + + @MinDuration(value = 1, unit = TimeUnit.MILLISECONDS) + private Duration associatePeriod = Duration.seconds(4); + + @Min(1) + private int maxAssociations = 16; + + @Nullable + private List refererHosts; + + @Nullable + private List refererPorts; + + @JsonProperty + public boolean isEnabled() { + return enabled; + } + + @JsonProperty + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @JsonProperty + public Duration getAssociatePeriod() { + return associatePeriod; + } + + @JsonProperty + public void setAssociatePeriod(Duration associatePeriod) { + this.associatePeriod = associatePeriod; + } + + @JsonProperty + public int getMaxAssociations() { + return maxAssociations; + } + + @JsonProperty + public void setMaxAssociations(int maxAssociations) { + this.maxAssociations = maxAssociations; + } + + @Nullable + @JsonProperty + public List getRefererHosts() { + return refererHosts; + } + + @JsonProperty + public void setRefererHosts(@Nullable List refererHosts) { + this.refererHosts = refererHosts; + } + + @Nullable + @JsonProperty + public List getRefererPorts() { + return refererPorts; + } + + @JsonProperty + public void setRefererPorts(@Nullable List refererPorts) { + this.refererPorts = refererPorts; + } + + public void addFilter(ServletContextHandler handler) { + if (!enabled) { + return; + } + + handler.setInitParameter("associatePeriod", String.valueOf(associatePeriod.toMilliseconds())); + handler.setInitParameter("maxAssociations", String.valueOf(maxAssociations)); + if (refererHosts != null) { + handler.setInitParameter("hosts", COMMA_JOINER.join(refererHosts)); + } + if (refererPorts != null) { + handler.setInitParameter("ports", COMMA_JOINER.join(refererPorts)); + } + handler.addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + } +} 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 00000000000..0a2cd9052c4 --- /dev/null +++ b/dropwizard-jetty/src/main/java/io/dropwizard/jetty/setup/ServletEnvironment.java @@ -0,0 +1,216 @@ +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.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceCollection; +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.Arrays; +import java.util.EventListener; +import java.util.HashSet; +import java.util.Set; + +import static java.util.Objects.requireNonNull; + +public class ServletEnvironment { + private static final Logger LOGGER = LoggerFactory.getLogger(ServletEnvironment.class); + + private final MutableServletContextHandler handler; + + private final Set servlets = new HashSet<>(); + private final Set 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(requireNonNull(servlet)); + holder.setName(name); + handler.getServletHandler().addServlet(holder); + + final 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 klass) { + final ServletHolder holder = new ServletHolder(requireNonNull(klass)); + holder.setName(name); + handler.getServletHandler().addServlet(holder); + + final 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) { + return addFilter(name, new FilterHolder(requireNonNull(filter))); + } + + /** + * 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 klass) { + return addFilter(name, new FilterHolder(requireNonNull(klass))); + } + + private FilterRegistration.Dynamic addFilter(String name, FilterHolder holder) { + holder.setName(name); + handler.getServletHandler().addFilter(holder); + + final 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); + } + } + + /** + * Set protected targets. + * + * @param targets Array of URL prefix. Each prefix is in the form /path and + * will match either /path exactly or /path/anything + */ + public void setProtectedTargets(String... targets) { + handler.setProtectedTargets(Arrays.copyOf(targets, targets.length)); + } + + /** + * Sets the base resource for this context. + * + * @param baseResource The resource to be used as the base for all static content of this context. + */ + public void setBaseResource(Resource baseResource) { + handler.setBaseResource(baseResource); + } + + /** + * Sets the base resources for this context. + * + * @param baseResources The list of resources to be used as the base for all static + * content of this context. + */ + public void setBaseResource(Resource... baseResources) { + handler.setBaseResource(new ResourceCollection(baseResources)); + } + + /** + * Sets the base resources for this context. + * + * @param resources A list of strings representing the base resources to serve static + * content for the context. Any string accepted by Resource.newResource(String) + * may be passed and the call is equivalent to {@link #setBaseResource(Resource...)}} + */ + public void setBaseResource(String... resources) { + handler.setBaseResource(new ResourceCollection(resources)); + } + + /** + * Sets the base resource for this context. + * @param resourceBase A string representing the base resource for the context. Any + * string accepted by Resource.newResource(String) may be passed + * and the call is equivalent to {@link #setBaseResource(Resource)}} + */ + public void setResourceBase(String resourceBase) { + handler.setResourceBase(resourceBase); + } + + /** + * Set an initialization parameter. + * + * @param name Parameter name + * @param value Parameter value + */ + public void setInitParameter(String name, String value) { + handler.setInitParameter(name, value); + } + + /** + * Set the session handler. + * + * @param sessionHandler The sessionHandler to set. + */ + public void setSessionHandler(SessionHandler sessionHandler) { + handler.setSessionsEnabled(sessionHandler != null); + handler.setSessionHandler(sessionHandler); + } + + /** + * Set the security handler. + * + * @param securityHandler The securityHandler to set. + */ + public void setSecurityHandler(SecurityHandler securityHandler) { + handler.setSecurityEnabled(securityHandler != null); + handler.setSecurityHandler(securityHandler); + } + + /** + * Set a mime mapping. + * + * @param extension Extension + * @param type Mime type + */ + public void addMimeMapping(String extension, String type) { + handler.getMimeTypes().addMimeMapping(extension, type); + } + + private void checkDuplicateRegistration(String name, Set 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 00000000000..97e7b116796 --- /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 00000000000..78e0dbaf885 --- /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/BiDiGzipHandlerTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/BiDiGzipHandlerTest.java new file mode 100644 index 00000000000..18b9fb3f303 --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/BiDiGzipHandlerTest.java @@ -0,0 +1,150 @@ +package io.dropwizard.jetty; + +import com.google.common.io.ByteStreams; +import com.google.common.io.CharStreams; +import com.google.common.io.Resources; +import com.google.common.net.HttpHeaders; +import com.google.common.net.MediaType; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.servlet.ServletTester; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.Deflater; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BiDiGzipHandlerTest { + + private static final String PLAIN_TEXT_UTF_8 = MediaType.PLAIN_TEXT_UTF_8.toString().replace(" ", ""); + + private final BiDiGzipHandler gzipHandler = new BiDiGzipHandler(); + + private final ServletTester servletTester = new ServletTester(); + private final HttpTester.Request request = HttpTester.newRequest(); + + @Before + public void setUp() throws Exception { + request.setHeader(HttpHeaders.HOST, "localhost"); + + gzipHandler.setExcludedAgentPatterns(); + gzipHandler.addIncludedMethods("POST"); + + servletTester.addServlet(BannerServlet.class, "/banner"); + servletTester.getContext().setGzipHandler(gzipHandler); + servletTester.start(); + } + + @After + public void tearDown() throws Exception { + servletTester.stop(); + } + + @Test + public void testCompressResponse() throws Exception { + request.setMethod("GET"); + request.setURI("/banner"); + request.setHeader(HttpHeaders.ACCEPT_ENCODING, "gzip"); + + HttpTester.Response response = HttpTester.parseResponse(servletTester.getResponses(request.generate())); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeader.CONTENT_ENCODING)).isEqualTo("gzip"); + assertThat(response.get(HttpHeader.VARY)).isEqualTo(HttpHeaders.ACCEPT_ENCODING); + assertThat(response.get(HttpHeader.CONTENT_TYPE)).isEqualToIgnoringCase(PLAIN_TEXT_UTF_8); + try (GZIPInputStream is = new GZIPInputStream(new ByteArrayInputStream(response.getContentBytes()))) { + assertThat(ByteStreams.toByteArray(is)).isEqualTo( + Resources.toByteArray(Resources.getResource("assets/banner.txt"))); + } + } + + @Test + public void testDecompressRequest() throws Exception { + request.setMethod("POST"); + request.setURI("/banner"); + request.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip"); + request.setHeader(HttpHeaders.CONTENT_TYPE, PLAIN_TEXT_UTF_8); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (GZIPOutputStream gz = new GZIPOutputStream(baos)) { + Resources.copy(Resources.getResource("assets/new-banner.txt"), gz); + } + request.setContent(baos.toByteArray()); + + HttpTester.Response response = HttpTester.parseResponse(servletTester.getResponses(request.generate())); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContent()).isEqualTo("Banner has been updated"); + + } + + @Test + public void testDecompressDeflateRequestGzipIncompatible() throws Exception { + request.setMethod("POST"); + request.setURI("/banner"); + request.setHeader(HttpHeaders.CONTENT_ENCODING, "deflate"); + request.setHeader(HttpHeaders.CONTENT_TYPE, PLAIN_TEXT_UTF_8); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (DeflaterOutputStream deflate = new DeflaterOutputStream(baos)) { + Resources.copy(Resources.getResource("assets/new-banner.txt"), deflate); + } + request.setContent(baos.toByteArray()); + gzipHandler.setInflateNoWrap(false); + + HttpTester.Response response = HttpTester.parseResponse(servletTester.getResponses(request.generate())); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContent()).isEqualTo("Banner has been updated"); + } + + @Test + public void testDecompressDeflateRequestGzipCompatible() throws Exception { + request.setMethod("POST"); + request.setURI("/banner"); + request.setHeader(HttpHeaders.CONTENT_ENCODING, "deflate"); + request.setHeader(HttpHeaders.CONTENT_TYPE, PLAIN_TEXT_UTF_8); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try (DeflaterOutputStream deflate = new DeflaterOutputStream(baos, new Deflater(-1, true))) { + Resources.copy(Resources.getResource("assets/new-banner.txt"), deflate); + } + request.setContent(baos.toByteArray()); + + HttpTester.Response response = HttpTester.parseResponse(servletTester.getResponses(request.generate())); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.getContent()).isEqualTo("Banner has been updated"); + } + + public static class BannerServlet extends HttpServlet { + + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + resp.setCharacterEncoding(StandardCharsets.UTF_8.toString()); + resp.setContentType(PLAIN_TEXT_UTF_8); + Resources.asCharSource(Resources.getResource("assets/banner.txt"), StandardCharsets.UTF_8) + .copyTo(resp.getWriter()); + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + assertThat(req.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualToIgnoringCase(PLAIN_TEXT_UTF_8); + assertThat(req.getHeader(HttpHeaders.CONTENT_ENCODING)).isNull(); + assertThat(CharStreams.toString(req.getReader())).isEqualTo( + Resources.toString(Resources.getResource("assets/new-banner.txt"), StandardCharsets.UTF_8)); + + resp.setContentType(PLAIN_TEXT_UTF_8); + resp.getWriter().write("Banner has been updated"); + } + } +} 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 00000000000..b1efb176bf5 --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/ContextRoutingHandlerTest.java @@ -0,0 +1,76 @@ +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.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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/GzipHandlerFactoryTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/GzipHandlerFactoryTest.java new file mode 100644 index 00000000000..ee758fce239 --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/GzipHandlerFactoryTest.java @@ -0,0 +1,84 @@ +package io.dropwizard.jetty; + +import com.google.common.collect.ImmutableSet; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.util.Size; +import io.dropwizard.validation.BaseValidator; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.util.zip.Deflater; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GzipHandlerFactoryTest { + private GzipHandlerFactory gzip; + + @Before + public void setUp() throws Exception { + this.gzip = new YamlConfigurationFactory<>(GzipHandlerFactory.class, + BaseValidator.newValidator(), 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 hasExcludedUserAgentPatterns() throws Exception { + assertThat(gzip.getExcludedUserAgentPatterns()) + .isEqualTo(ImmutableSet.of("OLD-2.+")); + } + + @Test + public void hasCompressedMimeTypes() throws Exception { + assertThat(gzip.getCompressedMimeTypes()) + .isEqualTo(ImmutableSet.of("text/plain")); + } + + @Test + public void testBuild() { + final BiDiGzipHandler handler = gzip.build(null); + + assertThat(handler.getMinGzipSize()).isEqualTo((int) gzip.getMinimumEntitySize().toBytes()); + assertThat(handler.getExcludedAgentPatterns()).hasSize(1); + assertThat(handler.getExcludedAgentPatterns()[0]).isEqualTo("OLD-2.+"); + assertThat(handler.getIncludedMimeTypes()).containsOnly("text/plain"); + assertThat(handler.getIncludedMethods()).containsOnly("GET", "POST"); + assertThat(handler.getCompressionLevel()).isEqualTo(Deflater.DEFAULT_COMPRESSION); + assertThat(handler.isInflateNoWrap()).isTrue(); + } + + @Test + public void testBuildDefault() throws Exception { + final BiDiGzipHandler handler = new YamlConfigurationFactory<>(GzipHandlerFactory.class, + BaseValidator.newValidator(), Jackson.newObjectMapper(), "dw") + .build(new File(Resources.getResource("yaml/default_gzip.yml").toURI())) + .build(null); + + assertThat(handler.getMinGzipSize()).isEqualTo(256); + assertThat(handler.getExcludedAgentPatterns()).isEmpty(); + assertThat(handler.getIncludedMimeTypes()).isEmpty(); // All apart excluded + assertThat(handler.getIncludedMethods()).containsOnly("GET"); + assertThat(handler.getCompressionLevel()).isEqualTo(Deflater.DEFAULT_COMPRESSION); + assertThat(handler.isInflateNoWrap()).isTrue(); + } +} 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 00000000000..9bb55e079ef --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpConnectorFactoryTest.java @@ -0,0 +1,184 @@ +package io.dropwizard.jetty; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.logging.ConsoleAppenderFactory; +import io.dropwizard.logging.FileAppenderFactory; +import io.dropwizard.logging.SyslogAppenderFactory; +import io.dropwizard.util.Duration; +import io.dropwizard.util.Size; +import io.dropwizard.validation.BaseValidator; +import org.eclipse.jetty.io.ArrayByteBufferPool; +import org.eclipse.jetty.io.ByteBufferPool; +import org.eclipse.jetty.server.ForwardedRequestCustomizer; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.Validator; +import java.io.File; + +import static org.apache.commons.lang3.reflect.FieldUtils.getField; +import static org.assertj.core.api.Assertions.assertThat; + +public class HttpConnectorFactoryTest { + + private final ObjectMapper objectMapper = Jackson.newObjectMapper(); + private final Validator validator = BaseValidator.newValidator(); + + @Before + public void setUp() throws Exception { + objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class, + FileAppenderFactory.class, SyslogAppenderFactory.class, HttpConnectorFactory.class); + } + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(HttpConnectorFactory.class); + } + + @Test + public void testParseMinimalConfiguration() throws Exception { + HttpConnectorFactory http = + new YamlConfigurationFactory<>(HttpConnectorFactory.class, validator, objectMapper, "dw") + .build(new File(Resources.getResource("yaml/http-connector-minimal.yml").toURI())); + + assertThat(http.getPort()).isEqualTo(8080); + assertThat(http.getBindHost()).isNull(); + assertThat(http.isInheritChannel()).isEqualTo(false); + assertThat(http.getHeaderCacheSize()).isEqualTo(Size.bytes(512)); + assertThat(http.getOutputBufferSize()).isEqualTo(Size.kilobytes(32)); + assertThat(http.getMaxRequestHeaderSize()).isEqualTo(Size.kilobytes(8)); + assertThat(http.getMaxResponseHeaderSize()).isEqualTo(Size.kilobytes(8)); + assertThat(http.getInputBufferSize()).isEqualTo(Size.kilobytes(8)); + assertThat(http.getIdleTimeout()).isEqualTo(Duration.seconds(30)); + assertThat(http.getMinBufferPoolSize()).isEqualTo(Size.bytes(64)); + assertThat(http.getBufferPoolIncrement()).isEqualTo(Size.bytes(1024)); + assertThat(http.getMaxBufferPoolSize()).isEqualTo(Size.kilobytes(64)); + assertThat(http.getAcceptorThreads()).isEqualTo(Math.max(1, Runtime.getRuntime().availableProcessors() / 2)); + assertThat(http.getSelectorThreads()).isEqualTo(Runtime.getRuntime().availableProcessors()); + assertThat(http.getAcceptQueueSize()).isNull(); + assertThat(http.isReuseAddress()).isTrue(); + assertThat(http.getSoLingerTime()).isNull(); + assertThat(http.isUseServerHeader()).isFalse(); + assertThat(http.isUseDateHeader()).isTrue(); + assertThat(http.isUseForwardedHeaders()).isTrue(); + } + + @Test + public void testParseFullConfiguration() throws Exception { + HttpConnectorFactory http = + new YamlConfigurationFactory<>(HttpConnectorFactory.class, validator, objectMapper, "dw") + .build(new File(Resources.getResource("yaml/http-connector.yml").toURI())); + + assertThat(http.getPort()).isEqualTo(9090); + assertThat(http.getBindHost()).isEqualTo("127.0.0.1"); + assertThat(http.isInheritChannel()).isEqualTo(true); + assertThat(http.getHeaderCacheSize()).isEqualTo(Size.bytes(256)); + assertThat(http.getOutputBufferSize()).isEqualTo(Size.kilobytes(128)); + assertThat(http.getMaxRequestHeaderSize()).isEqualTo(Size.kilobytes(4)); + assertThat(http.getMaxResponseHeaderSize()).isEqualTo(Size.kilobytes(4)); + assertThat(http.getInputBufferSize()).isEqualTo(Size.kilobytes(4)); + assertThat(http.getIdleTimeout()).isEqualTo(Duration.seconds(10)); + assertThat(http.getMinBufferPoolSize()).isEqualTo(Size.bytes(128)); + assertThat(http.getBufferPoolIncrement()).isEqualTo(Size.bytes(500)); + assertThat(http.getMaxBufferPoolSize()).isEqualTo(Size.kilobytes(32)); + assertThat(http.getAcceptorThreads()).isEqualTo(1); + assertThat(http.getSelectorThreads()).isEqualTo(4); + assertThat(http.getAcceptQueueSize()).isEqualTo(1024); + assertThat(http.isReuseAddress()).isFalse(); + assertThat(http.getSoLingerTime()).isEqualTo(Duration.seconds(30)); + assertThat(http.isUseServerHeader()).isTrue(); + assertThat(http.isUseDateHeader()).isFalse(); + assertThat(http.isUseForwardedHeaders()).isFalse(); + } + + @Test + public void testBuildConnector() throws Exception { + HttpConnectorFactory http = new HttpConnectorFactory(); + http.setBindHost("127.0.0.1"); + http.setAcceptorThreads(1); + http.setSelectorThreads(2); + http.setAcceptQueueSize(1024); + http.setSoLingerTime(Duration.seconds(30)); + + Server server = new Server(); + MetricRegistry metrics = new MetricRegistry(); + ThreadPool threadPool = new QueuedThreadPool(); + + ServerConnector connector = (ServerConnector) http.build(server, metrics, "test-http-connector", threadPool); + + assertThat(connector.getPort()).isEqualTo(8080); + assertThat(connector.getHost()).isEqualTo("127.0.0.1"); + assertThat(connector.getAcceptQueueSize()).isEqualTo(1024); + assertThat(connector.getReuseAddress()).isTrue(); + assertThat(connector.getSoLingerTime()).isEqualTo(30); + assertThat(connector.getIdleTimeout()).isEqualTo(30000); + assertThat(connector.getName()).isEqualTo("test-http-connector"); + + assertThat(connector.getServer()).isSameAs(server); + assertThat(connector.getScheduler()).isInstanceOf(ScheduledExecutorScheduler.class); + assertThat(connector.getExecutor()).isSameAs(threadPool); + + // That's gross, but unfortunately ArrayByteBufferPool doesn't have public API for configuration + ByteBufferPool byteBufferPool = connector.getByteBufferPool(); + assertThat(byteBufferPool).isInstanceOf(ArrayByteBufferPool.class); + assertThat(getField(ArrayByteBufferPool.class, "_min", true).get(byteBufferPool)).isEqualTo(64); + assertThat(getField(ArrayByteBufferPool.class, "_inc", true).get(byteBufferPool)).isEqualTo(1024); + assertThat(((Object[]) getField(ArrayByteBufferPool.class, "_direct", true) + .get(byteBufferPool)).length).isEqualTo(64); + + assertThat(connector.getAcceptors()).isEqualTo(1); + assertThat(connector.getSelectorManager().getSelectorCount()).isEqualTo(2); + + Jetty93InstrumentedConnectionFactory connectionFactory = + (Jetty93InstrumentedConnectionFactory) connector.getConnectionFactory("http/1.1"); + assertThat(connectionFactory).isInstanceOf(Jetty93InstrumentedConnectionFactory.class); + assertThat(connectionFactory.getTimer()) + .isSameAs(metrics.timer("org.eclipse.jetty.server.HttpConnectionFactory.127.0.0.1.8080.connections")); + HttpConnectionFactory httpConnectionFactory = (HttpConnectionFactory) connectionFactory.getConnectionFactory(); + assertThat(httpConnectionFactory.getInputBufferSize()).isEqualTo(8192); + + HttpConfiguration httpConfiguration = httpConnectionFactory.getHttpConfiguration(); + assertThat(httpConfiguration.getHeaderCacheSize()).isEqualTo(512); + assertThat(httpConfiguration.getOutputBufferSize()).isEqualTo(32768); + assertThat(httpConfiguration.getRequestHeaderSize()).isEqualTo(8192); + assertThat(httpConfiguration.getResponseHeaderSize()).isEqualTo(8192); + assertThat(httpConfiguration.getSendDateHeader()).isTrue(); + assertThat(httpConfiguration.getSendServerVersion()).isFalse(); + assertThat(httpConfiguration.getCustomizers()).hasAtLeastOneElementOfType(ForwardedRequestCustomizer.class); + + connector.stop(); + server.stop(); + } + + @Test + public void testDefaultAcceptQueueSize() throws Exception { + HttpConnectorFactory http = new HttpConnectorFactory(); + http.setBindHost("127.0.0.1"); + http.setAcceptorThreads(1); + http.setSelectorThreads(2); + http.setSoLingerTime(Duration.seconds(30)); + + Server server = new Server(); + MetricRegistry metrics = new MetricRegistry(); + ThreadPool threadPool = new QueuedThreadPool(); + + ServerConnector connector = (ServerConnector) http.build(server, metrics, "test-http-connector", threadPool); + assertThat(connector.getAcceptQueueSize()).isEqualTo(NetUtil.getTcpBacklog()); + + connector.stop(); + } + +} 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 00000000000..22c3c080eba --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/HttpsConnectorFactoryTest.java @@ -0,0 +1,262 @@ +package io.dropwizard.jetty; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.Collections2; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.validation.BaseValidator; +import org.apache.commons.lang3.SystemUtils; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.HttpConfiguration; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.SecureRequestCustomizer; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import org.eclipse.jetty.server.SslConnectionFactory; +import org.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; +import org.eclipse.jetty.util.thread.ThreadPool; +import org.junit.Test; + +import javax.validation.ConstraintViolation; +import javax.validation.Validator; +import java.io.File; +import java.net.URI; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import static org.apache.commons.lang3.reflect.FieldUtils.getField; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assume.assumeFalse; +import static org.junit.Assume.assumeTrue; + +public class HttpsConnectorFactoryTest { + private static final String WINDOWS_MY_KEYSTORE_NAME = "Windows-MY"; + private final Validator validator = BaseValidator.newValidator(); + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(HttpsConnectorFactory.class); + } + + @Test + public void testParsingConfiguration() throws Exception { + HttpsConnectorFactory https = new YamlConfigurationFactory<>(HttpsConnectorFactory.class, validator, + Jackson.newObjectMapper(), "dw-https") + .build(new File(Resources.getResource("yaml/https-connector.yml").toURI())); + + assertThat(https.getPort()).isEqualTo(8443); + assertThat(https.getKeyStorePath()).isEqualTo("/path/to/ks_file"); + assertThat(https.getKeyStorePassword()).isEqualTo("changeit"); + assertThat(https.getKeyStoreType()).isEqualTo("JKS"); + assertThat(https.getTrustStorePath()).isEqualTo("/path/to/ts_file"); + assertThat(https.getTrustStorePassword()).isEqualTo("changeit"); + assertThat(https.getTrustStoreType()).isEqualTo("JKS"); + assertThat(https.getTrustStoreProvider()).isEqualTo("BC"); + assertThat(https.getKeyManagerPassword()).isEqualTo("changeit"); + assertThat(https.getNeedClientAuth()).isTrue(); + assertThat(https.getWantClientAuth()).isTrue(); + assertThat(https.getCertAlias()).isEqualTo("http_server"); + assertThat(https.getCrlPath()).isEqualTo(new File("/path/to/crl_file")); + assertThat(https.getEnableCRLDP()).isTrue(); + assertThat(https.getEnableOCSP()).isTrue(); + assertThat(https.getMaxCertPathLength()).isEqualTo(3); + assertThat(https.getOcspResponderUrl()).isEqualTo(new URI("http://ip.example.com:9443/ca/ocsp")); + assertThat(https.getJceProvider()).isEqualTo("BC"); + assertThat(https.getValidatePeers()).isTrue(); + assertThat(https.getValidatePeers()).isTrue(); + assertThat(https.getSupportedProtocols()).containsExactly("TLSv1.1", "TLSv1.2"); + assertThat(https.getExcludedProtocols()).isEmpty(); + assertThat(https.getSupportedCipherSuites()) + .containsExactly("ECDHE-RSA-AES128-GCM-SHA256", "ECDHE-ECDSA-AES128-GCM-SHA256"); + assertThat(https.getExcludedCipherSuites()).isEmpty(); + assertThat(https.getAllowRenegotiation()).isFalse(); + assertThat(https.getEndpointIdentificationAlgorithm()).isEqualTo("HTTPS"); + } + + @Test + public void testSupportedProtocols() { + List supportedProtocols = ImmutableList.of("SSLv3", "TLS1"); + + HttpsConnectorFactory factory = new HttpsConnectorFactory(); + factory.setKeyStorePassword("password"); // necessary to avoid a prompt for a password + factory.setSupportedProtocols(supportedProtocols); + + SslContextFactory sslContextFactory = factory.buildSslContextFactory(); + assertThat(ImmutableList.copyOf(sslContextFactory.getIncludeProtocols())).isEqualTo(supportedProtocols); + } + + @Test + public void testExcludedProtocols() { + List excludedProtocols = ImmutableList.of("SSLv3", "TLS1"); + + HttpsConnectorFactory factory = new HttpsConnectorFactory(); + factory.setKeyStorePassword("password"); // necessary to avoid a prompt for a password + factory.setExcludedProtocols(excludedProtocols); + + SslContextFactory sslContextFactory = factory.buildSslContextFactory(); + assertThat(ImmutableList.copyOf(sslContextFactory.getExcludeProtocols())).isEqualTo(excludedProtocols); + } + + @Test + public void nonWindowsKeyStoreValidation() throws Exception { + HttpsConnectorFactory factory = new HttpsConnectorFactory(); + Collection 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 properties = getViolationProperties(validator.validate(factory)); + assertThat(properties.contains("validKeyStorePassword")).isEqualTo(false); + assertThat(properties.contains("validKeyStorePath")).isEqualTo(false); + } + + @Test + public void canBuildContextFactoryWhenWindowsKeyStoreAvailable() throws Exception { + // ignore test when Windows Keystore unavailable + assumeTrue(canAccessWindowsKeyStore()); + + final HttpsConnectorFactory factory = new HttpsConnectorFactory(); + factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME); + + assertNotNull(factory.buildSslContextFactory()); + } + + @Test(expected = IllegalStateException.class) + public void windowsKeyStoreUnavailableThrowsException() throws Exception { + assumeFalse(canAccessWindowsKeyStore()); + + final HttpsConnectorFactory factory = new HttpsConnectorFactory(); + factory.setKeyStoreType(WINDOWS_MY_KEYSTORE_NAME); + factory.buildSslContextFactory(); + } + + @Test + public void testBuild() throws Exception { + final HttpsConnectorFactory https = new HttpsConnectorFactory(); + https.setBindHost("127.0.0.1"); + https.setPort(8443); + + https.setKeyStorePath("/etc/app/server.ks"); + https.setKeyStoreType("JKS"); + https.setKeyStorePassword("correct_horse"); + https.setKeyStoreProvider("BC"); + https.setTrustStorePath("/etc/app/server.ts"); + https.setTrustStoreType("JKS"); + https.setTrustStorePassword("battery_staple"); + https.setTrustStoreProvider("BC"); + + https.setKeyManagerPassword("new_overlords"); + https.setNeedClientAuth(true); + https.setWantClientAuth(true); + https.setCertAlias("alt_server"); + https.setCrlPath(new File("/etc/ctr_list.txt")); + https.setEnableCRLDP(true); + https.setEnableOCSP(true); + https.setMaxCertPathLength(4); + https.setOcspResponderUrl(new URI("http://windc1/ocsp")); + https.setJceProvider("BC"); + https.setAllowRenegotiation(false); + https.setEndpointIdentificationAlgorithm("HTTPS"); + https.setValidateCerts(true); + https.setValidatePeers(true); + https.setSupportedProtocols(ImmutableList.of("TLSv1.1", "TLSv1.2")); + https.setSupportedCipherSuites(ImmutableList.of("TLS_DHE_RSA.*", "TLS_ECDHE.*")); + + final Server server = new Server(); + final MetricRegistry metrics = new MetricRegistry(); + final ThreadPool threadPool = new QueuedThreadPool(); + final Connector connector = https.build(server, metrics, "test-https-connector", threadPool); + assertThat(connector).isInstanceOf(ServerConnector.class); + + final ServerConnector serverConnector = (ServerConnector) connector; + assertThat(serverConnector.getPort()).isEqualTo(8443); + assertThat(serverConnector.getHost()).isEqualTo("127.0.0.1"); + assertThat(serverConnector.getName()).isEqualTo("test-https-connector"); + assertThat(serverConnector.getServer()).isSameAs(server); + assertThat(serverConnector.getScheduler()).isInstanceOf(ScheduledExecutorScheduler.class); + assertThat(serverConnector.getExecutor()).isSameAs(threadPool); + + final Jetty93InstrumentedConnectionFactory jetty93SslConnectionFacttory = + (Jetty93InstrumentedConnectionFactory) serverConnector.getConnectionFactory("ssl"); + assertThat(jetty93SslConnectionFacttory).isInstanceOf(Jetty93InstrumentedConnectionFactory.class); + assertThat(jetty93SslConnectionFacttory.getTimer()).isSameAs( + metrics.timer("org.eclipse.jetty.server.HttpConnectionFactory.127.0.0.1.8443.connections")); + final SslContextFactory sslContextFactory = ((SslConnectionFactory) jetty93SslConnectionFacttory + .getConnectionFactory()).getSslContextFactory(); + + assertThat(getField(SslContextFactory.class, "_keyStoreResource", true).get(sslContextFactory)) + .isEqualTo(Resource.newResource("/etc/app/server.ks")); + assertThat(sslContextFactory.getKeyStoreType()).isEqualTo("JKS"); + assertThat(getField(SslContextFactory.class, "_keyStorePassword", true).get(sslContextFactory).toString()) + .isEqualTo("correct_horse"); + assertThat(sslContextFactory.getKeyStoreProvider()).isEqualTo("BC"); + assertThat(getField(SslContextFactory.class, "_trustStoreResource", true).get(sslContextFactory)) + .isEqualTo(Resource.newResource("/etc/app/server.ts")); + assertThat(sslContextFactory.getKeyStoreType()).isEqualTo("JKS"); + assertThat(getField(SslContextFactory.class, "_trustStorePassword", true).get(sslContextFactory).toString()) + .isEqualTo("battery_staple"); + assertThat(sslContextFactory.getKeyStoreProvider()).isEqualTo("BC"); + assertThat(getField(SslContextFactory.class, "_keyManagerPassword", true).get(sslContextFactory).toString()) + .isEqualTo("new_overlords"); + assertThat(sslContextFactory.getNeedClientAuth()).isTrue(); + assertThat(sslContextFactory.getWantClientAuth()).isTrue(); + assertThat(sslContextFactory.getCertAlias()).isEqualTo("alt_server"); + assertThat(sslContextFactory.getCrlPath()).isEqualTo(new File("/etc/ctr_list.txt").getAbsolutePath()); + assertThat(sslContextFactory.isEnableCRLDP()).isTrue(); + assertThat(sslContextFactory.isEnableOCSP()).isTrue(); + assertThat(sslContextFactory.getMaxCertPathLength()).isEqualTo(4); + assertThat(sslContextFactory.getOcspResponderURL()).isEqualTo("http://windc1/ocsp"); + assertThat(sslContextFactory.getProvider()).isEqualTo("BC"); + assertThat(sslContextFactory.isRenegotiationAllowed()).isFalse(); + assertThat(getField(SslContextFactory.class, "_endpointIdentificationAlgorithm", true).get(sslContextFactory)) + .isEqualTo("HTTPS"); + assertThat(sslContextFactory.isValidateCerts()).isTrue(); + assertThat(sslContextFactory.isValidatePeerCerts()).isTrue(); + assertThat(sslContextFactory.getIncludeProtocols()).containsOnly("TLSv1.1", "TLSv1.2"); + assertThat(sslContextFactory.getIncludeCipherSuites()).containsOnly("TLS_DHE_RSA.*", "TLS_ECDHE.*"); + + final ConnectionFactory httpConnectionFactory = serverConnector.getConnectionFactory("http/1.1"); + assertThat(httpConnectionFactory).isInstanceOf(HttpConnectionFactory.class); + final HttpConfiguration httpConfiguration = ((HttpConnectionFactory) httpConnectionFactory) + .getHttpConfiguration(); + assertThat(httpConfiguration.getSecureScheme()).isEqualTo("https"); + assertThat(httpConfiguration.getSecurePort()).isEqualTo(8443); + assertThat(httpConfiguration.getCustomizers()).hasAtLeastOneElementOfType(SecureRequestCustomizer.class); + + connector.stop(); + server.stop(); + } + + + 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 Collection getViolationProperties(Set> violations) { + return Collections2.transform(violations, input -> 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 00000000000..ad319843fa8 --- /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.assertj.core.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/NetUtilTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/NetUtilTest.java new file mode 100644 index 00000000000..7c65e5268be --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/NetUtilTest.java @@ -0,0 +1,91 @@ +package io.dropwizard.jetty; + +import static org.assertj.core.api.Assertions.assertThat; +import org.junit.Test; +import java.io.File; +import java.net.InetAddress; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Collection; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assume.assumeThat; + +public class NetUtilTest { + + private static final String OS_NAME_PROPERTY = "os.name"; + + /** + * Assuming Windows + */ + @Test + public void testDefaultTcpBacklogForWindows() { + assumeThat(System.getProperty(OS_NAME_PROPERTY), containsString("win")); + assumeThat(isTcpBacklogSettingReadable(), is(false)); + assertEquals(NetUtil.DEFAULT_TCP_BACKLOG_WINDOWS, NetUtil.getTcpBacklog()); + } + + /** + * Assuming Mac (which does not have /proc) + */ + @Test + public void testNonWindowsDefaultTcpBacklog() { + assumeThat(System.getProperty(OS_NAME_PROPERTY), containsString("Mac OS X")); + assumeThat(isTcpBacklogSettingReadable(), is(false)); + assertEquals(NetUtil.DEFAULT_TCP_BACKLOG_LINUX, NetUtil.getTcpBacklog()); + } + + /** + * Assuming Mac (which does not have /proc) + */ + @Test + public void testNonWindowsSpecifiedTcpBacklog() { + assumeThat(System.getProperty(OS_NAME_PROPERTY), containsString("Mac OS X")); + assumeThat(isTcpBacklogSettingReadable(), is(false)); + assertEquals(100, NetUtil.getTcpBacklog(100)); + } + + /** + * Assuming Linux (which has /proc) + */ + @Test + public void testOsSetting() { + assumeThat(System.getProperty(OS_NAME_PROPERTY), containsString("Linux")); + assumeThat(isTcpBacklogSettingReadable(), is(true)); + assertNotEquals(-1, NetUtil.getTcpBacklog(-1)); + } + + @Test + public void testAllLocalIps() throws Exception { + NetUtil.setLocalIpFilter((nif, adr) -> + (adr != null) && !adr.isLoopbackAddress() && (nif.isPointToPoint() || !adr.isLinkLocalAddress())); + final Collection addresses = NetUtil.getAllLocalIPs(); + assertThat(addresses.size()).isGreaterThan(0); + assertThat(addresses).doesNotContain(InetAddress.getLoopbackAddress()); + } + + @Test + public void testLocalIpsWithLocalFilter() throws Exception { + NetUtil.setLocalIpFilter((inf, adr) -> adr != null); + final Collection addresses = NetUtil.getAllLocalIPs(); + assertThat(addresses.size()).isGreaterThan(0); + assertThat(addresses).contains(InetAddress.getLoopbackAddress()); + } + + public boolean isTcpBacklogSettingReadable() { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Boolean run() { + try { + File f = new File(NetUtil.TCP_BACKLOG_SETTING_LOCATION); + return f.exists() && f.canRead(); + } catch (Exception e) { + return false; + } + + } + }); + } +} 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 00000000000..0c9d5bcb206 --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/NonblockingServletHolderTest.java @@ -0,0 +1,53 @@ +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.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +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, null); + 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/RoutingHandlerTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RoutingHandlerTest.java new file mode 100644 index 00000000000..702fe5489a1 --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/RoutingHandlerTest.java @@ -0,0 +1,68 @@ +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.Server; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.junit.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +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 { + handler1.setServer(mock(Server.class)); + handler2.setServer(mock(Server.class)); + 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/ServerPushFilterFactoryTest.java b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/ServerPushFilterFactoryTest.java new file mode 100644 index 00000000000..3387353ba06 --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/ServerPushFilterFactoryTest.java @@ -0,0 +1,90 @@ +package io.dropwizard.jetty; + +import com.google.common.collect.ImmutableList; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.util.Duration; +import io.dropwizard.validation.BaseValidator; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlets.PushCacheFilter; +import org.junit.Test; + +import javax.servlet.DispatcherType; +import java.io.File; +import java.util.EnumSet; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ServerPushFilterFactoryTest { + + @Test + public void testLoadConfiguration() throws Exception { + final ServerPushFilterFactory serverPush = new YamlConfigurationFactory<>( + ServerPushFilterFactory.class, BaseValidator.newValidator(), + Jackson.newObjectMapper(), "dw-server-push") + .build(new File(Resources.getResource("yaml/server-push.yml").toURI())); + assertThat(serverPush.isEnabled()).isTrue(); + assertThat(serverPush.getAssociatePeriod()).isEqualTo(Duration.seconds(5)); + assertThat(serverPush.getMaxAssociations()).isEqualTo(8); + assertThat(serverPush.getRefererHosts()).contains("dropwizard.io", "dropwizard.github.io"); + assertThat(serverPush.getRefererPorts()).contains(8444, 8445); + } + + @Test + public void testDefaultConfiguration() { + final ServerPushFilterFactory serverPush = new ServerPushFilterFactory(); + assertThat(serverPush.isEnabled()).isFalse(); + assertThat(serverPush.getAssociatePeriod()).isEqualTo(Duration.seconds(4)); + assertThat(serverPush.getMaxAssociations()).isEqualTo(16); + assertThat(serverPush.getRefererHosts()).isNull(); + assertThat(serverPush.getRefererPorts()).isNull(); + } + + @Test + public void testDontAddFilterByDefault() { + final ServerPushFilterFactory serverPush = new ServerPushFilterFactory(); + + ServletContextHandler servletContextHandler = mock(ServletContextHandler.class); + serverPush.addFilter(servletContextHandler); + verify(servletContextHandler, never()) + .addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + } + + @Test + public void testAddFilter() { + final ServerPushFilterFactory serverPush = new ServerPushFilterFactory(); + serverPush.setRefererHosts(ImmutableList.of("dropwizard.io", "dropwizard.github.io")); + serverPush.setRefererPorts(ImmutableList.of(8444, 8445)); + serverPush.setEnabled(true); + + ServletContextHandler servletContextHandler = mock(ServletContextHandler.class); + + serverPush.addFilter(servletContextHandler); + + verify(servletContextHandler).setInitParameter("associatePeriod", "4000"); + verify(servletContextHandler).setInitParameter("maxAssociations", "16"); + verify(servletContextHandler).setInitParameter("hosts", "dropwizard.io,dropwizard.github.io"); + verify(servletContextHandler).setInitParameter("ports", "8444,8445"); + verify(servletContextHandler).addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + } + + @Test + public void testRefererHostsAndPortsAreNotSet() { + final ServerPushFilterFactory serverPush = new ServerPushFilterFactory(); + serverPush.setEnabled(true); + + ServletContextHandler servletContextHandler = mock(ServletContextHandler.class); + + serverPush.addFilter(servletContextHandler); + + verify(servletContextHandler, never()).setInitParameter(eq("hosts"), anyString()); + verify(servletContextHandler, never()).setInitParameter(eq("ports"), anyString()); + verify(servletContextHandler).addFilter(PushCacheFilter.class, "/*", EnumSet.of(DispatcherType.REQUEST)); + } +} 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 00000000000..371a1ad740c --- /dev/null +++ b/dropwizard-jetty/src/test/java/io/dropwizard/jetty/setup/ServletEnvironmentTest.java @@ -0,0 +1,220 @@ +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.eclipse.jetty.util.resource.Resource; +import org.eclipse.jetty.util.resource.ResourceCollection; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.mockito.ArgumentCaptor; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.GenericServlet; +import javax.servlet.Servlet; +import javax.servlet.ServletContextListener; +import javax.servlet.ServletRegistration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ServletEnvironmentTest { + private final ServletHandler servletHandler = mock(ServletHandler.class); + private final MutableServletContextHandler handler = mock(MutableServletContextHandler.class); + private final ServletEnvironment environment = new ServletEnvironment(handler); + + @Rule + public TemporaryFolder tempDir = new TemporaryFolder(); + + @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 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 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 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 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 setsBaseResource() throws Exception { + final Resource testResource = Resource.newResource(tempDir.newFolder()); + environment.setBaseResource(testResource); + + verify(handler).setBaseResource(testResource); + } + + @Test + public void setsBaseResourceList() throws Exception { + Resource wooResource = Resource.newResource(tempDir.newFolder()); + Resource fooResource = Resource.newResource(tempDir.newFolder()); + + final Resource[] testResources = new Resource[]{wooResource, fooResource}; + environment.setBaseResource(testResources); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Resource.class); + verify(handler).setBaseResource(captor.capture()); + + Resource actualResource = captor.getValue(); + assertThat(actualResource).isInstanceOf(ResourceCollection.class); + + ResourceCollection actualResourceCollection = (ResourceCollection) actualResource; + assertThat(actualResourceCollection.getResources()).contains(wooResource, fooResource); + + } + + @Test + public void setsResourceBase() throws Exception { + environment.setResourceBase("/woo"); + + verify(handler).setResourceBase("/woo"); + } + + @Test + public void setsBaseResourceStringList() throws Exception { + String wooResource = tempDir.newFolder().getAbsolutePath(); + String fooResource = tempDir.newFolder().getAbsolutePath(); + + final String[] testResources = new String[]{wooResource, fooResource}; + environment.setBaseResource(testResources); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Resource.class); + verify(handler).setBaseResource(captor.capture()); + + Resource actualResource = captor.getValue(); + assertThat(actualResource).isInstanceOf(ResourceCollection.class); + + ResourceCollection actualResourceCollection = (ResourceCollection) actualResource; + assertThat(actualResourceCollection.getResources()).contains(Resource.newResource(wooResource), + Resource.newResource(fooResource)); + + } + + @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/assets/banner.txt b/dropwizard-jetty/src/test/resources/assets/banner.txt new file mode 100644 index 00000000000..e345370c772 --- /dev/null +++ b/dropwizard-jetty/src/test/resources/assets/banner.txt @@ -0,0 +1,8 @@ + | | + __| |_ _ _ __ ___ _ __ ___ _ _ + / _` | | | | '_ ` _ \| '_ ` _ \| | | | +| (_| | |_| | | | | | | | | | | | |_| | + \__,_|\__,_|_| |_| |_|_| |_| |_|\__, | + __/ | + |___/ + diff --git a/dropwizard-jetty/src/test/resources/assets/new-banner.txt b/dropwizard-jetty/src/test/resources/assets/new-banner.txt new file mode 100644 index 00000000000..98e7bf0d78b --- /dev/null +++ b/dropwizard-jetty/src/test/resources/assets/new-banner.txt @@ -0,0 +1,7 @@ +d8888b. db db .88b d88. .88b d88. db db +88 `8D 88 88 88'YbdP`88 88'YbdP`88 `8b d8' +88 88 88 88 88 88 88 88 88 88 `8bd8' +88 88 88 88 88 88 88 88 88 88 88 +88 .8D 88b d88 88 88 88 88 88 88 88 +Y8888D' ~Y8888P' YP YP YP YP YP YP YP + \ No newline at end of file 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 00000000000..5c207796541 --- /dev/null +++ b/dropwizard-jetty/src/test/resources/logback-test.xml @@ -0,0 +1,14 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + diff --git a/dropwizard-jetty/src/test/resources/yaml/default_gzip.yml b/dropwizard-jetty/src/test/resources/yaml/default_gzip.yml new file mode 100644 index 00000000000..d4ca94189e1 --- /dev/null +++ b/dropwizard-jetty/src/test/resources/yaml/default_gzip.yml @@ -0,0 +1 @@ +enabled: true 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 00000000000..1a1d631254f --- /dev/null +++ b/dropwizard-jetty/src/test/resources/yaml/gzip.yml @@ -0,0 +1,6 @@ +enabled: false +minimumEntitySize: 12KB +bufferSize: 32KB +excludedUserAgentPatterns: ["OLD-2.+"] +compressedMimeTypes: ["text/plain"] +includedMethods: ["GET", "POST"] diff --git a/dropwizard-jetty/src/test/resources/yaml/http-connector-minimal.yml b/dropwizard-jetty/src/test/resources/yaml/http-connector-minimal.yml new file mode 100644 index 00000000000..eb35f2a67f4 --- /dev/null +++ b/dropwizard-jetty/src/test/resources/yaml/http-connector-minimal.yml @@ -0,0 +1 @@ +type: http diff --git a/dropwizard-jetty/src/test/resources/yaml/http-connector.yml b/dropwizard-jetty/src/test/resources/yaml/http-connector.yml new file mode 100644 index 00000000000..3c90fc0d507 --- /dev/null +++ b/dropwizard-jetty/src/test/resources/yaml/http-connector.yml @@ -0,0 +1,21 @@ +type: http +port: 9090 +bindHost: 127.0.0.1 +inheritChannel: true +headerCacheSize: 256B +outputBufferSize: 128KiB +maxRequestHeaderSize: 4KiB +maxResponseHeaderSize: 4KiB +inputBufferSize: 4KiB +idleTimeout: 10 seconds +minBufferPoolSize: 128B +bufferPoolIncrement: 500B +maxBufferPoolSize: 32KiB +acceptorThreads: 1 +selectorThreads: 4 +acceptQueueSize: 1024 +reuseAddress: false +soLingerTime: 30s +useServerHeader: true +useDateHeader: false +useForwardedHeaders: false diff --git a/dropwizard-jetty/src/test/resources/yaml/https-connector.yml b/dropwizard-jetty/src/test/resources/yaml/https-connector.yml new file mode 100644 index 00000000000..8f078e8c51a --- /dev/null +++ b/dropwizard-jetty/src/test/resources/yaml/https-connector.yml @@ -0,0 +1,28 @@ +type: https +port: 8443 +keyStorePath: '/path/to/ks_file' +keyStorePassword: changeit +keyStoreType: JKS +keyStoreProvider: BC +trustStorePath: '/path/to/ts_file' +trustStorePassword: changeit +trustStoreType: JKS +trustStoreProvider: BC +keyManagerPassword: changeit +needClientAuth: true +wantClientAuth: true +certAlias: http_server +crlPath: '/path/to/crl_file' +enableCRLDP: true +enableOCSP: true +maxCertPathLength: 3 +ocspResponderUrl: 'http://ip.example.com:9443/ca/ocsp' +jceProvider: BC +validateCerts: true +validatePeers: true +supportedProtocols: ['TLSv1.1', 'TLSv1.2'] +excludedProtocols: [] +supportedCipherSuites: ['ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES128-GCM-SHA256'] +excludedCipherSuites: [] +allowRenegotiation: false +endpointIdentificationAlgorithm: HTTPS diff --git a/dropwizard-jetty/src/test/resources/yaml/server-push.yml b/dropwizard-jetty/src/test/resources/yaml/server-push.yml new file mode 100644 index 00000000000..5758d17150e --- /dev/null +++ b/dropwizard-jetty/src/test/resources/yaml/server-push.yml @@ -0,0 +1,5 @@ +enabled: true +associatePeriod: '5 seconds' +maxAssociations: 8 +refererHosts: ['dropwizard.io', 'dropwizard.github.io'] +refererPorts: [8444, 8445] diff --git a/dropwizard-lifecycle/pom.xml b/dropwizard-lifecycle/pom.xml new file mode 100644 index 00000000000..61b0e41c669 --- /dev/null +++ b/dropwizard-lifecycle/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-lifecycle + Dropwizard Lifecycle Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + org.slf4j + slf4j-api + + + com.google.guava + guava + + + org.eclipse.jetty + jetty-server + + + io.dropwizard + dropwizard-util + + + io.dropwizard + dropwizard-logging + test + + + 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 00000000000..46b1f23919c --- /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/src/main/java/com/yammer/dropwizard/jetty/JettyManaged.java b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/JettyManaged.java similarity index 66% rename from dropwizard/src/main/java/com/yammer/dropwizard/jetty/JettyManaged.java rename to dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/JettyManaged.java index 3fd51d18ee2..920800b9617 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/jetty/JettyManaged.java +++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/JettyManaged.java @@ -1,17 +1,16 @@ -package com.yammer.dropwizard.jetty; +package io.dropwizard.lifecycle; -import com.yammer.dropwizard.lifecycle.Managed; 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}. + * 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 {@link JettyManaged} wrapping {@code managed}. + * Creates a new JettyManaged wrapping {@code managed}. * * @param managed a {@link Managed} instance to be wrapped */ @@ -19,6 +18,10 @@ public JettyManaged(Managed managed) { this.managed = managed; } + public Managed getManaged() { + return managed; + } + @Override protected void doStart() throws Exception { managed.start(); @@ -28,4 +31,9 @@ protected void doStart() throws Exception { 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 00000000000..25755954a59 --- /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 before the application becomes available. + * + * @throws Exception if something goes wrong; this will halt the application startup. + */ + void start() throws Exception; + + /** + * Stops the object. Called after the application is no longer accepting requests. + * + * @throws Exception if something goes wrong. + */ + 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 00000000000..c06454475e8 --- /dev/null +++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/ServerLifecycleListener.java @@ -0,0 +1,35 @@ +package io.dropwizard.lifecycle; + +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; +import java.util.EventListener; + +public interface ServerLifecycleListener extends EventListener { + + void serverStarted(Server server); + + /** + * Return the local port of the first {@link ServerConnector} in the + * provided {@link Server} instance. + * + * @param server Server instance to use + * @return First local port of the server instance + */ + default int getLocalPort(Server server) { + return ((ServerConnector) server.getConnectors()[0]).getLocalPort(); + } + + /** + * Return the local port of the last {@link ServerConnector} in the + * provided {@link Server} instance. This may be the same value as returned + * by {@link #getLocalPort(Server)} if using the "simple" server configuration. + * + * @param server Server instance to use + * @return Last local port or the server instance + */ + default int getAdminPort(Server server) { + final Connector[] connectors = server.getConnectors(); + return ((ServerConnector) connectors[connectors.length -1]).getLocalPort(); + } +} 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 00000000000..2c696560205 --- /dev/null +++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilder.java @@ -0,0 +1,104 @@ +package io.dropwizard.lifecycle.setup; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.dropwizard.lifecycle.ExecutorServiceManager; +import io.dropwizard.util.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; + +public class ExecutorServiceBuilder { + private static Logger log = LoggerFactory.getLogger(ExecutorServiceBuilder.class); + + private final LifecycleEnvironment environment; + private final String nameFormat; + private int corePoolSize; + private int maximumPoolSize; + private Duration keepAliveTime; + private Duration shutdownTime; + private BlockingQueue workQueue; + private ThreadFactory threadFactory; + private RejectedExecutionHandler handler; + + public ExecutorServiceBuilder(LifecycleEnvironment environment, String nameFormat, ThreadFactory factory) { + this.environment = environment; + this.nameFormat = nameFormat; + this.corePoolSize = 0; + this.maximumPoolSize = 1; + this.keepAliveTime = Duration.seconds(60); + this.shutdownTime = Duration.seconds(5); + this.workQueue = new LinkedBlockingQueue<>(); + this.threadFactory = factory; + this.handler = new ThreadPoolExecutor.AbortPolicy(); + } + + public ExecutorServiceBuilder(LifecycleEnvironment environment, String nameFormat) { + this(environment, nameFormat, new ThreadFactoryBuilder().setNameFormat(nameFormat).build()); + } + + 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 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() { + if (corePoolSize != maximumPoolSize && maximumPoolSize > 1 && !isBoundedQueue()) { + log.warn("Parameter 'maximumPoolSize' is conflicting with unbounded work queues"); + } + final ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, + maximumPoolSize, + keepAliveTime.getQuantity(), + keepAliveTime.getUnit(), + workQueue, + threadFactory, + handler); + environment.manage(new ExecutorServiceManager(executor, shutdownTime, nameFormat)); + return executor; + } + + private boolean isBoundedQueue() { + return workQueue.remainingCapacity() != Integer.MAX_VALUE; + } + + @VisibleForTesting + static synchronized void setLog(Logger newLog) { + log = newLog; + } +} 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 00000000000..9fc38a85847 --- /dev/null +++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/LifecycleEnvironment.java @@ -0,0 +1,111 @@ +package io.dropwizard.lifecycle.setup; + +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.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadFactory; + +import static java.util.Objects.requireNonNull; + +public class LifecycleEnvironment { + private static final Logger LOGGER = LoggerFactory.getLogger(LifecycleEnvironment.class); + + private final List managedObjects; + private final List lifecycleListeners; + + public LifecycleEnvironment() { + this.managedObjects = new ArrayList<>(); + this.lifecycleListeners = new ArrayList<>(); + } + + public List getManagedObjects() { + return managedObjects; + } + + /** + * 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(requireNonNull(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(requireNonNull(managed)); + } + + public ExecutorServiceBuilder executorService(String nameFormat) { + return new ExecutorServiceBuilder(this, nameFormat); + } + + public ExecutorServiceBuilder executorService(String nameFormat, ThreadFactory factory) { + return new ExecutorServiceBuilder(this, nameFormat, factory); + } + + public ScheduledExecutorServiceBuilder scheduledExecutorService(String nameFormat) { + return scheduledExecutorService(nameFormat, false); + } + + public ScheduledExecutorServiceBuilder scheduledExecutorService(String nameFormat, ThreadFactory factory) { + return new ScheduledExecutorServiceBuilder(this, nameFormat, factory); + } + + public ScheduledExecutorServiceBuilder scheduledExecutorService(String nameFormat, boolean useDaemonThreads) { + return new ScheduledExecutorServiceBuilder(this, nameFormat, useDaemonThreads); + } + + 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 00000000000..a6dd16d7365 --- /dev/null +++ b/dropwizard-lifecycle/src/main/java/io/dropwizard/lifecycle/setup/ScheduledExecutorServiceBuilder.java @@ -0,0 +1,59 @@ +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.RejectedExecutionHandler; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; + +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, ThreadFactory factory) { + this.environment = environment; + this.nameFormat = nameFormat; + this.poolSize = 1; + this.threadFactory = factory; + this.shutdownTime = Duration.seconds(5); + this.handler = new ThreadPoolExecutor.AbortPolicy(); + } + + public ScheduledExecutorServiceBuilder(LifecycleEnvironment environment, String nameFormat, boolean useDaemonThreads) { + this(environment, nameFormat, new ThreadFactoryBuilder().setNameFormat(nameFormat).setDaemon(useDaemonThreads).build()); + } + + 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/src/test/java/com/yammer/dropwizard/jetty/tests/JettyManagedTest.java b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/JettyManagedTest.java similarity index 80% rename from dropwizard/src/test/java/com/yammer/dropwizard/jetty/tests/JettyManagedTest.java rename to dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/JettyManagedTest.java index af5224d15be..1ffee9a49fb 100644 --- a/dropwizard/src/test/java/com/yammer/dropwizard/jetty/tests/JettyManagedTest.java +++ b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/JettyManagedTest.java @@ -1,7 +1,5 @@ -package com.yammer.dropwizard.jetty.tests; +package io.dropwizard.lifecycle; -import com.yammer.dropwizard.jetty.JettyManaged; -import com.yammer.dropwizard.lifecycle.Managed; import org.junit.Test; import org.mockito.InOrder; diff --git a/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilderTest.java b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilderTest.java new file mode 100644 index 00000000000..7efdb376478 --- /dev/null +++ b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/ExecutorServiceBuilderTest.java @@ -0,0 +1,145 @@ +package io.dropwizard.lifecycle.setup; + +import com.google.common.base.Throwables; +import io.dropwizard.util.Duration; +import org.junit.Before; +import org.junit.Test; +import org.mockito.exceptions.verification.WantedButNotInvoked; +import org.slf4j.Logger; + +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +public class ExecutorServiceBuilderTest { + + private static final String WARNING = "Parameter 'maximumPoolSize' is conflicting with unbounded work queues"; + + private ExecutorServiceBuilder executorServiceBuilder; + private Logger log; + + @Before + public void setUp() throws Exception { + executorServiceBuilder = new ExecutorServiceBuilder(new LifecycleEnvironment(), "test"); + log = mock(Logger.class); + ExecutorServiceBuilder.setLog(log); + } + + @Test + public void testGiveAWarningAboutMaximumPoolSizeAndUnboundedQueue() { + executorServiceBuilder + .minThreads(4) + .maxThreads(8) + .build(); + + verify(log).warn(WARNING); + } + + @Test + public void testGiveNoWarningAboutMaximumPoolSizeAndBoundedQueue() throws InterruptedException { + ExecutorService exe = executorServiceBuilder + .minThreads(4) + .maxThreads(8) + .workQueue(new ArrayBlockingQueue<>(16)) + .build(); + + verify(log, never()).warn(WARNING); + assertCanExecuteAtLeast2ConcurrentTasks(exe); + } + + /** + * There should be no warning about using a Executors.newSingleThreadExecutor() equivalent + * @see java.util.concurrent.Executors#newSingleThreadExecutor() + */ + @Test + public void shouldNotWarnWhenSettingUpSingleThreadedPool() { + executorServiceBuilder + .minThreads(1) + .maxThreads(1) + .keepAliveTime(Duration.milliseconds(0)) + .workQueue(new LinkedBlockingQueue<>()) + .build(); + + verify(log, never()).warn(anyString()); + } + + /** + * There should be no warning about using a Executors.newCachedThreadPool() equivalent + * @see java.util.concurrent.Executors#newCachedThreadPool() + */ + @Test + public void shouldNotWarnWhenSettingUpCachedThreadPool() throws InterruptedException { + ExecutorService exe = executorServiceBuilder + .minThreads(0) + .maxThreads(Integer.MAX_VALUE) + .keepAliveTime(Duration.seconds(60)) + .workQueue(new SynchronousQueue<>()) + .build(); + + verify(log, never()).warn(anyString()); + assertCanExecuteAtLeast2ConcurrentTasks(exe); // cached thread pools work right? + } + + @Test + public void shouldNotWarnWhenUsingTheDefaultConfiguration() { + executorServiceBuilder.build(); + verify(log, never()).warn(anyString()); + } + + /** + * Setting large max threads without large min threads is misleading on the default queue implementation + * It should warn or work + */ + @Test + public void shouldBeAbleToExecute2TasksAtOnceWithLargeMaxThreadsOrBeWarnedOtherwise() { + ExecutorService exe = executorServiceBuilder + .maxThreads(Integer.MAX_VALUE) + .build(); + + try { + verify(log).warn(anyString()); + } catch (WantedButNotInvoked error) { + // no warning has been given so we should be able to execute at least 2 things at once + assertCanExecuteAtLeast2ConcurrentTasks(exe); + } + } + + /** + * Tries to run 2 tasks that on the executor that rely on each others side-effect to complete. If they fail to + * complete within a short time then we can assume they are not running concurrently + * @param exe an executor to try to run 2 tasks on + */ + private void assertCanExecuteAtLeast2ConcurrentTasks(Executor exe) { + CountDownLatch latch = new CountDownLatch(2); + Runnable concurrentLatchCountDownAndWait = () -> { + latch.countDown(); + try { + latch.await(); + } catch (InterruptedException ex) { + Throwables.propagate(ex); + } + }; + + exe.execute(concurrentLatchCountDownAndWait); + exe.execute(concurrentLatchCountDownAndWait); + + try { + // 1 second is ages even on a slow VM + assertThat(latch.await(1, TimeUnit.SECONDS)) + .as("2 tasks executed concurrently on " + exe) + .isTrue(); + } catch (InterruptedException ex) { + Throwables.propagate(ex); + } + } +} 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 00000000000..0e5389ab4b3 --- /dev/null +++ b/dropwizard-lifecycle/src/test/java/io/dropwizard/lifecycle/setup/LifecycleEnvironmentTest.java @@ -0,0 +1,102 @@ +package io.dropwizard.lifecycle.setup; + +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.ThreadFactoryBuilder; + +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 java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; + +import static org.assertj.core.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); + } + + @Test + public void scheduledExecutorServiceBuildsDaemonThreads() throws ExecutionException, InterruptedException { + final ScheduledExecutorService executorService = environment.scheduledExecutorService("daemon-%d", true).build(); + final Future isDaemon = executorService.submit(() -> Thread.currentThread().isDaemon()); + + assertThat(isDaemon.get()).isTrue(); + } + + @Test + public void scheduledExecutorServiceBuildsUserThreadsByDefault() throws ExecutionException, InterruptedException { + final ScheduledExecutorService executorService = environment.scheduledExecutorService("user-%d").build(); + final Future isDaemon = executorService.submit(() -> Thread.currentThread().isDaemon()); + + assertThat(isDaemon.get()).isFalse(); + } + + @Test + public void scheduledExecutorServiceThreadFactory() throws ExecutionException, InterruptedException { + final String expectedName = "DropWizard ThreadFactory Test"; + final String expectedNamePattern = expectedName + "-%d"; + + final ThreadFactory tfactory = (new ThreadFactoryBuilder()) + .setDaemon(false) + .setNameFormat(expectedNamePattern) + .build(); + + final ScheduledExecutorService executorService = environment.scheduledExecutorService("DropWizard Service", tfactory).build(); + final Future isFactoryInUse = executorService.submit(() -> Thread.currentThread().getName().startsWith(expectedName)); + + assertThat(isFactoryInUse.get()).isTrue(); + } + + @Test + public void executorServiceThreadFactory() throws ExecutionException, InterruptedException { + final String expectedName = "DropWizard ThreadFactory Test"; + final String expectedNamePattern = expectedName + "-%d"; + + final ThreadFactory tfactory = (new ThreadFactoryBuilder()) + .setDaemon(false) + .setNameFormat(expectedNamePattern) + .build(); + + final ExecutorService executorService = environment.executorService("Dropwizard Service", tfactory).build(); + final Future isFactoryInUse = executorService.submit(() -> Thread.currentThread().getName().startsWith(expectedName)); + + assertThat(isFactoryInUse.get()).isTrue(); + } +} 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-lifecycle/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-logging/.gitignore b/dropwizard-logging/.gitignore new file mode 100644 index 00000000000..cfce1ade950 --- /dev/null +++ b/dropwizard-logging/.gitignore @@ -0,0 +1,2 @@ +*.log + diff --git a/dropwizard-logging/pom.xml b/dropwizard-logging/pom.xml new file mode 100644 index 00000000000..15833d6dd47 --- /dev/null +++ b/dropwizard-logging/pom.xml @@ -0,0 +1,73 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-logging + Dropwizard Logging Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-jackson + + + io.dropwizard + dropwizard-validation + + + io.dropwizard.metrics + metrics-logback + + + org.slf4j + slf4j-api + + + org.slf4j + jul-to-slf4j + + + ch.qos.logback + logback-core + + + ch.qos.logback + logback-classic + + + org.slf4j + log4j-over-slf4j + + + org.slf4j + jcl-over-slf4j + + + org.eclipse.jetty + jetty-util + + + io.dropwizard + dropwizard-configuration + test + + + 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 00000000000..d698a29ecdd --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/AbstractAppenderFactory.java @@ -0,0 +1,200 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.AsyncAppenderBase; +import ch.qos.logback.core.Context; +import ch.qos.logback.core.pattern.PatternLayoutBase; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.FilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.TimeZone; + +/** + * A base implementation of {@link AppenderFactory}. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code threshold}ALLThe minimum event level the appender will handle.
    {@code logFormat}(none)An appender-specific log format.
    {@code timeZone}{@code UTC} + * The time zone to which event timestamps will be converted. + * Ignored if logFormat is supplied. + *
    {@code queueSize}{@link AsyncAppenderBase}The maximum capacity of the blocking queue.
    {@code includeCallerData}{@link AsyncAppenderBase} + * Whether to include caller data, required for line numbers. + * Beware, is considered expensive. + *
    {@code discardingThreshold}{@link AsyncAppenderBase} + * By default, when the blocking queue has 20% capacity remaining, + * it will drop events of level TRACE, DEBUG and INFO, keeping only + * events of level WARN and ERROR. To keep all events, set discardingThreshold to 0. + *
    {@code filterFactories}(none) + * A list of {@link FilterFactory filters} to apply to the appender, in order, + * after the {@code threshold}. + *
    + */ +public abstract class AbstractAppenderFactory implements AppenderFactory { + + @NotNull + protected Level threshold = Level.ALL; + + protected String logFormat; + + @NotNull + protected TimeZone timeZone = TimeZone.getTimeZone("UTC"); + + @Min(1) + @Max(Integer.MAX_VALUE) + private int queueSize = AsyncAppenderBase.DEFAULT_QUEUE_SIZE; + + private int discardingThreshold = -1; + + private boolean includeCallerData = false; + + private ImmutableList> filterFactories = ImmutableList.of(); + + @JsonProperty + public int getQueueSize() { + return queueSize; + } + + @JsonProperty + public void setQueueSize(int queueSize) { + this.queueSize = queueSize; + } + + @JsonProperty + public int getDiscardingThreshold() { + return discardingThreshold; + } + + @JsonProperty + public void setDiscardingThreshold(int discardingThreshold) { + this.discardingThreshold = discardingThreshold; + } + + @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; + } + + @JsonProperty + public TimeZone getTimeZone() { + return timeZone; + } + + @JsonProperty + public void setTimeZone(TimeZone timeZone) { + this.timeZone = timeZone; + } + + @JsonProperty + public boolean isIncludeCallerData() { + return includeCallerData; + } + + @JsonProperty + public void setIncludeCallerData(boolean includeCallerData) { + this.includeCallerData = includeCallerData; + } + + @JsonProperty + public ImmutableList> getFilterFactories() { + return filterFactories; + } + + @JsonProperty + public void setFilterFactories(List> appenders) { + this.filterFactories = ImmutableList.copyOf(appenders); + } + + protected Appender wrapAsync(Appender appender, AsyncAppenderFactory asyncAppenderFactory) { + return wrapAsync(appender, asyncAppenderFactory, appender.getContext()); + } + + protected Appender wrapAsync(Appender appender, AsyncAppenderFactory asyncAppenderFactory, Context context) { + final AsyncAppenderBase asyncAppender = asyncAppenderFactory.build(); + if (asyncAppender instanceof AsyncAppender) { + ((AsyncAppender) asyncAppender).setIncludeCallerData(includeCallerData); + } + asyncAppender.setQueueSize(queueSize); + asyncAppender.setDiscardingThreshold(discardingThreshold); + asyncAppender.setContext(context); + asyncAppender.setName("async-" + appender.getName()); + asyncAppender.addAppender(appender); + asyncAppender.start(); + return asyncAppender; + } + + protected PatternLayoutBase buildLayout(LoggerContext context, LayoutFactory layoutFactory) { + final PatternLayoutBase formatter = layoutFactory.build(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 00000000000..aec405ceb95 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/AppenderFactory.java @@ -0,0 +1,45 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; + +/** + * A service provider interface for creating Logback {@link Appender} instances. + *

    + * To create your own, just: + *

      + *
    1. Create a class which implements {@link AppenderFactory}.
    2. + *
    3. Annotate it with {@code @JsonTypeName} and give it a unique type name.
    4. + *
    5. add a {@code META-INF/services/io.dropwizard.logging.AppenderFactory} file with your + * implementation's full class name to the class path.
    6. + *
    + * + * @see ConsoleAppenderFactory + * @see FileAppenderFactory + * @see SyslogAppenderFactory + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface AppenderFactory extends Discoverable { + /** + * Given a Logback context, an application name, a layout, + * a levelFilterFactory, and an asyncAppenderFactory build a new appender. + * + * @param context the Logback context + * @param applicationName the application name + * @param layoutFactory the factory for the layout for logging + * @param levelFilterFactory the factory for the level filter + * @param asyncAppenderFactory the factory for the async appender + * @return a new, started {@link Appender} + */ + Appender build(LoggerContext context, + String applicationName, + LayoutFactory layoutFactory, + LevelFilterFactory levelFilterFactory, + AsyncAppenderFactory asyncAppenderFactory); +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/BootstrapLogging.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/BootstrapLogging.java new file mode 100644 index 00000000000..a6ed7a6cd1f --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/BootstrapLogging.java @@ -0,0 +1,70 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.filter.ThresholdFilter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; + +import javax.annotation.concurrent.GuardedBy; +import java.util.TimeZone; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Utility class to configure logging before the dropwizard yml + * configuration has been read, parsed, and the provided logging + * strategy has been applied. + *

    + * N.B. The methods in this class have run once semantics, + * multiple calls are idempotent + */ +public class BootstrapLogging { + + @GuardedBy("BOOTSTRAPPING_LOCK") + private static boolean bootstrapped = false; + private static final Lock BOOTSTRAPPING_LOCK = new ReentrantLock(); + + private BootstrapLogging() { + } + + // initially configure for WARN+ console logging + public static void bootstrap() { + bootstrap(Level.WARN); + } + + public static void bootstrap(Level level) { + LoggingUtil.hijackJDKLogging(); + + BOOTSTRAPPING_LOCK.lock(); + try { + if (bootstrapped) { + return; + } + final Logger root = LoggingUtil.getLoggerContext().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 appender = new ConsoleAppender<>(); + appender.addFilter(filter); + appender.setContext(root.getLoggerContext()); + + final LayoutWrappingEncoder layoutEncoder = new LayoutWrappingEncoder<>(); + layoutEncoder.setLayout(formatter); + appender.setEncoder(layoutEncoder); + appender.start(); + + root.addAppender(appender); + bootstrapped = true; + } finally { + BOOTSTRAPPING_LOCK.unlock(); + } + } +} 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 00000000000..71bffed9c30 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/ConsoleAppenderFactory.java @@ -0,0 +1,111 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.ConsoleAppender; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; + +import javax.validation.constraints.NotNull; + +/** + * An {@link AppenderFactory} implementation which provides an appender that writes events to the console. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code type}REQUIREDThe appender type. Must be {@code console}.
    {@code threshold}{@code ALL}The lowest level of events to print to the console.
    {@code timeZone}{@code UTC}The time zone to which event timestamps will be converted.
    {@code target}{@code stdout} + * The name of the standard stream to which events will be written. + * Can be {@code stdout} or {@code stderr}. + *
    {@code logFormat}the default format + * The Logback pattern with which events will be formatted. See + * the Logback documentation + * for details. + *
    + * + * @see AbstractAppenderFactory + */ +@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 ConsoleStream target = ConsoleStream.STDOUT; + + @JsonProperty + public ConsoleStream getTarget() { + return target; + } + + @JsonProperty + public void setTarget(ConsoleStream target) { + this.target = target; + } + + @Override + public Appender build(LoggerContext context, String applicationName, LayoutFactory layoutFactory, + LevelFilterFactory levelFilterFactory, AsyncAppenderFactory asyncAppenderFactory) { + final ConsoleAppender appender = new ConsoleAppender<>(); + appender.setName("console-appender"); + appender.setContext(context); + appender.setTarget(target.get()); + + final LayoutWrappingEncoder layoutEncoder = new LayoutWrappingEncoder<>(); + layoutEncoder.setLayout(buildLayout(context, layoutFactory)); + appender.setEncoder(layoutEncoder); + + appender.addFilter(levelFilterFactory.build(threshold)); + getFilterFactories().stream().forEach(f -> appender.addFilter(f.build())); + appender.start(); + + return wrapAsync(appender, asyncAppenderFactory); + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/DefaultLoggingFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/DefaultLoggingFactory.java new file mode 100644 index 00000000000..e6364b4f1b2 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/DefaultLoggingFactory.java @@ -0,0 +1,235 @@ +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.jmx.JMXConfigurator; +import ch.qos.logback.classic.jul.LevelChangePropagator; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.util.StatusPrinter; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.logback.InstrumentedAppender; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.MoreObjects; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.async.AsyncLoggingEventAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.filter.ThresholdLevelFilterFactory; +import io.dropwizard.logging.layout.DropwizardLayoutFactory; +import io.dropwizard.logging.layout.LayoutFactory; + +import javax.management.InstanceAlreadyExistsException; +import javax.management.MBeanRegistrationException; +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.NotCompliantMBeanException; +import javax.management.ObjectName; +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.io.PrintStream; +import java.lang.management.ManagementFactory; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; + +import static java.util.Objects.requireNonNull; + +@JsonTypeName("default") +public class DefaultLoggingFactory implements LoggingFactory { + private static final ReentrantLock MBEAN_REGISTRATION_LOCK = new ReentrantLock(); + private static final ReentrantLock CHANGE_LOGGER_CONTEXT_LOCK = new ReentrantLock(); + + @NotNull + private Level level = Level.INFO; + + @NotNull + private ImmutableMap loggers = ImmutableMap.of(); + + @Valid + @NotNull + private ImmutableList> appenders = ImmutableList.of( + new ConsoleAppenderFactory<>() + ); + + @JsonIgnore + private final LoggerContext loggerContext; + + @JsonIgnore + private final PrintStream configurationErrorsStream; + + public DefaultLoggingFactory() { + this(LoggingUtil.getLoggerContext(), System.err); + } + + @VisibleForTesting + DefaultLoggingFactory(LoggerContext loggerContext, PrintStream configurationErrorsStream) { + this.loggerContext = requireNonNull(loggerContext); + this.configurationErrorsStream = requireNonNull(configurationErrorsStream); + } + + @VisibleForTesting + LoggerContext getLoggerContext() { + return loggerContext; + } + + @VisibleForTesting + PrintStream getConfigurationErrorsStream() { + return configurationErrorsStream; + } + + @JsonProperty + public Level getLevel() { + return level; + } + + @JsonProperty + public void setLevel(Level level) { + this.level = level; + } + + @JsonProperty + public ImmutableMap getLoggers() { + return loggers; + } + + @JsonProperty + public void setLoggers(Map loggers) { + this.loggers = ImmutableMap.copyOf(loggers); + } + + @JsonProperty + public ImmutableList> getAppenders() { + return appenders; + } + + @JsonProperty + public void setAppenders(List> appenders) { + this.appenders = ImmutableList.copyOf(appenders); + } + + @Override + public void configure(MetricRegistry metricRegistry, String name) { + LoggingUtil.hijackJDKLogging(); + + CHANGE_LOGGER_CONTEXT_LOCK.lock(); + final Logger root; + try { + root = configureLoggers(name); + } finally { + CHANGE_LOGGER_CONTEXT_LOCK.unlock(); + } + + final LevelFilterFactory levelFilterFactory = new ThresholdLevelFilterFactory(); + final AsyncAppenderFactory asyncAppenderFactory = new AsyncLoggingEventAppenderFactory(); + final LayoutFactory layoutFactory = new DropwizardLayoutFactory(); + + for (AppenderFactory output : appenders) { + root.addAppender(output.build(loggerContext, name, layoutFactory, levelFilterFactory, asyncAppenderFactory)); + } + + StatusPrinter.setPrintStream(configurationErrorsStream); + try { + StatusPrinter.printIfErrorsOccured(loggerContext); + } finally { + StatusPrinter.setPrintStream(System.out); + } + + final MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + MBEAN_REGISTRATION_LOCK.lock(); + try { + final ObjectName objectName = new ObjectName("io.dropwizard:type=Logging"); + if (!server.isRegistered(objectName)) { + server.registerMBean(new JMXConfigurator(loggerContext, + server, + objectName), + objectName); + } + } catch (MalformedObjectNameException | InstanceAlreadyExistsException | + NotCompliantMBeanException | MBeanRegistrationException e) { + throw new RuntimeException(e); + } finally { + MBEAN_REGISTRATION_LOCK.unlock(); + } + + configureInstrumentation(root, metricRegistry); + } + + @Override + public void stop() { + // Should acquire the lock to avoid concurrent listener changes + CHANGE_LOGGER_CONTEXT_LOCK.lock(); + try { + loggerContext.stop(); + } finally { + CHANGE_LOGGER_CONTEXT_LOCK.unlock(); + } + } + + private void configureInstrumentation(Logger root, MetricRegistry metricRegistry) { + final InstrumentedAppender appender = new InstrumentedAppender(metricRegistry); + appender.setContext(loggerContext); + appender.start(); + root.addAppender(appender); + } + + private Logger configureLoggers(String name) { + final Logger root = loggerContext.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + loggerContext.reset(); + + final LevelChangePropagator propagator = new LevelChangePropagator(); + propagator.setContext(loggerContext); + propagator.setResetJUL(true); + + loggerContext.addListener(propagator); + + root.setLevel(level); + + final LevelFilterFactory levelFilterFactory = new ThresholdLevelFilterFactory(); + final AsyncAppenderFactory asyncAppenderFactory = new AsyncLoggingEventAppenderFactory(); + final LayoutFactory layoutFactory = new DropwizardLayoutFactory(); + + for (Map.Entry entry : loggers.entrySet()) { + final Logger logger = loggerContext.getLogger(entry.getKey()); + final JsonNode jsonNode = entry.getValue(); + if (jsonNode.isTextual()) { + // Just a level as a string + logger.setLevel(Level.valueOf(jsonNode.asText())); + } else if (jsonNode.isObject()) { + // A level and an appender + final LoggerConfiguration configuration; + try { + configuration = Jackson.newObjectMapper().treeToValue(jsonNode, LoggerConfiguration.class); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Wrong format of logger '" + entry.getKey() + "'", e); + } + logger.setLevel(configuration.getLevel()); + logger.setAdditive(configuration.isAdditive()); + + for (AppenderFactory appender : configuration.getAppenders()) { + logger.addAppender(appender.build(loggerContext, name, layoutFactory, levelFilterFactory, asyncAppenderFactory)); + } + } else { + throw new IllegalArgumentException("Unsupported format of logger '" + entry.getKey() + "'"); + } + } + + return root; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("level", level) + .add("loggers", loggers) + .add("appenders", appenders) + .toString(); + } +} 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 00000000000..24da69edc49 --- /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. + *

      + *
    • Disables pattern headers.
    • + *
    • Prefixes logged exceptions with {@code !}.
    • + *
    • Sets the pattern to the given timezone.
    • + *
    + */ +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 00000000000..f648fed89e5 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/FileAppenderFactory.java @@ -0,0 +1,269 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.Appender; +import ch.qos.logback.core.FileAppender; +import ch.qos.logback.core.encoder.LayoutWrappingEncoder; +import ch.qos.logback.core.rolling.DefaultTimeBasedFileNamingAndTriggeringPolicy; +import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; +import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP; +import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; +import ch.qos.logback.core.rolling.TimeBasedFileNamingAndTriggeringPolicy; +import ch.qos.logback.core.rolling.TimeBasedRollingPolicy; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; +import io.dropwizard.util.Size; +import io.dropwizard.validation.ValidationMethod; + +import javax.validation.constraints.Min; + +/** + * An {@link AppenderFactory} implementation which provides an appender that writes events to a file, archiving older + * files as it goes. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code type}REQUIREDThe appender type. Must be {@code file}.
    {@code threshold}{@code ALL}The lowest level of events to write to the file.
    {@code currentLogFilename}REQUIREDThe filename where current events are logged.
    {@code archive}{@code true}Whether or not to archive old events in separate files.
    {@code archivedLogFilenamePattern}REQUIRED if {@code archive} is {@code true}. + * The filename pattern for archived files. + * If {@code maxFileSize} is specified, rollover is size-based, and the pattern must contain {@code %i} for + * an integer index of the archived file. + * Otherwise rollover is date-based, and the pattern must contain {@code %d}, which is replaced with the + * date in {@code yyyy-MM-dd} form. + * If the pattern ends with {@code .gz} or {@code .zip}, files will be compressed as they are archived. + *
    {@code archivedFileCount}{@code 5} + * The number of archived files to keep. Must be greater than or equal to {@code 0}. Zero is a + * special value signifying to keep infinite logs (use with caution) + *
    {@code maxFileSize}(unlimited) + * The maximum size of the currently active file before a rollover is triggered. The value can be expressed + * in bytes, kilobytes, megabytes, gigabytes, and terabytes by appending B, K, MB, GB, or TB to the + * numeric value. Examples include 100MB, 1GB, 1TB. Sizes can also be spelled out, such as 100 megabytes, + * 1 gigabyte, 1 terabyte. + *
    {@code timeZone}{@code UTC}The time zone to which event timestamps will be converted.
    {@code logFormat}the default format + * The Logback pattern with which events will be formatted. See + * the Logback documentation + * for details. + *
    + * + * @see AbstractAppenderFactory + */ +@JsonTypeName("file") +public class FileAppenderFactory extends AbstractAppenderFactory { + + private String currentLogFilename; + + private boolean archive = true; + + private String archivedLogFilenamePattern; + + @Min(0) + private int archivedFileCount = 5; + + private Size maxFileSize; + + @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 Size getMaxFileSize() { + return maxFileSize; + } + + @JsonProperty + public void setMaxFileSize(Size maxFileSize) { + this.maxFileSize = maxFileSize; + } + + @JsonIgnore + @ValidationMethod(message = "must have archivedLogFilenamePattern if archive is true") + public boolean isValidArchiveConfiguration() { + return !archive || (archivedLogFilenamePattern != null); + } + + @JsonIgnore + @ValidationMethod(message = "when specifying maxFileSize, archivedLogFilenamePattern must contain %i") + public boolean isValidForMaxFileSizeSetting() { + return !archive || maxFileSize == null || + (archivedLogFilenamePattern != null && archivedLogFilenamePattern.contains("%i")); + } + + @JsonIgnore + @ValidationMethod(message = "when archivedLogFilenamePattern contains %i, maxFileSize must be specified") + public boolean isMaxFileSizeSettingSpecified() { + return !archive || !(archivedLogFilenamePattern != null && archivedLogFilenamePattern.contains("%i")) || + maxFileSize != null; + } + + @JsonIgnore + @ValidationMethod(message = "currentLogFilename can only be null when archiving is enabled") + public boolean isValidFileConfiguration() { + return archive || currentLogFilename != null; + } + + @Override + public Appender build(LoggerContext context, String applicationName, LayoutFactory layoutFactory, + LevelFilterFactory levelFilterFactory, AsyncAppenderFactory asyncAppenderFactory) { + final FileAppender appender = buildAppender(context); + appender.setName("file-appender"); + + appender.setAppend(true); + appender.setContext(context); + + final LayoutWrappingEncoder layoutEncoder = new LayoutWrappingEncoder<>(); + layoutEncoder.setLayout(buildLayout(context, layoutFactory)); + appender.setEncoder(layoutEncoder); + + appender.setPrudent(false); + appender.addFilter(levelFilterFactory.build(threshold)); + getFilterFactories().stream().forEach(f -> appender.addFilter(f.build())); + appender.start(); + + return wrapAsync(appender, asyncAppenderFactory); + } + + protected FileAppender buildAppender(LoggerContext context) { + if (archive) { + final RollingFileAppender appender = new RollingFileAppender<>(); + appender.setFile(currentLogFilename); + + if (maxFileSize != null && !archivedLogFilenamePattern.contains("%d")) { + final FixedWindowRollingPolicy rollingPolicy = new FixedWindowRollingPolicy(); + rollingPolicy.setContext(context); + rollingPolicy.setMaxIndex(getArchivedFileCount()); + rollingPolicy.setFileNamePattern(getArchivedLogFilenamePattern()); + rollingPolicy.setParent(appender); + rollingPolicy.start(); + appender.setRollingPolicy(rollingPolicy); + + final SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); + triggeringPolicy.setMaxFileSize(String.valueOf(maxFileSize.toBytes())); + triggeringPolicy.setContext(context); + triggeringPolicy.start(); + appender.setTriggeringPolicy(triggeringPolicy); + + return appender; + } else { + final TimeBasedFileNamingAndTriggeringPolicy triggeringPolicy; + if (maxFileSize == null) { + triggeringPolicy = new DefaultTimeBasedFileNamingAndTriggeringPolicy<>(); + } else { + final SizeAndTimeBasedFNATP maxFileSizeTriggeringPolicy = new SizeAndTimeBasedFNATP<>(); + maxFileSizeTriggeringPolicy.setMaxFileSize(String.valueOf(maxFileSize.toBytes())); + triggeringPolicy = maxFileSizeTriggeringPolicy; + } + triggeringPolicy.setContext(context); + + final TimeBasedRollingPolicy 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; + } + } + + final FileAppender appender = new FileAppender<>(); + appender.setFile(currentLogFilename); + return appender; + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggerConfiguration.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggerConfiguration.java new file mode 100644 index 00000000000..b664c35fe1c --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggerConfiguration.java @@ -0,0 +1,49 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.google.common.collect.ImmutableList; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.List; + +/** + * Individual {@link Logger} configuration + */ +public class LoggerConfiguration { + + @NotNull + private Level level = Level.INFO; + + @Valid + @NotNull + private ImmutableList> appenders = ImmutableList.of(); + + private boolean additive = true; + + public boolean isAdditive() { + return additive; + } + + public void setAdditive(boolean additive) { + this.additive = additive; + } + + public Level getLevel() { + return level; + } + + public void setLevel(Level level) { + this.level = level; + } + + public ImmutableList> getAppenders() { + return appenders; + } + + public void setAppenders(List> appenders) { + this.appenders = ImmutableList.copyOf(appenders); + } +} 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 00000000000..acbf4f350f4 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingFactory.java @@ -0,0 +1,12 @@ +package io.dropwizard.logging; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = DefaultLoggingFactory.class) +public interface LoggingFactory extends Discoverable { + void configure(MetricRegistry metricRegistry, String name); + + void stop(); +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingUtil.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingUtil.java new file mode 100644 index 00000000000..f878836caa6 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/LoggingUtil.java @@ -0,0 +1,74 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.LoggerContext; +import io.dropwizard.util.Duration; +import org.slf4j.ILoggerFactory; +import org.slf4j.LoggerFactory; +import org.slf4j.bridge.SLF4JBridgeHandler; + +import javax.annotation.concurrent.GuardedBy; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public class LoggingUtil { + private static final Duration LOGGER_CONTEXT_AWAITING_TIMEOUT = Duration.seconds(10); + private static final Duration LOGGER_CONTEXT_AWAITING_SLEEP_TIME = Duration.milliseconds(100); + + @GuardedBy("JUL_HIJACKING_LOCK") + private static boolean julHijacked = false; + private static final Lock JUL_HIJACKING_LOCK = new ReentrantLock(); + + private LoggingUtil() { + } + + /** + * Acquires the logger context. + *

    + *

    It tries to correctly acquire the logger context in the multi-threaded environment. + * Because of the http://jira.qos.ch/browse/SLF4J-167 a thread, that didn't + * start initialization has a possibility to get a reference not to a real context, but to a + * substitute.

    + *

    To work around this bug we spin-loop the thread with a sensible timeout, while the + * context is not initialized. We can't just make this method synchronized, because + * {@code LoggerFactory.getILoggerFactory} doesn't safely publish own state. Threads can + * observe a stale state, even if the logger has been already initialized. That's why this + * method is not thread-safe, but it makes the best effort to return the correct result in + * the multi-threaded environment.

    + */ + public static LoggerContext getLoggerContext() { + final long startTime = System.nanoTime(); + while (true) { + final ILoggerFactory iLoggerFactory = LoggerFactory.getILoggerFactory(); + if (iLoggerFactory instanceof LoggerContext) { + return (LoggerContext) iLoggerFactory; + } + if ((System.nanoTime() - startTime) > LOGGER_CONTEXT_AWAITING_TIMEOUT.toNanoseconds()) { + throw new IllegalStateException("Unable to acquire the logger context"); + } + try { + Thread.sleep(LOGGER_CONTEXT_AWAITING_SLEEP_TIME.toMilliseconds()); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + } + + /** + * Gets the root j.u.l.Logger and removes all registered handlers + * then redirects all active j.u.l. to SLF4J + *

    + * N.B. This should only happen once, hence the flag and locking + */ + public static void hijackJDKLogging() { + JUL_HIJACKING_LOCK.lock(); + try { + if (!julHijacked) { + SLF4JBridgeHandler.removeHandlersForRootLogger(); + SLF4JBridgeHandler.install(); + julHijacked = true; + } + } finally { + JUL_HIJACKING_LOCK.unlock(); + } + } +} 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 00000000000..dc1f009cbaa --- /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 00000000000..759fcd984dd --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverter.java @@ -0,0 +1,26 @@ +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.PATTERN; +import static io.dropwizard.logging.PrefixedThrowableProxyConverter.PREFIX; + +/** + * 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 00000000000..f13bfe12ba9 --- /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 00000000000..e73972d217f --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/SyslogAppenderFactory.java @@ -0,0 +1,223 @@ +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.net.SyslogConstants; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; + +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. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code host}{@code localhost}The hostname of the syslog server.
    {@code port}{@code 514}The port on which the syslog server is listening.
    {@code facility}{@code local0} + * 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}. + *
    {@code threshold}{@code ALL}The lowest level of events to write to the file.
    {@code logFormat}the default format + * The Logback pattern with which events will be formatted. See + * the Logback documentation + * for details. + *
    + * + * @see AbstractAppenderFactory + */ +@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 build(LoggerContext context, String applicationName, LayoutFactory layoutFactory, + LevelFilterFactory levelFilterFactory, AsyncAppenderFactory asyncAppenderFactory) { + 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); + appender.addFilter(levelFilterFactory.build(threshold)); + getFilterFactories().stream().forEach(f -> appender.addFilter(f.build())); + appender.start(); + return wrapAsync(appender, asyncAppenderFactory); + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/async/AsyncAppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/async/AsyncAppenderFactory.java new file mode 100644 index 00000000000..5c4b0aa3877 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/async/AsyncAppenderFactory.java @@ -0,0 +1,17 @@ +package io.dropwizard.logging.async; + +import ch.qos.logback.core.AsyncAppenderBase; +import ch.qos.logback.core.spi.DeferredProcessingAware; + +/** + * Factory used to create an {@link AsyncAppenderBase} of type E + * @param The type of log event + */ +public interface AsyncAppenderFactory { + + /** + * Creates an {@link AsyncAppenderBase} of type E + * @return a new {@link AsyncAppenderBase} + */ + AsyncAppenderBase build(); +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/async/AsyncLoggingEventAppenderFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/async/AsyncLoggingEventAppenderFactory.java new file mode 100644 index 00000000000..5f76b6fa56f --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/async/AsyncLoggingEventAppenderFactory.java @@ -0,0 +1,20 @@ +package io.dropwizard.logging.async; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.AsyncAppenderBase; + +/** + * An implementation of {@link AsyncAppenderFactory} for {@link ILoggingEvent}. + */ +public class AsyncLoggingEventAppenderFactory implements AsyncAppenderFactory { + + /** + * Creates an {@link AsyncAppenderBase} of type {@link ILoggingEvent} + * @return the {@link AsyncAppenderBase} + */ + @Override + public AsyncAppenderBase build() { + return new AsyncAppender(); + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/FilterFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/FilterFactory.java new file mode 100644 index 00000000000..37e16461fe5 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/FilterFactory.java @@ -0,0 +1,23 @@ +package io.dropwizard.logging.filter; + +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; + +/** + * A service provider interface for creating Logback {@link Filter} instances. + *

    + * To create your own, just: + *

      + *
    1. Create a class which implements {@link FilterFactory}.
    2. + *
    3. Annotate it with {@code @JsonTypeName} and give it a unique type name.
    4. + *
    5. add a {@code META-INF/services/io.dropwizard.logging.filter.FilterFactory} file with your + * implementation's full class name to the class path.
    6. + *
    + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +public interface FilterFactory extends Discoverable { + + Filter build(); +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/LevelFilterFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/LevelFilterFactory.java new file mode 100644 index 00000000000..e7f5d76b90f --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/LevelFilterFactory.java @@ -0,0 +1,18 @@ +package io.dropwizard.logging.filter; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.DeferredProcessingAware; + +/** + * An interface for building Logback {@link Filter Filters} with a specified {@link Level}. + * @param The type of log event + */ +public interface LevelFilterFactory { + + /** + * Creates a {@link Filter} of type E + * @return a new {@link Filter} + */ + Filter build(Level threshold); +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/NullLevelFilterFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/NullLevelFilterFactory.java new file mode 100644 index 00000000000..e6b15fcaa24 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/NullLevelFilterFactory.java @@ -0,0 +1,28 @@ +package io.dropwizard.logging.filter; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.DeferredProcessingAware; +import ch.qos.logback.core.spi.FilterReply; + +/** + * Factory for building a logback {@link Filter} that will always defer to the next Filter. + * @param The type of log event + */ +public class NullLevelFilterFactory implements LevelFilterFactory { + + /** + * Creates a {@link Filter} that will always defer to the next Filter in the chain, if any. + * @param threshold the parameter is ignored + * @return a {@link Filter} with a {@link Filter#decide(Object)} method that will always return {@link FilterReply#NEUTRAL}. + */ + @Override + public Filter build(Level threshold) { + return new Filter() { + @Override + public FilterReply decide(E event) { + return FilterReply.NEUTRAL; + } + }; + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/ThresholdLevelFilterFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/ThresholdLevelFilterFactory.java new file mode 100644 index 00000000000..c7960dc6a20 --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/filter/ThresholdLevelFilterFactory.java @@ -0,0 +1,25 @@ +package io.dropwizard.logging.filter; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.filter.ThresholdFilter; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; + +/** + * Factory that creates a {@link Filter} of type {@link ILoggingEvent} + */ +public class ThresholdLevelFilterFactory implements LevelFilterFactory { + + /** + * Creates and starts a {@link Filter} for the given threshold. + * @param threshold The minimum event level for this filter. + * @return a new, started {@link Filter} + */ + @Override + public Filter build(Level threshold) { + final ThresholdFilter filter = new ThresholdFilter(); + filter.setLevel(threshold.toString()); + filter.start(); + return filter; + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/layout/DropwizardLayoutFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/layout/DropwizardLayoutFactory.java new file mode 100644 index 00000000000..db4542479ab --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/layout/DropwizardLayoutFactory.java @@ -0,0 +1,18 @@ +package io.dropwizard.logging.layout; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.PatternLayoutBase; +import io.dropwizard.logging.DropwizardLayout; + +import java.util.TimeZone; + +/** + * Factory that creates a {@link DropwizardLayout} + */ +public class DropwizardLayoutFactory implements LayoutFactory { + @Override + public PatternLayoutBase build(LoggerContext context, TimeZone timeZone) { + return new DropwizardLayout(context, timeZone); + } +} diff --git a/dropwizard-logging/src/main/java/io/dropwizard/logging/layout/LayoutFactory.java b/dropwizard-logging/src/main/java/io/dropwizard/logging/layout/LayoutFactory.java new file mode 100644 index 00000000000..b8717e4f04a --- /dev/null +++ b/dropwizard-logging/src/main/java/io/dropwizard/logging/layout/LayoutFactory.java @@ -0,0 +1,22 @@ +package io.dropwizard.logging.layout; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.pattern.PatternLayoutBase; +import ch.qos.logback.core.spi.DeferredProcessingAware; + +import java.util.TimeZone; + +/** + * An interface for building Logback {@link PatternLayoutBase} layouts + * @param The type of log event + */ +public interface LayoutFactory { + + /** + * Creates a {@link PatternLayoutBase} of type E + * @param context the Logback context + * @param timeZone the TimeZone + * @return a new {@link PatternLayoutBase} + */ + PatternLayoutBase build(LoggerContext context, TimeZone timeZone); +} 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 100755 index 00000000000..096aa427026 --- /dev/null +++ b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.jackson.Discoverable @@ -0,0 +1,3 @@ +io.dropwizard.logging.AppenderFactory +io.dropwizard.logging.LoggingFactory +io.dropwizard.logging.filter.FilterFactory 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 00000000000..7b88e1d65be --- /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/main/resources/META-INF/services/io.dropwizard.logging.LoggingFactory b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.logging.LoggingFactory new file mode 100644 index 00000000000..ed0414dd94d --- /dev/null +++ b/dropwizard-logging/src/main/resources/META-INF/services/io.dropwizard.logging.LoggingFactory @@ -0,0 +1 @@ +io.dropwizard.logging.DefaultLoggingFactory 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 00000000000..47c8a147f27 --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/ConsoleAppenderFactoryTest.java @@ -0,0 +1,56 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.logging.async.AsyncLoggingEventAppenderFactory; +import io.dropwizard.logging.filter.NullLevelFilterFactory; +import io.dropwizard.logging.layout.DropwizardLayoutFactory; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ConsoleAppenderFactoryTest { + static { + BootstrapLogging.bootstrap(); + } + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(ConsoleAppenderFactory.class); + } + + @Test + public void includesCallerData() { + ConsoleAppenderFactory consoleAppenderFactory = new ConsoleAppenderFactory<>(); + AsyncAppender asyncAppender = (AsyncAppender) consoleAppenderFactory.build(new LoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + assertThat(asyncAppender.isIncludeCallerData()).isFalse(); + + consoleAppenderFactory.setIncludeCallerData(true); + asyncAppender = (AsyncAppender) consoleAppenderFactory.build(new LoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + assertThat(asyncAppender.isIncludeCallerData()).isTrue(); + } + + @Test + public void appenderContextIsSet() throws Exception { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final ConsoleAppenderFactory appenderFactory = new ConsoleAppenderFactory<>(); + final Appender appender = appenderFactory.build(root.getLoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + + assertThat(appender.getContext()).isEqualTo(root.getLoggerContext()); + } + + @Test + public void appenderNameIsSet() throws Exception { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final ConsoleAppenderFactory appenderFactory = new ConsoleAppenderFactory<>(); + final Appender appender = appenderFactory.build(root.getLoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + + assertThat(appender.getName()).isEqualTo("async-console-appender"); + } +} diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/DefaultLoggingFactoryPrintErrorMessagesTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/DefaultLoggingFactoryPrintErrorMessagesTest.java new file mode 100644 index 00000000000..b7a1f0da4b8 --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/DefaultLoggingFactoryPrintErrorMessagesTest.java @@ -0,0 +1,106 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.util.StatusPrinter; +import com.codahale.metrics.MetricRegistry; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +public class DefaultLoggingFactoryPrintErrorMessagesTest { + + @Rule + public final TemporaryFolder tempDir = new TemporaryFolder(); + + DefaultLoggingFactory factory; + ByteArrayOutputStream output; + + @Before + public void setUp() throws Exception { + output = new ByteArrayOutputStream(); + factory = new DefaultLoggingFactory(new LoggerContext(), new PrintStream(output)); + } + + @After + public void tearDown() throws Exception { + factory.stop(); + } + + private void configureLoggingFactoryWithFileAppender(File file) { + factory.setAppenders(singletonList(newFileAppenderFactory(file))); + } + + private AppenderFactory newFileAppenderFactory(File file) { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory<>(); + + fileAppenderFactory.setCurrentLogFilename(file.toString() + File.separator + "my-log-file.log"); + fileAppenderFactory.setArchive(false); + + return fileAppenderFactory; + } + + private String configureAndGetOutputWrittenToErrorStream() throws UnsupportedEncodingException { + factory.configure(new MetricRegistry(), "logger-test"); + + return output.toString(StandardCharsets.UTF_8.name()); + } + + @Test + public void testWhenUsingDefaultConstructor_SystemErrIsSet() throws Exception { + PrintStream configurationErrorsStream = new DefaultLoggingFactory().getConfigurationErrorsStream(); + + assertThat(configurationErrorsStream).isSameAs(System.err); + } + + @Test + public void testWhenUsingDefaultConstructor_StaticILoggerFactoryIsSet() throws Exception { + LoggerContext loggerContext = new DefaultLoggingFactory().getLoggerContext(); + + assertThat(loggerContext).isSameAs(LoggerFactory.getILoggerFactory()); + } + + @Test + public void testWhenFileAppenderDoesNotHaveWritePermissionToFolder_PrintsErrorMessageToConsole() throws Exception { + File folderWithoutWritePermission = tempDir.newFolder("folder-without-write-permission"); + assumeTrue(folderWithoutWritePermission.setWritable(false)); + + configureLoggingFactoryWithFileAppender(folderWithoutWritePermission); + + assertThat(folderWithoutWritePermission.canWrite()).isFalse(); + assertThat(configureAndGetOutputWrittenToErrorStream()).contains(folderWithoutWritePermission.toString()); + } + + @Test + public void testWhenSettingUpLoggingWithValidConfiguration_NoErrorMessageIsPrintedToConsole() throws Exception { + File folderWithWritePermission = tempDir.newFolder("folder-with-write-permission"); + + configureLoggingFactoryWithFileAppender(folderWithWritePermission); + + assertThat(folderWithWritePermission.canWrite()).isTrue(); + assertThat(configureAndGetOutputWrittenToErrorStream()).isEmpty(); + } + + @Test + public void testLogbackStatusPrinterPrintStreamIsRestoredToSystemOut() throws Exception { + Field field = StatusPrinter.class.getDeclaredField("ps"); + field.setAccessible(true); + + PrintStream out = (PrintStream) field.get(null); + assertThat(out).isSameAs(System.out); + } +} diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/DefaultLoggingFactoryTest.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/DefaultLoggingFactoryTest.java new file mode 100644 index 00000000000..47c28009646 --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/DefaultLoggingFactoryTest.java @@ -0,0 +1,134 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.TextNode; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Files; +import com.google.common.io.Resources; +import io.dropwizard.configuration.FileConfigurationSourceProvider; +import io.dropwizard.configuration.SubstitutingSourceProvider; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.logging.filter.FilterFactory; +import io.dropwizard.validation.BaseValidator; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.text.StrSubstitutor; +import org.assertj.core.data.MapEntry; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; + +public class DefaultLoggingFactoryTest { + private final ObjectMapper objectMapper = Jackson.newObjectMapper(); + private final YamlConfigurationFactory factory = new YamlConfigurationFactory<>( + DefaultLoggingFactory.class, + BaseValidator.newValidator(), + objectMapper, "dw"); + + private DefaultLoggingFactory config; + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class, + FileAppenderFactory.class, + SyslogAppenderFactory.class); + + 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 canParseNewLoggerFormat() throws Exception { + final DefaultLoggingFactory config = factory.build( + new File(Resources.getResource("yaml/logging_advanced.yml").toURI())); + + assertThat(config.getLoggers()).contains(MapEntry.entry("com.example.app", new TextNode("INFO"))); + + final JsonNode newApp = config.getLoggers().get("com.example.newApp"); + assertThat(newApp).isNotNull(); + final LoggerConfiguration newAppConfiguration = objectMapper.treeToValue(newApp, LoggerConfiguration.class); + assertThat(newAppConfiguration.getLevel()).isEqualTo(Level.DEBUG); + assertThat(newAppConfiguration.getAppenders()).hasSize(1); + final AppenderFactory appenderFactory = newAppConfiguration.getAppenders().get(0); + assertThat(appenderFactory).isInstanceOf(FileAppenderFactory.class); + final FileAppenderFactory fileAppenderFactory = (FileAppenderFactory) appenderFactory; + assertThat(fileAppenderFactory.getCurrentLogFilename()).isEqualTo("${new_app}.log"); + assertThat(fileAppenderFactory.getArchivedLogFilenamePattern()).isEqualTo("${new_app}-%d.log.gz"); + assertThat(fileAppenderFactory.getArchivedFileCount()).isEqualTo(5); + final ImmutableList> filterFactories = fileAppenderFactory.getFilterFactories(); + assertThat(filterFactories).hasSize(2); + assertThat(filterFactories.get(0)).isExactlyInstanceOf(TestFilterFactory.class); + assertThat(filterFactories.get(1)).isExactlyInstanceOf(SecondTestFilterFactory.class); + + final JsonNode legacyApp = config.getLoggers().get("com.example.legacyApp"); + assertThat(legacyApp).isNotNull(); + final LoggerConfiguration legacyAppConfiguration = objectMapper.treeToValue(legacyApp, LoggerConfiguration.class); + assertThat(legacyAppConfiguration.getLevel()).isEqualTo(Level.DEBUG); + // We should not create additional appenders, if they are not specified + assertThat(legacyAppConfiguration.getAppenders()).isEmpty(); + } + + @Test + public void testConfigure() throws Exception { + final File newAppLog = folder.newFile("example-new-app.log"); + final File newAppNotAdditiveLog = folder.newFile("example-new-app-not-additive.log"); + final File defaultLog = folder.newFile("example.log"); + final StrSubstitutor substitutor = new StrSubstitutor(ImmutableMap.of( + "new_app", StringUtils.removeEnd(newAppLog.getAbsolutePath(), ".log"), + "new_app_not_additive", StringUtils.removeEnd(newAppNotAdditiveLog.getAbsolutePath(), ".log"), + "default", StringUtils.removeEnd(defaultLog.getAbsolutePath(), ".log") + )); + + final String configPath = Resources.getResource("yaml/logging_advanced.yml").getFile(); + final DefaultLoggingFactory config = factory.build( + new SubstitutingSourceProvider(new FileConfigurationSourceProvider(), substitutor), + configPath); + config.configure(new MetricRegistry(), "test-logger"); + + LoggerFactory.getLogger("com.example.app").debug("Application debug log"); + LoggerFactory.getLogger("com.example.app").info("Application log"); + LoggerFactory.getLogger("com.example.newApp").debug("New application debug log"); + LoggerFactory.getLogger("com.example.newApp").info("New application info log"); + LoggerFactory.getLogger("com.example.legacyApp").debug("Legacy application debug log"); + LoggerFactory.getLogger("com.example.legacyApp").info("Legacy application info log"); + LoggerFactory.getLogger("com.example.notAdditive").debug("Not additive application debug log"); + LoggerFactory.getLogger("com.example.notAdditive").info("Not additive application info log"); + + config.stop(); + + assertThat(Files.readLines(defaultLog, StandardCharsets.UTF_8)).containsOnly( + "INFO com.example.app: Application log", + "DEBUG com.example.newApp: New application debug log", + "INFO com.example.newApp: New application info log", + "DEBUG com.example.legacyApp: Legacy application debug log", + "INFO com.example.legacyApp: Legacy application info log"); + + assertThat(Files.readLines(newAppLog, StandardCharsets.UTF_8)).containsOnly( + "DEBUG com.example.newApp: New application debug log", + "INFO com.example.newApp: New application info log"); + + assertThat(Files.readLines(newAppNotAdditiveLog, StandardCharsets.UTF_8)).containsOnly( + "DEBUG com.example.notAdditive: Not additive application debug log", + "INFO com.example.notAdditive: Not additive application info log"); + } + +} 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 00000000000..089a29263b7 --- /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.assertj.core.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 00000000000..22a9e9aabaf --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/FileAppenderFactoryTest.java @@ -0,0 +1,219 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Logger; +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.rolling.FixedWindowRollingPolicy; +import ch.qos.logback.core.rolling.RollingFileAppender; +import ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP; +import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; +import com.google.common.collect.ImmutableList; +import com.google.common.io.Files; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.logging.async.AsyncLoggingEventAppenderFactory; +import io.dropwizard.logging.filter.NullLevelFilterFactory; +import io.dropwizard.logging.layout.DropwizardLayoutFactory; +import io.dropwizard.util.Size; +import io.dropwizard.validation.BaseValidator; +import io.dropwizard.validation.ConstraintViolations; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.slf4j.LoggerFactory; + +import javax.validation.Validator; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FileAppenderFactoryTest { + + static { + BootstrapLogging.bootstrap(); + } + + private final Validator validator = BaseValidator.newValidator(); + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(FileAppenderFactory.class); + } + + @Test + public void includesCallerData() { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory<>(); + fileAppenderFactory.setArchive(false); + AsyncAppender asyncAppender = (AsyncAppender) fileAppenderFactory.build(new LoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + assertThat(asyncAppender.isIncludeCallerData()).isFalse(); + + fileAppenderFactory.setIncludeCallerData(true); + asyncAppender = (AsyncAppender) fileAppenderFactory.build(new LoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + assertThat(asyncAppender.isIncludeCallerData()).isTrue(); + } + + @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 buildAppender(LoggerContext context) { + return super.buildAppender(context); + } + }; + + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + fileAppenderFactory.setArchive(true); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d.log.gz").toString()); + assertThat(fileAppenderFactory.buildAppender(new LoggerContext())).isInstanceOf(RollingFileAppender.class); + } + + @Test + public void hasArchivedLogFilenamePattern() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + ImmutableList errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors) + .containsOnly("must have archivedLogFilenamePattern if archive is true"); + fileAppenderFactory.setArchive(false); + errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors).isEmpty(); + } + + @Test + public void isValidForInfiniteRolledFiles() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + fileAppenderFactory.setArchivedFileCount(0); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d.log.gz").toString()); + ImmutableList errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors).isEmpty(); + assertThat(fileAppenderFactory.buildAppender(new LoggerContext())).isNotNull(); + } + + @Test + public void isValidForMaxFileSize() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + fileAppenderFactory.setMaxFileSize(Size.kilobytes(1)); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d.log.gz").toString()); + ImmutableList errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors) + .containsOnly("when specifying maxFileSize, archivedLogFilenamePattern must contain %i"); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d-%i.log.gz").toString()); + errors = ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors).isEmpty(); + } + + @Test + public void hasMaxFileSizeValidation() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%i.log.gz").toString()); + ImmutableList errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors) + .containsOnly("when archivedLogFilenamePattern contains %i, maxFileSize must be specified"); + fileAppenderFactory.setMaxFileSize(Size.kilobytes(1)); + errors = ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors).isEmpty(); + } + + @Test + public void testCurrentFileNameErrorWhenArchiveIsNotEnabled() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setArchive(false); + ImmutableList errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors) + .containsOnly("currentLogFilename can only be null when archiving is enabled"); + fileAppenderFactory.setCurrentLogFilename("test"); + errors = ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors).isEmpty(); + } + + @Test + public void testCurrentFileNameCanBeNullWhenArchiveIsEnabled() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setArchive(true); + fileAppenderFactory.setArchivedLogFilenamePattern("name-to-be-used"); + fileAppenderFactory.setCurrentLogFilename(null); + ImmutableList errors = + ConstraintViolations.format(validator.validate(fileAppenderFactory)); + assertThat(errors).isEmpty(); + } + + @Test + public void testCurrentLogFileNameIsEmptyAndAppenderUsesArchivedNameInstead() throws Exception { + final FileAppenderFactory appenderFactory = new FileAppenderFactory<>(); + appenderFactory.setArchivedLogFilenamePattern(folder.newFile("test-archived-name-%d.log").toString()); + final FileAppender rollingAppender = appenderFactory.buildAppender(new LoggerContext()); + + final String file = rollingAppender.getFile(); + final String dateSuffix = LocalDateTime.now().format(DateTimeFormatter.ofPattern("YYYY-MM-dd")); + final String name = Files.getNameWithoutExtension(file); + Assert.assertEquals("test-archived-name-" + dateSuffix, name); + } + + @Test + public void hasMaxFileSize() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + fileAppenderFactory.setArchive(true); + fileAppenderFactory.setMaxFileSize(Size.kilobytes(1)); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d-%i.log.gz").toString()); + RollingFileAppender appender = (RollingFileAppender) fileAppenderFactory.buildAppender(new LoggerContext()); + + assertThat(appender.getTriggeringPolicy()).isInstanceOf(SizeAndTimeBasedFNATP.class); + assertThat(((SizeAndTimeBasedFNATP) appender.getTriggeringPolicy()).getMaxFileSize()).isEqualTo("1024"); + } + + @Test + public void hasMaxFileSizeFixedWindow() throws Exception { + FileAppenderFactory fileAppenderFactory = new FileAppenderFactory(); + fileAppenderFactory.setCurrentLogFilename(folder.newFile("logfile.log").toString()); + fileAppenderFactory.setArchive(true); + fileAppenderFactory.setMaxFileSize(Size.kilobytes(1)); + fileAppenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%i.log.gz").toString()); + RollingFileAppender appender = (RollingFileAppender) fileAppenderFactory.buildAppender(new LoggerContext()); + + assertThat(appender.getRollingPolicy()).isInstanceOf(FixedWindowRollingPolicy.class); + assertThat(appender.getRollingPolicy().isStarted()).isTrue(); + + assertThat(appender.getTriggeringPolicy()).isInstanceOf(SizeBasedTriggeringPolicy.class); + assertThat(appender.getTriggeringPolicy().isStarted()).isTrue(); + assertThat(((SizeBasedTriggeringPolicy) appender.getTriggeringPolicy()).getMaxFileSize()).isEqualTo("1024"); + } + + @Test + public void appenderContextIsSet() throws Exception { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final FileAppenderFactory appenderFactory = new FileAppenderFactory<>(); + appenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d.log.gz").toString()); + final Appender appender = appenderFactory.build(root.getLoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + + assertThat(appender.getContext()).isEqualTo(root.getLoggerContext()); + } + + @Test + public void appenderNameIsSet() throws Exception { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final FileAppenderFactory appenderFactory = new FileAppenderFactory<>(); + appenderFactory.setArchivedLogFilenamePattern(folder.newFile("example-%d.log.gz").toString()); + final Appender appender = appenderFactory.build(root.getLoggerContext(), "test", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + + assertThat(appender.getName()).isEqualTo("async-file-appender"); + } +} 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 00000000000..8272a8764ce --- /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 org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +import static org.assertj.core.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(Collections.singletonList("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.(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 00000000000..d7ebf3d8cbb --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/PrefixedRootCauseFirstThrowableProxyConverterTest.java @@ -0,0 +1,80 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.spi.ThrowableProxy; +import com.google.common.base.Splitter; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import static org.assertj.core.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 static 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(Collections.singletonList("full")); + converter.start(); + } + + @Test + public void prefixesExceptionsWithExclamationMarks() { + final List stackTrace = Splitter.on(System.lineSeparator()).omitEmptyStrings() + .splitToList(converter.throwableProxyToString(proxy)); + assertThat(stackTrace).isNotEmpty(); + for (String line : stackTrace) { + assertThat(line).startsWith("!"); + } + } + + @Test + public void placesRootCauseIsFirst() { + assertThat(converter.throwableProxyToString(proxy)).matches(Pattern.compile(".+" + + "java\\.net\\.SocketTimeoutException: Timed-out reading from socket.+" + + "java\\.io\\.IOException: Fairly general error doing some IO.+" + + "java\\.lang\\.RuntimeException: Very general error doing something" + + ".+", Pattern.DOTALL)); + } +} 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 00000000000..33cb24c8466 --- /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 org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +import static org.assertj.core.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(Collections.singletonList("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.(PrefixedThrowableProxyConverterTest.java:14)%n")); + } +} diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/SecondTestFilterFactory.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/SecondTestFilterFactory.java new file mode 100644 index 00000000000..bf8e6608c22 --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/SecondTestFilterFactory.java @@ -0,0 +1,21 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.logging.filter.FilterFactory; + +@JsonTypeName("second-test-filter-factory") +public class SecondTestFilterFactory implements FilterFactory { + + @Override + public Filter build() { + return new Filter() { + @Override + public FilterReply decide(ILoggingEvent event) { + return FilterReply.NEUTRAL; + } + }; + } +} 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 00000000000..020bca5f2f7 --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/SyslogAppenderFactoryTest.java @@ -0,0 +1,79 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.AsyncAppender; +import ch.qos.logback.classic.Logger; +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 io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.logging.async.AsyncLoggingEventAppenderFactory; +import io.dropwizard.logging.filter.NullLevelFilterFactory; +import io.dropwizard.logging.layout.DropwizardLayoutFactory; +import org.junit.Test; +import org.slf4j.LoggerFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SyslogAppenderFactoryTest { + + static { + BootstrapLogging.bootstrap(); + } + + @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 { + final AsyncAppender wrapper = (AsyncAppender) new SyslogAppenderFactory() + .build(new LoggerContext(), "MyApplication", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + assertThat(((SyslogAppender) wrapper.getAppender("syslog-appender")).getSuffixPattern()) + .matches("^MyApplication\\[\\d+\\].+"); + } + + @Test + public void stackTracePatternCanBeSet() throws Exception { + final SyslogAppenderFactory syslogAppenderFactory = new SyslogAppenderFactory(); + syslogAppenderFactory.setStackTracePrefix("--->"); + final AsyncAppender wrapper = (AsyncAppender) syslogAppenderFactory + .build(new LoggerContext(), "MyApplication", new DropwizardLayoutFactory(), new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + assertThat(((SyslogAppender) wrapper.getAppender("syslog-appender")) + .getStackTracePattern()).isEqualTo("--->"); + } + + @Test + public void appenderContextIsSet() throws Exception { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final SyslogAppenderFactory appenderFactory = new SyslogAppenderFactory(); + final Appender appender = appenderFactory.build(root.getLoggerContext(), "test", new DropwizardLayoutFactory(), + new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + + assertThat(appender.getContext()).isEqualTo(root.getLoggerContext()); + } + + @Test + public void appenderNameIsSet() throws Exception { + final Logger root = (Logger) LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME); + final SyslogAppenderFactory appenderFactory = new SyslogAppenderFactory(); + final Appender appender = appenderFactory.build(root.getLoggerContext(), "test", new DropwizardLayoutFactory(), + new NullLevelFilterFactory<>(), new AsyncLoggingEventAppenderFactory()); + + assertThat(appender.getName()).isEqualTo("async-syslog-appender"); + } +} diff --git a/dropwizard-logging/src/test/java/io/dropwizard/logging/TestFilterFactory.java b/dropwizard-logging/src/test/java/io/dropwizard/logging/TestFilterFactory.java new file mode 100644 index 00000000000..ad975f76356 --- /dev/null +++ b/dropwizard-logging/src/test/java/io/dropwizard/logging/TestFilterFactory.java @@ -0,0 +1,21 @@ +package io.dropwizard.logging; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.filter.Filter; +import ch.qos.logback.core.spi.FilterReply; +import com.fasterxml.jackson.annotation.JsonTypeName; +import io.dropwizard.logging.filter.FilterFactory; + +@JsonTypeName("test-filter-factory") +public class TestFilterFactory implements FilterFactory { + + @Override + public Filter build() { + return new Filter() { + @Override + public FilterReply decide(ILoggingEvent event) { + return FilterReply.NEUTRAL; + } + }; + } +} diff --git a/dropwizard-logging/src/test/resources/META-INF/services/io.dropwizard.logging.filter.FilterFactory b/dropwizard-logging/src/test/resources/META-INF/services/io.dropwizard.logging.filter.FilterFactory new file mode 100644 index 00000000000..d28049c63db --- /dev/null +++ b/dropwizard-logging/src/test/resources/META-INF/services/io.dropwizard.logging.filter.FilterFactory @@ -0,0 +1,2 @@ +io.dropwizard.logging.TestFilterFactory +io.dropwizard.logging.SecondTestFilterFactory 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-logging/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + 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 00000000000..5e8812ab0ad --- /dev/null +++ b/dropwizard-logging/src/test/resources/yaml/logging.yml @@ -0,0 +1,26 @@ +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: file + threshold: ALL + maxFileSize: 100MB + currentLogFilename: ./logs/max-file-size-example.log + archivedLogFilenamePattern: ./logs/max-file-size-example-%d-%i.log.gz + archivedFileCount: 5 + - type: file + threshold: ALL + maxFileSize: 100MB + currentLogFilename: ./logs/max-file-size-example.log + archivedLogFilenamePattern: ./logs/max-file-size-example-%i.log.gz + archivedFileCount: 5 + - type: syslog + host: localhost + facility: local0 diff --git a/dropwizard-logging/src/test/resources/yaml/logging_advanced.yml b/dropwizard-logging/src/test/resources/yaml/logging_advanced.yml new file mode 100644 index 00000000000..57d4ec03664 --- /dev/null +++ b/dropwizard-logging/src/test/resources/yaml/logging_advanced.yml @@ -0,0 +1,32 @@ +level: INFO +loggers: + "com.example.app": INFO + "com.example.newApp": + level: DEBUG + appenders: + - type: file + filterFactories: + - type: test-filter-factory + - type: second-test-filter-factory + currentLogFilename: '${new_app}.log' + archivedLogFilenamePattern: '${new_app}-%d.log.gz' + logFormat: "%-5level %logger: %msg%n" + archivedFileCount: 5 + "com.example.legacyApp": + level: DEBUG + "com.example.notAdditive": + level: DEBUG + additive: false + appenders: + - type: file + currentLogFilename: '${new_app_not_additive}.log' + archivedLogFilenamePattern: '${new_app_not_additive}-%d.log.gz' + logFormat: "%-5level %logger: %msg%n" + archivedFileCount: 5 +appenders: + - type: console + - type: file + currentLogFilename: '${default}.log' + archivedLogFilenamePattern: '${default}-%d.log.gz' + logFormat: "%-5level %logger: %msg%n" + archivedFileCount: 5 diff --git a/dropwizard-metrics-ganglia/pom.xml b/dropwizard-metrics-ganglia/pom.xml new file mode 100644 index 00000000000..8649740e977 --- /dev/null +++ b/dropwizard-metrics-ganglia/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-metrics-ganglia + Dropwizard Metrics Support for Ganglia + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-metrics + + + io.dropwizard.metrics + metrics-ganglia + + + io.dropwizard + dropwizard-configuration + test + + + 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 00000000000..c14c8c396c8 --- /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 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.Optional; +import java.util.UUID; + +/** + * A factory for {@link GangliaReporter} instances. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    hostlocalhostThe hostname (or group) of the Ganglia server(s) to report to.
    port8649The port of the Ganglia server(s) to report to.
    modeunicastThe UDP addressing mode to announce the metrics with. One of {@code unicast} or + * {@code multicast}.
    ttl1The time-to-live of the UDP packets for the announced metrics.
    uuidNoneThe UUID to tag announced metrics with.
    spoofNoneThe hostname and port to use instead of this nodes for the announced metrics. In the + * format {@code hostname:port}.
    tmax60The tmax value to annouce metrics with.
    dmax0The dmax value to announce metrics with.
    + */ +@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 getUuid() { + return Optional.ofNullable(uuid); + } + + @JsonProperty + public void setUuid(UUID uuid) { + this.uuid = uuid; + } + + @JsonProperty + public Optional getSpoof() { + return Optional.ofNullable(spoof); + } + + @JsonProperty + public void setSpoof(String spoof) { + this.spoof = spoof; + } + + @Override + public ScheduledReporter build(MetricRegistry registry) { + try { + final 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 00000000000..c4024362b30 --- /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 00000000000..646e31c433b --- /dev/null +++ b/dropwizard-metrics-ganglia/src/test/java/io/dropwizard/metrics/ganglia/GangliaReporterFactoryTest.java @@ -0,0 +1,28 @@ +package io.dropwizard.metrics.ganglia; + +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.validation.BaseValidator; +import org.junit.Test; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GangliaReporterFactoryTest { + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(GangliaReporterFactory.class); + } + + @Test + public void createDefaultFactory() throws Exception { + final GangliaReporterFactory factory = new YamlConfigurationFactory<>(GangliaReporterFactory.class, + BaseValidator.newValidator(), Jackson.newObjectMapper(), "dw") + .build(); + assertThat(factory.getFrequency()).isEqualTo(Optional.empty()); + } +} diff --git a/dropwizard-metrics-graphite/pom.xml b/dropwizard-metrics-graphite/pom.xml new file mode 100644 index 00000000000..48930c62310 --- /dev/null +++ b/dropwizard-metrics-graphite/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-metrics-graphite + Dropwizard Metrics Support for Graphite + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-metrics + + + io.dropwizard.metrics + metrics-graphite + + + io.dropwizard + dropwizard-configuration + test + + + org.apache.commons + commons-lang3 + test + + + 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 00000000000..b879f961ce7 --- /dev/null +++ b/dropwizard-metrics-graphite/src/main/java/io/dropwizard/metrics/graphite/GraphiteReporterFactory.java @@ -0,0 +1,125 @@ +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.codahale.metrics.graphite.GraphiteUDP; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.common.annotations.VisibleForTesting; +import io.dropwizard.metrics.BaseReporterFactory; +import io.dropwizard.validation.OneOf; +import io.dropwizard.validation.PortRange; +import org.hibernate.validator.constraints.NotEmpty; + +import javax.validation.constraints.NotNull; + +/** + * A factory for {@link GraphiteReporter} instances. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    hostlocalhostThe hostname of the Graphite server to report to.
    port2003The port of the Graphite server to report to.
    prefixNoneThe prefix for Metric key names to report to Graphite.
    transporttcpThe transport used to report to Graphite. One of {@code tcp} or + * {@code udp}.
    + */ +@JsonTypeName("graphite") +public class GraphiteReporterFactory extends BaseReporterFactory { + @NotEmpty + private String host = "localhost"; + + @PortRange + private int port = 2003; + + @NotNull + private String prefix = ""; + + @NotNull + @OneOf(value = {"tcp", "udp"}, ignoreCase = true) + private String transport = "tcp"; + + @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; + } + + @JsonProperty + public String getTransport() { + return transport; + } + + @JsonProperty + public void setTransport(String transport) { + this.transport = transport; + } + + @Override + public ScheduledReporter build(MetricRegistry registry) { + GraphiteReporter.Builder builder = builder(registry); + + if ("udp".equalsIgnoreCase(transport)) { + return builder.build(new GraphiteUDP(host, port)); + } else { + return builder.build(new Graphite(host, port)); + } + } + + @VisibleForTesting + protected GraphiteReporter.Builder builder(MetricRegistry registry) { + return GraphiteReporter.forRegistry(registry) + .convertDurationsTo(getDurationUnit()) + .convertRatesTo(getRateUnit()) + .filter(getFilter()) + .prefixedWith(getPrefix()); + } +} 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 00000000000..22810cc4ce9 --- /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 00000000000..b4424566cef --- /dev/null +++ b/dropwizard-metrics-graphite/src/test/java/io/dropwizard/metrics/graphite/GraphiteReporterFactoryTest.java @@ -0,0 +1,88 @@ +package io.dropwizard.metrics.graphite; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.graphite.Graphite; +import com.codahale.metrics.graphite.GraphiteReporter; +import com.codahale.metrics.graphite.GraphiteUDP; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.validation.BaseValidator; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class GraphiteReporterFactoryTest { + + private final GraphiteReporter.Builder builderSpy = mock(GraphiteReporter.Builder.class); + + private GraphiteReporterFactory graphiteReporterFactory = new GraphiteReporterFactory() { + @Override + protected GraphiteReporter.Builder builder(MetricRegistry registry) { + return builderSpy; + } + }; + + @Test + public void isDiscoverable() throws Exception { + assertThat(new DiscoverableSubtypeResolver().getDiscoveredSubtypes()) + .contains(GraphiteReporterFactory.class); + } + + @Test + public void createDefaultFactory() throws Exception { + final GraphiteReporterFactory factory = new YamlConfigurationFactory<>(GraphiteReporterFactory.class, + BaseValidator.newValidator(), Jackson.newObjectMapper(), "dw") + .build(); + assertThat(factory.getFrequency()).isEqualTo(Optional.empty()); + } + + @Test + public void testNoAddressResolutionForGraphite() throws Exception { + graphiteReporterFactory.build(new MetricRegistry()); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(Graphite.class); + verify(builderSpy).build(argument.capture()); + + final Graphite graphite = argument.getValue(); + assertThat(getField(graphite, "hostname")).isEqualTo("localhost"); + assertThat(getField(graphite, "port")).isEqualTo(2003); + assertThat(getField(graphite, "address")).isNull(); + } + + @Test + public void testCorrectTransportForGraphiteUDP() throws Exception { + graphiteReporterFactory.setTransport("udp"); + graphiteReporterFactory.build(new MetricRegistry()); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(GraphiteUDP.class); + verify(builderSpy).build(argument.capture()); + + final GraphiteUDP graphite = argument.getValue(); + assertThat(getField(graphite, "hostname")).isEqualTo("localhost"); + assertThat(getField(graphite, "port")).isEqualTo(2003); + assertThat(getField(graphite, "address")).isNull(); + } + + private static Object getField(GraphiteUDP graphite, String name) { + try { + return FieldUtils.getDeclaredField(GraphiteUDP.class, name, true).get(graphite); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } + + private static Object getField(Graphite graphite, String name) { + try { + return FieldUtils.getDeclaredField(Graphite.class, name, true).get(graphite); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/dropwizard-metrics/pom.xml b/dropwizard-metrics/pom.xml new file mode 100644 index 00000000000..10abe7d4bed --- /dev/null +++ b/dropwizard-metrics/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-metrics + Dropwizard Metrics Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-lifecycle + + + io.dropwizard + dropwizard-jackson + + + io.dropwizard + dropwizard-validation + + + io.dropwizard.metrics + metrics-core + + + org.slf4j + slf4j-api + + + io.dropwizard + dropwizard-logging + test + + + io.dropwizard + dropwizard-configuration + test + + + 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 00000000000..ec600f11d9a --- /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. + *

    + * Configures formatting options common to some {@link com.codahale.metrics.ScheduledReporter}s. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    localeSystem default {@link Locale}.The {@link Locale} for formatting numbers, dates and times.
    See {@link BaseReporterFactory} for more options.
    + */ +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 00000000000..c1dd40c7f56 --- /dev/null +++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/BaseReporterFactory.java @@ -0,0 +1,228 @@ +package io.dropwizard.metrics; + +import com.codahale.metrics.MetricFilter; +import com.codahale.metrics.ScheduledReporter; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.util.Duration; +import io.dropwizard.validation.MinDuration; +import org.hibernate.validator.valuehandling.UnwrapValidatedValue; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; + +/** + * A base {@link ReporterFactory} for configuring metric reporters. + *

    + * Configures options common to all {@link ScheduledReporter}s. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    durationUnitmillisecondsThe unit to report durations as. Overrides per-metric duration units.
    rateUnitsecondsThe unit to report rates as. Overrides per-metric rate units.
    excludesNo excluded metrics.Metrics to exclude from reports, by name. When defined, matching metrics will not be + * reported. See {@link #getFilter()}.
    includesAll metrics included.Metrics to include in reports, by name. When defined, only these metrics will be + * reported. See {@link #getFilter()}. Exclusion rules (excludes) take precedence, + * so if a name matches both excludes and includes, it is excluded.
    useRegexFiltersfalseIndicates whether the values of the 'includes' and 'excludes' fields should be + * treated as regular expressions or not.
    frequencynoneThe frequency to report metrics. Overrides the {@link + * MetricsFactory#getFrequency() default}.
    + */ +public abstract class BaseReporterFactory implements ReporterFactory { + + private static final DefaultStringMatchingStrategy DEFAULT_STRING_MATCHING_STRATEGY = + new DefaultStringMatchingStrategy(); + + private static final RegexStringMatchingStrategy REGEX_STRING_MATCHING_STRATEGY = + new RegexStringMatchingStrategy(); + + @NotNull + private TimeUnit durationUnit = TimeUnit.MILLISECONDS; + + @NotNull + private TimeUnit rateUnit = TimeUnit.SECONDS; + + @NotNull + private ImmutableSet excludes = ImmutableSet.of(); + + @NotNull + private ImmutableSet includes = ImmutableSet.of(); + + @Valid + @MinDuration(0) + @UnwrapValidatedValue + private Optional frequency = Optional.empty(); + + private boolean useRegexFilters = false; + + 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 getIncludes() { + return includes; + } + + @JsonProperty + public void setIncludes(ImmutableSet includes) { + this.includes = includes; + } + + @JsonProperty + public ImmutableSet getExcludes() { + return excludes; + } + + @JsonProperty + public void setExcludes(ImmutableSet excludes) { + this.excludes = excludes; + } + + @Override + @JsonProperty + public Optional getFrequency() { + return frequency; + } + + @JsonProperty + public void setFrequency(Optional frequency) { + this.frequency = frequency; + } + + @JsonProperty + public boolean getUseRegexFilters() { + return useRegexFilters; + } + + @JsonProperty + public void setUseRegexFilters(boolean useRegexFilters) { + this.useRegexFilters = useRegexFilters; + } + + /** + * Gets a {@link MetricFilter} that specifically includes and excludes configured metrics. + *

    + * Filtering works in 4 ways: + *

    + *
    unfiltered
    + *
    All metrics are reported
    + *
    excludes-only
    + *
    All metrics are reported, except those whose name is listed in excludes.
    + *
    includes-only
    + *
    Only metrics whose name is listed in includes are reported.
    + *
    mixed (both includes and excludes
    + *
    Only metrics whose name is listed in includes and + * not listed in excludes are reported; + * excludes takes precedence over includes.
    + *
    + * + * @return the filter for selecting metrics based on the configured excludes/includes. + * @see #getIncludes() + * @see #getExcludes() + */ + @JsonIgnore + public MetricFilter getFilter() { + final StringMatchingStrategy stringMatchingStrategy = getUseRegexFilters() ? + REGEX_STRING_MATCHING_STRATEGY : DEFAULT_STRING_MATCHING_STRATEGY; + + return (name, metric) -> { + // Include the metric if its name is not excluded and its name is included + // Where, by default, with no includes setting, all names are included. + return !stringMatchingStrategy.containsMatch(getExcludes(), name) && + (getIncludes().isEmpty() || stringMatchingStrategy.containsMatch(getIncludes(), name)); + }; + } + + private interface StringMatchingStrategy { + boolean containsMatch(ImmutableSet matchExpressions, String metricName); + } + + private static class DefaultStringMatchingStrategy implements StringMatchingStrategy { + @Override + public boolean containsMatch(ImmutableSet matchExpressions, String metricName) { + return matchExpressions.contains(metricName); + } + } + + private static class RegexStringMatchingStrategy implements StringMatchingStrategy { + private final LoadingCache patternCache; + + private RegexStringMatchingStrategy() { + patternCache = CacheBuilder.newBuilder() + .expireAfterWrite(1, TimeUnit.HOURS) + .build(new CacheLoader() { + @Override + public Pattern load(String regex) throws Exception { + return Pattern.compile(regex); + } + }); + } + + @Override + public boolean containsMatch(ImmutableSet matchExpressions, String metricName) { + for (String regexExpression : matchExpressions) { + if (patternCache.getUnchecked(regexExpression).matcher(metricName).matches()) { + // just need to match on a single value - return as soon as we do + return true; + } + } + + return false; + } + } +} 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 00000000000..b1cfb636aab --- /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. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    timeZoneUTCThe timezone to display dates/times for.
    outputstdoutThe stream to write to. One of {@code stdout} or {@code stderr}.
    See {@link BaseFormattedReporterFactory} for more options.
    See {@link BaseReporterFactory} for more options.
    + */ +@JsonTypeName("console") +public class ConsoleReporterFactory extends BaseFormattedReporterFactory { + 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; + } + + @Override + 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 00000000000..1ed7ccf5e28 --- /dev/null +++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/CsvReporterFactory.java @@ -0,0 +1,69 @@ +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. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    fileNo default. You must define a directory.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.
    See {@link BaseFormattedReporterFactory} for more options.
    See {@link BaseReporterFactory} for more options.
    + */ +@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) { + final boolean creation = file.mkdirs(); + if (!creation && !file.exists()) { + throw new RuntimeException("Failed to create" + file.getAbsolutePath()); + } + + 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 00000000000..f3ae380372f --- /dev/null +++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/MetricsFactory.java @@ -0,0 +1,103 @@ +package io.dropwizard.metrics; + +import com.codahale.metrics.MetricRegistry; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.MoreObjects; +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. + *

    + * Configures an optional list of {@link com.codahale.metrics.ScheduledReporter reporters} with a + * default {@link #frequency}. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    frequency1 minuteThe frequency to report metrics. Overridable per-reporter.
    reportersNo reporters.A list of {@link ReporterFactory reporters} to report metrics.
    + */ +public class MetricsFactory { + private static final Logger LOGGER = LoggerFactory.getLogger(MetricsFactory.class); + + @Valid + @NotNull + private Duration frequency = Duration.minutes(1); + + @Valid + @NotNull + private ImmutableList reporters = ImmutableList.of(); + + @JsonProperty + public ImmutableList getReporters() { + return reporters; + } + + @JsonProperty + public void setReporters(ImmutableList 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. + *

    + * 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().orElseGet(this::getFrequency)); + environment.manage(manager); + } catch (Exception e) { + LOGGER.warn("Failed to create reporter, metrics may not be properly reported.", e); + } + } + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("frequency", frequency) + .add("reporters", reporters) + .toString(); + } +} 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 00000000000..38128511afb --- /dev/null +++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/ReporterFactory.java @@ -0,0 +1,43 @@ +package io.dropwizard.metrics; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.ScheduledReporter; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import io.dropwizard.util.Duration; + +import java.util.Optional; + +/** + * A service provider interface for creating metrics {@link ScheduledReporter reporters}. + *

    + * To create your own, just: + *

      + *
    1. Create a class which implements {@link ReporterFactory}.
    2. + *
    3. Annotate it with {@code @JsonTypeName} and give it a unique type name.
    4. + *
    5. Add a {@code META-INF/services/io.dropwizard.metrics.ReporterFactory} + * file with your implementation's full class name to the class path.
    6. + *
    + * + * @see ConsoleReporterFactory + * @see CsvReporterFactory + * @see Slf4jReporterFactory + */ +@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 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 00000000000..42f5b8550bf --- /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 00000000000..10890ca4882 --- /dev/null +++ b/dropwizard-metrics/src/main/java/io/dropwizard/metrics/Slf4jReporterFactory.java @@ -0,0 +1,82 @@ +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. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    loggermetricsThe name of the logger to write metrics to.
    markerName(none)The name of the marker to mark logged metrics with.
    See {@link BaseReporterFactory} for more options.
    + */ +@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; + } + + @Override + 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 00000000000..ded53910c9f --- /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 00000000000..c7a251cdba0 --- /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/BaseReporterFactoryTest.java b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/BaseReporterFactoryTest.java new file mode 100644 index 00000000000..fc558897d58 --- /dev/null +++ b/dropwizard-metrics/src/test/java/io/dropwizard/metrics/BaseReporterFactoryTest.java @@ -0,0 +1,125 @@ +package io.dropwizard.metrics; + +import com.codahale.metrics.Metric; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.ScheduledReporter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@RunWith(Parameterized.class) +public class BaseReporterFactoryTest { + + + private static final ImmutableSet INCLUDES = ImmutableSet.of("inc", "both", "inc.+"); + private static final ImmutableSet EXCLUDES = ImmutableSet.of("both", "exc", "exc.+"); + private static final ImmutableSet EMPTY = ImmutableSet.of(); + + + @Parameterized.Parameters(name = "{index} {4} {2}={3}") + public static List data() { + + return ImmutableList.of( + /** + * case1: If include list is empty and exclude list is empty, everything should be + * included. + */ + new Object[]{EMPTY, EMPTY, "inc", true, true, "case1"}, + new Object[]{EMPTY, EMPTY, "both", true, true, "case1"}, + new Object[]{EMPTY, EMPTY, "exc", true, true, "case1"}, + new Object[]{EMPTY, EMPTY, "any", true, true, "case1"}, + new Object[]{EMPTY, EMPTY, "incWithSuffix", true, true, "case1"}, + new Object[]{EMPTY, EMPTY, "excWithSuffix", true, true, "case1"}, + + /** + * case2: If include list is NOT empty and exclude list is empty, only the ones + * specified in the include list should be included. + */ + new Object[]{INCLUDES, EMPTY, "inc", true, true, "case2"}, + new Object[]{INCLUDES, EMPTY, "both", true, true, "case2"}, + new Object[]{INCLUDES, EMPTY, "exc", false, false, "case2"}, + new Object[]{INCLUDES, EMPTY, "any", false, false, "case2"}, + new Object[]{INCLUDES, EMPTY, "incWithSuffix", false, true, "case2"}, + new Object[]{INCLUDES, EMPTY, "excWithSuffix", false, false, "case2"}, + + /** + * case3: If include list is empty and exclude list is NOT empty, everything should be + * included except the ones in the exclude list. + */ + new Object[]{EMPTY, EXCLUDES, "inc", true, true, "case3"}, + new Object[]{EMPTY, EXCLUDES, "both", false, false, "case3"}, + new Object[]{EMPTY, EXCLUDES, "exc", false, false, "case3"}, + new Object[]{EMPTY, EXCLUDES, "any", true, true, "case3"}, + new Object[]{EMPTY, EXCLUDES, "incWithSuffix", true, true, "case3"}, + new Object[]{EMPTY, EXCLUDES, "excWithSuffix", true, false, "case3"}, + + /** + * case4: If include list is NOT empty and exclude list is NOT empty, only things not excluded + * and specifically included should show up. Excludes takes precedence. + */ + new Object[]{INCLUDES, EXCLUDES, "inc", true, true, "case4"}, + new Object[]{INCLUDES, EXCLUDES, "both", false, false, "case4"}, + new Object[]{INCLUDES, EXCLUDES, "exc", false, false, "case4"}, + new Object[]{INCLUDES, EXCLUDES, "any", false, false, "case4"}, + new Object[]{INCLUDES, EXCLUDES, "incWithSuffix", false, true, "case4"}, + new Object[]{INCLUDES, EXCLUDES, "excWithSuffix", false, false, "case4"} + ); + } + + private final BaseReporterFactory factory = new BaseReporterFactory() { + @Override + public ScheduledReporter build(MetricRegistry registry) { + throw new UnsupportedOperationException("not implemented"); + } + }; + + @Parameterized.Parameter(0) + public ImmutableSet includes; + + @Parameterized.Parameter(1) + public ImmutableSet excludes; + + @Parameterized.Parameter(2) + public String name; + + @Parameterized.Parameter(3) + public boolean expectedDefaultResult; + + @Parameterized.Parameter(4) + public boolean expectedRegexResult; + + @Parameterized.Parameter(5) + public String msg; + + private final Metric metric = mock(Metric.class); + + @Test + public void testDefaultMatching() { + factory.setIncludes(includes); + factory.setExcludes(excludes); + + factory.setUseRegexFilters(false); + assertThat(factory.getFilter().matches(name, metric)) + .overridingErrorMessage(msg + ": expected 'matches(%s)=%s' for default matcher", name, expectedDefaultResult) + .isEqualTo(expectedDefaultResult); + } + + @Test + public void testRegexMatching() { + factory.setIncludes(includes); + factory.setExcludes(excludes); + + factory.setUseRegexFilters(true); + assertThat(factory.getFilter().matches(name, metric)) + .overridingErrorMessage(msg + ": expected 'matches(%s)=%s' for regex matcher", name, expectedRegexResult) + .isEqualTo(expectedRegexResult); + } + +} \ No newline at end of file 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 00000000000..42f5b87b036 --- /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.assertj.core.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 00000000000..e0338147ad2 --- /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.YamlConfigurationFactory; +import io.dropwizard.jackson.DiscoverableSubtypeResolver; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.lifecycle.setup.LifecycleEnvironment; +import io.dropwizard.validation.BaseValidator; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CsvReporterFactoryTest { + private final ObjectMapper objectMapper = Jackson.newObjectMapper(); + private final YamlConfigurationFactory factory = + new YamlConfigurationFactory<>(MetricsFactory.class, + BaseValidator.newValidator(), + 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 00000000000..d5c5ce14090 --- /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.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.util.Duration; +import io.dropwizard.validation.BaseValidator; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MetricsFactoryTest { + static { + BootstrapLogging.bootstrap(); + } + + private final ObjectMapper objectMapper = Jackson.newObjectMapper(); + private final YamlConfigurationFactory factory = + new YamlConfigurationFactory<>(MetricsFactory.class, + BaseValidator.newValidator(), + 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 00000000000..43cfb126616 --- /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.assertj.core.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 00000000000..b66d4d4ec93 --- /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 00000000000..4f0adca792f --- /dev/null +++ b/dropwizard-migrations/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-migrations + Dropwizard Migrations + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-db + + + org.liquibase + liquibase-core + + + com.mattbertolini + liquibase-slf4j + + + com.h2database + h2 + test + + + + io.dropwizard + dropwizard-jdbi + test + + + + net.jcip + jcip-annotations + test + + + + 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 00000000000..66649dfa2e6 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/AbstractLiquibaseCommand.java @@ -0,0 +1,101 @@ +package io.dropwizard.migrations; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.Configuration; +import io.dropwizard.cli.ConfiguredCommand; +import io.dropwizard.db.DatabaseConfiguration; +import io.dropwizard.db.ManagedDataSource; +import io.dropwizard.db.PooledDataSourceFactory; +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 extends ConfiguredCommand { + private final DatabaseConfiguration strategy; + private final Class configurationClass; + private final String migrationsFileName; + + protected AbstractLiquibaseCommand(String name, + String description, + DatabaseConfiguration strategy, + Class configurationClass, + String migrationsFileName) { + super(name, description); + this.strategy = strategy; + this.configurationClass = configurationClass; + this.migrationsFileName = migrationsFileName; + } + + @Override + protected Class 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 bootstrap, Namespace namespace, T configuration) throws Exception { + final PooledDataSourceFactory dbConfig = strategy.getDataSourceFactory(configuration); + dbConfig.asSingleConnectionPool(); + + try (final CloseableLiquibase liquibase = openLiquibase(dbConfig, namespace)) { + run(namespace, liquibase); + } catch (ValidationFailedException e) { + e.printDescriptiveError(System.err); + throw e; + } + } + + private CloseableLiquibase openLiquibase(final PooledDataSourceFactory dataSourceFactory, final Namespace namespace) + throws 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 CloseableLiquibaseWithClassPathMigrationsFile(dataSource, migrationsFileName) { + }; + } else { + liquibase = new CloseableLiquibaseWithFileSystemMigrationsFile(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 00000000000..4ce88952f23 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibase.java @@ -0,0 +1,27 @@ +package io.dropwizard.migrations; + +import io.dropwizard.db.ManagedDataSource; +import liquibase.Liquibase; +import liquibase.database.DatabaseConnection; +import liquibase.exception.LiquibaseException; +import liquibase.resource.ResourceAccessor; + +import java.sql.SQLException; + +public abstract class CloseableLiquibase extends Liquibase implements AutoCloseable { + private final ManagedDataSource dataSource; + + public CloseableLiquibase(String changeLogFile, ResourceAccessor resourceAccessor, DatabaseConnection conn, ManagedDataSource dataSource) throws LiquibaseException, SQLException { + super(changeLogFile, resourceAccessor, conn); + this.dataSource = dataSource; + } + + @Override + public void close() throws Exception { + try { + database.close(); + } finally { + dataSource.stop(); + } + } +} diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibaseWithClassPathMigrationsFile.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibaseWithClassPathMigrationsFile.java new file mode 100644 index 00000000000..a9e65baf30e --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibaseWithClassPathMigrationsFile.java @@ -0,0 +1,19 @@ +package io.dropwizard.migrations; + +import io.dropwizard.db.ManagedDataSource; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.LiquibaseException; +import liquibase.resource.ClassLoaderResourceAccessor; + +import java.sql.SQLException; + +public class CloseableLiquibaseWithClassPathMigrationsFile extends CloseableLiquibase implements AutoCloseable { + + public CloseableLiquibaseWithClassPathMigrationsFile(ManagedDataSource dataSource, String file) throws LiquibaseException, SQLException { + super(file, + new ClassLoaderResourceAccessor(), + new JdbcConnection(dataSource.getConnection()), + dataSource); + } + +} diff --git a/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibaseWithFileSystemMigrationsFile.java b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibaseWithFileSystemMigrationsFile.java new file mode 100644 index 00000000000..d9b248e81ab --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/CloseableLiquibaseWithFileSystemMigrationsFile.java @@ -0,0 +1,19 @@ +package io.dropwizard.migrations; + +import io.dropwizard.db.ManagedDataSource; +import liquibase.database.jvm.JdbcConnection; +import liquibase.exception.LiquibaseException; +import liquibase.resource.FileSystemResourceAccessor; + +import java.sql.SQLException; + +public class CloseableLiquibaseWithFileSystemMigrationsFile extends CloseableLiquibase implements AutoCloseable { + + public CloseableLiquibaseWithFileSystemMigrationsFile(ManagedDataSource dataSource, String file) throws LiquibaseException, SQLException { + super(file, + new FileSystemResourceAccessor(), + new JdbcConnection(dataSource.getConnection()), + dataSource); + } + +} 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 00000000000..4d898bc55e1 --- /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 extends AbstractLiquibaseCommand { + private static final Logger LOGGER = LoggerFactory.getLogger("liquibase"); + + public DbCalculateChecksumCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("calculate-checksum", "Calculates and prints a checksum for a change set", strategy, configurationClass, migrationsFileName); + } + + @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.getList("id").get(0), + namespace.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 00000000000..d4e88950265 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbClearChecksumsCommand.java @@ -0,0 +1,22 @@ +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 extends AbstractLiquibaseCommand { + public DbClearChecksumsCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("clear-checksums", + "Removes all saved checksums from the database log", + strategy, + configurationClass, + migrationsFileName); + } + + @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 00000000000..41bef0962ee --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbCommand.java @@ -0,0 +1,54 @@ +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; + +import java.util.SortedMap; +import java.util.TreeMap; + +public class DbCommand extends AbstractLiquibaseCommand { + private static final String COMMAND_NAME_ATTR = "subcommand"; + private final SortedMap> subcommands; + + public DbCommand(String name, DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super(name, "Run database migration tasks", strategy, configurationClass, migrationsFileName); + this.subcommands = new TreeMap<>(); + addSubcommand(new DbCalculateChecksumCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbClearChecksumsCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbDropAllCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbDumpCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbFastForwardCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbGenerateDocsCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbLocksCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbMigrateCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbPrepareRollbackCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbRollbackCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbStatusCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbTagCommand<>(strategy, configurationClass, migrationsFileName)); + addSubcommand(new DbTestCommand<>(strategy, configurationClass, migrationsFileName)); + } + + private void addSubcommand(AbstractLiquibaseCommand subcommand) { + subcommands.put(subcommand.getName(), subcommand); + } + + @Override + public void configure(Subparser subparser) { + for (AbstractLiquibaseCommand 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 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 00000000000..2eee683d3e7 --- /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 extends AbstractLiquibaseCommand { + public DbDropAllCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("drop-all", "Delete all user-owned objects from the database.", strategy, configurationClass, migrationsFileName); + } + + @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 00000000000..cd3c0ae29cb --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbDumpCommand.java @@ -0,0 +1,236 @@ +package io.dropwizard.migrations; + +import com.google.common.annotations.VisibleForTesting; +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 extends AbstractLiquibaseCommand { + + private PrintStream outputStream = System.out; + + @VisibleForTesting + void setOutputStream(PrintStream outputStream) { + this.outputStream = outputStream; + } + + public DbDumpCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("dump", + "Generate a dump of the existing database state.", + strategy, + configurationClass, + migrationsFileName); + } + + @Override + public void configure(Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("-o", "--output") + .dest("output") + .help("Write output to 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 columns (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 sequences"); + + 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> compareTypes = new HashSet<>(); + + if (isTrue(namespace.getBoolean("columns"))) { + compareTypes.add(Column.class); + } + if (isTrue(namespace.getBoolean("data"))) { + compareTypes.add(Data.class); + } + if (isTrue(namespace.getBoolean("foreign-keys"))) { + compareTypes.add(ForeignKey.class); + } + if (isTrue(namespace.getBoolean("indexes"))) { + compareTypes.add(Index.class); + } + if (isTrue(namespace.getBoolean("primary-keys"))) { + compareTypes.add(PrimaryKey.class); + } + if (isTrue(namespace.getBoolean("sequences"))) { + compareTypes.add(Sequence.class); + } + if (isTrue(namespace.getBoolean("tables"))) { + compareTypes.add(Table.class); + } + if (isTrue(namespace.getBoolean("unique-constraints"))) { + compareTypes.add(UniqueConstraint.class); + } + if (isTrue(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, outputStream, compareTypes); + } + } + + private void generateChangeLog(final Database database, final CatalogAndSchema catalogAndSchema, + final DiffToChangeLog changeLogWriter, PrintStream outputStream, + final Set> compareTypes) + throws DatabaseException, IOException, ParserConfigurationException { + @SuppressWarnings({"unchecked", "rawtypes"}) + 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); + } + } + + private static boolean isTrue(Boolean nullableCondition) { + return nullableCondition != null && nullableCondition; + } +} 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 00000000000..cc6c9448f8a --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbFastForwardCommand.java @@ -0,0 +1,73 @@ +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.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class DbFastForwardCommand extends AbstractLiquibaseCommand { + protected DbFastForwardCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("fast-forward", + "Mark the next pending change set as applied without running it", + strategy, + configurationClass, + migrationsFileName); + } + + @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, StandardCharsets.UTF_8)); + } else { + liquibase.changeLogSync(context); + } + } else { + if (namespace.getBoolean("dry-run")) { + liquibase.markNextChangeSetRan(context, new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + } else { + liquibase.markNextChangeSetRan(context); + } + } + } + + private String getContext(Namespace namespace) { + final List 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 00000000000..e5e3bced270 --- /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 extends AbstractLiquibaseCommand { + public DbGenerateDocsCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("generate-docs", "Generate documentation about the database state.", strategy, configurationClass, migrationsFileName); + } + + @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.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 00000000000..7c76521ee7c --- /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 extends AbstractLiquibaseCommand { + public DbLocksCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("locks", "Manage database migration locks", strategy, configurationClass, migrationsFileName); + } + + @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 00000000000..3b49f18da58 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbMigrateCommand.java @@ -0,0 +1,80 @@ +package io.dropwizard.migrations; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; +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.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class DbMigrateCommand extends AbstractLiquibaseCommand { + + private PrintStream outputStream = System.out; + + @VisibleForTesting + void setOutputStream(PrintStream outputStream) { + this.outputStream = outputStream; + } + + public DbMigrateCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("migrate", "Apply all pending change sets.", strategy, configurationClass, migrationsFileName); + } + + @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 = MoreObjects.firstNonNull(namespace.getBoolean("dry-run"), false); + if (count != null) { + if (dryRun) { + liquibase.update(count, context, new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); + } else { + liquibase.update(count, context); + } + } else { + if (dryRun) { + liquibase.update(context, new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); + } else { + liquibase.update(context); + } + } + } + + private String getContext(Namespace namespace) { + final List 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 00000000000..dbebbde6a8d --- /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.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.nio.charset.StandardCharsets; +import java.util.List; + +public class DbPrepareRollbackCommand extends AbstractLiquibaseCommand { + public DbPrepareRollbackCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("prepare-rollback", "Generate rollback DDL scripts for pending change sets.", strategy, configurationClass, migrationsFileName); + } + + @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, StandardCharsets.UTF_8)); + } else { + liquibase.futureRollbackSQL(context, new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + } + } + + private String getContext(Namespace namespace) { + final List 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 00000000000..0fac6eb13fa --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbRollbackCommand.java @@ -0,0 +1,93 @@ +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.io.OutputStreamWriter; +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.List; + +public class DbRollbackCommand extends AbstractLiquibaseCommand { + public DbRollbackCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("rollback", + "Rollback the database schema to a previous version.", + strategy, + configurationClass, + migrationsFileName); + } + + @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 = 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, StandardCharsets.UTF_8)); + } else { + liquibase.rollback(count, context); + } + } else if (tag != null) { + if (dryRun) { + liquibase.rollback(tag, context, new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + } else { + liquibase.rollback(tag, context); + } + } else { + if (dryRun) { + liquibase.rollback(date, context, new OutputStreamWriter(System.out, StandardCharsets.UTF_8)); + } else { + liquibase.rollback(date, context); + } + } + } + + private String getContext(Namespace namespace) { + final List 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 00000000000..8cc3cb9ead8 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbStatusCommand.java @@ -0,0 +1,60 @@ +package io.dropwizard.migrations; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Joiner; +import com.google.common.base.MoreObjects; +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.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class DbStatusCommand extends AbstractLiquibaseCommand { + + private PrintStream outputStream = System.out; + + @VisibleForTesting + void setOutputStream(PrintStream outputStream) { + this.outputStream = outputStream; + } + + public DbStatusCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("status", "Check for pending change sets.", strategy, configurationClass, migrationsFileName); + } + + @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(MoreObjects.firstNonNull(namespace.getBoolean("verbose"), false), + getContext(namespace), + new OutputStreamWriter(outputStream, StandardCharsets.UTF_8)); + } + + private String getContext(Namespace namespace) { + final List 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 00000000000..23dc3ccfc91 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/DbTagCommand.java @@ -0,0 +1,29 @@ +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 extends AbstractLiquibaseCommand { + public DbTagCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("tag", "Tag the database schema.", strategy, configurationClass, migrationsFileName); + } + + @Override + public void configure(Subparser subparser) { + super.configure(subparser); + + subparser.addArgument("tag-name") + .dest("tag-name") + .nargs(1) + .required(true) + .help("The tag name"); + } + + @Override + public void run(Namespace namespace, Liquibase liquibase) throws Exception { + liquibase.tag(namespace.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 00000000000..a628de5e348 --- /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 extends AbstractLiquibaseCommand { + public DbTestCommand(DatabaseConfiguration strategy, Class configurationClass, String migrationsFileName) { + super("test", "Apply and rollback pending change sets.", strategy, configurationClass, migrationsFileName); + } + + @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 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 00000000000..e9a1a84dc17 --- /dev/null +++ b/dropwizard-migrations/src/main/java/io/dropwizard/migrations/MigrationsBundle.java @@ -0,0 +1,32 @@ +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; + +public abstract class MigrationsBundle implements Bundle, DatabaseConfiguration { + private static final String DEFAULT_NAME = "db"; + private static final String DEFAULT_MIGRATIONS_FILE = "migrations.xml"; + + @Override + @SuppressWarnings("unchecked") + public final void initialize(Bootstrap bootstrap) { + final Class klass = (Class) bootstrap.getApplication().getConfigurationClass(); + bootstrap.addCommand(new DbCommand<>(name(), this, klass, getMigrationsFileName())); + } + + public String getMigrationsFileName() { + return DEFAULT_MIGRATIONS_FILE; + } + + public String name() { + return DEFAULT_NAME; + } + + @Override + public final void run(Environment environment) { + // nothing doing + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/AbstractMigrationTest.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/AbstractMigrationTest.java new file mode 100644 index 00000000000..59e9effaafb --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/AbstractMigrationTest.java @@ -0,0 +1,40 @@ +package io.dropwizard.migrations; + +import io.dropwizard.db.DataSourceFactory; +import net.sourceforge.argparse4j.ArgumentParsers; +import net.sourceforge.argparse4j.inf.Subparser; + +import java.io.File; +import java.io.IOException; + +public class AbstractMigrationTest { + + static { + ArgumentParsers.setTerminalWidthDetection(false); + } + + protected static Subparser createSubparser(AbstractLiquibaseCommand command) { + final Subparser subparser = ArgumentParsers.newArgumentParser("db") + .addSubparsers() + .addParser(command.getName()) + .description(command.getDescription()); + command.configure(subparser); + return subparser; + } + + protected static TestMigrationConfiguration createConfiguration(String databaseUrl) { + final DataSourceFactory dataSource = new DataSourceFactory(); + dataSource.setDriverClass("org.h2.Driver"); + dataSource.setUser("sa"); + dataSource.setUrl(databaseUrl); + return new TestMigrationConfiguration(dataSource); + } + + protected static String createTempFile() { + try { + return File.createTempFile("test-example", null).getAbsolutePath(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/CloseableLiquibaseTest.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/CloseableLiquibaseTest.java new file mode 100644 index 00000000000..1862cbc7dc2 --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/CloseableLiquibaseTest.java @@ -0,0 +1,40 @@ +package io.dropwizard.migrations; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.db.ManagedPooledDataSource; +import net.jcip.annotations.NotThreadSafe; +import org.apache.tomcat.jdbc.pool.ConnectionPool; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@NotThreadSafe +public class CloseableLiquibaseTest { + + CloseableLiquibase liquibase; + ManagedPooledDataSource dataSource; + + @Before + public void setUp() throws Exception { + DataSourceFactory factory = new DataSourceFactory(); + + factory.setDriverClass(org.h2.Driver.class.getName()); + factory.setUrl("jdbc:h2:mem:DbTest-" + System.currentTimeMillis()); + factory.setUser("DbTest"); + + dataSource = (ManagedPooledDataSource) factory.build(new MetricRegistry(), "DbTest"); + liquibase = new CloseableLiquibaseWithClassPathMigrationsFile(dataSource, "migrations.xml"); + } + + @Test + public void testWhenClosingAllConnectionsInPoolIsReleased() throws Exception { + ConnectionPool pool = dataSource.getPool(); + liquibase.close(); + + assertThat(pool.getActive()).isZero(); + assertThat(pool.getIdle()).isZero(); + assertThat(pool.isClosed()).isTrue(); + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbDumpCommandTest.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbDumpCommandTest.java new file mode 100644 index 00000000000..8967acccac4 --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbDumpCommandTest.java @@ -0,0 +1,239 @@ +package io.dropwizard.migrations; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.common.io.Files; +import com.google.common.io.Resources; +import net.jcip.annotations.NotThreadSafe; +import net.sourceforge.argparse4j.inf.Namespace; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@NotThreadSafe +public class DbDumpCommandTest extends AbstractMigrationTest { + + private static DocumentBuilder xmlParser; + private static List attributeNames; + + private final DbDumpCommand dumpCommand = + new DbDumpCommand<>(new TestMigrationDatabaseConfiguration(), TestMigrationConfiguration.class, "migrations.xml"); + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private TestMigrationConfiguration existedDbConf; + + @BeforeClass + public static void initXmlParser() throws Exception { + xmlParser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); + attributeNames = ImmutableList.of("columns", "foreign-keys", "indexes", "primary-keys", "sequences", + "tables", "unique-constraints", "views"); + } + + @Before + public void setUp() throws Exception { + final String existedDbPath = new File(Resources.getResource("test-db.mv.db").toURI()).getAbsolutePath(); + final String existedDbUrl = "jdbc:h2:" + StringUtils.removeEnd(existedDbPath, ".mv.db"); + existedDbConf = createConfiguration(existedDbUrl); + dumpCommand.setOutputStream(new PrintStream(baos)); + } + + @Test + public void testDumpSchema() throws Exception { + final Map attributes = new HashMap<>(); + for (String name : attributeNames) { + attributes.put(name, true); + } + dumpCommand.run(null, new Namespace(attributes), existedDbConf); + + final Element changeSet = getFirstElement(toXmlDocument(baos).getDocumentElement(), "changeSet"); + assertCreateTable(changeSet); + } + + @Test + public void testDumpSchemaAndData() throws Exception { + final Map attributes = new HashMap<>(); + for (String name : Iterables.concat(attributeNames, ImmutableList.of("data"))) { + attributes.put(name, true); + } + dumpCommand.run(null, new Namespace(attributes), existedDbConf); + + final NodeList changeSets = toXmlDocument(baos).getDocumentElement().getElementsByTagName("changeSet"); + assertCreateTable((Element) changeSets.item(0)); + assertInsertData((Element) changeSets.item(1)); + } + + @Test + public void testDumpOnlyData() throws Exception { + dumpCommand.run(null, new Namespace(ImmutableMap.of("data", (Object) true)), existedDbConf); + + final Element changeSet = getFirstElement(toXmlDocument(baos).getDocumentElement(), "changeSet"); + assertInsertData(changeSet); + } + + @Test + public void testWriteToFile() throws Exception { + final File file = File.createTempFile("migration", ".xml"); + final Map attributes = ImmutableMap.of("output", file.getAbsolutePath()); + dumpCommand.run(null, new Namespace(attributes), existedDbConf); + // Check that file is exist, and has some XML content (no reason to make a full-blown XML assertion) + assertThat(Files.toString(file, StandardCharsets.UTF_8)).startsWith(""); + } + + @Test + public void testHelpPage() throws Exception { + createSubparser(dumpCommand).printHelp(new PrintWriter(baos, true)); + assertThat(baos.toString("UTF-8")).isEqualTo(String.format( + "usage: db dump [-h] [--migrations MIGRATIONS-FILE] [--catalog CATALOG]%n" + + " [--schema SCHEMA] [-o OUTPUT] [--tables] [--ignore-tables]%n" + + " [--columns] [--ignore-columns] [--views] [--ignore-views]%n" + + " [--primary-keys] [--ignore-primary-keys] [--unique-constraints]%n" + + " [--ignore-unique-constraints] [--indexes] [--ignore-indexes]%n" + + " [--foreign-keys] [--ignore-foreign-keys] [--sequences]%n" + + " [--ignore-sequences] [--data] [--ignore-data] [file]%n" + + "%n" + + "Generate a dump of the existing database state.%n" + + "%n" + + "positional arguments:%n" + + " file application configuration file%n" + + "%n" + + "optional arguments:%n" + + " -h, --help show this help message and exit%n" + + " --migrations MIGRATIONS-FILE%n" + + " the file containing the Liquibase migrations for%n" + + " the application%n" + + " --catalog CATALOG Specify the database catalog (use database%n" + + " default if omitted)%n" + + " --schema SCHEMA Specify the database schema (use database default%n" + + " if omitted)%n" + + " -o OUTPUT, --output OUTPUT%n" + + " Write output to instead of stdout%n" + + "%n" + + "Tables:%n" + + " --tables Check for added or removed tables (default)%n" + + " --ignore-tables Ignore tables%n" + + "%n" + + "Columns:%n" + + " --columns Check for added, removed, or modified columns%n" + + " (default)%n" + + " --ignore-columns Ignore columns%n" + + "%n" + + "Views:%n" + + " --views Check for added, removed, or modified views%n" + + " (default)%n" + + " --ignore-views Ignore views%n" + + "%n" + + "Primary Keys:%n" + + " --primary-keys Check for changed primary keys (default)%n" + + " --ignore-primary-keys Ignore primary keys%n" + + "%n" + + "Unique Constraints:%n" + + " --unique-constraints Check for changed unique constraints (default)%n" + + " --ignore-unique-constraints%n" + + " Ignore unique constraints%n" + + "%n" + + "Indexes:%n" + + " --indexes Check for changed indexes (default)%n" + + " --ignore-indexes Ignore indexes%n" + + "%n" + + "Foreign Keys:%n" + + " --foreign-keys Check for changed foreign keys (default)%n" + + " --ignore-foreign-keys Ignore foreign keys%n" + + "%n" + + "Sequences:%n" + + " --sequences Check for changed sequences (default)%n" + + " --ignore-sequences Ignore sequences%n" + + "%n" + + "Data:%n" + + " --data Check for changed data%n" + + " --ignore-data Ignore data (default)%n")); + } + + + private static Document toXmlDocument(ByteArrayOutputStream baos) throws SAXException, IOException { + return xmlParser.parse(new ByteArrayInputStream(baos.toByteArray())); + } + + /** + * Assert correctness of a change set with creation of a table + * + * @param changeSet actual XML element + */ + private static void assertCreateTable(Element changeSet) { + final Element createTable = getFirstElement(changeSet, "createTable"); + + assertThat(createTable.getAttribute("catalogName")).isEqualTo("TEST-DB"); + assertThat(createTable.getAttribute("schemaName")).isEqualTo("PUBLIC"); + assertThat(createTable.getAttribute("tableName")).isEqualTo("PERSONS"); + + final NodeList columns = createTable.getElementsByTagName("column"); + + final Element idColumn = (Element) columns.item(0); + assertThat(idColumn.getAttribute("autoIncrement")).isEqualTo("true"); + assertThat(idColumn.getAttribute("name")).isEqualTo("ID"); + assertThat(idColumn.getAttribute("type")).isEqualTo("INT(10)"); + final Element idColumnConstraints = getFirstElement(idColumn, "constraints"); + assertThat(idColumnConstraints.getAttribute("primaryKey")).isEqualTo("true"); + assertThat(idColumnConstraints.getAttribute("primaryKeyName")).isEqualTo("PK_PERSONS"); + + final Element nameColumn = (Element) columns.item(1); + assertThat(nameColumn.getAttribute("name")).isEqualTo("NAME"); + assertThat(nameColumn.getAttribute("type")).isEqualTo("VARCHAR(256)"); + final Element nameColumnConstraints = getFirstElement(nameColumn, "constraints"); + assertThat(nameColumnConstraints.getAttribute("nullable")).isEqualTo("false"); + + final Element emailColumn = (Element) columns.item(2); + assertThat(emailColumn.getAttribute("name")).isEqualTo("EMAIL"); + assertThat(emailColumn.getAttribute("type")).isEqualTo("VARCHAR(128)"); + } + + /** + * Assert a correctness of a change set with insertion data into a table + * + * @param changeSet actual XML element + */ + private static void assertInsertData(Element changeSet) { + final Element insert = getFirstElement(changeSet, "insert"); + + assertThat(insert.getAttribute("catalogName")).isEqualTo("TEST-DB"); + assertThat(insert.getAttribute("schemaName")).isEqualTo("PUBLIC"); + assertThat(insert.getAttribute("tableName")).isEqualTo("PERSONS"); + + final NodeList columns = insert.getElementsByTagName("column"); + + final Element idColumn = (Element) columns.item(0); + assertThat(idColumn.getAttribute("name")).isEqualTo("ID"); + assertThat(idColumn.getAttribute("valueNumeric")).isEqualTo("1"); + + final Element nameColumn = (Element) columns.item(1); + assertThat(nameColumn.getAttribute("name")).isEqualTo("NAME"); + assertThat(nameColumn.getAttribute("value")).isEqualTo("Bill Smith"); + + final Element emailColumn = (Element) columns.item(2); + assertThat(emailColumn.getAttribute("name")).isEqualTo("EMAIL"); + assertThat(emailColumn.getAttribute("value")).isEqualTo("bill@smith.me"); + } + + private static Element getFirstElement(Element root, String tagName) { + return (Element) root.getElementsByTagName(tagName).item(0); + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbMigrateCommandTest.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbMigrateCommandTest.java new file mode 100644 index 00000000000..3501b5e9e30 --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbMigrateCommandTest.java @@ -0,0 +1,100 @@ +package io.dropwizard.migrations; + +import com.google.common.collect.ImmutableMap; +import net.jcip.annotations.NotThreadSafe; +import net.sourceforge.argparse4j.ArgumentParsers; +import net.sourceforge.argparse4j.inf.Namespace; +import net.sourceforge.argparse4j.inf.Subparser; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@NotThreadSafe +public class DbMigrateCommandTest extends AbstractMigrationTest { + + private DbMigrateCommand migrateCommand = new DbMigrateCommand<>( + TestMigrationConfiguration::getDataSource, TestMigrationConfiguration.class, "migrations.xml"); + private TestMigrationConfiguration conf; + private String databaseUrl; + + @Before + public void setUp() throws Exception { + databaseUrl = "jdbc:h2:" + createTempFile(); + conf = createConfiguration(databaseUrl); + } + + @Test + public void testRun() throws Exception { + migrateCommand.run(null, new Namespace(ImmutableMap.of()), conf); + try (Handle handle = new DBI(databaseUrl, "sa", "").open()) { + final List> rows = handle.select("select * from persons"); + assertThat(rows).hasSize(1); + assertThat(rows.get(0)).isEqualTo( + ImmutableMap.of("id", 1, "name", "Bill Smith", "email", "bill@smith.me")); + } + } + + @Test + public void testRunFirstTwoMigration() throws Exception { + migrateCommand.run(null, new Namespace(ImmutableMap.of("count", (Object) 2)), conf); + try (Handle handle = new DBI(databaseUrl, "sa", "").open()) { + assertThat(handle.select("select * from persons")).isEmpty(); + } + } + + @Test + public void testDryRun() throws Exception { + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + migrateCommand.setOutputStream(new PrintStream(baos)); + migrateCommand.run(null, new Namespace(ImmutableMap.of("dry-run", (Object) true)), conf); + assertThat(baos.toString("UTF-8")).startsWith(String.format( + "-- *********************************************************************%n" + + "-- Update Database Script%n" + + "-- *********************************************************************%n")); + } + + @Test + public void testPrintHelp() throws Exception { + final Subparser subparser = ArgumentParsers.newArgumentParser("db") + .addSubparsers() + .addParser(migrateCommand.getName()) + .description(migrateCommand.getDescription()); + migrateCommand.configure(subparser); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + subparser.printHelp(new PrintWriter(baos, true)); + + assertThat(baos.toString("UTF-8")).isEqualTo(String.format( + "usage: db migrate [-h] [--migrations MIGRATIONS-FILE] [--catalog CATALOG]%n" + + " [--schema SCHEMA] [-n] [-c COUNT] [-i CONTEXTS] [file]%n" + + "%n" + + "Apply all pending change sets.%n" + + "%n" + + "positional arguments:%n" + + " file application configuration file%n" + + "%n" + + "optional arguments:%n" + + " -h, --help show this help message and exit%n" + + " --migrations MIGRATIONS-FILE%n" + + " the file containing the Liquibase migrations for%n" + + " the application%n" + + " --catalog CATALOG Specify the database catalog (use database%n" + + " default if omitted)%n" + + " --schema SCHEMA Specify the database schema (use database default%n" + + " if omitted)%n" + + " -n, --dry-run output the DDL to stdout, don't run it%n" + + " -c COUNT, --count COUNT%n" + + " only apply the next N change sets%n" + + " -i CONTEXTS, --include CONTEXTS%n" + + " include change sets from the given context%n")); + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbMigrateDifferentFileCommandTest.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbMigrateDifferentFileCommandTest.java new file mode 100644 index 00000000000..1475c7c670a --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbMigrateDifferentFileCommandTest.java @@ -0,0 +1,39 @@ +package io.dropwizard.migrations; + +import com.google.common.collect.ImmutableMap; +import net.jcip.annotations.NotThreadSafe; +import net.sourceforge.argparse4j.inf.Namespace; +import org.junit.Before; +import org.junit.Test; +import org.skife.jdbi.v2.DBI; +import org.skife.jdbi.v2.Handle; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@NotThreadSafe +public class DbMigrateDifferentFileCommandTest extends AbstractMigrationTest { + + private DbMigrateCommand migrateCommand = new DbMigrateCommand<>( + TestMigrationConfiguration::getDataSource, TestMigrationConfiguration.class, "migrations-test.xml"); + private TestMigrationConfiguration conf; + private String databaseUrl; + + @Before + public void setUp() throws Exception { + databaseUrl = "jdbc:h2:" + createTempFile(); + conf = createConfiguration(databaseUrl); + } + + @Test + public void testRun() throws Exception { + migrateCommand.run(null, new Namespace(ImmutableMap.of()), conf); + try (Handle handle = new DBI(databaseUrl, "sa", "").open()) { + final List> rows = handle.select("select * from persons"); + assertThat(rows).hasSize(0); + } + } + +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbStatusCommandTest.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbStatusCommandTest.java new file mode 100644 index 00000000000..fe5b130068d --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/DbStatusCommandTest.java @@ -0,0 +1,87 @@ +package io.dropwizard.migrations; + +import com.google.common.collect.ImmutableMap; +import com.google.common.io.Resources; +import net.jcip.annotations.NotThreadSafe; +import net.sourceforge.argparse4j.inf.Namespace; +import org.apache.commons.lang3.StringUtils; +import org.junit.Before; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@NotThreadSafe +public class DbStatusCommandTest extends AbstractMigrationTest { + + private final DbStatusCommand statusCommand = + new DbStatusCommand<>(new TestMigrationDatabaseConfiguration(), TestMigrationConfiguration.class, "migrations.xml"); + private final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + private TestMigrationConfiguration conf; + + @Before + public void setUp() throws Exception { + final String databaseUrl = "jdbc:h2:mem:" + UUID.randomUUID(); + conf = createConfiguration(databaseUrl); + + statusCommand.setOutputStream(new PrintStream(baos)); + } + + @Test + public void testRunOnMigratedDb() throws Exception { + final String existedDbPath = new File(Resources.getResource("test-db.mv.db").toURI()).getAbsolutePath(); + final String existedDbUrl = "jdbc:h2:" + StringUtils.removeEnd(existedDbPath, ".mv.db"); + final TestMigrationConfiguration existedDbConf = createConfiguration(existedDbUrl); + + statusCommand.run(null, new Namespace(ImmutableMap.of()), existedDbConf); + assertThat(baos.toString("UTF-8")).matches("\\S+ is up to date" + System.lineSeparator()); + } + + @Test + public void testRun() throws Exception { + statusCommand.run(null, new Namespace(ImmutableMap.of()), conf); + assertThat(baos.toString("UTF-8")).matches( + "3 change sets have not been applied to \\S+" + System.lineSeparator()); + } + + @Test + public void testVerbose() throws Exception { + statusCommand.run(null, new Namespace(ImmutableMap.of("verbose", (Object) true)), conf); + assertThat(baos.toString("UTF-8")).matches( + "3 change sets have not been applied to \\S+" + System.lineSeparator() + + "\\s*migrations\\.xml::1::db_dev" + System.lineSeparator() + + "\\s*migrations\\.xml::2::db_dev" + System.lineSeparator() + + "\\s*migrations\\.xml::3::db_dev" + System.lineSeparator()); + } + + @Test + public void testPrintHelp() throws Exception { + createSubparser(statusCommand).printHelp(new PrintWriter(baos, true)); + assertThat(baos.toString("UTF-8")).isEqualTo(String.format( + "usage: db status [-h] [--migrations MIGRATIONS-FILE] [--catalog CATALOG]%n" + + " [--schema SCHEMA] [-v] [-i CONTEXTS] [file]%n" + + "%n" + + "Check for pending change sets.%n" + + "%n" + + "positional arguments:%n" + + " file application configuration file%n" + + "%n" + + "optional arguments:%n" + + " -h, --help show this help message and exit%n" + + " --migrations MIGRATIONS-FILE%n" + + " the file containing the Liquibase migrations for%n" + + " the application%n" + + " --catalog CATALOG Specify the database catalog (use database%n" + + " default if omitted)%n" + + " --schema SCHEMA Specify the database schema (use database default%n" + + " if omitted)%n" + + " -v, --verbose Output verbose information%n" + + " -i CONTEXTS, --include CONTEXTS%n" + + " include change sets from the given context%n")); + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/TestMigrationConfiguration.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/TestMigrationConfiguration.java new file mode 100644 index 00000000000..5d67b9c5c16 --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/TestMigrationConfiguration.java @@ -0,0 +1,17 @@ +package io.dropwizard.migrations; + +import io.dropwizard.Configuration; +import io.dropwizard.db.DataSourceFactory; + +public class TestMigrationConfiguration extends Configuration { + + private DataSourceFactory dataSource; + + public TestMigrationConfiguration(DataSourceFactory dataSource) { + this.dataSource = dataSource; + } + + public DataSourceFactory getDataSource() { + return dataSource; + } +} diff --git a/dropwizard-migrations/src/test/java/io/dropwizard/migrations/TestMigrationDatabaseConfiguration.java b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/TestMigrationDatabaseConfiguration.java new file mode 100644 index 00000000000..5eb284b5242 --- /dev/null +++ b/dropwizard-migrations/src/test/java/io/dropwizard/migrations/TestMigrationDatabaseConfiguration.java @@ -0,0 +1,12 @@ +package io.dropwizard.migrations; + +import io.dropwizard.db.DataSourceFactory; +import io.dropwizard.db.DatabaseConfiguration; + +public class TestMigrationDatabaseConfiguration implements DatabaseConfiguration { + + @Override + public DataSourceFactory getDataSourceFactory(TestMigrationConfiguration configuration) { + return configuration.getDataSource(); + } +} diff --git a/dropwizard-migrations/src/test/resources/logback-test.xml b/dropwizard-migrations/src/test/resources/logback-test.xml new file mode 100644 index 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-migrations/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-migrations/src/test/resources/migrations-test.xml b/dropwizard-migrations/src/test/resources/migrations-test.xml new file mode 100644 index 00000000000..ad61beb4761 --- /dev/null +++ b/dropwizard-migrations/src/test/resources/migrations-test.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/dropwizard-migrations/src/test/resources/migrations.xml b/dropwizard-migrations/src/test/resources/migrations.xml new file mode 100644 index 00000000000..ac8a23a9d4b --- /dev/null +++ b/dropwizard-migrations/src/test/resources/migrations.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dropwizard-migrations/src/test/resources/test-db.mv.db b/dropwizard-migrations/src/test/resources/test-db.mv.db new file mode 100644 index 00000000000..9735a0dbe0e Binary files /dev/null and b/dropwizard-migrations/src/test/resources/test-db.mv.db differ diff --git a/dropwizard-request-logging/pom.xml b/dropwizard-request-logging/pom.xml new file mode 100644 index 00000000000..36613ad4e20 --- /dev/null +++ b/dropwizard-request-logging/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-request-logging + Dropwizard Request Logging Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-jetty + + + io.dropwizard + dropwizard-logging + + + ch.qos.logback + logback-access + + + + io.dropwizard + dropwizard-configuration + test + + + diff --git a/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/LogbackAccessRequestLog.java b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/LogbackAccessRequestLog.java new file mode 100644 index 00000000000..fe7e8197361 --- /dev/null +++ b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/LogbackAccessRequestLog.java @@ -0,0 +1,14 @@ +package io.dropwizard.request.logging; + +import ch.qos.logback.access.jetty.RequestLogImpl; + +/** + * The Dropwizard request log uses logback-access, but we override it to remove the requirement for logback-access.xml + * based configuration. + */ +public class LogbackAccessRequestLog extends RequestLogImpl { + @Override + public void configure() { + setName("LogbackAccessRequestLog"); + } +} diff --git a/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/LogbackAccessRequestLogFactory.java b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/LogbackAccessRequestLogFactory.java new file mode 100644 index 00000000000..709000404c4 --- /dev/null +++ b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/LogbackAccessRequestLogFactory.java @@ -0,0 +1,84 @@ +package io.dropwizard.request.logging; + +import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.google.common.collect.ImmutableList; +import io.dropwizard.logging.AppenderFactory; +import io.dropwizard.logging.ConsoleAppenderFactory; +import io.dropwizard.logging.async.AsyncAppenderFactory; +import io.dropwizard.logging.filter.LevelFilterFactory; +import io.dropwizard.logging.filter.NullLevelFilterFactory; +import io.dropwizard.logging.layout.LayoutFactory; +import io.dropwizard.request.logging.async.AsyncAccessEventAppenderFactory; +import io.dropwizard.request.logging.layout.LogbackAccessRequestLayoutFactory; +import org.eclipse.jetty.server.RequestLog; +import org.slf4j.LoggerFactory; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +/** + * A factory for creating {@link LogbackAccessRequestLog} instances. + *

    + * Configuration Parameters: + * + * + * + * + * + * + * + * + * + * + * + *
    NameDefaultDescription
    {@code appenders}a default {@link ConsoleAppenderFactory console} appenderThe set of {@link AppenderFactory appenders} to which requests will be logged.
    + */ +@JsonTypeName("logback-access") +public class LogbackAccessRequestLogFactory implements RequestLogFactory { + + @Valid + @NotNull + private ImmutableList> appenders = ImmutableList + .of(new ConsoleAppenderFactory<>()); + + @JsonProperty + public ImmutableList> getAppenders() { + return appenders; + } + + @JsonProperty + public void setAppenders(ImmutableList> appenders) { + this.appenders = appenders; + } + + @JsonIgnore + @Override + public boolean isEnabled() { + return !appenders.isEmpty(); + } + + @Override + public RequestLog build(String name) { + final Logger logger = (Logger) LoggerFactory.getLogger("http.request"); + logger.setAdditive(false); + + final LoggerContext context = logger.getLoggerContext(); + + final LogbackAccessRequestLog requestLog = new LogbackAccessRequestLog(); + + final LevelFilterFactory levelFilterFactory = new NullLevelFilterFactory<>(); + final AsyncAppenderFactory asyncAppenderFactory = new AsyncAccessEventAppenderFactory(); + final LayoutFactory layoutFactory = new LogbackAccessRequestLayoutFactory(); + + for (AppenderFactory output : appenders) { + requestLog.addAppender(output.build(context, name, layoutFactory, levelFilterFactory, asyncAppenderFactory)); + } + + return requestLog; + } +} diff --git a/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/RequestLogFactory.java b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/RequestLogFactory.java new file mode 100644 index 00000000000..9d7d5479052 --- /dev/null +++ b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/RequestLogFactory.java @@ -0,0 +1,18 @@ +package io.dropwizard.request.logging; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import io.dropwizard.jackson.Discoverable; +import org.eclipse.jetty.server.RequestLog; + +/** + * A service provider interface for creating a Jetty {@link RequestLog} + * + * @param type of a {@link RequestLog} implementation + */ +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = LogbackAccessRequestLogFactory.class) +public interface RequestLogFactory extends Discoverable { + + boolean isEnabled(); + + T build(String name); +} diff --git a/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/async/AsyncAccessEventAppenderFactory.java b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/async/AsyncAccessEventAppenderFactory.java new file mode 100644 index 00000000000..f941496031b --- /dev/null +++ b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/async/AsyncAccessEventAppenderFactory.java @@ -0,0 +1,20 @@ +package io.dropwizard.request.logging.async; + +import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.core.AsyncAppenderBase; +import io.dropwizard.logging.async.AsyncAppenderFactory; + +/** + * An implementation of {@link AsyncAppenderFactory} for {@link IAccessEvent}. + */ +public class AsyncAccessEventAppenderFactory implements AsyncAppenderFactory { + + /** + * Creates an {@link AsyncAppenderBase} of type {@link IAccessEvent}. + * @return the {@link AsyncAppenderBase} + */ + @Override + public AsyncAppenderBase build() { + return new AsyncAppenderBase(); + } +} diff --git a/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayout.java b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayout.java new file mode 100644 index 00000000000..3317da92db0 --- /dev/null +++ b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayout.java @@ -0,0 +1,24 @@ +package io.dropwizard.request.logging.layout; + +import ch.qos.logback.access.PatternLayout; +import ch.qos.logback.core.Context; + +import java.util.TimeZone; + +/** + * A base layout for Logback Access request logs. + *

      + *
    • Extends {@link PatternLayout}.
    • + *
    • Disables pattern headers.
    • + *
    • Sets the pattern to the given timezone.
    • + *
    + */ +public class LogbackAccessRequestLayout extends PatternLayout { + + public LogbackAccessRequestLayout(Context context, TimeZone timeZone) { + setOutputPatternAsHeader(false); + setPattern("%h %l %u [%t{dd/MMM/yyyy:HH:mm:ss Z," + timeZone.getID() + + "}] \"%r\" %s %b \"%i{Referer}\" \"%i{User-Agent}\" %D"); + setContext(context); + } +} diff --git a/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayoutFactory.java b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayoutFactory.java new file mode 100644 index 00000000000..e538af390fc --- /dev/null +++ b/dropwizard-request-logging/src/main/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayoutFactory.java @@ -0,0 +1,18 @@ +package io.dropwizard.request.logging.layout; + +import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.pattern.PatternLayoutBase; +import io.dropwizard.logging.layout.LayoutFactory; + +import java.util.TimeZone; + +/** + * Factory that creates a {@link LogbackAccessRequestLayout} + */ +public class LogbackAccessRequestLayoutFactory implements LayoutFactory { + @Override + public PatternLayoutBase build(LoggerContext context, TimeZone timeZone) { + return new LogbackAccessRequestLayout(context, timeZone); + } +} diff --git a/dropwizard-request-logging/src/main/resources/META-INF.services/io.dropwizard.jackson.Discoverable b/dropwizard-request-logging/src/main/resources/META-INF.services/io.dropwizard.jackson.Discoverable new file mode 100644 index 00000000000..8e2ab5f3ce4 --- /dev/null +++ b/dropwizard-request-logging/src/main/resources/META-INF.services/io.dropwizard.jackson.Discoverable @@ -0,0 +1 @@ +io.dropwizard.request.logging.RequestLogFactory diff --git a/dropwizard-request-logging/src/main/resources/META-INF.services/io.dropwizard.request.logging.RequestLogFactory b/dropwizard-request-logging/src/main/resources/META-INF.services/io.dropwizard.request.logging.RequestLogFactory new file mode 100644 index 00000000000..2e850696eed --- /dev/null +++ b/dropwizard-request-logging/src/main/resources/META-INF.services/io.dropwizard.request.logging.RequestLogFactory @@ -0,0 +1 @@ +io.dropwizard.request.logging.LogbackAccessRequestLogFactory diff --git a/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/LogbackAccessRequestLogTest.java b/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/LogbackAccessRequestLogTest.java new file mode 100644 index 00000000000..eb969bb6391 --- /dev/null +++ b/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/LogbackAccessRequestLogTest.java @@ -0,0 +1,76 @@ +package io.dropwizard.request.logging; + +import ch.qos.logback.access.spi.IAccessEvent; +import ch.qos.logback.core.Appender; +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.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class LogbackAccessRequestLogTest { + + @SuppressWarnings("unchecked") + private final Appender appender = mock(Appender.class); + private final LogbackAccessRequestLog requestLog = new LogbackAccessRequestLog(); + + 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.getRequestURI()).thenReturn("/test/things?yay"); + when(request.getProtocol()).thenReturn("HTTP/1.1"); + when(request.getHttpChannelState()).thenReturn(channelState); + + when(response.getStatus()).thenReturn(200); + when(response.getContentCount()).thenReturn(8290L); + + requestLog.addAppender(appender); + + requestLog.start(); + } + + @After + public void tearDown() throws Exception { + requestLog.stop(); + } + + @Test + public void logsRequestsToTheAppender() { + final IAccessEvent event = logAndCapture(); + + assertThat(event.getRemoteAddr()).isEqualTo("10.0.0.1"); + assertThat(event.getMethod()).isEqualTo("GET"); + assertThat(event.getRequestURI()).isEqualTo("/test/things?yay"); + assertThat(event.getProtocol()).isEqualTo("HTTP/1.1"); + + assertThat(event.getStatusCode()).isEqualTo(200); + assertThat(event.getContentLength()).isEqualTo(8290L); + } + + private IAccessEvent logAndCapture() { + requestLog.log(request, response); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(IAccessEvent.class); + verify(appender, timeout(1000)).doAppend(captor.capture()); + + return captor.getValue(); + } +} diff --git a/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/RequestLogFactoryTest.java b/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/RequestLogFactoryTest.java new file mode 100644 index 00000000000..eab2c202143 --- /dev/null +++ b/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/RequestLogFactoryTest.java @@ -0,0 +1,41 @@ +package io.dropwizard.request.logging; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.io.Resources; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.logging.ConsoleAppenderFactory; +import io.dropwizard.logging.FileAppenderFactory; +import io.dropwizard.logging.SyslogAppenderFactory; +import io.dropwizard.validation.BaseValidator; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; + +import static org.assertj.core.api.Assertions.assertThat; + +public class RequestLogFactoryTest { + private LogbackAccessRequestLogFactory logbackAccessRequestLogFactory; + + @Before + public void setUp() throws Exception { + final ObjectMapper objectMapper = Jackson.newObjectMapper(); + objectMapper.getSubtypeResolver().registerSubtypes(ConsoleAppenderFactory.class, + FileAppenderFactory.class, + SyslogAppenderFactory.class); + this.logbackAccessRequestLogFactory = new YamlConfigurationFactory<>(LogbackAccessRequestLogFactory.class, + BaseValidator.newValidator(), + objectMapper, "dw") + .build(new File(Resources.getResource("yaml/requestLog.yml").toURI())); + } + + @Test + public void fileAppenderFactoryIsSet() { + assertThat(logbackAccessRequestLogFactory).isNotNull(); + assertThat(logbackAccessRequestLogFactory.getAppenders()).isNotNull(); + assertThat(logbackAccessRequestLogFactory.getAppenders().size()).isEqualTo(1); + assertThat(logbackAccessRequestLogFactory.getAppenders().get(0)) + .isInstanceOf(FileAppenderFactory.class); + } +} diff --git a/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayoutTest.java b/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayoutTest.java new file mode 100644 index 00000000000..d3488de8634 --- /dev/null +++ b/dropwizard-request-logging/src/test/java/io/dropwizard/request/logging/layout/LogbackAccessRequestLayoutTest.java @@ -0,0 +1,33 @@ +package io.dropwizard.request.logging.layout; + +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.core.Context; +import org.junit.Test; + +import java.util.TimeZone; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class LogbackAccessRequestLayoutTest { + final Context context = mock(LoggerContext.class); + private final TimeZone timeZone = TimeZone.getTimeZone("UTC"); + final LogbackAccessRequestLayout layout = new LogbackAccessRequestLayout(context, timeZone); + + @Test + public void outputPatternAsHeaderIsFalse() { + assertThat(layout.isOutputPatternAsHeader()).isFalse(); + } + + @Test + public void hasAContext() throws Exception { + assertThat(layout.getContext()) + .isEqualTo(context); + } + + @Test + public void hasAPatternWithATimeZone() throws Exception { + assertThat(layout.getPattern()) + .isEqualTo("%h %l %u [%t{dd/MMM/yyyy:HH:mm:ss Z,UTC}] \"%r\" %s %b \"%i{Referer}\" \"%i{User-Agent}\" %D"); + } +} diff --git a/dropwizard-request-logging/src/test/resources/yaml/requestLog.yml b/dropwizard-request-logging/src/test/resources/yaml/requestLog.yml new file mode 100644 index 00000000000..e476e2563ec --- /dev/null +++ b/dropwizard-request-logging/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-servlets/pom.xml b/dropwizard-servlets/pom.xml new file mode 100644 index 00000000000..d32d598d284 --- /dev/null +++ b/dropwizard-servlets/pom.xml @@ -0,0 +1,86 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-servlets + Dropwizard Servlet Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + org.slf4j + slf4j-api + + + io.dropwizard + dropwizard-util + + + io.dropwizard.metrics + metrics-annotation + + + io.dropwizard.metrics + metrics-core + + + ch.qos.logback + logback-classic + + + + javax.servlet + javax.servlet-api + provided + + + org.eclipse.jetty + jetty-servlet + test + + + org.eclipse.jetty + jetty-servlet + tests + test + + + org.eclipse.jetty + jetty-http + tests + test + + + + + + + src/test/more-resources + + + src/test/resources + + + + diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/servlets/CacheBustingFilter.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/CacheBustingFilter.java similarity index 57% rename from dropwizard/src/main/java/com/yammer/dropwizard/servlets/CacheBustingFilter.java rename to dropwizard-servlets/src/main/java/io/dropwizard/servlets/CacheBustingFilter.java index fca1e61accf..b8e6526e1d9 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/servlets/CacheBustingFilter.java +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/CacheBustingFilter.java @@ -1,17 +1,21 @@ -package com.yammer.dropwizard.servlets; +package io.dropwizard.servlets; -import org.eclipse.jetty.http.HttpHeaders; +import com.google.common.net.HttpHeaders; -import javax.servlet.*; +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.HttpServletResponse; import java.io.IOException; -// TODO: 10/12/11 -- write tests for CacheBustingFilter -// TODO: 10/12/11 -- write docs for CacheBustingFilter - -@SuppressWarnings("UnusedDeclaration") +/** + * Adds a no-cache header to all responses. + */ public class CacheBustingFilter implements Filter { - private static final String MUST_REVALIDATE_NO_CACHE_NO_STORE = "must-revalidate,no-cache,no-store"; + private static final String CACHE_SETTINGS = "must-revalidate,no-cache,no-store"; @Override public void doFilter(ServletRequest request, @@ -19,7 +23,7 @@ public void doFilter(ServletRequest request, FilterChain chain) throws IOException, ServletException { if (response instanceof HttpServletResponse) { final HttpServletResponse resp = (HttpServletResponse) response; - resp.setHeader(HttpHeaders.CACHE_CONTROL, MUST_REVALIDATE_NO_CACHE_NO_STORE); + resp.setHeader(HttpHeaders.CACHE_CONTROL, CACHE_SETTINGS); } chain.doFilter(request, response); } 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 00000000000..7ae5d74a91c --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/Servlets.java @@ -0,0 +1,25 @@ +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) { + + if (request.getQueryString() == null) { + return request.getRequestURI(); + } + + return request.getRequestURI() + "?" + request.getQueryString(); + } +} 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 00000000000..1785b8a9140 --- /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. + */ +@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 00000000000..c81ab2c895d --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/ThreadNameFilter.java @@ -0,0 +1,43 @@ +package io.dropwizard.servlets; + +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; + +/** + * 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 00000000000..22eae41edde --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/AssetServlet.java @@ -0,0 +1,257 @@ +package io.dropwizard.servlets.assets; + +import com.google.common.base.CharMatcher; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +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 java.util.List; + +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; + } + + final String rangeHeader = req.getHeader(HttpHeaders.RANGE); + + final int resourceLength = cachedAsset.getResource().length; + ImmutableList ranges = ImmutableList.of(); + + boolean usingRanges = false; + // Support for HTTP Byte Ranges + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + if (rangeHeader != null) { + + final String ifRange = req.getHeader(HttpHeaders.IF_RANGE); + + if (ifRange == null || cachedAsset.getETag().equals(ifRange)) { + + try { + ranges = parseRangeHeader(rangeHeader, resourceLength); + } catch (NumberFormatException e) { + resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + + if (ranges.isEmpty()) { + resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + + resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + usingRanges = true; + + resp.addHeader(HttpHeaders.CONTENT_RANGE, "bytes " + + Joiner.on(",").join(ranges) + "/" + resourceLength); + } + } + + 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) { + // ignore + } + } + + if (mediaType.is(MediaType.ANY_VIDEO_TYPE) + || mediaType.is(MediaType.ANY_AUDIO_TYPE) || usingRanges) { + resp.addHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); + } + + resp.setContentType(mediaType.type() + '/' + mediaType.subtype()); + + if (mediaType.charset().isPresent()) { + resp.setCharacterEncoding(mediaType.charset().get().toString()); + } + + try (ServletOutputStream output = resp.getOutputStream()) { + if (usingRanges) { + for (ByteRange range : ranges) { + output.write(cachedAsset.getResource(), range.getStart(), + range.getEnd() - range.getStart() + 1); + } + } else { + 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 = getResourceUrl(absoluteRequestedResourcePath); + if (ResourceURL.isDirectory(requestedResourceURL)) { + if (indexFile != null) { + requestedResourceURL = getResourceUrl(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(readResource(requestedResourceURL), lastModified); + } + + protected URL getResourceUrl(String absoluteRequestedResourcePath) { + return Resources.getResource(absoluteRequestedResourcePath); + } + + protected byte[] readResource(URL requestedResourceURL) throws IOException { + return Resources.toByteArray(requestedResourceURL); + } + + 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()); + } + + /** + * Parses a given Range header for one or more byte ranges. + * + * @param rangeHeader Range header to parse + * @param resourceLength Length of the resource in bytes + * @return List of parsed ranges + */ + private ImmutableList parseRangeHeader(final String rangeHeader, + final int resourceLength) { + final ImmutableList.Builder builder = ImmutableList.builder(); + if (rangeHeader.indexOf("=") != -1) { + final String[] parts = rangeHeader.split("="); + if (parts.length > 1) { + final List ranges = Splitter.on(",").trimResults().splitToList(parts[1]); + for (final String range : ranges) { + builder.add(ByteRange.parse(range, resourceLength)); + } + } + } + return builder.build(); + } +} diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ByteRange.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ByteRange.java new file mode 100644 index 00000000000..427cce55e6e --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ByteRange.java @@ -0,0 +1,73 @@ +package io.dropwizard.servlets.assets; + +import javax.annotation.concurrent.Immutable; +import java.util.Objects; + +@Immutable +public final class ByteRange { + + private final int start; + private final int end; + + public ByteRange(final int start, final int end) { + this.start = start; + this.end = end; + } + + public int getStart() { + return start; + } + + public int getEnd() { + return end; + } + + public static ByteRange parse(final String byteRange, + final int resourceLength) { + // missing separator + if (byteRange.indexOf("-") == -1) { + final int start = Integer.parseInt(byteRange); + return new ByteRange(start, resourceLength - 1); + } + // negative range + if (byteRange.indexOf("-") == 0) { + final int start = Integer.parseInt(byteRange); + return new ByteRange(resourceLength + start, resourceLength - 1); + } + final String[] parts = byteRange.split("-"); + if (parts.length == 2) { + final int start = Integer.parseInt(parts[0]); + int end = Integer.parseInt(parts[1]); + if (end > resourceLength) { + end = resourceLength - 1; + } + return new ByteRange(start, end); + } else { + final int start = Integer.parseInt(parts[0]); + return new ByteRange(start, resourceLength - 1); + } + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + final ByteRange other = (ByteRange) obj; + return Objects.equals(start, other.start) && Objects.equals(end, other.end); + } + + @Override + public int hashCode() { + return Objects.hash(start, end); + } + + @Override + public String toString() { + return String.format("%d-%d", start, end); + } +} 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 00000000000..7dc2b679819 --- /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 00000000000..7753f10123c --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/assets/ResourceURL.java @@ -0,0 +1,125 @@ +package io.dropwizard.servlets.assets; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.JarURLConnection; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +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. + + final String fileName = resourceURL.getFile(); + // leaves just the relative file path inside the jar + final String relativeFilePath = fileName.substring(fileName.lastIndexOf('!') + 2); + final JarFile jarFile = jarConnection.getJarFile(); + final ZipEntry zipEntry = jarFile.getEntry(relativeFilePath); + 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/src/main/java/com/yammer/dropwizard/tasks/GarbageCollectionTask.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/GarbageCollectionTask.java similarity index 85% rename from dropwizard/src/main/java/com/yammer/dropwizard/tasks/GarbageCollectionTask.java rename to dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/GarbageCollectionTask.java index 4e75c3319c0..20e4e50ed31 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/tasks/GarbageCollectionTask.java +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/GarbageCollectionTask.java @@ -1,4 +1,4 @@ -package com.yammer.dropwizard.tasks; +package io.dropwizard.servlets.tasks; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; @@ -12,18 +12,18 @@ public class GarbageCollectionTask extends Task { private final Runtime runtime; /** - * Creates a new {@link GarbageCollectionTask}. + * Creates a new GarbageCollectionTask. */ public GarbageCollectionTask() { this(Runtime.getRuntime()); } /** - * Creates a new {@link GarbageCollectionTask} with the given {@link Runtime} instance. + * Creates a new GarbageCollectionTask with the given {@link Runtime} instance. *

    * Use {@link GarbageCollectionTask#GarbageCollectionTask()} instead. * - * @param runtime a {@link Runtime} instance + * @param runtime a {@link Runtime} instance */ public GarbageCollectionTask(Runtime runtime) { super("gc"); diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/LogConfigurationTask.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/LogConfigurationTask.java new file mode 100644 index 00000000000..5d5aae55526 --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/LogConfigurationTask.java @@ -0,0 +1,74 @@ +package io.dropwizard.servlets.tasks; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.LoggerContext; +import com.google.common.collect.ImmutableMultimap; +import org.slf4j.LoggerFactory; + +import java.io.PrintWriter; +import java.util.List; + +/** + * Sets the logging level for a number of loggers + *

    + * Parameters: + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    NameDescription
    loggerOne or more logger names to be configured with the specified log level.
    levelAn optional {@link Level} to configure. If not provided, the log level will be set to null.
    + *

    + */ +public class LogConfigurationTask extends Task { + + private final LoggerContext loggerContext; + + /** + * Creates a new LogConfigurationTask. + */ + public LogConfigurationTask() { + this((LoggerContext) LoggerFactory.getILoggerFactory()); + } + + /** + * Creates a new LogConfigurationTask with the given {@link ch.qos.logback.classic.LoggerContext} instance. + *

    + * Use {@link LogConfigurationTask#LogConfigurationTask()} instead. + * + * @param loggerContext a {@link ch.qos.logback.classic.LoggerContext} instance + */ + public LogConfigurationTask(LoggerContext loggerContext) { + super("log-level"); + this.loggerContext = loggerContext; + } + + @Override + public void execute(ImmutableMultimap parameters, PrintWriter output) throws Exception { + final List loggerNames = getLoggerNames(parameters); + final Level loggerLevel = getLoggerLevel(parameters); + + for (String loggerName : loggerNames) { + loggerContext.getLogger(loggerName).setLevel(loggerLevel); + output.println(String.format("Configured logging level for %s to %s", loggerName, loggerLevel)); + output.flush(); + } + } + + private List getLoggerNames(ImmutableMultimap parameters) { + return parameters.get("logger").asList(); + } + + private Level getLoggerLevel(ImmutableMultimap parameters) { + final List loggerLevels = parameters.get("level").asList(); + return loggerLevels.isEmpty() ? null : Level.valueOf(loggerLevels.get(0)); + } +} diff --git a/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/PostBodyTask.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/PostBodyTask.java new file mode 100644 index 00000000000..bfac1785ed0 --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/PostBodyTask.java @@ -0,0 +1,45 @@ +package io.dropwizard.servlets.tasks; + +import com.google.common.collect.ImmutableMultimap; + +import java.io.PrintWriter; + +/** + * A task which can be performed via the admin interface and provides the post body of the request. + * + * @see Task + * @see TaskServlet + */ +public abstract class PostBodyTask extends Task { + /** + * Create a new task with the given name. + * + * @param name the task's name + */ + protected PostBodyTask(String name) { + super(name); + } + + /** + * @param parameters the query string parameters + * @param body the plain text request body + * @param output a {@link PrintWriter} wrapping the output stream of the task + * @throws Exception + */ + public abstract void execute(ImmutableMultimap parameters, + String body, + PrintWriter output) throws Exception; + + /** + * Deprecated, use `execute(parameters, body, output)` or inherit from Task instead. + * + * @param parameters the query string parameters + * @param output a {@link PrintWriter} wrapping the output stream of the task + * @throws Exception + */ + @Override + @Deprecated + public void execute(ImmutableMultimap parameters, PrintWriter output) throws Exception { + throw new UnsupportedOperationException("Use `execute(parameters, body, output)`"); + } +} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/tasks/Task.java b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/Task.java similarity index 93% rename from dropwizard/src/main/java/com/yammer/dropwizard/tasks/Task.java rename to dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/Task.java index 38c9868f397..64116e711f8 100644 --- a/dropwizard/src/main/java/com/yammer/dropwizard/tasks/Task.java +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/Task.java @@ -1,11 +1,11 @@ -package com.yammer.dropwizard.tasks; +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 internal service interface. + * An arbitrary administrative task which can be performed via the admin interface. * * @see TaskServlet */ 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 00000000000..90395eca1b9 --- /dev/null +++ b/dropwizard-servlets/src/main/java/io/dropwizard/servlets/tasks/TaskServlet.java @@ -0,0 +1,238 @@ +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 com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMultimap; +import com.google.common.io.CharStreams; +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.InputStreamReader; +import java.io.PrintWriter; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Enumeration; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import static com.codahale.metrics.MetricRegistry.name; + +/** + * 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 tasks; + private final ConcurrentMap taskExecutors; + + private final MetricRegistry metricRegistry; + + /** + * Creates a new TaskServlet. + */ + public TaskServlet(MetricRegistry metricRegistry) { + this.metricRegistry = metricRegistry; + this.tasks = new ConcurrentHashMap<>(); + this.taskExecutors = new ConcurrentHashMap<>(); + } + + public void add(Task task) { + tasks.put('/' + task.getName(), task); + + TaskExecutor taskExecutor = new TaskExecutor(task); + try { + final Method executeMethod = task.getClass().getMethod("execute", + ImmutableMultimap.class, PrintWriter.class); + + if (executeMethod.isAnnotationPresent(Timed.class)) { + final Timed annotation = executeMethod.getAnnotation(Timed.class); + final String name = chooseName(annotation.name(), + annotation.absolute(), + task); + taskExecutor = new TimedTask(taskExecutor, metricRegistry.timer(name)); + } + + if (executeMethod.isAnnotationPresent(Metered.class)) { + final Metered annotation = executeMethod.getAnnotation(Metered.class); + final String name = chooseName(annotation.name(), + annotation.absolute(), + task); + taskExecutor = new MeteredTask(taskExecutor, metricRegistry.meter(name)); + } + + if (executeMethod.isAnnotationPresent(ExceptionMetered.class)) { + final ExceptionMetered annotation = executeMethod.getAnnotation(ExceptionMetered.class); + final String name = chooseName(annotation.name(), + annotation.absolute(), + task, + ExceptionMetered.DEFAULT_NAME_SUFFIX); + taskExecutor = new ExceptionMeteredTask(taskExecutor, metricRegistry.meter(name), annotation.cause()); + } + } catch (NoSuchMethodException ignored) { + } + + 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 { + final TaskExecutor taskExecutor = taskExecutors.get(task); + taskExecutor.executeTask(getParams(req), getBody(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 getParams(HttpServletRequest req) { + final ImmutableMultimap.Builder results = ImmutableMultimap.builder(); + final Enumeration names = req.getParameterNames(); + while (names.hasMoreElements()) { + final String name = names.nextElement(); + final String[] values = req.getParameterValues(name); + results.putAll(name, values); + } + return results.build(); + } + + private String getBody(HttpServletRequest req) throws IOException { + return CharStreams.toString(new InputStreamReader(req.getInputStream(), Charsets.UTF_8)); + } + + public Collection 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 params, String body, PrintWriter output) throws Exception { + try { + if (task instanceof PostBodyTask) { + PostBodyTask postBodyTask = (PostBodyTask) task; + postBodyTask.execute(params, body, output); + } else { + 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 params, String body, PrintWriter output) throws Exception { + final Timer.Context context = timer.time(); + try { + underlying.executeTask(params, body, 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 params, String body, PrintWriter output) throws Exception { + meter.mark(); + underlying.executeTask(params, body, 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 exceptionClass) { + super(underlying.task); + this.underlying = underlying; + this.exceptionMeter = exceptionMeter; + this.exceptionClass = exceptionClass; + } + + private boolean isReallyAssignableFrom(Exception e) { + return exceptionClass.isAssignableFrom(e.getClass()) || + (e.getCause() != null && exceptionClass.isAssignableFrom(e.getCause().getClass())); + } + + @Override + public void executeTask(ImmutableMultimap params, String body, PrintWriter output) throws Exception { + try { + underlying.executeTask(params, body, output); + } catch (Exception e) { + if (exceptionMeter != null && isReallyAssignableFrom(e)) { + exceptionMeter.mark(); + } else { + 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 00000000000..32eb61c7193 --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/CacheBustingFilterTest.java @@ -0,0 +1,42 @@ +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.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +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 00000000000..9fb55aceafd --- /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.assertj.core.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 00000000000..80ba1a69b74 --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/AssetServletTest.java @@ -0,0 +1,454 @@ +package io.dropwizard.servlets.assets; + +import com.google.common.net.HttpHeaders; +import org.eclipse.jetty.http.HttpHeader; +import org.eclipse.jetty.http.HttpTester; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.http.MimeTypes; +import org.eclipse.jetty.servlet.ServletTester; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.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", StandardCharsets.UTF_8); + } + } + + public static class NoIndexAssetServlet extends AssetServlet { + private static final long serialVersionUID = -1L; + + public NoIndexAssetServlet() { + super(RESOURCE_PATH, DUMMY_SERVLET, null, StandardCharsets.UTF_8); + } + } + + public static class RootAssetServlet extends AssetServlet { + private static final long serialVersionUID = 1L; + + public RootAssetServlet() { + super("/", ROOT_SERVLET, null, StandardCharsets.UTF_8); + } + } + + public static class NoCharsetAssetServlet extends AssetServlet { + private static final long serialVersionUID = 1L; + + public NoCharsetAssetServlet() { + super(RESOURCE_PATH, NOCHARSET_SERVLET, null, null); + } + } + + private static final ServletTester SERVLET_TESTER = new ServletTester(); + private final HttpTester.Request request = HttpTester.newRequest(); + private HttpTester.Response response; + + @BeforeClass + public static void startServletTester() throws Exception { + SERVLET_TESTER.addServlet(DummyAssetServlet.class, DUMMY_SERVLET + '*'); + SERVLET_TESTER.addServlet(NoIndexAssetServlet.class, NOINDEX_SERVLET + '*'); + SERVLET_TESTER.addServlet(NoCharsetAssetServlet.class, NOCHARSET_SERVLET + '*'); + SERVLET_TESTER.addServlet(RootAssetServlet.class, ROOT_SERVLET + '*'); + SERVLET_TESTER.start(); + + SERVLET_TESTER.getContext().getMimeTypes().addMimeMapping("mp4", "video/mp4"); + SERVLET_TESTER.getContext().getMimeTypes().addMimeMapping("m4a", "audio/mp4"); + } + + @AfterClass + public static void stopServletTester() throws Exception { + SERVLET_TESTER.stop(); + } + + @Before + public void setUp() throws Exception { + request.setMethod("GET"); + request.setURI(DUMMY_SERVLET + "example.txt"); + request.setVersion(HttpVersion.HTTP_1_0); + } + + @Test + public void servesFilesMappedToRoot() throws Exception { + request.setURI(ROOT_SERVLET + "assets/example.txt"); + response = HttpTester.parseResponse(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(200); + assertThat(response.getContent()) + .isEqualTo("HELLO THERE 2"); + } + + @Test + public void servesFilesWithA200() throws Exception { + response = HttpTester.parseResponse(SERVLET_TESTER.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(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(404); + } + + @Test + public void consistentlyAssignsETags() throws Exception { + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final String firstEtag = response.get(HttpHeaders.ETAG); + + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final String secondEtag = response.get(HttpHeaders.ETAG); + + assertThat(firstEtag) + .isEqualTo("\"174a6dd7325e64c609eab14ab1d30b86\"") + .isEqualTo(secondEtag); + } + + @Test + public void assignsDifferentETagsForDifferentFiles() throws Exception { + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final String firstEtag = response.get(HttpHeaders.ETAG); + + request.setURI(DUMMY_SERVLET + "foo.bar"); + response = HttpTester.parseResponse(SERVLET_TESTER.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(SERVLET_TESTER.getResponses(request.generate())); + final String correctEtag = response.get(HttpHeaders.ETAG); + + request.setHeader(HttpHeaders.IF_NONE_MATCH, correctEtag); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final int statusWithMatchingEtag = response.getStatus(); + + request.setHeader(HttpHeaders.IF_NONE_MATCH, correctEtag + "FOO"); + response = HttpTester.parseResponse(SERVLET_TESTER.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(SERVLET_TESTER.getResponses(request.generate())); + final long firstLastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED); + + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final long secondLastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED); + + assertThat(firstLastModifiedTime) + .isEqualTo(secondLastModifiedTime); + } + + @Test + public void supportsByteRangeForMedia() throws Exception { + request.setURI(ROOT_SERVLET + "assets/foo.mp4"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + + request.setURI(ROOT_SERVLET + "assets/foo.m4a"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + } + + @Test + public void supportsFullByteRange() throws Exception { + request.setURI(ROOT_SERVLET + "assets/example.txt"); + request.setHeader(HttpHeaders.RANGE, "bytes=0-"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(206); + assertThat(response.getContent()).isEqualTo("HELLO THERE"); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( + "bytes 0-10/11"); + } + + @Test + public void supportsCentralByteRange() throws Exception { + request.setURI(ROOT_SERVLET + "assets/example.txt"); + request.setHeader(HttpHeaders.RANGE, "bytes=4-8"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(206); + assertThat(response.getContent()).isEqualTo("O THE"); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( + "bytes 4-8/11"); + assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("5"); + } + + @Test + public void supportsFinalByteRange() throws Exception { + request.setURI(ROOT_SERVLET + "assets/example.txt"); + request.setHeader(HttpHeaders.RANGE, "bytes=10-10"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(206); + assertThat(response.getContent()).isEqualTo("E"); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( + "bytes 10-10/11"); + assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("1"); + + request.setHeader(HttpHeaders.RANGE, "bytes=-1"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(206); + assertThat(response.getContent()).isEqualTo("E"); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( + "bytes 10-10/11"); + assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("1"); + } + + @Test + public void rejectsInvalidByteRanges() throws Exception { + request.setURI(ROOT_SERVLET + "assets/example.txt"); + request.setHeader(HttpHeaders.RANGE, "bytes=test"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(416); + + request.setHeader(HttpHeaders.RANGE, "bytes="); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(416); + + request.setHeader(HttpHeaders.RANGE, "bytes=1-infinity"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(416); + + request.setHeader(HttpHeaders.RANGE, "test"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(416); + } + + @Test + public void supportsMultipleByteRanges() throws Exception { + request.setURI(ROOT_SERVLET + "assets/example.txt"); + request.setHeader(HttpHeaders.RANGE, "bytes=0-0,-1"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(206); + assertThat(response.getContent()).isEqualTo("HE"); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( + "bytes 0-0,10-10/11"); + assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("2"); + + request.setHeader(HttpHeaders.RANGE, "bytes=5-6,7-10"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + assertThat(response.getStatus()).isEqualTo(206); + assertThat(response.getContent()).isEqualTo(" THERE"); + assertThat(response.get(HttpHeaders.ACCEPT_RANGES)).isEqualTo("bytes"); + assertThat(response.get(HttpHeaders.CONTENT_RANGE)).isEqualTo( + "bytes 5-6,7-10/11"); + assertThat(response.get(HttpHeaders.CONTENT_LENGTH)).isEqualTo("6"); + } + + @Test + public void supportsIfRangeMatchRequests() throws Exception { + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + final String correctEtag = response.get(HttpHeaders.ETAG); + + request.setHeader(HttpHeaders.RANGE, "bytes=10-10"); + + request.setHeader(HttpHeaders.IF_RANGE, correctEtag); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + final int statusWithMatchingEtag = response.getStatus(); + + request.setHeader(HttpHeaders.IF_RANGE, correctEtag + "FOO"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request + .generate())); + final int statusWithNonMatchingEtag = response.getStatus(); + + assertThat(statusWithMatchingEtag).isEqualTo(206); + assertThat(statusWithNonMatchingEtag).isEqualTo(200); + } + + @Test + public void supportsIfModifiedSinceRequests() throws Exception { + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final long lastModifiedTime = response.getDateField(HttpHeaders.LAST_MODIFIED); + + request.putDateField(HttpHeaders.IF_MODIFIED_SINCE, lastModifiedTime); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final int statusWithMatchingLastModifiedTime = response.getStatus(); + + request.putDateField(HttpHeaders.IF_MODIFIED_SINCE, + lastModifiedTime - 100); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + final int statusWithStaleLastModifiedTime = response.getStatus(); + + request.putDateField(HttpHeaders.IF_MODIFIED_SINCE, + lastModifiedTime + 100); + response = HttpTester.parseResponse(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.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(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(404); + + // Subdirectory listing: + request.setURI(NOINDEX_SERVLET + "some_directory"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(404); + + // Subdirectory listing with slash: + request.setURI(NOINDEX_SERVLET + "some_directory/"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(404); + } + + @Test + public void doesNotAllowOverridingUrls() throws Exception { + request.setURI(DUMMY_SERVLET + "file:/etc/passwd"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(404); + } + + @Test + public void doesNotAllowOverridingPaths() throws Exception { + request.setURI(DUMMY_SERVLET + "/etc/passwd"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(404); + } + + @Test + public void allowsEncodedAssetNames() throws Exception { + request.setURI(DUMMY_SERVLET + "encoded%20example.txt"); + response = HttpTester.parseResponse(SERVLET_TESTER.getResponses(request.generate())); + assertThat(response.getStatus()) + .isEqualTo(200); + } +} diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ByteRangeTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ByteRangeTest.java new file mode 100644 index 00000000000..f06a93814b5 --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ByteRangeTest.java @@ -0,0 +1,52 @@ +package io.dropwizard.servlets.assets; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ByteRangeTest { + + private static final int RESOURCE_LENGTH = 10000; + + @Test + public void firstBytes() { + final ByteRange actual = ByteRange.parse("0-499", RESOURCE_LENGTH); + assertThat(actual.getStart()).isEqualTo(0); + assertThat(actual.getEnd()).isEqualTo(499); + } + + @Test + public void secondBytes() { + final ByteRange actual = ByteRange.parse("500-999", RESOURCE_LENGTH); + assertThat(actual.getStart()).isEqualTo(500); + assertThat(actual.getEnd()).isEqualTo(999); + } + + @Test + public void finalBytes() { + final ByteRange actual = ByteRange.parse("-500", RESOURCE_LENGTH); + assertThat(actual.getStart()).isEqualTo(9500); + assertThat(actual.getEnd()).isEqualTo(9999); + } + + @Test + public void noEndBytes() { + final ByteRange actual = ByteRange.parse("9500-", RESOURCE_LENGTH); + assertThat(actual.getStart()).isEqualTo(9500); + assertThat(actual.getEnd()).isEqualTo(9999); + } + + @Test + public void startBytes() { + final ByteRange actual = ByteRange.parse("9500", RESOURCE_LENGTH); + assertThat(actual.getStart()).isEqualTo(9500); + assertThat(actual.getEnd()).isEqualTo(9999); + } + + @Test + public void tooManyBytes() { + final ByteRange actual = ByteRange.parse("9000-20000", RESOURCE_LENGTH); + assertThat(actual.getStart()).isEqualTo(9000); + assertThat(actual.getEnd()).isEqualTo(9999); + } +} 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 00000000000..dbdc71f41d3 --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/assets/ResourceURLTest.java @@ -0,0 +1,146 @@ +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.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.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/src/test/java/com/yammer/dropwizard/tasks/tests/GarbageCollectionTaskTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/GarbageCollectionTaskTest.java similarity index 78% rename from dropwizard/src/test/java/com/yammer/dropwizard/tasks/tests/GarbageCollectionTaskTest.java rename to dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/GarbageCollectionTaskTest.java index b8c68b4fc71..360a6df9d93 100644 --- a/dropwizard/src/test/java/com/yammer/dropwizard/tasks/tests/GarbageCollectionTaskTest.java +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/GarbageCollectionTaskTest.java @@ -1,13 +1,13 @@ -package com.yammer.dropwizard.tasks.tests; +package io.dropwizard.servlets.tasks; import com.google.common.collect.ImmutableMultimap; -import com.yammer.dropwizard.tasks.GarbageCollectionTask; -import com.yammer.dropwizard.tasks.Task; import org.junit.Test; import java.io.PrintWriter; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; @SuppressWarnings("CallToSystemGC") public class GarbageCollectionTaskTest { @@ -17,7 +17,7 @@ public class GarbageCollectionTaskTest { @Test public void runsOnceWithNoParameters() throws Exception { - task.execute(ImmutableMultimap.of(), output); + task.execute(ImmutableMultimap.of(), output); verify(runtime, times(1)).gc(); } diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/LogConfigurationTaskTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/LogConfigurationTaskTest.java new file mode 100644 index 00000000000..886487b6e3c --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/LogConfigurationTaskTest.java @@ -0,0 +1,85 @@ +package io.dropwizard.servlets.tasks; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import com.google.common.collect.ImmutableMultimap; +import org.junit.Before; +import org.junit.Test; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LogConfigurationTaskTest { + + private static final Level DEFAULT_LEVEL = Level.ALL; + + private final LoggerContext loggerContext = new LoggerContext(); + private final Logger logger1 = loggerContext.getLogger("logger.one"); + private final Logger logger2 = loggerContext.getLogger("logger.two"); + + private final StringWriter stringWriter = new StringWriter(); + private final PrintWriter output = new PrintWriter(stringWriter); + + private final LogConfigurationTask task = new LogConfigurationTask(loggerContext); + + @Before + public void setUp() throws Exception { + logger1.setLevel(DEFAULT_LEVEL); + logger2.setLevel(DEFAULT_LEVEL); + } + + @Test + public void configuresSpecificLevelForALogger() throws Exception { + // given + ImmutableMultimap parameters = ImmutableMultimap.of( + "logger", "logger.one", + "level", "debug"); + + // when + task.execute(parameters, output); + + // then + assertThat(logger1.getLevel()).isEqualTo(Level.DEBUG); + assertThat(logger2.getLevel()).isEqualTo(DEFAULT_LEVEL); + + assertThat(stringWriter.toString()).isEqualTo(String.format("Configured logging level for logger.one to DEBUG%n")); + } + + @Test + public void configuresDefaultLevelForALogger() throws Exception { + // given + ImmutableMultimap parameters = ImmutableMultimap.of( + "logger", "logger.one"); + + // when + task.execute(parameters, output); + + // then + assertThat(logger1.getLevel()).isNull(); + assertThat(logger2.getLevel()).isEqualTo(DEFAULT_LEVEL); + + assertThat(stringWriter.toString()).isEqualTo(String.format("Configured logging level for logger.one to null%n")); + } + + @Test + public void configuresLevelForMultipleLoggers() throws Exception { + // given + ImmutableMultimap parameters = ImmutableMultimap.of( + "logger", "logger.one", + "logger", "logger.two", + "level", "INFO"); + + // when + task.execute(parameters, output); + + // then + assertThat(logger1.getLevel()).isEqualTo(Level.INFO); + assertThat(logger2.getLevel()).isEqualTo(Level.INFO); + + assertThat(stringWriter.toString()) + .isEqualTo(String.format("Configured logging level for logger.one to INFO%nConfigured logging level for logger.two to INFO%n")); + } +} diff --git a/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/PostBodyTaskTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/PostBodyTaskTest.java new file mode 100644 index 00000000000..14c24ba9ded --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/PostBodyTaskTest.java @@ -0,0 +1,20 @@ +package io.dropwizard.servlets.tasks; + +import com.google.common.collect.ImmutableMultimap; +import org.junit.Test; + +import java.io.PrintWriter; + +public class PostBodyTaskTest { + private final PostBodyTask task = new PostBodyTask("test") { + @Override + public void execute(ImmutableMultimap parameters, String body, PrintWriter output) throws Exception { + + } + }; + + @Test(expected = UnsupportedOperationException.class) + public void throwsExceptionWhenCallingExecuteWithoutThePostBody() throws Exception { + task.execute(new ImmutableMultimap.Builder().build(), new PrintWriter(System.out)); + } +} 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 00000000000..6de3f949932 --- /dev/null +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskServletTest.java @@ -0,0 +1,174 @@ +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.ReadListener; +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TaskServletTest { + private final Task gc = mock(Task.class); + private final PostBodyTask printJSON = mock(PostBodyTask.class); + + { + when(gc.getName()).thenReturn("gc"); + when(printJSON.getName()).thenReturn("print-json"); + } + + 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(printJSON); + } + + @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); + final ServletInputStream bodyStream = new TestServletInputStream(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))); + + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/gc"); + when(request.getParameterNames()).thenReturn(Collections.enumeration(ImmutableList.of())); + when(response.getWriter()).thenReturn(output); + when(request.getInputStream()).thenReturn(bodyStream); + + servlet.service(request, response); + + verify(gc).execute(ImmutableMultimap.of(), output); + } + + @Test + public void passesQueryStringParamsAlong() throws Exception { + final PrintWriter output = mock(PrintWriter.class); + final ServletInputStream bodyStream = new TestServletInputStream(new ByteArrayInputStream("".getBytes(StandardCharsets.UTF_8))); + + 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(request.getInputStream()).thenReturn(bodyStream); + when(response.getWriter()).thenReturn(output); + + servlet.service(request, response); + + verify(gc).execute(ImmutableMultimap.of("runs", "1"), output); + } + + @Test + public void passesPostBodyAlongToPostBodyTasks() throws Exception { + String body = "{\"json\": true}"; + final PrintWriter output = mock(PrintWriter.class); + final ServletInputStream bodyStream = new TestServletInputStream(new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8))); + + when(request.getMethod()).thenReturn("POST"); + when(request.getPathInfo()).thenReturn("/print-json"); + when(request.getParameterNames()).thenReturn(Collections.enumeration(ImmutableList.of())); + when(request.getInputStream()).thenReturn(bodyStream); + when(response.getWriter()).thenReturn(output); + + servlet.service(request, response); + + verify(printJSON).execute(ImmutableMultimap.of(), body, 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.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"); + } + } + + @Test + public void verifyPostBodyTaskExecuteMethod() { + try { + PostBodyTask.class.getMethod("execute", ImmutableMultimap.class, String.class, PrintWriter.class); + } catch (NoSuchMethodException e) { + Assert.fail("Execute method for " + PostBodyTask.class.getName() + " not found"); + } + } + + private static class TestServletInputStream extends ServletInputStream { + private InputStream delegate; + + public TestServletInputStream(InputStream delegate) { + this.delegate = delegate; + } + + @Override + public boolean isFinished() { + return false; + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void setReadListener(ReadListener readListener) { + + } + + @Override + public int read() throws IOException { + return delegate.read(); + } + } +} diff --git a/dropwizard/src/test/java/com/yammer/dropwizard/tasks/tests/TaskTest.java b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskTest.java similarity index 64% rename from dropwizard/src/test/java/com/yammer/dropwizard/tasks/tests/TaskTest.java rename to dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskTest.java index bd8b263b0c0..a6d325e4842 100644 --- a/dropwizard/src/test/java/com/yammer/dropwizard/tasks/tests/TaskTest.java +++ b/dropwizard-servlets/src/test/java/io/dropwizard/servlets/tasks/TaskTest.java @@ -1,13 +1,11 @@ -package com.yammer.dropwizard.tasks.tests; +package io.dropwizard.servlets.tasks; import com.google.common.collect.ImmutableMultimap; -import com.yammer.dropwizard.tasks.Task; import org.junit.Test; import java.io.PrintWriter; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class TaskTest { private final Task task = new Task("test") { @@ -20,7 +18,7 @@ public void execute(ImmutableMultimap parameters, @Test public void hasAName() throws Exception { - assertThat(task.getName(), - is("test")); + 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 00000000000..df81343d853 --- /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 00000000000..8435ed0fcde --- /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 00000000000..25cd1c9cd5e --- /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 00000000000..f497738854e --- /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/foo.m4a b/dropwizard-servlets/src/test/resources/assets/foo.m4a new file mode 100644 index 00000000000..f04103f223b --- /dev/null +++ b/dropwizard-servlets/src/test/resources/assets/foo.m4a @@ -0,0 +1 @@ +NOT REALLY AN MP4 FILE \ No newline at end of file diff --git a/dropwizard-servlets/src/test/resources/assets/foo.mp4 b/dropwizard-servlets/src/test/resources/assets/foo.mp4 new file mode 100644 index 00000000000..f04103f223b --- /dev/null +++ b/dropwizard-servlets/src/test/resources/assets/foo.mp4 @@ -0,0 +1 @@ +NOT REALLY AN MP4 FILE \ 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 00000000000..bace7b48a40 --- /dev/null +++ b/dropwizard-servlets/src/test/resources/assets/index.htm @@ -0,0 +1,5 @@ + + +/assets Index File + + \ 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 00000000000..0dc44dd52f5 --- /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 00000000000..f0a8ea371c8 --- /dev/null +++ b/dropwizard-servlets/src/test/resources/assets/some_directory/index.htm @@ -0,0 +1,5 @@ + + +/assets/some_directory Index File + + \ 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-servlets/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-testing/pom.xml b/dropwizard-testing/pom.xml new file mode 100644 index 00000000000..09759a4e358 --- /dev/null +++ b/dropwizard-testing/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-testing + Dropwizard Test Helpers + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + junit + junit + compile + + + org.mockito + mockito-core + compile + + + org.objenesis + objenesis + + + org.assertj + assertj-core + compile + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + compile + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-grizzly2 + test + + + diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/ConfigOverride.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/ConfigOverride.java new file mode 100644 index 00000000000..55535e39e9f --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/ConfigOverride.java @@ -0,0 +1,72 @@ +package io.dropwizard.testing; + +import java.util.function.Supplier; +import io.dropwizard.testing.junit.DropwizardAppRule; + +/** + * An override for a field in dropwizard configuration intended for use with + * {@link DropwizardAppRule}. + *

    + * Given a configuration file containing + *

    + * ---
    + * server:
    + *   applicationConnectors:
    + *     - type: http
    + *       port: 8000
    + *   adminConnectors:
    + *     - type: http
    + *       port: 8001
    + *
    + * logging:
    + *   loggers:
    + *     com.example.foo: INFO
    + * 
    + *
      + *
    • ConfigOverride.config("debug", "true") will add a top level + * field named "debug" mapped to the string "true".
    • + *
    • ConfigOverride.config("server.applicationConnectors[0].type", + * "https") will change the sole application connector to have type + * "https" instead of type "http". + *
    • ConfigOverride.config("logging.loggers.com\\.example\\.bar", + * "DEBUG") will add a logger with the name "com.example.bar" configured + * for debug logging.
    • + *
    + */ +public class ConfigOverride { + + public static final String DEFAULT_PREFIX = "dw."; + private final String key; + private final Supplier value; + private final String propertyPrefix; + + private ConfigOverride(String propertyPrefix, String key, Supplier value) { + this.key = key; + this.value = value; + this.propertyPrefix = propertyPrefix.endsWith(".") ? propertyPrefix : propertyPrefix + "."; + } + + public static ConfigOverride config(String key, String value) { + return new ConfigOverride(DEFAULT_PREFIX, key, () -> value); + } + + public static ConfigOverride config(String propertyPrefix, String key, String value) { + return new ConfigOverride(propertyPrefix, key, () -> value); + } + + public static ConfigOverride config(String key, Supplier value) { + return new ConfigOverride(DEFAULT_PREFIX, key, value); + } + + public static ConfigOverride config(String propertyPrefix, String key, Supplier value) { + return new ConfigOverride(propertyPrefix, key, value); + } + + public void addToSystemProperties() { + System.setProperty(propertyPrefix + key, value.get()); + } + + public void removeFromSystemProperties() { + System.clearProperty(propertyPrefix + key); + } +} diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/DropwizardTestSupport.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/DropwizardTestSupport.java new file mode 100644 index 00000000000..a719d4b3403 --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/DropwizardTestSupport.java @@ -0,0 +1,275 @@ +package io.dropwizard.testing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.cli.Command; +import io.dropwizard.cli.ServerCommand; +import io.dropwizard.configuration.YamlConfigurationFactory; +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; +import net.sourceforge.argparse4j.inf.Namespace; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.ServerConnector; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; + +import static com.google.common.base.MoreObjects.firstNonNull; +import static com.google.common.base.Throwables.propagate; + +/** + * A test support class for starting and stopping your application at the start and end of a test class. + *

    + * 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 the configuration type + */ +public class DropwizardTestSupport { + protected final Class> applicationClass; + protected final String configPath; + protected final Set configOverrides; + protected final Optional customPropertyPrefix; + protected final Function, Command> commandInstantiator; + + /** + * Flag that indicates whether instance was constructed with an explicit + * Configuration object or not; handling of the two cases differ. + * Needed because state of {@link #configuration} changes during lifecycle. + */ + protected final boolean explicitConfig; + + protected C configuration; + protected Application application; + protected Environment environment; + protected Server jettyServer; + protected List> listeners = new ArrayList<>(); + + public DropwizardTestSupport(Class> applicationClass, + @Nullable String configPath, + ConfigOverride... configOverrides) { + this(applicationClass, configPath, Optional.empty(), configOverrides); + } + + public DropwizardTestSupport(Class> applicationClass, String configPath, + Optional customPropertyPrefix, ConfigOverride... configOverrides) { + this(applicationClass, configPath, customPropertyPrefix, ServerCommand::new, configOverrides); + } + + public DropwizardTestSupport(Class> applicationClass, String configPath, + Optional customPropertyPrefix, + Function, Command> commandInstantiator, + ConfigOverride... configOverrides) { + this.applicationClass = applicationClass; + this.configPath = configPath; + this.configOverrides = ImmutableSet.copyOf(firstNonNull(configOverrides, new ConfigOverride[0])); + this.customPropertyPrefix = customPropertyPrefix; + explicitConfig = false; + this.commandInstantiator = commandInstantiator; + } + + /** + * Alternative constructor that may be used to directly provide Configuration + * to use, instead of specifying resource path for locating data to create + * Configuration. + * + * @since 0.9 + * + * @param applicationClass Type of Application to create + * @param configuration Pre-constructed configuration object caller provides; will not + * be manipulated in any way, no overriding + */ + public DropwizardTestSupport(Class> applicationClass, + C configuration) { + this(applicationClass, configuration, ServerCommand::new); + } + + + /** + * Alternate constructor that allows specifying the command the Dropwizard application is started with. + * @since 1.1.0 + * @param applicationClass Type of Application to create + * @param configuration Pre-constructed configuration object caller provides; will not + * be manipulated in any way, no overriding + * @param commandInstantiator The {@link Function} used to instantiate the {@link Command} used to + * start the Application + */ + public DropwizardTestSupport(Class> applicationClass, + C configuration, Function, + Command> commandInstantiator) { + if (configuration == null) { + throw new IllegalArgumentException("Can not pass null configuration for explicitly configured instance"); + } + this.applicationClass = applicationClass; + configPath = ""; + configOverrides = ImmutableSet.of(); + customPropertyPrefix = Optional.empty(); + this.configuration = configuration; + explicitConfig = true; + this.commandInstantiator = commandInstantiator; + } + + public DropwizardTestSupport addListener(ServiceListener listener) { + this.listeners.add(listener); + return this; + } + + public DropwizardTestSupport manage(final Managed managed) { + return addListener(new ServiceListener() { + @Override + public void onRun(C configuration, Environment environment, DropwizardTestSupport rule) throws Exception { + environment.lifecycle().manage(managed); + } + }); + } + + public void before() { + applyConfigOverrides(); + startIfRequired(); + } + + public void after() { + try { + stopIfRequired(); + } finally { + resetConfigOverrides(); + } + } + + private void stopIfRequired() { + if (jettyServer != null) { + for (ServiceListener listener : listeners) { + try { + listener.onStop(this); + } catch (Exception ignored) { + } + } + try { + jettyServer.stop(); + } catch (Exception e) { + throw propagate(e); + } finally { + jettyServer = null; + } + } + } + + private void applyConfigOverrides() { + for (ConfigOverride configOverride : configOverrides) { + configOverride.addToSystemProperties(); + } + } + + private void resetConfigOverrides() { + for (ConfigOverride configOverride : configOverrides) { + configOverride.removeFromSystemProperties(); + } + } + + private void startIfRequired() { + if (jettyServer != null) { + return; + } + + try { + application = newApplication(); + + final Bootstrap bootstrap = new Bootstrap(application) { + @Override + public void run(C configuration, Environment environment) throws Exception { + environment.lifecycle().addServerLifecycleListener(server -> jettyServer = server); + DropwizardTestSupport.this.configuration = configuration; + DropwizardTestSupport.this.environment = environment; + super.run(configuration, environment); + for (ServiceListener listener : listeners) { + try { + listener.onRun(configuration, environment, DropwizardTestSupport.this); + } catch (Exception ex) { + throw new RuntimeException("Error running app rule start listener", ex); + } + } + } + }; + if (explicitConfig) { + bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) -> + new POJOConfigurationFactory<>(configuration)); + } else if (customPropertyPrefix.isPresent()) { + bootstrap.setConfigurationFactoryFactory((klass, validator, objectMapper, propertyPrefix) -> + new YamlConfigurationFactory<>(klass, validator, objectMapper, customPropertyPrefix.get())); + } + + application.initialize(bootstrap); + final Command command = commandInstantiator.apply(application); + + final ImmutableMap.Builder 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 propagate(e); + } + } + + public C getConfiguration() { + return configuration; + } + + public int getLocalPort() { + return ((ServerConnector) jettyServer.getConnectors()[0]).getLocalPort(); + } + + public int getAdminPort() { + final Connector[] connectors = jettyServer.getConnectors(); + return ((ServerConnector) connectors[connectors.length - 1]).getLocalPort(); + } + + public int getPort(int connectorIndex) { + return ((ServerConnector) jettyServer.getConnectors()[connectorIndex]).getLocalPort(); + } + + public Application newApplication() { + try { + return applicationClass.getConstructor().newInstance(); + } catch (Exception e) { + throw propagate(e); + } + } + + @SuppressWarnings("unchecked") + public > A getApplication() { + return (A) application; + } + + public Environment getEnvironment() { + return environment; + } + + public ObjectMapper getObjectMapper() { + return getEnvironment().getObjectMapper(); + } + + public abstract static class ServiceListener { + public void onRun(T configuration, Environment environment, DropwizardTestSupport rule) throws Exception { + // Default NOP + } + + public void onStop(DropwizardTestSupport rule) throws Exception { + // Default NOP + } + } +} 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 00000000000..ffc6ccba246 --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/FixtureHelpers.java @@ -0,0 +1,43 @@ +package io.dropwizard.testing; + +import com.google.common.io.Resources; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * A set of helper method for fixture files. + */ +public class FixtureHelpers { + private FixtureHelpers() { /* singleton */ } + + /** + * Reads the given fixture file from the classpath (e. g. {@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 IllegalArgumentException if an I/O error occurs. + */ + public static String fixture(String filename) { + return fixture(filename, StandardCharsets.UTF_8); + } + + /** + * Reads the given fixture file from the classpath (e. g. {@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 IllegalArgumentException if an I/O error occurs. + */ + private static String fixture(String filename, Charset charset) { + try { + return Resources.toString(Resources.getResource(filename), charset).trim(); + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/POJOConfigurationFactory.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/POJOConfigurationFactory.java new file mode 100644 index 00000000000..61cdbff520f --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/POJOConfigurationFactory.java @@ -0,0 +1,39 @@ +package io.dropwizard.testing; + +import com.fasterxml.jackson.databind.JsonNode; +import io.dropwizard.Configuration; +import io.dropwizard.configuration.ConfigurationSourceProvider; +import io.dropwizard.configuration.YamlConfigurationFactory; + +import java.io.File; + +public class POJOConfigurationFactory + extends YamlConfigurationFactory { + protected final C configuration; + + @SuppressWarnings("unchecked") + public POJOConfigurationFactory(C cfg) { + super((Class) cfg.getClass(), null, null, null); + configuration = cfg; + } + + @Override + public C build(ConfigurationSourceProvider provider, String path) { + return configuration; + } + + @Override + public C build(File file) { + return configuration; + } + + @Override + public C build() { + return configuration; + } + + @Override + protected C build(JsonNode node, String path) { + return configuration; + } +} diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/ResourceHelpers.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/ResourceHelpers.java new file mode 100644 index 00000000000..28748c6cbcd --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/ResourceHelpers.java @@ -0,0 +1,26 @@ +package io.dropwizard.testing; + +import com.google.common.io.Resources; + +import java.io.File; + +/** + * A set of helper methods for working with classpath resources. + */ +public class ResourceHelpers { + private ResourceHelpers() { /* singleton */ } + + /** + * Detects the absolute path of a class path resource. + * + * @param resourceClassPathLocation the filename of the class path resource + * @return the absolute path to the denoted resource + */ + public static String resourceFilePath(final String resourceClassPathLocation) { + try { + return new File(Resources.getResource(resourceClassPathLocation).toURI()).getAbsolutePath(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} 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 00000000000..ce50f32cba2 --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardAppRule.java @@ -0,0 +1,204 @@ +package io.dropwizard.testing.junit; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.cli.Command; +import io.dropwizard.cli.ServerCommand; +import io.dropwizard.lifecycle.Managed; +import io.dropwizard.setup.Environment; +import io.dropwizard.testing.ConfigOverride; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.rules.ExternalResource; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; + + +/** + * A JUnit rule for starting and stopping your application at the start and end of a test class. + *

    + * 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). + *

    + * + *

    + * Using DropwizardAppRule at the suite level can speed up test runs, as the application is only started and stopped + * once for the entire suite: + *

    + * + *
    + * @RunWith(Suite.class)
    + * @SuiteClasses({FooTest.class, BarTest.class})
    + * public class MySuite {
    + *   @ClassRule
    + *   public static final DropwizardAppRule<MyConfig> DROPWIZARD = new DropwizardAppRule<>(...);
    + * }
    + * 
    + * + *

    + * If the same instance of DropwizardAppRule is reused at the suite- and class-level, then the application will be + * started and stopped once, regardless of whether the entire suite or a single test is executed. + *

    + * + *
    + * public class FooTest {
    + *   @ClassRule public static final DropwizardAppRule<MyConfig> DROPWIZARD = MySuite.DROPWIZARD;
    + *
    + *   public void testFoo() { ... }
    + * }
    + *
    + * public class BarTest {
    + *   @ClassRule public static final DropwizardAppRule<MyConfig> DROPWIZARD = MySuite.DROPWIZARD;
    + *
    + *   public void testBar() { ... }
    + * }
    + * 
    + * + *

    + * + *

    + * + * @param the configuration type + */ +public class DropwizardAppRule extends ExternalResource { + + private final DropwizardTestSupport testSupport; + + private final AtomicInteger recursiveCallCount = new AtomicInteger(0); + + public DropwizardAppRule(Class> applicationClass) { + this(applicationClass, (String) null); + } + + public DropwizardAppRule(Class> applicationClass, + @Nullable String configPath, + ConfigOverride... configOverrides) { + this(applicationClass, configPath, Optional.empty(), configOverrides); + } + + public DropwizardAppRule(Class> applicationClass, String configPath, + Optional customPropertyPrefix, ConfigOverride... configOverrides) { + this(applicationClass, configPath, customPropertyPrefix, ServerCommand::new, configOverrides); + } + + public DropwizardAppRule(Class> applicationClass, String configPath, + Optional customPropertyPrefix, Function, + Command> commandInstantiator, ConfigOverride... configOverrides) { + this(new DropwizardTestSupport<>(applicationClass, configPath, customPropertyPrefix, commandInstantiator, + configOverrides)); + } + + /** + * Alternate constructor that allows specifying exact Configuration object to + * use, instead of reading a resource and binding it as Configuration object. + * + * @since 0.9 + */ + public DropwizardAppRule(Class> applicationClass, + C configuration) { + this(new DropwizardTestSupport<>(applicationClass, configuration)); + } + + /** + * Alternate constructor that allows specifying the command the Dropwizard application is started with. + * + * @since 1.1.0 + */ + public DropwizardAppRule(Class> applicationClass, + C configuration, Function, Command> commandInstantiator) { + this(new DropwizardTestSupport<>(applicationClass, configuration, commandInstantiator)); + } + + public DropwizardAppRule(DropwizardTestSupport testSupport) { + this.testSupport = testSupport; + } + + public DropwizardAppRule addListener(final ServiceListener listener) { + this.testSupport.addListener(new DropwizardTestSupport.ServiceListener() { + @Override + public void onRun(C configuration, Environment environment, DropwizardTestSupport rule) throws Exception { + listener.onRun(configuration, environment, DropwizardAppRule.this); + } + + @Override + public void onStop(DropwizardTestSupport rule) throws Exception { + listener.onStop(DropwizardAppRule.this); + } + }); + return this; + } + + public DropwizardAppRule manage(final Managed managed) { + return addListener(new ServiceListener() { + @Override + public void onRun(C configuration, Environment environment, DropwizardAppRule rule) throws Exception { + environment.lifecycle().manage(managed); + } + }); + } + + @Override + protected void before() { + if (recursiveCallCount.getAndIncrement() == 0) { + testSupport.before(); + } + } + + @Override + protected void after() { + if (recursiveCallCount.decrementAndGet() == 0) { + testSupport.after(); + } + } + + public C getConfiguration() { + return testSupport.getConfiguration(); + } + + public int getLocalPort() { + return testSupport.getLocalPort(); + } + + public int getPort(int connectorIndex) { + return testSupport.getPort(connectorIndex); + } + + public int getAdminPort() { + return testSupport.getAdminPort(); + } + + public Application newApplication() { + return testSupport.newApplication(); + } + + public
    > A getApplication() { + return testSupport.getApplication(); + } + + public Environment getEnvironment() { + return testSupport.getEnvironment(); + } + + public ObjectMapper getObjectMapper() { + return testSupport.getObjectMapper(); + } + + public abstract static class ServiceListener { + + public void onRun(T configuration, Environment environment, DropwizardAppRule rule) throws Exception { + // Default NOP + } + + public void onStop(DropwizardAppRule rule) throws Exception { + // Default NOP + } + } + + public DropwizardTestSupport getTestSupport() { + return testSupport; + } +} diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardClientRule.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardClientRule.java new file mode 100644 index 00000000000..c28e24b8c7a --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardClientRule.java @@ -0,0 +1,117 @@ +package io.dropwizard.testing.junit; + +import com.codahale.metrics.health.HealthCheck; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.jetty.HttpConnectorFactory; +import io.dropwizard.server.SimpleServerFactory; +import io.dropwizard.setup.Environment; +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.rules.ExternalResource; + +import java.net.URI; +import java.net.URL; + +/** + * Test your HTTP client code by writing a JAX-RS test double class and let this rule start and stop a + * Dropwizard application containing your doubles. + *

    + * Example: + *

    
    +    {@literal @}Path("/ping")
    +    public static class PingResource {
    +        {@literal @}GET
    +        public String ping() {
    +            return "pong";
    +        }
    +    }
    +
    +    {@literal @}ClassRule
    +    public static DropwizardClientRule dropwizard = new DropwizardClientRule(new PingResource());
    +
    +    {@literal @}Test
    +    public void shouldPing() throws IOException {
    +        URL url = new URL(dropwizard.baseUri() + "/ping");
    +        String response = new BufferedReader(new InputStreamReader(url.openStream())).readLine();
    +        assertEquals("pong", response);
    +    }
    +
    + * Of course, you'd use your http client, not {@link URL#openStream()}. + *

    + *

    + * The {@link DropwizardClientRule} takes care of: + *

      + *
    • Creating a simple default configuration.
    • + *
    • Creating a simplistic application.
    • + *
    • Adding a dummy health check to the application to suppress the startup warning.
    • + *
    • Adding your resources to the application.
    • + *
    • Choosing a free random port number.
    • + *
    • Starting the application.
    • + *
    • Stopping the application.
    • + *
    + *

    + */ +public class DropwizardClientRule extends ExternalResource { + private final Object[] resources; + private final DropwizardTestSupport testSupport; + + public DropwizardClientRule(Object... resources) { + testSupport = new DropwizardTestSupport(FakeApplication.class, "") { + @Override + public Application newApplication() { + return new FakeApplication(); + } + }; + this.resources = resources; + } + + public URI baseUri() { + return URI.create("http://localhost:" + testSupport.getLocalPort() + "/application"); + } + + public ObjectMapper getObjectMapper() { + return testSupport.getObjectMapper(); + } + + public Environment getEnvironment() { + return testSupport.getEnvironment(); + } + + @Override + protected void before() throws Throwable { + testSupport.before(); + } + + @Override + protected void after() { + testSupport.after(); + } + + private static class DummyHealthCheck extends HealthCheck { + @Override + protected Result check() { + return Result.healthy(); + } + } + + private class FakeApplication extends Application { + @Override + public void run(Configuration configuration, Environment environment) { + final SimpleServerFactory serverConfig = new SimpleServerFactory(); + configuration.setServerFactory(serverConfig); + final HttpConnectorFactory connectorConfig = (HttpConnectorFactory) serverConfig.getConnector(); + connectorConfig.setPort(0); + + environment.healthChecks().register("dummy", new DummyHealthCheck()); + + for (Object resource : resources) { + if (resource instanceof Class) { + environment.jersey().register((Class) resource); + } else { + environment.jersey().register(resource); + } + } + } + } +} diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardTestResourceConfig.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardTestResourceConfig.java new file mode 100644 index 00000000000..d2878fab9b1 --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/DropwizardTestResourceConfig.java @@ -0,0 +1,61 @@ +package io.dropwizard.testing.junit; + +import com.codahale.metrics.MetricRegistry; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.jersey.errors.EarlyEofExceptionMapper; +import io.dropwizard.jersey.errors.LoggingExceptionMapper; +import io.dropwizard.jersey.jackson.JacksonMessageBodyProvider; +import io.dropwizard.jersey.jackson.JsonProcessingExceptionMapper; +import io.dropwizard.jersey.validation.HibernateValidationFeature; +import io.dropwizard.jersey.validation.JerseyViolationExceptionMapper; +import org.glassfish.jersey.server.ServerProperties; + +import javax.servlet.ServletConfig; +import javax.ws.rs.core.Context; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static java.util.Objects.requireNonNull; + +/** + * A configuration of a Jersey web application by {@link ResourceTestJerseyConfiguration} with + * support of injecting a configuration from a {@link ServletConfig}. It allows to use it along + * with the Grizzly web test container. + */ +class DropwizardTestResourceConfig extends DropwizardResourceConfig { + + /** + * A registry of passed configuration objects. It's used for obtaining the current configuration + * via a servlet context. + */ + static final Map CONFIGURATION_REGISTRY = new ConcurrentHashMap<>(); + static final String CONFIGURATION_ID = "io.dropwizard.testing.junit.resourceTestJerseyConfigurationId"; + + DropwizardTestResourceConfig(ResourceTestJerseyConfiguration configuration) { + super(true, new MetricRegistry()); + + if (configuration.registerDefaultExceptionMappers) { + register(new LoggingExceptionMapper() { + }); + register(new JerseyViolationExceptionMapper()); + register(new JsonProcessingExceptionMapper()); + register(new EarlyEofExceptionMapper()); + } + for (Class provider : configuration.providers) { + register(provider); + } + property(ServerProperties.RESPONSE_SET_STATUS_OVER_SEND_ERROR, "true"); + for (Map.Entry property : configuration.properties.entrySet()) { + property(property.getKey(), property.getValue()); + } + register(new JacksonMessageBodyProvider(configuration.mapper)); + register(new HibernateValidationFeature(configuration.validator)); + for (Object singleton : configuration.singletons) { + register(singleton); + } + } + + DropwizardTestResourceConfig(@Context ServletConfig servletConfig) { + this(CONFIGURATION_REGISTRY.get(requireNonNull(servletConfig.getInitParameter(CONFIGURATION_ID)))); + } +} diff --git a/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestJerseyConfiguration.java b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestJerseyConfiguration.java new file mode 100644 index 00000000000..26e5be43b25 --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestJerseyConfiguration.java @@ -0,0 +1,44 @@ +package io.dropwizard.testing.junit; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.test.spi.TestContainerFactory; + +import javax.validation.Validator; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * A configuration of a Jersey testing environment. + * Encapsulates data required to configure a {@link ResourceTestRule}. + * Primarily accessed via {@link DropwizardTestResourceConfig}. + */ +class ResourceTestJerseyConfiguration { + + final Set singletons; + final Set> providers; + final Map properties; + final ObjectMapper mapper; + final Validator validator; + final Consumer clientConfigurator; + final TestContainerFactory testContainerFactory; + final boolean registerDefaultExceptionMappers; + + ResourceTestJerseyConfiguration(Set singletons, Set> providers, Map properties, + ObjectMapper mapper, Validator validator, Consumer clientConfigurator, + TestContainerFactory testContainerFactory, boolean registerDefaultExceptionMappers) { + this.singletons = singletons; + this.providers = providers; + this.properties = properties; + this.mapper = mapper; + this.validator = validator; + this.clientConfigurator = clientConfigurator; + this.testContainerFactory = testContainerFactory; + this.registerDefaultExceptionMappers = registerDefaultExceptionMappers; + } + + String getId() { + return String.valueOf(hashCode()); + } +} 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 00000000000..6f748a9393b --- /dev/null +++ b/dropwizard-testing/src/main/java/io/dropwizard/testing/junit/ResourceTestRule.java @@ -0,0 +1,183 @@ +package io.dropwizard.testing.junit; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.Validators; +import io.dropwizard.logging.BootstrapLogging; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.servlet.ServletProperties; +import org.glassfish.jersey.test.DeploymentContext; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.ServletDeploymentContext; +import org.glassfish.jersey.test.inmemory.InMemoryTestContainerFactory; +import org.glassfish.jersey.test.spi.TestContainerFactory; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import javax.validation.Validator; +import javax.ws.rs.client.Client; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +/** + * A JUnit {@link TestRule} for testing Jersey resources. + */ +public class ResourceTestRule implements TestRule { + + static { + BootstrapLogging.bootstrap(); + } + + /** + * A {@link ResourceTestRule} builder which enables configuration of a Jersey testing environment. + */ + public static class Builder { + + private final Set singletons = new HashSet<>(); + private final Set> providers = new HashSet<>(); + private final Map properties = new HashMap<>(); + private ObjectMapper mapper = Jackson.newObjectMapper(); + private Validator validator = Validators.newValidator(); + private Consumer clientConfigurator = c -> { + }; + private TestContainerFactory testContainerFactory = new InMemoryTestContainerFactory(); + private boolean registerDefaultExceptionMappers = true; + + public Builder setMapper(ObjectMapper mapper) { + this.mapper = mapper; + return this; + } + + public Builder setValidator(Validator validator) { + this.validator = validator; + return this; + } + + public Builder setClientConfigurator(Consumer clientConfigurator) { + this.clientConfigurator = clientConfigurator; + 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 addProperty(String property, Object value) { + properties.put(property, value); + return this; + } + + public Builder setTestContainerFactory(TestContainerFactory factory) { + this.testContainerFactory = factory; + return this; + } + + public Builder setRegisterDefaultExceptionMappers(boolean value) { + registerDefaultExceptionMappers = value; + return this; + } + + /** + * Builds a {@link ResourceTestRule} with a configured Jersey testing environment. + * + * @return a new {@link ResourceTestRule} + */ + public ResourceTestRule build() { + return new ResourceTestRule(new ResourceTestJerseyConfiguration( + singletons, providers, properties, mapper, validator, + clientConfigurator, testContainerFactory, registerDefaultExceptionMappers)); + } + } + + /** + * Creates a new Jersey testing environment builder for {@link ResourceTestRule} + * + * @return a new {@link Builder} + */ + public static Builder builder() { + return new Builder(); + } + + private ResourceTestJerseyConfiguration configuration; + private JerseyTest test; + + private ResourceTestRule(ResourceTestJerseyConfiguration configuration) { + this.configuration = configuration; + } + + public Validator getValidator() { + return configuration.validator; + } + + public ObjectMapper getObjectMapper() { + return configuration.mapper; + } + + public Consumer getClientConfigurator() { + return configuration.clientConfigurator; + } + + public Client client() { + return test.client(); + } + + public JerseyTest getJerseyTest() { + return test; + } + + @Override + public Statement apply(Statement base, Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + DropwizardTestResourceConfig.CONFIGURATION_REGISTRY.put(configuration.getId(), configuration); + try { + test = new JerseyTest() { + @Override + protected TestContainerFactory getTestContainerFactory() { + return configuration.testContainerFactory; + } + + @Override + protected DeploymentContext configureDeployment() { + return ServletDeploymentContext.builder(new DropwizardTestResourceConfig(configuration)) + .initParam(ServletProperties.JAXRS_APPLICATION_CLASS, + DropwizardTestResourceConfig.class.getName()) + .initParam(DropwizardTestResourceConfig.CONFIGURATION_ID, configuration.getId()) + .build(); + } + + @Override + protected void configureClient(ClientConfig clientConfig) { + final JacksonJsonProvider jsonProvider = new JacksonJsonProvider(); + jsonProvider.setMapper(configuration.mapper); + configuration.clientConfigurator.accept(clientConfig); + clientConfig.register(jsonProvider); + } + }; + test.setUp(); + base.evaluate(); + } finally { + DropwizardTestResourceConfig.CONFIGURATION_REGISTRY.remove(configuration.getId()); + test.tearDown(); + } + } + }; + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/DropwizardTestSupportTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/DropwizardTestSupportTest.java new file mode 100644 index 00000000000..db4b51573ff --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/DropwizardTestSupportTest.java @@ -0,0 +1,157 @@ +package io.dropwizard.testing; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMultimap; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +import io.dropwizard.servlets.tasks.PostBodyTask; +import io.dropwizard.servlets.tasks.Task; +import io.dropwizard.setup.Environment; +import org.hibernate.validator.constraints.NotEmpty; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import java.io.PrintWriter; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +public class DropwizardTestSupportTest { + + public static final DropwizardTestSupport TEST_SUPPORT = + new DropwizardTestSupport<>(TestApplication.class, resourceFilePath("test-config.yaml")); + + @BeforeClass + public static void setUp() { + TEST_SUPPORT.before(); + } + + @AfterClass + public static void tearDown() { + TEST_SUPPORT.after(); + } + + @Test + public void canGetExpectedResourceOverHttp() { + final String content = ClientBuilder.newClient().target( + "http://localhost:" + TEST_SUPPORT.getLocalPort() + "/test").request().get(String.class); + + assertThat(content, is("Yes, it's here")); + } + + @Test + public void returnsConfiguration() { + final TestConfiguration config = TEST_SUPPORT.getConfiguration(); + assertThat(config.getMessage(), is("Yes, it's here")); + } + + @Test + public void returnsApplication() { + final TestApplication application = TEST_SUPPORT.getApplication(); + assertNotNull(application); + } + + @Test + public void returnsEnvironment() { + final Environment environment = TEST_SUPPORT.getEnvironment(); + assertThat(environment.getName(), is("TestApplication")); + } + + @Test + public void canPerformAdminTask() { + final String response + = ClientBuilder.newClient().target("http://localhost:" + + TEST_SUPPORT.getAdminPort() + "/tasks/hello?name=test_user") + .request() + .post(Entity.entity("", MediaType.TEXT_PLAIN), String.class); + + assertThat(response, is("Hello has been said to test_user")); + } + + @Test + public void canPerformAdminTaskWithPostBody() { + final String response + = ClientBuilder.newClient().target("http://localhost:" + + TEST_SUPPORT.getAdminPort() + "/tasks/echo") + .request() + .post(Entity.entity("Custom message", MediaType.TEXT_PLAIN), String.class); + + assertThat(response, is("Custom message")); + } + + public static class TestApplication extends Application { + @Override + public void run(TestConfiguration configuration, Environment environment) throws Exception { + environment.jersey().register(new TestResource(configuration.getMessage())); + environment.admin().addTask(new HelloTask()); + environment.admin().addTask(new EchoTask()); + } + } + + @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; + + @NotEmpty + @JsonProperty + private String extra; + + public String getMessage() { + return message; + } + } + + public static class HelloTask extends Task { + + public HelloTask() { + super("hello"); + } + + @Override + public void execute(ImmutableMultimap parameters, PrintWriter output) throws Exception { + ImmutableCollection names = parameters.get("name"); + String name = !names.isEmpty() ? names.asList().get(0) : "Anonymous"; + output.print("Hello has been said to " + name); + output.flush(); + } + } + + public static class EchoTask extends PostBodyTask { + + public EchoTask() { + super("echo"); + } + + @Override + public void execute(ImmutableMultimap parameters, String body, PrintWriter output) throws Exception { + output.print(body); + output.flush(); + } + } +} 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 00000000000..64c543ed9e5 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/FixtureHelpersTest.java @@ -0,0 +1,18 @@ +package io.dropwizard.testing; + +import org.junit.Test; + +import static io.dropwizard.testing.FixtureHelpers.fixture; +import static org.assertj.core.api.Assertions.assertThat; + +public class FixtureHelpersTest { + @Test + public void readsTheFileAsAString() { + assertThat(fixture("fixtures/fixture.txt")).isEqualTo("YAY FOR ME"); + } + + @Test(expected = IllegalArgumentException.class) + public void throwsIllegalStateExceptionWhenFileDoesNotExist() { + fixture("this-does-not-exist.foo"); + } +} 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 00000000000..0b0fd0c6e9e --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/Person.java @@ -0,0 +1,58 @@ +package io.dropwizard.testing; + +import com.google.common.base.MoreObjects; + +import java.util.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 int hashCode() { + return Objects.hash(name, email); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Person other = (Person) obj; + return Objects.equals(this.name, other.name) && Objects.equals(this.email, other.email); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("name", name) + .add("email", email) + .toString(); + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/ContextInjectionResource.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/ContextInjectionResource.java new file mode 100644 index 00000000000..70b85496da5 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/ContextInjectionResource.java @@ -0,0 +1,26 @@ +package io.dropwizard.testing.app; + +import com.codahale.metrics.annotation.Timed; + +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.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +@Path("/test") +@Produces(MediaType.APPLICATION_JSON) +public class ContextInjectionResource { + @GET + @Timed + public String getUriPath(@Context UriInfo uriInfo) { + return uriInfo.getPath(); + } + + @POST + public String getThis() { + throw new RuntimeException("Can't touch this"); + } +} 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 00000000000..5e16407ef0f --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/GzipDefaultVaryBehaviourTest.java @@ -0,0 +1,33 @@ +package io.dropwizard.testing.app; + +import io.dropwizard.testing.junit.DropwizardAppRule; +import io.dropwizard.testing.junit.TestApplication; +import io.dropwizard.testing.junit.TestConfiguration; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +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.assertj.core.api.Assertions.assertThat; + +public class GzipDefaultVaryBehaviourTest { + + @ClassRule + public static final DropwizardAppRule RULE = + new DropwizardAppRule<>(TestApplication.class, resourceFilePath("gzip-vary-test-config.yaml")); + + @Test + public void testDefaultVaryHeader() { + final Response clientResponse = ClientBuilder.newClient().target( + "http://localhost:" + RULE.getLocalPort() + "/test").request().header(ACCEPT_ENCODING, "gzip").get(); + + assertThat(clientResponse.getHeaders().get(VARY)).isEqualTo(asList((Object) ACCEPT_ENCODING)); + assertThat(clientResponse.getHeaders().get(CONTENT_ENCODING)).isEqualTo(asList((Object) "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 00000000000..a25875079df --- /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 00000000000..c61e9dab876 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResource.java @@ -0,0 +1,78 @@ +package io.dropwizard.testing.app; + +import com.codahale.metrics.annotation.Timed; +import com.google.common.collect.ImmutableList; +import io.dropwizard.jersey.params.IntParam; +import io.dropwizard.testing.Person; +import io.dropwizard.validation.Validated; +import org.eclipse.jetty.io.EofException; + +import javax.validation.Valid; +import javax.validation.constraints.Min; +import javax.validation.groups.Default; +import javax.ws.rs.BeanParam; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import java.util.Map; + +@Path("/person/{name}") +@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); + } + + @GET + @Timed + @Path("/list") + public ImmutableList getPersonList(@PathParam("name") String name) { + return ImmutableList.of(getPerson(name)); + } + + @GET + @Timed + @Path("/index") + public Person getPersonWithIndex(@Min(0) @QueryParam("ind") IntParam index, + @PathParam("name") String name) { + return getPersonList(name).get(index.get()); + } + + @POST + @Path("/runtime-exception") + public Person exceptional(Map mapper) throws Exception { + throw new Exception("I'm an exception!"); + } + + @GET + @Path("/eof-exception") + public Person eofException() throws Exception { + throw new EofException("I'm an eof exception!"); + } + + @POST + @Path("/validation-groups-exception") + public String validationGroupsException( + @Valid @Validated(Partial1.class) @BeanParam BeanParameter params, + @Valid @Validated(Default.class) byte[] entity) { + return params.age.toString() + entity.length; + } + + public interface Partial1 { } + public static class BeanParameter { + @QueryParam("age") + public Integer age; + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceExceptionMapperTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceExceptionMapperTest.java new file mode 100644 index 00000000000..ae1a407671b --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceExceptionMapperTest.java @@ -0,0 +1,94 @@ +package io.dropwizard.testing.app; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.google.common.base.Strings; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.validation.JerseyViolationException; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.glassfish.jersey.spi.ExtendedExceptionMapper; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +public class PersonResourceExceptionMapperTest { + private static final PeopleStore PEOPLE_STORE = mock(PeopleStore.class); + + private static final ObjectMapper OBJECT_MAPPER = Jackson.newObjectMapper() + .registerModule(new GuavaModule()); + + @ClassRule + public static final ResourceTestRule RESOURCES = ResourceTestRule.builder() + .addResource(new PersonResource(PEOPLE_STORE)) + .setRegisterDefaultExceptionMappers(false) + .addProvider(new MyJerseyExceptionMapper()) + .addProvider(new GenericExceptionMapper()) + .setMapper(OBJECT_MAPPER) + .build(); + + @Test + public void testDefaultConstraintViolation() { + assertThat(RESOURCES.client().target("/person/blah/index") + .queryParam("ind", -1).request() + .get().readEntity(String.class)) + .isEqualTo("Invalid data"); + } + + @Test + public void testDefaultJsonProcessingMapper() { + assertThat(RESOURCES.client().target("/person/blah/runtime-exception") + .request() + .post(Entity.json("{ \"he: \"ho\"}")) + .readEntity(String.class)) + .startsWith("Something went wrong: Unexpected character"); + } + + @Test + public void testDefaultExceptionMapper() { + assertThat(RESOURCES.client().target("/person/blah/runtime-exception") + .request() + .post(Entity.json("{}")) + .readEntity(String.class)) + .isEqualTo("Something went wrong: I'm an exception!"); + } + + @Test + public void testDefaultEofExceptionMapper() { + assertThat(RESOURCES.client().target("/person/blah/eof-exception") + .request() + .get().readEntity(String.class)) + .isEqualTo("Something went wrong: I'm an eof exception!"); + } + + private static class MyJerseyExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(JerseyViolationException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.TEXT_PLAIN) + .entity("Invalid data") + .build(); + } + } + + private static class GenericExceptionMapper implements ExtendedExceptionMapper { + @Override + public boolean isMappable(Throwable throwable) { + return !(throwable instanceof JerseyViolationException); + } + + @Override + public Response toResponse(Throwable exception) { + return Response.status(Response.Status.BAD_REQUEST) + .type(MediaType.TEXT_PLAIN) + .entity("Something went wrong: " + Strings.nullToEmpty(exception.getMessage())) + .build(); + } + } +} 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 00000000000..9134af39211 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/PersonResourceTest.java @@ -0,0 +1,132 @@ +package io.dropwizard.testing.app; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.guava.GuavaModule; +import com.google.common.collect.ImmutableList; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.testing.Person; +import io.dropwizard.testing.junit.ResourceTestRule; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests {@link ResourceTestRule}. + */ +public class PersonResourceTest { + private static class DummyExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(WebApplicationException e) { + throw new UnsupportedOperationException(); + } + } + + private static final PeopleStore PEOPLE_STORE = mock(PeopleStore.class); + + private static final ObjectMapper OBJECT_MAPPER = Jackson.newObjectMapper() + .registerModule(new GuavaModule()); + + @ClassRule + public static final ResourceTestRule RESOURCES = ResourceTestRule.builder() + .addResource(new PersonResource(PEOPLE_STORE)) + .setMapper(OBJECT_MAPPER) + .setClientConfigurator(clientConfig -> clientConfig.register(DummyExceptionMapper.class)) + .build(); + + private final Person person = new Person("blah", "blah@example.com"); + + @Before + public void setup() { + reset(PEOPLE_STORE); + when(PEOPLE_STORE.fetchPerson(eq("blah"))).thenReturn(person); + } + + @Test + public void testGetPerson() { + assertThat(RESOURCES.client().target("/person/blah").request() + .get(Person.class)) + .isEqualTo(person); + verify(PEOPLE_STORE).fetchPerson("blah"); + } + + @Test + public void testGetImmutableListOfPersons() { + assertThat(RESOURCES.client().target("/person/blah/list").request() + .get(new GenericType>() { + })).isEqualTo(ImmutableList.of(person)); + } + + @Test + public void testGetPersonWithQueryParam() { + // Test to ensure that the dropwizard validator is registered so that + // it can validate the "ind" IntParam. + assertThat(RESOURCES.client().target("/person/blah/index") + .queryParam("ind", 0).request() + .get(Person.class)) + .isEqualTo(person); + verify(PEOPLE_STORE).fetchPerson("blah"); + } + + @Test + public void testDefaultConstraintViolation() { + assertThat(RESOURCES.client().target("/person/blah/index") + .queryParam("ind", -1).request() + .get().readEntity(String.class)) + .isEqualTo("{\"errors\":[\"query param ind must be greater than or equal to 0\"]}"); + } + + @Test + public void testDefaultJsonProcessingMapper() { + assertThat(RESOURCES.client().target("/person/blah/runtime-exception") + .request() + .post(Entity.json("{ \"he: \"ho\"}")) + .readEntity(String.class)) + .isEqualTo("{\"code\":400,\"message\":\"Unable to process JSON\"}"); + } + + @Test + public void testDefaultExceptionMapper() { + assertThat(RESOURCES.client().target("/person/blah/runtime-exception") + .request() + .post(Entity.json("{}")) + .readEntity(String.class)) + .startsWith("{\"code\":500,\"message\":\"There was an error processing your request. It has been logged"); + } + + @Test + public void testDefaultEofExceptionMapper() { + assertThat(RESOURCES.client().target("/person/blah/eof-exception") + .request() + .get().getStatus()) + .isEqualTo(Response.Status.BAD_REQUEST.getStatusCode()); + } + + @Test + public void testValidationGroupsException() { + final Response resp = RESOURCES.client().target("/person/blah/validation-groups-exception") + .request() + .post(Entity.json("{}")); + assertThat(resp.getStatus()).isEqualTo(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + assertThat(resp.readEntity(String.class)) + .isEqualTo("{\"code\":500,\"message\":\"Parameters must have the same" + + " validation groups in validationGroupsException\"}"); + } + + @Test + public void testCustomClientConfiguration() { + assertThat(RESOURCES.client().getConfiguration().isRegistered(DummyExceptionMapper.class)).isTrue(); + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/app/ResourceTestWithGrizzly.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/ResourceTestWithGrizzly.java new file mode 100644 index 00000000000..b750c7765a8 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/app/ResourceTestWithGrizzly.java @@ -0,0 +1,51 @@ +package io.dropwizard.testing.app; + +import io.dropwizard.testing.junit.ResourceTestRule; +import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests {@link io.dropwizard.testing.junit.ResourceTestRule} with a different + * test container factory. + */ +public class ResourceTestWithGrizzly { + @ClassRule + public static final ResourceTestRule RESOURCES = ResourceTestRule.builder() + .addResource(new ContextInjectionResource()) + .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) + .addProvider(new RuntimeExceptionMapper()) + .build(); + + @Test + public void testResource() { + assertThat(RESOURCES.getJerseyTest().target("test").request() + .get(String.class)) + .isEqualTo("test"); + } + + @Test + public void testExceptionMapper() { + final Response resp = RESOURCES.getJerseyTest().target("test").request() + .post(Entity.json("")); + assertThat(resp.getStatus()).isEqualTo(500); + assertThat(resp.readEntity(String.class)).isEqualTo("Can't touch this"); + } + + private static class RuntimeExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(RuntimeException exception) { + return Response.serverError() + .type(MediaType.TEXT_PLAIN_TYPE) + .entity(exception.getMessage()) + .build(); + } + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleConfigOverrideTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleConfigOverrideTest.java new file mode 100644 index 00000000000..9c0bf0aa403 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleConfigOverrideTest.java @@ -0,0 +1,37 @@ +package io.dropwizard.testing.junit; + +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.client.ClientBuilder; +import java.util.Optional; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +public class DropwizardAppRuleConfigOverrideTest { + + @ClassRule + public static final DropwizardAppRule RULE = + new DropwizardAppRule<>(TestApplication.class, resourceFilePath("test-config.yaml"), + Optional.of("app-rule"), + config("app-rule", "message", "A new way to say Hooray!"), + config("app-rule", "extra", () -> "supplied"), + config("extra", () -> "supplied again")); + + @Test + public void supportsConfigAttributeOverrides() { + final String content = ClientBuilder.newClient().target("http://localhost:" + RULE.getLocalPort() + "/test") + .request().get(String.class); + + assertThat(content, is("A new way to say Hooray!")); + } + + @Test + public void supportsSuppliedConfigAttributeOverrides() throws Exception { + assertThat(System.getProperty("app-rule.extra"), is("supplied")); + assertThat(System.getProperty("dw.extra"), is("supplied again")); + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleReentrantTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleReentrantTest.java new file mode 100644 index 00000000000..8ea0e344536 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleReentrantTest.java @@ -0,0 +1,46 @@ +package io.dropwizard.testing.junit; + +import io.dropwizard.testing.DropwizardTestSupport; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.times; + +public class DropwizardAppRuleReentrantTest { + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + DropwizardTestSupport testSupport; + + @Mock + Statement statement; + + @Mock + Description description; + + @Test + public void testReentrantRuleStartsApplicationOnlyOnce() throws Throwable { + DropwizardAppRule dropwizardAppRule = new DropwizardAppRule<>(testSupport); + + RuleChain.outerRule(dropwizardAppRule) + .around(dropwizardAppRule) // recursive + .apply(statement, description) + .evaluate(); + + InOrder inOrder = inOrder(testSupport, statement, description); + inOrder.verify(testSupport, times(1)).before(); + inOrder.verify(statement).evaluate(); + inOrder.verify(testSupport, times(1)).after(); + inOrder.verifyNoMoreInteractions(); + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleResetConfigOverrideTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleResetConfigOverrideTest.java new file mode 100644 index 00000000000..7231d257c2f --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleResetConfigOverrideTest.java @@ -0,0 +1,35 @@ +package io.dropwizard.testing.junit; + +import org.junit.Test; + +import java.util.Optional; + +import static io.dropwizard.testing.ConfigOverride.config; +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +import static org.assertj.core.api.Assertions.assertThat; + +public class DropwizardAppRuleResetConfigOverrideTest { + private final DropwizardAppRule dropwizardAppRule = new DropwizardAppRule<>( + TestApplication.class, + resourceFilePath("test-config.yaml"), + Optional.of("app-rule-reset"), + config("app-rule-reset", "message", "A new way to say Hooray!")); + + @Test + public void test2() throws Exception { + dropwizardAppRule.before(); + assertThat(System.getProperty("app-rule-reset.message")).isEqualTo("A new way to say Hooray!"); + assertThat(System.getProperty("app-rule-reset.extra")).isNull(); + dropwizardAppRule.after(); + + System.setProperty("app-rule-reset.extra", "Some extra system property"); + dropwizardAppRule.before(); + assertThat(System.getProperty("app-rule-reset.message")).isEqualTo("A new way to say Hooray!"); + assertThat(System.getProperty("app-rule-reset.extra")).isEqualTo("Some extra system property"); + dropwizardAppRule.after(); + + assertThat(System.getProperty("app-rule-reset.message")).isNull(); + assertThat(System.getProperty("app-rule-reset.extra")).isEqualTo("Some extra system property"); + System.clearProperty("app-rule-reset.extra"); + } +} 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 00000000000..de6e685c4a2 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleTest.java @@ -0,0 +1,130 @@ +package io.dropwizard.testing.junit; + +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMultimap; +import io.dropwizard.Application; +import io.dropwizard.servlets.tasks.PostBodyTask; +import io.dropwizard.servlets.tasks.Task; +import io.dropwizard.setup.Environment; +import org.junit.ClassRule; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import java.io.PrintWriter; + +import static io.dropwizard.testing.ResourceHelpers.resourceFilePath; +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 RULE = + new DropwizardAppRule<>(TestApplication.class, resourceFilePath("test-config.yaml")); + + @Test + public void canGetExpectedResourceOverHttp() { + final String content = ClientBuilder.newClient().target( + "http://localhost:" + RULE.getLocalPort() + "/test").request().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 + = ClientBuilder.newClient().target("http://localhost:" + + RULE.getAdminPort() + "/tasks/hello?name=test_user") + .request() + .post(Entity.entity("", MediaType.TEXT_PLAIN), String.class); + + assertThat(response, is("Hello has been said to test_user")); + } + + @Test + public void canPerformAdminTaskWithPostBody() { + final String response + = ClientBuilder.newClient().target("http://localhost:" + + RULE.getAdminPort() + "/tasks/echo") + .request() + .post(Entity.entity("Custom message", MediaType.TEXT_PLAIN), String.class); + + assertThat(response, is("Custom message")); + } + + public static class TestApplication extends Application { + @Override + public void run(TestConfiguration configuration, Environment environment) throws Exception { + environment.jersey().register(new TestResource(configuration.getMessage())); + environment.admin().addTask(new HelloTask()); + environment.admin().addTask(new EchoTask()); + } + } + + @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 HelloTask extends Task { + + public HelloTask() { + super("hello"); + } + + @Override + public void execute(ImmutableMultimap parameters, PrintWriter output) throws Exception { + ImmutableCollection names = parameters.get("name"); + String name = !names.isEmpty() ? names.asList().get(0) : "Anonymous"; + output.print("Hello has been said to " + name); + output.flush(); + } + } + + public static class EchoTask extends PostBodyTask { + + public EchoTask() { + super("echo"); + } + + @Override + public void execute(ImmutableMultimap parameters, String body, PrintWriter output) throws Exception { + output.print(body); + output.flush(); + } + } +} diff --git a/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithExplicitTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithExplicitTest.java new file mode 100644 index 00000000000..c8ae427a46b --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithExplicitTest.java @@ -0,0 +1,70 @@ +package io.dropwizard.testing.junit; + +import com.google.common.collect.ImmutableMap; +import io.dropwizard.Application; +import io.dropwizard.jetty.HttpConnectorFactory; +import io.dropwizard.server.DefaultServerFactory; +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.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Map; + +public class DropwizardAppRuleWithExplicitTest { + + @Test + public void bogusTest() { } + + @ClassRule + public static final DropwizardAppRule RULE; + static { + // Bit complicated, as we want to avoid using the default http port (8080) + // as there is another test that uses it already. So force bogus value of + // 0, similar to what `test-config.yaml` defines. + TestConfiguration config = new TestConfiguration("stuff!", "extra"); + DefaultServerFactory sf = (DefaultServerFactory) config.getServerFactory(); + ((HttpConnectorFactory) sf.getApplicationConnectors().get(0)).setPort(0); + ((HttpConnectorFactory) sf.getAdminConnectors().get(0)).setPort(0); + RULE = new DropwizardAppRule<>(TestApplication.class, config); + } + + Client client = ClientBuilder.newClient(); + + @Test + public void runWithExplicitConfig() { + Map response = client.target("http://localhost:" + RULE.getLocalPort() + "/test") + .request() + .get(Map.class); + Assert.assertEquals(ImmutableMap.of("message", "stuff!"), response); + } + + public static class TestApplication extends Application { + @Override + public void run(TestConfiguration configuration, Environment environment) throws Exception { + environment.jersey().register(new TestResource(configuration.getMessage())); + } + } + + @Path("test") + @Produces(MediaType.APPLICATION_JSON) + public static class TestResource { + private final String message; + + public TestResource(String m) { + message = m; + } + + @GET + public Response get() { + return Response.ok(ImmutableMap.of("message", message)).build(); + } + } +} 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 00000000000..94e47e570f6 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardAppRuleWithoutConfigTest.java @@ -0,0 +1,50 @@ +package io.dropwizard.testing.junit; + +import com.google.common.collect.ImmutableMap; +import io.dropwizard.Application; +import io.dropwizard.Configuration; +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.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.Map; + +public class DropwizardAppRuleWithoutConfigTest { + + @ClassRule + public static final DropwizardAppRule RULE = new DropwizardAppRule<>(TestApplication.class); + + Client client = ClientBuilder.newClient(); + + @Test + public void runWithoutConfigFile() { + Map response = client.target("http://localhost:" + RULE.getLocalPort() + "/test") + .request() + .get(Map.class); + Assert.assertEquals(ImmutableMap.of("color", "orange"), response); + } + + public static class TestApplication extends Application { + @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/DropwizardClientRuleTest.java b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardClientRuleTest.java new file mode 100644 index 00000000000..fc2a0eeedc3 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/DropwizardClientRuleTest.java @@ -0,0 +1,33 @@ +package io.dropwizard.testing.junit; + +import org.junit.ClassRule; +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import static org.junit.Assert.assertEquals; + +public class DropwizardClientRuleTest { + @ClassRule + public static final DropwizardClientRule RULE_WITH_INSTANCE = new DropwizardClientRule(new TestResource("foo")); + + @ClassRule + public static final DropwizardClientRule RULE_WITH_CLASS = new DropwizardClientRule(TestResource.class); + + @Test(timeout = 5000) + public void shouldGetStringBodyFromDropWizard() throws IOException { + final URL url = new URL(RULE_WITH_INSTANCE.baseUri() + "/test"); + final String response = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)).readLine(); + assertEquals("foo", response); + } + + @Test(timeout = 5000) + public void shouldGetDefaultStringBodyFromDropWizard() throws IOException { + final URL url = new URL(RULE_WITH_CLASS.baseUri() + "/test"); + final String response = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)).readLine(); + assertEquals(TestResource.DEFAULT_MESSAGE, response); + } +} 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 00000000000..abaaa505aae --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestApplication.java @@ -0,0 +1,11 @@ +package io.dropwizard.testing.junit; + +import io.dropwizard.Application; +import io.dropwizard.setup.Environment; + +public class TestApplication extends Application { + @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 00000000000..aca2b5bc9e8 --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestConfiguration.java @@ -0,0 +1,31 @@ +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; + + @JsonProperty + @NotEmpty + private String extra; + + public TestConfiguration() { } + + public TestConfiguration(String message, String extra) { + this.message = message; + this.extra = extra; + } + + public String getMessage() { + return message; + } + + public String getExtra() { + return extra; + } +} 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 00000000000..ffae2adf1ac --- /dev/null +++ b/dropwizard-testing/src/test/java/io/dropwizard/testing/junit/TestResource.java @@ -0,0 +1,26 @@ +package io.dropwizard.testing.junit; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +@Path("/") +public class TestResource { + + public static final String DEFAULT_MESSAGE = "Default message"; + + private final String message; + + public TestResource() { + this(DEFAULT_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 00000000000..cc6ef95eb0f --- /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 00000000000..542f91d3dfe --- /dev/null +++ b/dropwizard-testing/src/test/resources/fixtures/person.json @@ -0,0 +1,4 @@ +{ + "name": "Coda", + "email": "coda@example.com" +} diff --git a/dropwizard-testing/src/test/resources/gzip-vary-test-config.yaml b/dropwizard-testing/src/test/resources/gzip-vary-test-config.yaml new file mode 100644 index 00000000000..2bd3808afc4 --- /dev/null +++ b/dropwizard-testing/src/test/resources/gzip-vary-test-config.yaml @@ -0,0 +1,16 @@ +message: "Yes, it's here" +extra: "Something extra" +server: + gzip: + enabled: true + minimumEntitySize: 0B # Always compress regardless of size + applicationConnectors: + - type: http + port: 0 + adminConnectors: + - type: http + port: 0 + requestLog: + appenders: [] +logging: + appenders: [] 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 00000000000..d3e72c9a6f8 --- /dev/null +++ b/dropwizard-testing/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + 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 00000000000..89afabae7b0 --- /dev/null +++ b/dropwizard-testing/src/test/resources/test-config.yaml @@ -0,0 +1,13 @@ +message: "Yes, it's here" +extra: "Something extra" +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 00000000000..da3c4dab9d2 --- /dev/null +++ b/dropwizard-util/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-util + Dropwizard Utility Classes + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + com.fasterxml.jackson.core + jackson-annotations + + + com.google.guava + guava + + + com.google.code.findbugs + jsr305 + + + joda-time + joda-time + + + com.fasterxml.jackson.core + jackson-databind + test + + + 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 00000000000..d9bacc28cc1 --- /dev/null +++ b/dropwizard-util/src/main/java/io/dropwizard/util/Duration.java @@ -0,0 +1,165 @@ +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 java.util.Objects.requireNonNull; + +public class Duration implements Comparable { + private static final Pattern DURATION_PATTERN = Pattern.compile("(\\d+)\\s*(\\S+)"); + + private static final Map SUFFIXES = new ImmutableMap.Builder() + .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.parseLong(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 = requireNonNull(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; + } + + @Override + public int compareTo(Duration other) { + if (unit == other.unit) { + return Long.compare(count, other.count); + } + + return Long.compare(toNanoseconds(), other.toNanoseconds()); + } +} 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 00000000000..bb9a5ae3121 --- /dev/null +++ b/dropwizard-util/src/main/java/io/dropwizard/util/Generics.java @@ -0,0 +1,79 @@ +package io.dropwizard.util; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; + +import static java.util.Objects.requireNonNull; + +/** + * Helper methods for class type parameters. + * @see Super Type Tokens + */ +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 the type bound + * @return the class's type parameter + */ + @SuppressWarnings("unchecked") + public static Class getTypeParameter(Class klass, Class bound) { + Type t = requireNonNull(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 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 cls = determineClass(bound, paramBound); + if (cls != null) { + return cls; + } + } + } + } + } + } + throw new IllegalStateException("Cannot figure out type parameterization for " + klass.getName()); + } + + @SuppressWarnings("unchecked") + private static Class determineClass(Class bound, Type candidate) { + if (candidate instanceof Class) { + final Class cls = (Class) candidate; + if (bound.isAssignableFrom(cls)) { + return (Class) 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 00000000000..a370b6740c6 --- /dev/null +++ b/dropwizard-util/src/main/java/io/dropwizard/util/JarLocation.java @@ -0,0 +1,36 @@ +package io.dropwizard.util; + +import java.io.File; +import java.net.URL; +import java.util.Optional; + +/** + * 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 getVersion() { + return Optional.ofNullable(klass.getPackage()) + .map(Package::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/Optionals.java b/dropwizard-util/src/main/java/io/dropwizard/util/Optionals.java new file mode 100644 index 00000000000..6b92eaa0bfc --- /dev/null +++ b/dropwizard-util/src/main/java/io/dropwizard/util/Optionals.java @@ -0,0 +1,25 @@ +package io.dropwizard.util; + +import java.util.Optional; + +public abstract class Optionals { + /** + * Convert a Guava {@link com.google.common.base.Optional} to an {@link Optional}. + * + * @param guavaOptional The Guava {@link com.google.common.base.Optional} + * @return An equivalent {@link Optional} + */ + public static Optional fromGuavaOptional(final com.google.common.base.Optional guavaOptional) { + return Optional.ofNullable(guavaOptional.orNull()); + } + + /** + * Convert an {@link Optional} to a Guava {@link com.google.common.base.Optional}. + * + * @param optional The {@link Optional} + * @return An equivalent Guava {@link com.google.common.base.Optional} + */ + public static com.google.common.base.Optional toGuavaOptional(final Optional optional) { + return com.google.common.base.Optional.fromNullable(optional.orElse(null)); + } +} 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 00000000000..1cf95a2e448 --- /dev/null +++ b/dropwizard-util/src/main/java/io/dropwizard/util/Size.java @@ -0,0 +1,149 @@ +package io.dropwizard.util; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import com.google.common.collect.ImmutableSortedMap; + +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 java.util.Objects.requireNonNull; + +public class Size implements Comparable { + private static final Pattern SIZE_PATTERN = Pattern.compile("(\\d+)\\s*(\\S+)"); + + private static final Map SUFFIXES = ImmutableSortedMap.orderedBy(String.CASE_INSENSITIVE_ORDER) + .put("B", SizeUnit.BYTES) + .put("byte", SizeUnit.BYTES) + .put("bytes", SizeUnit.BYTES) + .put("K", SizeUnit.KILOBYTES) + .put("KB", SizeUnit.KILOBYTES) + .put("KiB", SizeUnit.KILOBYTES) + .put("kilobyte", SizeUnit.KILOBYTES) + .put("kilobytes", SizeUnit.KILOBYTES) + .put("M", SizeUnit.MEGABYTES) + .put("MB", SizeUnit.MEGABYTES) + .put("MiB", SizeUnit.MEGABYTES) + .put("megabyte", SizeUnit.MEGABYTES) + .put("megabytes", SizeUnit.MEGABYTES) + .put("G", SizeUnit.GIGABYTES) + .put("GB", SizeUnit.GIGABYTES) + .put("GiB", SizeUnit.GIGABYTES) + .put("gigabyte", SizeUnit.GIGABYTES) + .put("gigabytes", SizeUnit.GIGABYTES) + .put("T", SizeUnit.TERABYTES) + .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.parseLong(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 = requireNonNull(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; + } + + @Override + public int compareTo(Size other) { + if (unit == other.unit) { + return Long.compare(count, other.count); + } + + return Long.compare(toBytes(), other.toBytes()); + } +} 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 00000000000..96120672e55 --- /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), + + /** + * Terabytes. + */ + 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 00000000000..f57bbfee22b --- /dev/null +++ b/dropwizard-util/src/test/java/io/dropwizard/util/DurationTest.java @@ -0,0 +1,860 @@ +package io.dropwizard.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.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); + } + + @Test + public void isComparable() throws Exception { + // both zero + assertThat(Duration.nanoseconds(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.nanoseconds(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.nanoseconds(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.nanoseconds(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.nanoseconds(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.nanoseconds(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.nanoseconds(0).compareTo(Duration.days(0))).isEqualTo(0); + + assertThat(Duration.microseconds(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.microseconds(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.microseconds(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.microseconds(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.microseconds(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.microseconds(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.microseconds(0).compareTo(Duration.days(0))).isEqualTo(0); + + assertThat(Duration.milliseconds(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.milliseconds(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.milliseconds(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.milliseconds(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.milliseconds(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.milliseconds(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.milliseconds(0).compareTo(Duration.days(0))).isEqualTo(0); + + assertThat(Duration.seconds(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.seconds(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.seconds(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.seconds(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.seconds(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.seconds(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.seconds(0).compareTo(Duration.days(0))).isEqualTo(0); + + assertThat(Duration.minutes(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.minutes(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.minutes(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.minutes(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.minutes(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.minutes(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.minutes(0).compareTo(Duration.days(0))).isEqualTo(0); + + assertThat(Duration.hours(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.hours(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.hours(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.hours(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.hours(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.hours(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.hours(0).compareTo(Duration.days(0))).isEqualTo(0); + + assertThat(Duration.days(0).compareTo(Duration.nanoseconds(0))).isEqualTo(0); + assertThat(Duration.days(0).compareTo(Duration.microseconds(0))).isEqualTo(0); + assertThat(Duration.days(0).compareTo(Duration.milliseconds(0))).isEqualTo(0); + assertThat(Duration.days(0).compareTo(Duration.seconds(0))).isEqualTo(0); + assertThat(Duration.days(0).compareTo(Duration.minutes(0))).isEqualTo(0); + assertThat(Duration.days(0).compareTo(Duration.hours(0))).isEqualTo(0); + assertThat(Duration.days(0).compareTo(Duration.days(0))).isEqualTo(0); + + // one zero, one negative + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.nanoseconds(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.microseconds(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.milliseconds(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.seconds(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.seconds(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.seconds(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.seconds(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.seconds(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.seconds(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.seconds(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.minutes(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.minutes(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.minutes(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.minutes(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.minutes(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.minutes(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.minutes(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.hours(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.hours(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.hours(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.hours(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.hours(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.hours(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.hours(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.days(0)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.days(0)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.days(0)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.days(0)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.days(0)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.days(0)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.days(0)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.days(0)); + + assertThat(Duration.microseconds(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.days(0)); + + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.days(0)); + + assertThat(Duration.seconds(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.days(0)); + + assertThat(Duration.minutes(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.days(0)); + + assertThat(Duration.hours(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.hours(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.hours(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.hours(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.hours(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.hours(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.hours(-1)).isLessThan(Duration.days(0)); + + assertThat(Duration.days(-1)).isLessThan(Duration.nanoseconds(0)); + assertThat(Duration.days(-1)).isLessThan(Duration.microseconds(0)); + assertThat(Duration.days(-1)).isLessThan(Duration.milliseconds(0)); + assertThat(Duration.days(-1)).isLessThan(Duration.seconds(0)); + assertThat(Duration.days(-1)).isLessThan(Duration.minutes(0)); + assertThat(Duration.days(-1)).isLessThan(Duration.hours(0)); + assertThat(Duration.days(-1)).isLessThan(Duration.days(0)); + + // one zero, one positive + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.nanoseconds(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.microseconds(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.microseconds(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.microseconds(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.microseconds(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.microseconds(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.microseconds(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.microseconds(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.milliseconds(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.milliseconds(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.milliseconds(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.milliseconds(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.milliseconds(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.milliseconds(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.milliseconds(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.seconds(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.seconds(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.seconds(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.seconds(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.seconds(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.seconds(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.seconds(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.minutes(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.minutes(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.minutes(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.minutes(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.minutes(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.minutes(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.minutes(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.hours(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.hours(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.hours(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.hours(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.hours(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.hours(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.hours(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.days(0)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.days(0)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.days(0)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.days(0)).isLessThan(Duration.seconds(1)); + assertThat(Duration.days(0)).isLessThan(Duration.minutes(1)); + assertThat(Duration.days(0)).isLessThan(Duration.hours(1)); + assertThat(Duration.days(0)).isLessThan(Duration.days(1)); + + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.days(0)); + + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.days(0)); + + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.days(0)); + + assertThat(Duration.seconds(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.days(0)); + + assertThat(Duration.minutes(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.days(0)); + + assertThat(Duration.hours(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.days(0)); + + assertThat(Duration.days(1)).isGreaterThan(Duration.nanoseconds(0)); + assertThat(Duration.days(1)).isGreaterThan(Duration.microseconds(0)); + assertThat(Duration.days(1)).isGreaterThan(Duration.milliseconds(0)); + assertThat(Duration.days(1)).isGreaterThan(Duration.seconds(0)); + assertThat(Duration.days(1)).isGreaterThan(Duration.minutes(0)); + assertThat(Duration.days(1)).isGreaterThan(Duration.hours(0)); + assertThat(Duration.days(1)).isGreaterThan(Duration.days(0)); + + // both negative + assertThat(Duration.nanoseconds(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.nanoseconds(-2)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.nanoseconds(-2)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.nanoseconds(-2)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.nanoseconds(-2)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.nanoseconds(-2)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.nanoseconds(-2)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.microseconds(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.microseconds(-2)).isLessThan(Duration.microseconds(-1)); + assertThat(Duration.microseconds(-2)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.microseconds(-2)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.microseconds(-2)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.microseconds(-2)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.microseconds(-2)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.milliseconds(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.milliseconds(-2)).isLessThan(Duration.microseconds(-1)); + assertThat(Duration.milliseconds(-2)).isLessThan(Duration.milliseconds(-1)); + assertThat(Duration.milliseconds(-2)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.milliseconds(-2)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.milliseconds(-2)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.milliseconds(-2)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.seconds(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.seconds(-2)).isLessThan(Duration.microseconds(-1)); + assertThat(Duration.seconds(-2)).isLessThan(Duration.milliseconds(-1)); + assertThat(Duration.seconds(-2)).isLessThan(Duration.seconds(-1)); + assertThat(Duration.seconds(-2)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.seconds(-2)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.seconds(-2)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.minutes(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.minutes(-2)).isLessThan(Duration.microseconds(-1)); + assertThat(Duration.minutes(-2)).isLessThan(Duration.milliseconds(-1)); + assertThat(Duration.minutes(-2)).isLessThan(Duration.seconds(-1)); + assertThat(Duration.minutes(-2)).isLessThan(Duration.minutes(-1)); + assertThat(Duration.minutes(-2)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.minutes(-2)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.hours(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.hours(-2)).isLessThan(Duration.microseconds(-1)); + assertThat(Duration.hours(-2)).isLessThan(Duration.milliseconds(-1)); + assertThat(Duration.hours(-2)).isLessThan(Duration.seconds(-1)); + assertThat(Duration.hours(-2)).isLessThan(Duration.minutes(-1)); + assertThat(Duration.hours(-2)).isLessThan(Duration.hours(-1)); + assertThat(Duration.hours(-2)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.days(-2)).isLessThan(Duration.nanoseconds(-1)); + assertThat(Duration.days(-2)).isLessThan(Duration.microseconds(-1)); + assertThat(Duration.days(-2)).isLessThan(Duration.milliseconds(-1)); + assertThat(Duration.days(-2)).isLessThan(Duration.seconds(-1)); + assertThat(Duration.days(-2)).isLessThan(Duration.minutes(-1)); + assertThat(Duration.days(-2)).isLessThan(Duration.hours(-1)); + assertThat(Duration.days(-2)).isLessThan(Duration.days(-1)); + + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.nanoseconds(-2)); + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.microseconds(-2)); + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.milliseconds(-2)); + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.seconds(-2)); + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.minutes(-2)); + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.hours(-2)); + assertThat(Duration.nanoseconds(-1)).isGreaterThan(Duration.days(-2)); + + assertThat(Duration.microseconds(-1)).isLessThan(Duration.nanoseconds(-2)); + assertThat(Duration.microseconds(-1)).isGreaterThan(Duration.microseconds(-2)); + assertThat(Duration.microseconds(-1)).isGreaterThan(Duration.milliseconds(-2)); + assertThat(Duration.microseconds(-1)).isGreaterThan(Duration.seconds(-2)); + assertThat(Duration.microseconds(-1)).isGreaterThan(Duration.minutes(-2)); + assertThat(Duration.microseconds(-1)).isGreaterThan(Duration.hours(-2)); + assertThat(Duration.microseconds(-1)).isGreaterThan(Duration.days(-2)); + + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.nanoseconds(-2)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.microseconds(-2)); + assertThat(Duration.milliseconds(-1)).isGreaterThan(Duration.milliseconds(-2)); + assertThat(Duration.milliseconds(-1)).isGreaterThan(Duration.seconds(-2)); + assertThat(Duration.milliseconds(-1)).isGreaterThan(Duration.minutes(-2)); + assertThat(Duration.milliseconds(-1)).isGreaterThan(Duration.hours(-2)); + assertThat(Duration.milliseconds(-1)).isGreaterThan(Duration.days(-2)); + + assertThat(Duration.seconds(-1)).isLessThan(Duration.nanoseconds(-2)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.microseconds(-2)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.milliseconds(-2)); + assertThat(Duration.seconds(-1)).isGreaterThan(Duration.seconds(-2)); + assertThat(Duration.seconds(-1)).isGreaterThan(Duration.minutes(-2)); + assertThat(Duration.seconds(-1)).isGreaterThan(Duration.hours(-2)); + assertThat(Duration.seconds(-1)).isGreaterThan(Duration.days(-2)); + + assertThat(Duration.minutes(-1)).isLessThan(Duration.nanoseconds(-2)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.microseconds(-2)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.milliseconds(-2)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.seconds(-2)); + assertThat(Duration.minutes(-1)).isGreaterThan(Duration.minutes(-2)); + assertThat(Duration.minutes(-1)).isGreaterThan(Duration.hours(-2)); + assertThat(Duration.minutes(-1)).isGreaterThan(Duration.days(-2)); + + assertThat(Duration.hours(-1)).isLessThan(Duration.nanoseconds(-2)); + assertThat(Duration.hours(-1)).isLessThan(Duration.microseconds(-2)); + assertThat(Duration.hours(-1)).isLessThan(Duration.milliseconds(-2)); + assertThat(Duration.hours(-1)).isLessThan(Duration.seconds(-2)); + assertThat(Duration.hours(-1)).isLessThan(Duration.minutes(-2)); + assertThat(Duration.hours(-1)).isGreaterThan(Duration.hours(-2)); + assertThat(Duration.hours(-1)).isGreaterThan(Duration.days(-2)); + + assertThat(Duration.days(-1)).isLessThan(Duration.nanoseconds(-2)); + assertThat(Duration.days(-1)).isLessThan(Duration.microseconds(-2)); + assertThat(Duration.days(-1)).isLessThan(Duration.milliseconds(-2)); + assertThat(Duration.days(-1)).isLessThan(Duration.seconds(-2)); + assertThat(Duration.days(-1)).isLessThan(Duration.minutes(-2)); + assertThat(Duration.days(-1)).isLessThan(Duration.hours(-2)); + assertThat(Duration.days(-1)).isGreaterThan(Duration.days(-2)); + + // both positive + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.nanoseconds(2)); + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.microseconds(2)); + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.milliseconds(2)); + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.seconds(2)); + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.minutes(2)); + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.hours(2)); + assertThat(Duration.nanoseconds(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.nanoseconds(2)); + assertThat(Duration.microseconds(1)).isLessThan(Duration.microseconds(2)); + assertThat(Duration.microseconds(1)).isLessThan(Duration.milliseconds(2)); + assertThat(Duration.microseconds(1)).isLessThan(Duration.seconds(2)); + assertThat(Duration.microseconds(1)).isLessThan(Duration.minutes(2)); + assertThat(Duration.microseconds(1)).isLessThan(Duration.hours(2)); + assertThat(Duration.microseconds(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.nanoseconds(2)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.microseconds(2)); + assertThat(Duration.milliseconds(1)).isLessThan(Duration.milliseconds(2)); + assertThat(Duration.milliseconds(1)).isLessThan(Duration.seconds(2)); + assertThat(Duration.milliseconds(1)).isLessThan(Duration.minutes(2)); + assertThat(Duration.milliseconds(1)).isLessThan(Duration.hours(2)); + assertThat(Duration.milliseconds(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.seconds(1)).isGreaterThan(Duration.nanoseconds(2)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.microseconds(2)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.milliseconds(2)); + assertThat(Duration.seconds(1)).isLessThan(Duration.seconds(2)); + assertThat(Duration.seconds(1)).isLessThan(Duration.minutes(2)); + assertThat(Duration.seconds(1)).isLessThan(Duration.hours(2)); + assertThat(Duration.seconds(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.minutes(1)).isGreaterThan(Duration.nanoseconds(2)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.microseconds(2)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.milliseconds(2)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.seconds(2)); + assertThat(Duration.minutes(1)).isLessThan(Duration.minutes(2)); + assertThat(Duration.minutes(1)).isLessThan(Duration.hours(2)); + assertThat(Duration.minutes(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.hours(1)).isGreaterThan(Duration.nanoseconds(2)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.microseconds(2)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.milliseconds(2)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.seconds(2)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.minutes(2)); + assertThat(Duration.hours(1)).isLessThan(Duration.hours(2)); + assertThat(Duration.hours(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.days(1)).isGreaterThan(Duration.nanoseconds(2)); + assertThat(Duration.days(1)).isGreaterThan(Duration.microseconds(2)); + assertThat(Duration.days(1)).isGreaterThan(Duration.milliseconds(2)); + assertThat(Duration.days(1)).isGreaterThan(Duration.seconds(2)); + assertThat(Duration.days(1)).isGreaterThan(Duration.minutes(2)); + assertThat(Duration.days(1)).isGreaterThan(Duration.hours(2)); + assertThat(Duration.days(1)).isLessThan(Duration.days(2)); + + assertThat(Duration.nanoseconds(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.nanoseconds(2)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.nanoseconds(2)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.nanoseconds(2)).isLessThan(Duration.seconds(1)); + assertThat(Duration.nanoseconds(2)).isLessThan(Duration.minutes(1)); + assertThat(Duration.nanoseconds(2)).isLessThan(Duration.hours(1)); + assertThat(Duration.nanoseconds(2)).isLessThan(Duration.days(1)); + + assertThat(Duration.microseconds(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.microseconds(2)).isGreaterThan(Duration.microseconds(1)); + assertThat(Duration.microseconds(2)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.microseconds(2)).isLessThan(Duration.seconds(1)); + assertThat(Duration.microseconds(2)).isLessThan(Duration.minutes(1)); + assertThat(Duration.microseconds(2)).isLessThan(Duration.hours(1)); + assertThat(Duration.microseconds(2)).isLessThan(Duration.days(1)); + + assertThat(Duration.milliseconds(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.milliseconds(2)).isGreaterThan(Duration.microseconds(1)); + assertThat(Duration.milliseconds(2)).isGreaterThan(Duration.milliseconds(1)); + assertThat(Duration.milliseconds(2)).isLessThan(Duration.seconds(1)); + assertThat(Duration.milliseconds(2)).isLessThan(Duration.minutes(1)); + assertThat(Duration.milliseconds(2)).isLessThan(Duration.hours(1)); + assertThat(Duration.milliseconds(2)).isLessThan(Duration.days(1)); + + assertThat(Duration.seconds(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.seconds(2)).isGreaterThan(Duration.microseconds(1)); + assertThat(Duration.seconds(2)).isGreaterThan(Duration.milliseconds(1)); + assertThat(Duration.seconds(2)).isGreaterThan(Duration.seconds(1)); + assertThat(Duration.seconds(2)).isLessThan(Duration.minutes(1)); + assertThat(Duration.seconds(2)).isLessThan(Duration.hours(1)); + assertThat(Duration.seconds(2)).isLessThan(Duration.days(1)); + + assertThat(Duration.minutes(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.minutes(2)).isGreaterThan(Duration.microseconds(1)); + assertThat(Duration.minutes(2)).isGreaterThan(Duration.milliseconds(1)); + assertThat(Duration.minutes(2)).isGreaterThan(Duration.seconds(1)); + assertThat(Duration.minutes(2)).isGreaterThan(Duration.minutes(1)); + assertThat(Duration.minutes(2)).isLessThan(Duration.hours(1)); + assertThat(Duration.minutes(2)).isLessThan(Duration.days(1)); + + assertThat(Duration.hours(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.hours(2)).isGreaterThan(Duration.microseconds(1)); + assertThat(Duration.hours(2)).isGreaterThan(Duration.milliseconds(1)); + assertThat(Duration.hours(2)).isGreaterThan(Duration.seconds(1)); + assertThat(Duration.hours(2)).isGreaterThan(Duration.minutes(1)); + assertThat(Duration.hours(2)).isGreaterThan(Duration.hours(1)); + assertThat(Duration.hours(2)).isLessThan(Duration.days(1)); + + assertThat(Duration.days(2)).isGreaterThan(Duration.nanoseconds(1)); + assertThat(Duration.days(2)).isGreaterThan(Duration.microseconds(1)); + assertThat(Duration.days(2)).isGreaterThan(Duration.milliseconds(1)); + assertThat(Duration.days(2)).isGreaterThan(Duration.seconds(1)); + assertThat(Duration.days(2)).isGreaterThan(Duration.minutes(1)); + assertThat(Duration.days(2)).isGreaterThan(Duration.hours(1)); + assertThat(Duration.days(2)).isGreaterThan(Duration.days(1)); + + // one negative, one positive + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.nanoseconds(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.microseconds(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.microseconds(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.milliseconds(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.seconds(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.seconds(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.minutes(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.minutes(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.hours(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.hours(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.hours(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.hours(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.hours(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.hours(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.hours(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.days(-1)).isLessThan(Duration.nanoseconds(1)); + assertThat(Duration.days(-1)).isLessThan(Duration.microseconds(1)); + assertThat(Duration.days(-1)).isLessThan(Duration.milliseconds(1)); + assertThat(Duration.days(-1)).isLessThan(Duration.seconds(1)); + assertThat(Duration.days(-1)).isLessThan(Duration.minutes(1)); + assertThat(Duration.days(-1)).isLessThan(Duration.hours(1)); + assertThat(Duration.days(-1)).isLessThan(Duration.days(1)); + + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.nanoseconds(1)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.microseconds(1)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.milliseconds(1)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.seconds(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.seconds(1)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.minutes(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.minutes(1)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.hours(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.hours(1)).isGreaterThan(Duration.days(-1)); + + assertThat(Duration.days(1)).isGreaterThan(Duration.nanoseconds(-1)); + assertThat(Duration.days(1)).isGreaterThan(Duration.microseconds(-1)); + assertThat(Duration.days(1)).isGreaterThan(Duration.milliseconds(-1)); + assertThat(Duration.days(1)).isGreaterThan(Duration.seconds(-1)); + assertThat(Duration.days(1)).isGreaterThan(Duration.minutes(-1)); + assertThat(Duration.days(1)).isGreaterThan(Duration.hours(-1)); + assertThat(Duration.days(1)).isGreaterThan(Duration.days(-1)); + } + + @Test + public void serializesCorrectlyWithJackson() throws IOException { + final ObjectMapper mapper = new ObjectMapper(); + + assertThat(mapper.writeValueAsString(Duration.nanoseconds(0L))).isEqualTo("\"0 nanoseconds\""); + assertThat(mapper.writeValueAsString(Duration.nanoseconds(1L))).isEqualTo("\"1 nanosecond\""); + assertThat(mapper.writeValueAsString(Duration.nanoseconds(2L))).isEqualTo("\"2 nanoseconds\""); + assertThat(mapper.writeValueAsString(Duration.microseconds(0L))).isEqualTo("\"0 microseconds\""); + assertThat(mapper.writeValueAsString(Duration.microseconds(1L))).isEqualTo("\"1 microsecond\""); + assertThat(mapper.writeValueAsString(Duration.microseconds(2L))).isEqualTo("\"2 microseconds\""); + assertThat(mapper.writeValueAsString(Duration.milliseconds(0L))).isEqualTo("\"0 milliseconds\""); + assertThat(mapper.writeValueAsString(Duration.milliseconds(1L))).isEqualTo("\"1 millisecond\""); + assertThat(mapper.writeValueAsString(Duration.milliseconds(2L))).isEqualTo("\"2 milliseconds\""); + assertThat(mapper.writeValueAsString(Duration.seconds(0L))).isEqualTo("\"0 seconds\""); + assertThat(mapper.writeValueAsString(Duration.seconds(1L))).isEqualTo("\"1 second\""); + assertThat(mapper.writeValueAsString(Duration.seconds(2L))).isEqualTo("\"2 seconds\""); + assertThat(mapper.writeValueAsString(Duration.minutes(0L))).isEqualTo("\"0 minutes\""); + assertThat(mapper.writeValueAsString(Duration.minutes(1L))).isEqualTo("\"1 minute\""); + assertThat(mapper.writeValueAsString(Duration.minutes(2L))).isEqualTo("\"2 minutes\""); + assertThat(mapper.writeValueAsString(Duration.hours(0L))).isEqualTo("\"0 hours\""); + assertThat(mapper.writeValueAsString(Duration.hours(1L))).isEqualTo("\"1 hour\""); + assertThat(mapper.writeValueAsString(Duration.hours(2L))).isEqualTo("\"2 hours\""); + assertThat(mapper.writeValueAsString(Duration.days(0L))).isEqualTo("\"0 days\""); + assertThat(mapper.writeValueAsString(Duration.days(1L))).isEqualTo("\"1 day\""); + assertThat(mapper.writeValueAsString(Duration.days(2L))).isEqualTo("\"2 days\""); + } + + @Test + public void deserializesCorrectlyWithJackson() throws IOException { + final ObjectMapper mapper = new ObjectMapper(); + + assertThat(mapper.readValue("\"0 nanoseconds\"", Duration.class)).isEqualTo(Duration.nanoseconds(0L)); + assertThat(mapper.readValue("\"1 nanosecond\"", Duration.class)).isEqualTo(Duration.nanoseconds(1L)); + assertThat(mapper.readValue("\"2 nanoseconds\"", Duration.class)).isEqualTo(Duration.nanoseconds(2L)); + assertThat(mapper.readValue("\"0 microseconds\"", Duration.class)).isEqualTo(Duration.microseconds(0L)); + assertThat(mapper.readValue("\"1 microsecond\"", Duration.class)).isEqualTo(Duration.microseconds(1L)); + assertThat(mapper.readValue("\"2 microseconds\"", Duration.class)).isEqualTo(Duration.microseconds(2L)); + assertThat(mapper.readValue("\"0 milliseconds\"", Duration.class)).isEqualTo(Duration.milliseconds(0L)); + assertThat(mapper.readValue("\"1 millisecond\"", Duration.class)).isEqualTo(Duration.milliseconds(1L)); + assertThat(mapper.readValue("\"2 milliseconds\"", Duration.class)).isEqualTo(Duration.milliseconds(2L)); + assertThat(mapper.readValue("\"0 seconds\"", Duration.class)).isEqualTo(Duration.seconds(0L)); + assertThat(mapper.readValue("\"1 second\"", Duration.class)).isEqualTo(Duration.seconds(1L)); + assertThat(mapper.readValue("\"2 seconds\"", Duration.class)).isEqualTo(Duration.seconds(2L)); + assertThat(mapper.readValue("\"0 minutes\"", Duration.class)).isEqualTo(Duration.minutes(0L)); + assertThat(mapper.readValue("\"1 minutes\"", Duration.class)).isEqualTo(Duration.minutes(1L)); + assertThat(mapper.readValue("\"2 minutes\"", Duration.class)).isEqualTo(Duration.minutes(2L)); + assertThat(mapper.readValue("\"0 hours\"", Duration.class)).isEqualTo(Duration.hours(0L)); + assertThat(mapper.readValue("\"1 hours\"", Duration.class)).isEqualTo(Duration.hours(1L)); + assertThat(mapper.readValue("\"2 hours\"", Duration.class)).isEqualTo(Duration.hours(2L)); + assertThat(mapper.readValue("\"0 days\"", Duration.class)).isEqualTo(Duration.days(0L)); + assertThat(mapper.readValue("\"1 day\"", Duration.class)).isEqualTo(Duration.days(1L)); + assertThat(mapper.readValue("\"2 days\"", Duration.class)).isEqualTo(Duration.days(2L)); + } +} 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 00000000000..2f98e2ac648 --- /dev/null +++ b/dropwizard-util/src/test/java/io/dropwizard/util/JarLocationTest.java @@ -0,0 +1,21 @@ +package io.dropwizard.util; + +import org.junit.Test; + +import java.util.Optional; + +import static org.assertj.core.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.empty()); + } +} diff --git a/dropwizard-util/src/test/java/io/dropwizard/util/OptionalsTest.java b/dropwizard-util/src/test/java/io/dropwizard/util/OptionalsTest.java new file mode 100644 index 00000000000..39c90201dfa --- /dev/null +++ b/dropwizard-util/src/test/java/io/dropwizard/util/OptionalsTest.java @@ -0,0 +1,29 @@ +package io.dropwizard.util; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class OptionalsTest { + @Test + public void testFromGuavaOptional() throws Exception { + assertFalse(Optionals.fromGuavaOptional(com.google.common.base.Optional.absent()).isPresent()); + assertTrue(Optionals.fromGuavaOptional(com.google.common.base.Optional.of("Foo")).isPresent()); + assertEquals( + java.util.Optional.of("Foo"), + Optionals.fromGuavaOptional(com.google.common.base.Optional.of("Foo")) + ); + } + + @Test + public void testToGuavaOptional() throws Exception { + assertFalse(Optionals.toGuavaOptional(java.util.Optional.empty()).isPresent()); + assertTrue(Optionals.toGuavaOptional(java.util.Optional.of("Foo")).isPresent()); + assertEquals( + com.google.common.base.Optional.of("Foo"), + Optionals.toGuavaOptional(java.util.Optional.of("Foo")) + ); + } +} 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 00000000000..adf7ba09a04 --- /dev/null +++ b/dropwizard-util/src/test/java/io/dropwizard/util/SizeTest.java @@ -0,0 +1,549 @@ +package io.dropwizard.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.Test; + +import java.io.IOException; + +import static org.assertj.core.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 + public void parseCaseInsensitive() { + assertThat(Size.parse("1b")).isEqualTo(Size.parse("1B")); + } + + @Test + public void parseSingleLetterSuffix() { + assertThat(Size.parse("1B")).isEqualTo(Size.bytes(1)); + assertThat(Size.parse("1K")).isEqualTo(Size.kilobytes(1)); + assertThat(Size.parse("1M")).isEqualTo(Size.megabytes(1)); + assertThat(Size.parse("1G")).isEqualTo(Size.gigabytes(1)); + assertThat(Size.parse("1T")).isEqualTo(Size.terabytes(1)); + } + + @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); + } + + @Test + public void isComparable() throws Exception { + // both zero + assertThat(Size.bytes(0).compareTo(Size.bytes(0))).isEqualTo(0); + assertThat(Size.bytes(0).compareTo(Size.kilobytes(0))).isEqualTo(0); + assertThat(Size.bytes(0).compareTo(Size.megabytes(0))).isEqualTo(0); + assertThat(Size.bytes(0).compareTo(Size.gigabytes(0))).isEqualTo(0); + assertThat(Size.bytes(0).compareTo(Size.terabytes(0))).isEqualTo(0); + + assertThat(Size.kilobytes(0).compareTo(Size.bytes(0))).isEqualTo(0); + assertThat(Size.kilobytes(0).compareTo(Size.kilobytes(0))).isEqualTo(0); + assertThat(Size.kilobytes(0).compareTo(Size.megabytes(0))).isEqualTo(0); + assertThat(Size.kilobytes(0).compareTo(Size.gigabytes(0))).isEqualTo(0); + assertThat(Size.kilobytes(0).compareTo(Size.terabytes(0))).isEqualTo(0); + + assertThat(Size.megabytes(0).compareTo(Size.bytes(0))).isEqualTo(0); + assertThat(Size.megabytes(0).compareTo(Size.kilobytes(0))).isEqualTo(0); + assertThat(Size.megabytes(0).compareTo(Size.megabytes(0))).isEqualTo(0); + assertThat(Size.megabytes(0).compareTo(Size.gigabytes(0))).isEqualTo(0); + assertThat(Size.megabytes(0).compareTo(Size.terabytes(0))).isEqualTo(0); + + assertThat(Size.gigabytes(0).compareTo(Size.bytes(0))).isEqualTo(0); + assertThat(Size.gigabytes(0).compareTo(Size.kilobytes(0))).isEqualTo(0); + assertThat(Size.gigabytes(0).compareTo(Size.megabytes(0))).isEqualTo(0); + assertThat(Size.gigabytes(0).compareTo(Size.gigabytes(0))).isEqualTo(0); + assertThat(Size.gigabytes(0).compareTo(Size.terabytes(0))).isEqualTo(0); + + assertThat(Size.terabytes(0).compareTo(Size.bytes(0))).isEqualTo(0); + assertThat(Size.terabytes(0).compareTo(Size.kilobytes(0))).isEqualTo(0); + assertThat(Size.terabytes(0).compareTo(Size.megabytes(0))).isEqualTo(0); + assertThat(Size.terabytes(0).compareTo(Size.gigabytes(0))).isEqualTo(0); + assertThat(Size.terabytes(0).compareTo(Size.terabytes(0))).isEqualTo(0); + + // one zero, one negative + assertThat(Size.bytes(0)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.bytes(0)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.bytes(0)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.bytes(0)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.bytes(0)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.kilobytes(0)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.kilobytes(0)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.kilobytes(0)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.kilobytes(0)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.kilobytes(0)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.megabytes(0)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.megabytes(0)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.megabytes(0)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.megabytes(0)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.megabytes(0)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.gigabytes(0)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.gigabytes(0)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.gigabytes(0)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.gigabytes(0)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.gigabytes(0)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.terabytes(0)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.terabytes(0)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.terabytes(0)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.terabytes(0)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.terabytes(0)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.bytes(-1)).isLessThan(Size.bytes(0)); + assertThat(Size.bytes(-1)).isLessThan(Size.kilobytes(0)); + assertThat(Size.bytes(-1)).isLessThan(Size.megabytes(0)); + assertThat(Size.bytes(-1)).isLessThan(Size.gigabytes(0)); + assertThat(Size.bytes(-1)).isLessThan(Size.terabytes(0)); + + assertThat(Size.kilobytes(-1)).isLessThan(Size.bytes(0)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.kilobytes(0)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.megabytes(0)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.gigabytes(0)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.terabytes(0)); + + assertThat(Size.megabytes(-1)).isLessThan(Size.bytes(0)); + assertThat(Size.megabytes(-1)).isLessThan(Size.kilobytes(0)); + assertThat(Size.megabytes(-1)).isLessThan(Size.megabytes(0)); + assertThat(Size.megabytes(-1)).isLessThan(Size.gigabytes(0)); + assertThat(Size.megabytes(-1)).isLessThan(Size.terabytes(0)); + + assertThat(Size.gigabytes(-1)).isLessThan(Size.bytes(0)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.kilobytes(0)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.megabytes(0)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.gigabytes(0)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.terabytes(0)); + + assertThat(Size.terabytes(-1)).isLessThan(Size.bytes(0)); + assertThat(Size.terabytes(-1)).isLessThan(Size.kilobytes(0)); + assertThat(Size.terabytes(-1)).isLessThan(Size.megabytes(0)); + assertThat(Size.terabytes(-1)).isLessThan(Size.gigabytes(0)); + assertThat(Size.terabytes(-1)).isLessThan(Size.terabytes(0)); + + // one zero, one positive + assertThat(Size.bytes(0)).isLessThan(Size.bytes(1)); + assertThat(Size.bytes(0)).isLessThan(Size.kilobytes(1)); + assertThat(Size.bytes(0)).isLessThan(Size.megabytes(1)); + assertThat(Size.bytes(0)).isLessThan(Size.gigabytes(1)); + assertThat(Size.bytes(0)).isLessThan(Size.terabytes(1)); + + assertThat(Size.kilobytes(0)).isLessThan(Size.bytes(1)); + assertThat(Size.kilobytes(0)).isLessThan(Size.kilobytes(1)); + assertThat(Size.kilobytes(0)).isLessThan(Size.megabytes(1)); + assertThat(Size.kilobytes(0)).isLessThan(Size.gigabytes(1)); + assertThat(Size.kilobytes(0)).isLessThan(Size.terabytes(1)); + + assertThat(Size.megabytes(0)).isLessThan(Size.bytes(1)); + assertThat(Size.megabytes(0)).isLessThan(Size.kilobytes(1)); + assertThat(Size.megabytes(0)).isLessThan(Size.megabytes(1)); + assertThat(Size.megabytes(0)).isLessThan(Size.gigabytes(1)); + assertThat(Size.megabytes(0)).isLessThan(Size.terabytes(1)); + + assertThat(Size.gigabytes(0)).isLessThan(Size.bytes(1)); + assertThat(Size.gigabytes(0)).isLessThan(Size.kilobytes(1)); + assertThat(Size.gigabytes(0)).isLessThan(Size.megabytes(1)); + assertThat(Size.gigabytes(0)).isLessThan(Size.gigabytes(1)); + assertThat(Size.gigabytes(0)).isLessThan(Size.terabytes(1)); + + assertThat(Size.terabytes(0)).isLessThan(Size.bytes(1)); + assertThat(Size.terabytes(0)).isLessThan(Size.kilobytes(1)); + assertThat(Size.terabytes(0)).isLessThan(Size.megabytes(1)); + assertThat(Size.terabytes(0)).isLessThan(Size.gigabytes(1)); + assertThat(Size.terabytes(0)).isLessThan(Size.terabytes(1)); + + assertThat(Size.bytes(1)).isGreaterThan(Size.bytes(0)); + assertThat(Size.bytes(1)).isGreaterThan(Size.kilobytes(0)); + assertThat(Size.bytes(1)).isGreaterThan(Size.megabytes(0)); + assertThat(Size.bytes(1)).isGreaterThan(Size.gigabytes(0)); + assertThat(Size.bytes(1)).isGreaterThan(Size.terabytes(0)); + + assertThat(Size.kilobytes(1)).isGreaterThan(Size.bytes(0)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.kilobytes(0)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.megabytes(0)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.gigabytes(0)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.terabytes(0)); + + assertThat(Size.megabytes(1)).isGreaterThan(Size.bytes(0)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.kilobytes(0)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.megabytes(0)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.gigabytes(0)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.terabytes(0)); + + assertThat(Size.gigabytes(1)).isGreaterThan(Size.bytes(0)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.kilobytes(0)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.megabytes(0)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.gigabytes(0)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.terabytes(0)); + + assertThat(Size.terabytes(1)).isGreaterThan(Size.bytes(0)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.kilobytes(0)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.megabytes(0)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.gigabytes(0)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.terabytes(0)); + + // both negative + assertThat(Size.bytes(-2)).isLessThan(Size.bytes(-1)); + assertThat(Size.bytes(-2)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.bytes(-2)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.bytes(-2)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.bytes(-2)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.kilobytes(-2)).isLessThan(Size.bytes(-1)); + assertThat(Size.kilobytes(-2)).isLessThan(Size.kilobytes(-1)); + assertThat(Size.kilobytes(-2)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.kilobytes(-2)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.kilobytes(-2)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.megabytes(-2)).isLessThan(Size.bytes(-1)); + assertThat(Size.megabytes(-2)).isLessThan(Size.kilobytes(-1)); + assertThat(Size.megabytes(-2)).isLessThan(Size.megabytes(-1)); + assertThat(Size.megabytes(-2)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.megabytes(-2)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.gigabytes(-2)).isLessThan(Size.bytes(-1)); + assertThat(Size.gigabytes(-2)).isLessThan(Size.kilobytes(-1)); + assertThat(Size.gigabytes(-2)).isLessThan(Size.megabytes(-1)); + assertThat(Size.gigabytes(-2)).isLessThan(Size.gigabytes(-1)); + assertThat(Size.gigabytes(-2)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.terabytes(-2)).isLessThan(Size.bytes(-1)); + assertThat(Size.terabytes(-2)).isLessThan(Size.kilobytes(-1)); + assertThat(Size.terabytes(-2)).isLessThan(Size.megabytes(-1)); + assertThat(Size.terabytes(-2)).isLessThan(Size.gigabytes(-1)); + assertThat(Size.terabytes(-2)).isLessThan(Size.terabytes(-1)); + + assertThat(Size.bytes(-1)).isGreaterThan(Size.bytes(-2)); + assertThat(Size.bytes(-1)).isGreaterThan(Size.kilobytes(-2)); + assertThat(Size.bytes(-1)).isGreaterThan(Size.megabytes(-2)); + assertThat(Size.bytes(-1)).isGreaterThan(Size.gigabytes(-2)); + assertThat(Size.bytes(-1)).isGreaterThan(Size.terabytes(-2)); + + assertThat(Size.kilobytes(-1)).isLessThan(Size.bytes(-2)); + assertThat(Size.kilobytes(-1)).isGreaterThan(Size.kilobytes(-2)); + assertThat(Size.kilobytes(-1)).isGreaterThan(Size.megabytes(-2)); + assertThat(Size.kilobytes(-1)).isGreaterThan(Size.gigabytes(-2)); + assertThat(Size.kilobytes(-1)).isGreaterThan(Size.terabytes(-2)); + + assertThat(Size.megabytes(-1)).isLessThan(Size.bytes(-2)); + assertThat(Size.megabytes(-1)).isLessThan(Size.kilobytes(-2)); + assertThat(Size.megabytes(-1)).isGreaterThan(Size.megabytes(-2)); + assertThat(Size.megabytes(-1)).isGreaterThan(Size.gigabytes(-2)); + assertThat(Size.megabytes(-1)).isGreaterThan(Size.terabytes(-2)); + + assertThat(Size.gigabytes(-1)).isLessThan(Size.bytes(-2)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.kilobytes(-2)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.megabytes(-2)); + assertThat(Size.gigabytes(-1)).isGreaterThan(Size.gigabytes(-2)); + assertThat(Size.gigabytes(-1)).isGreaterThan(Size.terabytes(-2)); + + assertThat(Size.terabytes(-1)).isLessThan(Size.bytes(-2)); + assertThat(Size.terabytes(-1)).isLessThan(Size.kilobytes(-2)); + assertThat(Size.terabytes(-1)).isLessThan(Size.megabytes(-2)); + assertThat(Size.terabytes(-1)).isLessThan(Size.gigabytes(-2)); + assertThat(Size.terabytes(-1)).isGreaterThan(Size.terabytes(-2)); + + // both positive + assertThat(Size.bytes(1)).isLessThan(Size.bytes(2)); + assertThat(Size.bytes(1)).isLessThan(Size.kilobytes(2)); + assertThat(Size.bytes(1)).isLessThan(Size.megabytes(2)); + assertThat(Size.bytes(1)).isLessThan(Size.gigabytes(2)); + assertThat(Size.bytes(1)).isLessThan(Size.terabytes(2)); + + assertThat(Size.kilobytes(1)).isGreaterThan(Size.bytes(2)); + assertThat(Size.kilobytes(1)).isLessThan(Size.kilobytes(2)); + assertThat(Size.kilobytes(1)).isLessThan(Size.megabytes(2)); + assertThat(Size.kilobytes(1)).isLessThan(Size.gigabytes(2)); + assertThat(Size.kilobytes(1)).isLessThan(Size.terabytes(2)); + + assertThat(Size.megabytes(1)).isGreaterThan(Size.bytes(2)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.kilobytes(2)); + assertThat(Size.megabytes(1)).isLessThan(Size.megabytes(2)); + assertThat(Size.megabytes(1)).isLessThan(Size.gigabytes(2)); + assertThat(Size.megabytes(1)).isLessThan(Size.terabytes(2)); + + assertThat(Size.gigabytes(1)).isGreaterThan(Size.bytes(2)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.kilobytes(2)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.megabytes(2)); + assertThat(Size.gigabytes(1)).isLessThan(Size.gigabytes(2)); + assertThat(Size.gigabytes(1)).isLessThan(Size.terabytes(2)); + + assertThat(Size.terabytes(1)).isGreaterThan(Size.bytes(2)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.kilobytes(2)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.megabytes(2)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.gigabytes(2)); + assertThat(Size.terabytes(1)).isLessThan(Size.terabytes(2)); + + assertThat(Size.bytes(2)).isGreaterThan(Size.bytes(1)); + assertThat(Size.bytes(2)).isLessThan(Size.kilobytes(1)); + assertThat(Size.bytes(2)).isLessThan(Size.megabytes(1)); + assertThat(Size.bytes(2)).isLessThan(Size.gigabytes(1)); + assertThat(Size.bytes(2)).isLessThan(Size.terabytes(1)); + + assertThat(Size.kilobytes(2)).isGreaterThan(Size.bytes(1)); + assertThat(Size.kilobytes(2)).isGreaterThan(Size.kilobytes(1)); + assertThat(Size.kilobytes(2)).isLessThan(Size.megabytes(1)); + assertThat(Size.kilobytes(2)).isLessThan(Size.gigabytes(1)); + assertThat(Size.kilobytes(2)).isLessThan(Size.terabytes(1)); + + assertThat(Size.megabytes(2)).isGreaterThan(Size.bytes(1)); + assertThat(Size.megabytes(2)).isGreaterThan(Size.kilobytes(1)); + assertThat(Size.megabytes(2)).isGreaterThan(Size.megabytes(1)); + assertThat(Size.megabytes(2)).isLessThan(Size.gigabytes(1)); + assertThat(Size.megabytes(2)).isLessThan(Size.terabytes(1)); + + assertThat(Size.gigabytes(2)).isGreaterThan(Size.bytes(1)); + assertThat(Size.gigabytes(2)).isGreaterThan(Size.kilobytes(1)); + assertThat(Size.gigabytes(2)).isGreaterThan(Size.megabytes(1)); + assertThat(Size.gigabytes(2)).isGreaterThan(Size.gigabytes(1)); + assertThat(Size.gigabytes(2)).isLessThan(Size.terabytes(1)); + + assertThat(Size.terabytes(2)).isGreaterThan(Size.bytes(1)); + assertThat(Size.terabytes(2)).isGreaterThan(Size.kilobytes(1)); + assertThat(Size.terabytes(2)).isGreaterThan(Size.megabytes(1)); + assertThat(Size.terabytes(2)).isGreaterThan(Size.gigabytes(1)); + assertThat(Size.terabytes(2)).isGreaterThan(Size.terabytes(1)); + + // one negative, one positive + assertThat(Size.bytes(-1)).isLessThan(Size.bytes(1)); + assertThat(Size.bytes(-1)).isLessThan(Size.kilobytes(1)); + assertThat(Size.bytes(-1)).isLessThan(Size.megabytes(1)); + assertThat(Size.bytes(-1)).isLessThan(Size.gigabytes(1)); + assertThat(Size.bytes(-1)).isLessThan(Size.terabytes(1)); + + assertThat(Size.kilobytes(-1)).isLessThan(Size.bytes(1)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.kilobytes(1)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.megabytes(1)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.gigabytes(1)); + assertThat(Size.kilobytes(-1)).isLessThan(Size.terabytes(1)); + + assertThat(Size.megabytes(-1)).isLessThan(Size.bytes(1)); + assertThat(Size.megabytes(-1)).isLessThan(Size.kilobytes(1)); + assertThat(Size.megabytes(-1)).isLessThan(Size.megabytes(1)); + assertThat(Size.megabytes(-1)).isLessThan(Size.gigabytes(1)); + assertThat(Size.megabytes(-1)).isLessThan(Size.terabytes(1)); + + assertThat(Size.gigabytes(-1)).isLessThan(Size.bytes(1)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.kilobytes(1)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.megabytes(1)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.gigabytes(1)); + assertThat(Size.gigabytes(-1)).isLessThan(Size.terabytes(1)); + + assertThat(Size.terabytes(-1)).isLessThan(Size.bytes(1)); + assertThat(Size.terabytes(-1)).isLessThan(Size.kilobytes(1)); + assertThat(Size.terabytes(-1)).isLessThan(Size.megabytes(1)); + assertThat(Size.terabytes(-1)).isLessThan(Size.gigabytes(1)); + assertThat(Size.terabytes(-1)).isLessThan(Size.terabytes(1)); + + assertThat(Size.bytes(1)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.bytes(1)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.bytes(1)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.bytes(1)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.bytes(1)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.kilobytes(1)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.kilobytes(1)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.megabytes(1)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.megabytes(1)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.gigabytes(1)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.gigabytes(1)).isGreaterThan(Size.terabytes(-1)); + + assertThat(Size.terabytes(1)).isGreaterThan(Size.bytes(-1)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.kilobytes(-1)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.megabytes(-1)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.gigabytes(-1)); + assertThat(Size.terabytes(1)).isGreaterThan(Size.terabytes(-1)); + } + + @Test + public void serializesCorrectlyWithJackson() throws IOException { + final ObjectMapper mapper = new ObjectMapper(); + + assertThat(mapper.writeValueAsString(Size.bytes(0L))).isEqualTo("\"0 bytes\""); + assertThat(mapper.writeValueAsString(Size.bytes(1L))).isEqualTo("\"1 byte\""); + assertThat(mapper.writeValueAsString(Size.bytes(2L))).isEqualTo("\"2 bytes\""); + assertThat(mapper.writeValueAsString(Size.kilobytes(0L))).isEqualTo("\"0 kilobytes\""); + assertThat(mapper.writeValueAsString(Size.kilobytes(1L))).isEqualTo("\"1 kilobyte\""); + assertThat(mapper.writeValueAsString(Size.kilobytes(2L))).isEqualTo("\"2 kilobytes\""); + assertThat(mapper.writeValueAsString(Size.megabytes(0L))).isEqualTo("\"0 megabytes\""); + assertThat(mapper.writeValueAsString(Size.megabytes(1L))).isEqualTo("\"1 megabyte\""); + assertThat(mapper.writeValueAsString(Size.megabytes(2L))).isEqualTo("\"2 megabytes\""); + assertThat(mapper.writeValueAsString(Size.gigabytes(0L))).isEqualTo("\"0 gigabytes\""); + assertThat(mapper.writeValueAsString(Size.gigabytes(1L))).isEqualTo("\"1 gigabyte\""); + assertThat(mapper.writeValueAsString(Size.gigabytes(2L))).isEqualTo("\"2 gigabytes\""); + assertThat(mapper.writeValueAsString(Size.terabytes(0L))).isEqualTo("\"0 terabytes\""); + assertThat(mapper.writeValueAsString(Size.terabytes(1L))).isEqualTo("\"1 terabyte\""); + assertThat(mapper.writeValueAsString(Size.terabytes(2L))).isEqualTo("\"2 terabytes\""); + } + + @Test + public void deserializesCorrectlyWithJackson() throws IOException { + final ObjectMapper mapper = new ObjectMapper(); + + assertThat(mapper.readValue("\"0 bytes\"", Size.class)).isEqualTo(Size.bytes(0L)); + assertThat(mapper.readValue("\"1 byte\"", Size.class)).isEqualTo(Size.bytes(1L)); + assertThat(mapper.readValue("\"2 bytes\"", Size.class)).isEqualTo(Size.bytes(2L)); + assertThat(mapper.readValue("\"0 kilobytes\"", Size.class)).isEqualTo(Size.kilobytes(0L)); + assertThat(mapper.readValue("\"1 kilobyte\"", Size.class)).isEqualTo(Size.kilobytes(1L)); + assertThat(mapper.readValue("\"2 kilobytes\"", Size.class)).isEqualTo(Size.kilobytes(2L)); + assertThat(mapper.readValue("\"0 megabytes\"", Size.class)).isEqualTo(Size.megabytes(0L)); + assertThat(mapper.readValue("\"1 megabyte\"", Size.class)).isEqualTo(Size.megabytes(1L)); + assertThat(mapper.readValue("\"2 megabytes\"", Size.class)).isEqualTo(Size.megabytes(2L)); + assertThat(mapper.readValue("\"0 gigabytes\"", Size.class)).isEqualTo(Size.gigabytes(0L)); + assertThat(mapper.readValue("\"1 gigabyte\"", Size.class)).isEqualTo(Size.gigabytes(1L)); + assertThat(mapper.readValue("\"2 gigabytes\"", Size.class)).isEqualTo(Size.gigabytes(2L)); + assertThat(mapper.readValue("\"0 terabytes\"", Size.class)).isEqualTo(Size.terabytes(0L)); + assertThat(mapper.readValue("\"1 terabytes\"", Size.class)).isEqualTo(Size.terabytes(1L)); + assertThat(mapper.readValue("\"2 terabytes\"", Size.class)).isEqualTo(Size.terabytes(2L)); + } +} diff --git a/dropwizard/src/test/java/com/yammer/dropwizard/util/tests/SizeUnitTest.java b/dropwizard-util/src/test/java/io/dropwizard/util/SizeUnitTest.java similarity index 52% rename from dropwizard/src/test/java/com/yammer/dropwizard/util/tests/SizeUnitTest.java rename to dropwizard-util/src/test/java/io/dropwizard/util/SizeUnitTest.java index 31595cfa0a7..a8f1fb76b48 100644 --- a/dropwizard/src/test/java/com/yammer/dropwizard/util/tests/SizeUnitTest.java +++ b/dropwizard-util/src/test/java/io/dropwizard/util/SizeUnitTest.java @@ -1,245 +1,243 @@ -package com.yammer.dropwizard.util.tests; +package io.dropwizard.util; -import com.yammer.dropwizard.util.SizeUnit; import org.junit.Test; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; +import static org.assertj.core.api.Assertions.assertThat; public class SizeUnitTest { // BYTES @Test public void oneByteInBytes() throws Exception { - assertThat(SizeUnit.BYTES.convert(1, SizeUnit.BYTES), - is(1L)); + assertThat(SizeUnit.BYTES.convert(1, SizeUnit.BYTES)) + .isEqualTo(1); - assertThat(SizeUnit.BYTES.toBytes(1), - is(1L)); + assertThat(SizeUnit.BYTES.toBytes(1)) + .isEqualTo(1); } @Test public void oneByteInKilobytes() throws Exception { - assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.BYTES), - is(0L)); + assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.BYTES)) + .isZero(); - assertThat(SizeUnit.BYTES.toKilobytes(1), - is(0L)); + assertThat(SizeUnit.BYTES.toKilobytes(1)) + .isZero(); } @Test public void oneByteInMegabytes() throws Exception { - assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.BYTES), - is(0L)); + assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.BYTES)) + .isZero(); - assertThat(SizeUnit.BYTES.toMegabytes(1), - is(0L)); + assertThat(SizeUnit.BYTES.toMegabytes(1)) + .isZero(); } @Test public void oneByteInGigabytes() throws Exception { - assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.BYTES), - is(0L)); + assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.BYTES)) + .isZero(); - assertThat(SizeUnit.BYTES.toGigabytes(1), - is(0L)); + assertThat(SizeUnit.BYTES.toGigabytes(1)) + .isZero(); } @Test public void oneByteInTerabytes() throws Exception { - assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.BYTES), - is(0L)); + assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.BYTES)) + .isZero(); - assertThat(SizeUnit.BYTES.toTerabytes(1), - is(0L)); + assertThat(SizeUnit.BYTES.toTerabytes(1)) + .isZero(); } // KILOBYTES @Test public void oneKilobyteInBytes() throws Exception { - assertThat(SizeUnit.BYTES.convert(1, SizeUnit.KILOBYTES), - is(1024L)); - - assertThat(SizeUnit.KILOBYTES.toBytes(1), - is(1024L)); + 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), - is(1L)); + assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.KILOBYTES)) + .isEqualTo(1); - assertThat(SizeUnit.KILOBYTES.toKilobytes(1), - is(1L)); + assertThat(SizeUnit.KILOBYTES.toKilobytes(1)) + .isEqualTo(1L); } @Test public void oneKilobyteInMegabytes() throws Exception { - assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.KILOBYTES), - is(0L)); + assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.KILOBYTES)) + .isZero(); - assertThat(SizeUnit.KILOBYTES.toMegabytes(1), - is(0L)); + assertThat(SizeUnit.KILOBYTES.toMegabytes(1)) + .isZero(); } @Test public void oneKilobyteInGigabytes() throws Exception { - assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.KILOBYTES), - is(0L)); + assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.KILOBYTES)) + .isZero(); - assertThat(SizeUnit.KILOBYTES.toGigabytes(1), - is(0L)); + assertThat(SizeUnit.KILOBYTES.toGigabytes(1)) + .isZero(); } @Test public void oneKilobyteInTerabytes() throws Exception { - assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.KILOBYTES), - is(0L)); + assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.KILOBYTES)) + .isZero(); - assertThat(SizeUnit.KILOBYTES.toTerabytes(1), - is(0L)); + assertThat(SizeUnit.KILOBYTES.toTerabytes(1)) + .isZero(); } // MEGABYTES @Test public void oneMegabyteInBytes() throws Exception { - assertThat(SizeUnit.BYTES.convert(1, SizeUnit.MEGABYTES), - is(1048576L)); + assertThat(SizeUnit.BYTES.convert(1, SizeUnit.MEGABYTES)) + .isEqualTo(1048576); - assertThat(SizeUnit.MEGABYTES.toBytes(1), - is(1048576L)); + assertThat(SizeUnit.MEGABYTES.toBytes(1)) + .isEqualTo(1048576L); } @Test public void oneMegabyteInKilobytes() throws Exception { - assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.MEGABYTES), - is(1024L)); + assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.MEGABYTES)) + .isEqualTo(1024); - assertThat(SizeUnit.MEGABYTES.toKilobytes(1), - is(1024L)); + assertThat(SizeUnit.MEGABYTES.toKilobytes(1)) + .isEqualTo(1024); } @Test public void oneMegabyteInMegabytes() throws Exception { - assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.MEGABYTES), - is(1L)); + assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.MEGABYTES)) + .isEqualTo(1); - assertThat(SizeUnit.MEGABYTES.toMegabytes(1), - is(1L)); + assertThat(SizeUnit.MEGABYTES.toMegabytes(1)) + .isEqualTo(1); } @Test public void oneMegabyteInGigabytes() throws Exception { - assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.MEGABYTES), - is(0L)); + assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.MEGABYTES)) + .isZero(); - assertThat(SizeUnit.MEGABYTES.toGigabytes(1), - is(0L)); + assertThat(SizeUnit.MEGABYTES.toGigabytes(1)) + .isZero(); } @Test public void oneMegabyteInTerabytes() throws Exception { - assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.MEGABYTES), - is(0L)); + assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.MEGABYTES)) + .isZero(); - assertThat(SizeUnit.MEGABYTES.toTerabytes(1), - is(0L)); + assertThat(SizeUnit.MEGABYTES.toTerabytes(1)) + .isZero(); } // GIGABYTES @Test public void oneGigabyteInBytes() throws Exception { - assertThat(SizeUnit.BYTES.convert(1, SizeUnit.GIGABYTES), - is(1073741824L)); + assertThat(SizeUnit.BYTES.convert(1, SizeUnit.GIGABYTES)) + .isEqualTo(1073741824); - assertThat(SizeUnit.GIGABYTES.toBytes(1), - is(1073741824L)); + assertThat(SizeUnit.GIGABYTES.toBytes(1)) + .isEqualTo(1073741824); } @Test public void oneGigabyteInKilobytes() throws Exception { - assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.GIGABYTES), - is(1048576L)); + assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.GIGABYTES)) + .isEqualTo(1048576); - assertThat(SizeUnit.GIGABYTES.toKilobytes(1), - is(1048576L)); + assertThat(SizeUnit.GIGABYTES.toKilobytes(1)) + .isEqualTo(1048576); } @Test public void oneGigabyteInMegabytes() throws Exception { - assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.GIGABYTES), - is(1024L)); + assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.GIGABYTES)) + .isEqualTo(1024); - assertThat(SizeUnit.GIGABYTES.toMegabytes(1), - is(1024L)); + assertThat(SizeUnit.GIGABYTES.toMegabytes(1)) + .isEqualTo(1024); } @Test public void oneGigabyteInGigabytes() throws Exception { - assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.GIGABYTES), - is(1L)); + assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.GIGABYTES)) + .isEqualTo(1L); - assertThat(SizeUnit.GIGABYTES.toGigabytes(1), - is(1L)); + assertThat(SizeUnit.GIGABYTES.toGigabytes(1)) + .isEqualTo(1L); } @Test public void oneGigabyteInTerabytes() throws Exception { - assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.GIGABYTES), - is(0L)); + assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.GIGABYTES)) + .isZero(); - assertThat(SizeUnit.GIGABYTES.toTerabytes(1), - is(0L)); + assertThat(SizeUnit.GIGABYTES.toTerabytes(1)) + .isZero(); } // TERABYTES @Test public void oneTerabyteInBytes() throws Exception { - assertThat(SizeUnit.BYTES.convert(1, SizeUnit.TERABYTES), - is(1099511627776L)); + assertThat(SizeUnit.BYTES.convert(1, SizeUnit.TERABYTES)) + .isEqualTo(1099511627776L); - assertThat(SizeUnit.TERABYTES.toBytes(1), - is(1099511627776L)); + assertThat(SizeUnit.TERABYTES.toBytes(1)) + .isEqualTo(1099511627776L); } @Test public void oneTerabyteInKilobytes() throws Exception { - assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.TERABYTES), - is(1073741824L)); + assertThat(SizeUnit.KILOBYTES.convert(1, SizeUnit.TERABYTES)) + .isEqualTo(1073741824L); - assertThat(SizeUnit.TERABYTES.toKilobytes(1), - is(1073741824L)); + assertThat(SizeUnit.TERABYTES.toKilobytes(1)) + .isEqualTo(1073741824L); } @Test public void oneTerabyteInMegabytes() throws Exception { - assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.TERABYTES), - is(1048576L)); + assertThat(SizeUnit.MEGABYTES.convert(1, SizeUnit.TERABYTES)) + .isEqualTo(1048576); - assertThat(SizeUnit.TERABYTES.toMegabytes(1), - is(1048576L)); + assertThat(SizeUnit.TERABYTES.toMegabytes(1)) + .isEqualTo(1048576L); } @Test public void oneTerabyteInGigabytes() throws Exception { - assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.TERABYTES), - is(1024L)); + assertThat(SizeUnit.GIGABYTES.convert(1, SizeUnit.TERABYTES)) + .isEqualTo(1024); - assertThat(SizeUnit.TERABYTES.toGigabytes(1), - is(1024L)); + assertThat(SizeUnit.TERABYTES.toGigabytes(1)) + .isEqualTo(1024); } @Test public void oneTerabyteInTerabytes() throws Exception { - assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.TERABYTES), - is(1L)); + assertThat(SizeUnit.TERABYTES.convert(1, SizeUnit.TERABYTES)) + .isEqualTo(1); - assertThat(SizeUnit.TERABYTES.toTerabytes(1), - is(1L)); + assertThat(SizeUnit.TERABYTES.toTerabytes(1)) + .isEqualTo(1); } } diff --git a/dropwizard-validation/pom.xml b/dropwizard-validation/pom.xml new file mode 100644 index 00000000000..2f66f0ed157 --- /dev/null +++ b/dropwizard-validation/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-validation + Dropwizard Validation Support + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-util + + + org.hibernate + hibernate-validator + + + org.glassfish + javax.el + + + org.apache.commons + commons-lang3 + test + + + diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/BaseValidator.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/BaseValidator.java new file mode 100644 index 00000000000..26e8aa94bd0 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/BaseValidator.java @@ -0,0 +1,36 @@ +package io.dropwizard.validation; + +import io.dropwizard.validation.valuehandling.GuavaOptionalValidatedValueUnwrapper; +import io.dropwizard.validation.valuehandling.OptionalDoubleValidatedValueUnwrapper; +import io.dropwizard.validation.valuehandling.OptionalIntValidatedValueUnwrapper; +import io.dropwizard.validation.valuehandling.OptionalLongValidatedValueUnwrapper; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; + +import javax.validation.Validation; +import javax.validation.Validator; + +public class BaseValidator { + private BaseValidator() { /* singleton */ } + + /** + * Creates a new {@link Validator} based on {@link #newConfiguration()} + */ + public static Validator newValidator() { + return newConfiguration().buildValidatorFactory().getValidator(); + } + + /** + * Creates a new {@link HibernateValidatorConfiguration} with the base custom {@link + * org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper} registered. + */ + public static HibernateValidatorConfiguration newConfiguration() { + return Validation + .byProvider(HibernateValidator.class) + .configure() + .addValidatedValueHandler(new GuavaOptionalValidatedValueUnwrapper()) + .addValidatedValueHandler(new OptionalDoubleValidatedValueUnwrapper()) + .addValidatedValueHandler(new OptionalIntValidatedValueUnwrapper()) + .addValidatedValueHandler(new OptionalLongValidatedValueUnwrapper()); + } +} 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 00000000000..e706c4f7889 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/ConstraintViolations.java @@ -0,0 +1,45 @@ +package io.dropwizard.validation; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Ordering; + +import javax.validation.ConstraintViolation; +import java.util.HashSet; +import java.util.Set; + +public class ConstraintViolations { + private ConstraintViolations() { /* singleton */ } + + public static String format(ConstraintViolation v) { + if (v.getConstraintDescriptor().getAnnotation() instanceof ValidationMethod) { + return v.getMessage(); + } else { + return String.format("%s %s", v.getPropertyPath(), v.getMessage()); + } + } + + public static ImmutableList format(Set> violations) { + final Set errors = new HashSet<>(); + for (ConstraintViolation v : violations) { + errors.add(format(v)); + } + return ImmutableList.copyOf(Ordering.natural().sortedCopy(errors)); + } + + public static ImmutableList formatUntyped(Set> violations) { + final Set errors = new HashSet<>(); + for (ConstraintViolation v : violations) { + errors.add(format(v)); + } + return ImmutableList.copyOf(Ordering.natural().sortedCopy(errors)); + } + + public static ImmutableSet> copyOf(Set> violations) { + final ImmutableSet.Builder> builder = ImmutableSet.builder(); + for (ConstraintViolation 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 00000000000..6f107fb4fc9 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/DurationRange.java @@ -0,0 +1,59 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +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. + */ +@Documented +@Constraint(validatedBy = { }) +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@MinDuration(0) +@MaxDuration(value = Long.MAX_VALUE, unit = TimeUnit.DAYS) +@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[] 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 00000000000..2629d64eb8f --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxDuration.java @@ -0,0 +1,44 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +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. + *

    + * null elements are considered valid + */ +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@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[] 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 00000000000..b84681ff496 --- /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 { + + 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 00000000000..3571bda561a --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MaxSize.java @@ -0,0 +1,45 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * The annotated element must be a {@link io.dropwizard.util.Size} + * whose value must be less than or equal to the specified maximum. + *

    + * null elements are considered valid + */ +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@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[] payload() default { }; + + /** + * @return value the element must be less than or equal to + */ + long value(); + + /** + * @return unit of the value the element must be less than 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 00000000000..7eb6a676422 --- /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 { + + 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 00000000000..63dcdc80caf --- /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 { + @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 00000000000..42a86292cbd --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinDuration.java @@ -0,0 +1,44 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +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. + *

    + * null elements are considered valid + */ +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@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[] 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 00000000000..48087ef33ec --- /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 { + + 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 00000000000..8646d133020 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/MinSize.java @@ -0,0 +1,45 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +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. + *

    + * null elements are considered valid + */ +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Documented +@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[] 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 00000000000..ffd5e224b0a --- /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 { + + 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 00000000000..e2764261519 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/OneOf.java @@ -0,0 +1,45 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * Checks to see that the value is one of a set of elements. + */ +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Documented +@Constraint(validatedBy = OneOfValidator.class) +public @interface OneOf { + String message() default "must be one of {value}"; + + Class[] groups() default {}; + + @SuppressWarnings("UnusedDeclaration") Class[] 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 00000000000..0da7297aa0b --- /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 { + 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 00000000000..b9ceec3a1ef --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/PortRange.java @@ -0,0 +1,34 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE_USE; +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. + * + */ +@Target({ METHOD, FIELD, ANNOTATION_TYPE, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = PortRangeValidator.class) +@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[] 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 00000000000..257b9d94641 --- /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 { + 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 00000000000..421509c2637 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/SizeRange.java @@ -0,0 +1,60 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.CONSTRUCTOR; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +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. + */ +@Documented +@Constraint(validatedBy = { }) +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@MinSize(0) +@MaxSize(value = Long.MAX_VALUE, unit = SizeUnit.TERABYTES) +@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[] payload() default { }; + + /** + * Defines several {@code @SizeRange} annotations on the same element. + */ + @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) + @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 00000000000..9e59f2b144d --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/Validated.java @@ -0,0 +1,23 @@ +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.METHOD; +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. + */ +@Target({PARAMETER, METHOD}) +@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 00000000000..4c7d07fe2bd --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/ValidationMethod.java @@ -0,0 +1,28 @@ +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.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +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. + */ +@Target({TYPE, ANNOTATION_TYPE, METHOD}) +@Retention(RUNTIME) +@Constraint(validatedBy = MethodValidator.class) +@Documented +public @interface ValidationMethod { + String message() default "is not valid"; + + Class[] groups() default {}; + + @SuppressWarnings("UnusedDeclaration") Class[] payload() default { }; +} diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/GuavaOptionalValidatedValueUnwrapper.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/GuavaOptionalValidatedValueUnwrapper.java new file mode 100644 index 00000000000..6438b51df4b --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/GuavaOptionalValidatedValueUnwrapper.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}. + * + * Extracts the value contained by the {@link Optional} for validation, or produces {@code null}. + */ +public class GuavaOptionalValidatedValueUnwrapper extends ValidatedValueUnwrapper> { + + private final TypeResolver resolver = new TypeResolver(); + + @Override + public Object handleValidatedValue(final Optional optional) { + return optional.orNull(); + } + + @Override + public Type getValidatedValueType(final Type type) { + final ResolvedType resolvedType = resolver.resolve(type); + return resolvedType.typeParametersFor(Optional.class).get(0).getErasedType(); + } +} diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalDoubleValidatedValueUnwrapper.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalDoubleValidatedValueUnwrapper.java new file mode 100644 index 00000000000..2239fcccdc5 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalDoubleValidatedValueUnwrapper.java @@ -0,0 +1,23 @@ +package io.dropwizard.validation.valuehandling; + +import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper; + +import java.lang.reflect.Type; +import java.util.OptionalDouble; + +/** + * A {@link ValidatedValueUnwrapper} for {@link OptionalDouble}. + * + * Extracts the value contained by the {@link OptionalDouble} for validation, or produces {@code null}. + */ +public class OptionalDoubleValidatedValueUnwrapper extends ValidatedValueUnwrapper { + @Override + public Object handleValidatedValue(final OptionalDouble optional) { + return optional.isPresent() ? optional.getAsDouble() : null; + } + + @Override + public Type getValidatedValueType(final Type type) { + return Double.class; + } +} diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalIntValidatedValueUnwrapper.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalIntValidatedValueUnwrapper.java new file mode 100644 index 00000000000..7c152543188 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalIntValidatedValueUnwrapper.java @@ -0,0 +1,23 @@ +package io.dropwizard.validation.valuehandling; + +import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper; + +import java.lang.reflect.Type; +import java.util.OptionalInt; + +/** + * A {@link ValidatedValueUnwrapper} for {@link OptionalInt}. + * + * Extracts the value contained by the {@link OptionalInt} for validation, or produces {@code null}. + */ +public class OptionalIntValidatedValueUnwrapper extends ValidatedValueUnwrapper { + @Override + public Object handleValidatedValue(final OptionalInt optional) { + return optional.isPresent() ? optional.getAsInt() : null; + } + + @Override + public Type getValidatedValueType(final Type type) { + return Integer.class; + } +} diff --git a/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalLongValidatedValueUnwrapper.java b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalLongValidatedValueUnwrapper.java new file mode 100644 index 00000000000..8f215cfbb71 --- /dev/null +++ b/dropwizard-validation/src/main/java/io/dropwizard/validation/valuehandling/OptionalLongValidatedValueUnwrapper.java @@ -0,0 +1,23 @@ +package io.dropwizard.validation.valuehandling; + +import org.hibernate.validator.spi.valuehandling.ValidatedValueUnwrapper; + +import java.lang.reflect.Type; +import java.util.OptionalLong; + +/** + * A {@link ValidatedValueUnwrapper} for {@link OptionalLong}. + * + * Extracts the value contained by the {@link OptionalLong} for validation, or produces {@code null}. + */ +public class OptionalLongValidatedValueUnwrapper extends ValidatedValueUnwrapper { + @Override + public Object handleValidatedValue(final OptionalLong optional) { + return optional.isPresent() ? optional.getAsLong() : null; + } + + @Override + public Type getValidatedValueType(final Type type) { + return Long.class; + } +} diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/ConstraintPerson.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/ConstraintPerson.java new file mode 100644 index 00000000000..204cf59c772 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/ConstraintPerson.java @@ -0,0 +1,19 @@ +package io.dropwizard.validation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import org.hibernate.validator.constraints.NotEmpty; + +public class ConstraintPerson { + @NotEmpty + private String name; + + @JsonProperty + public String getName() { + return name; + } + + @JsonProperty + public void setName(String name) { + this.name = name; + } +} 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 00000000000..629a14203a3 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/DurationValidatorTest.java @@ -0,0 +1,91 @@ +package io.dropwizard.validation; + +import com.google.common.collect.ImmutableList; +import io.dropwizard.util.Duration; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.Validator; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.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); + + @Valid + private List<@MaxDuration(value = 30, unit = TimeUnit.SECONDS) Duration> maxDurs = + ImmutableList.of(Duration.minutes(10)); + + @Valid + private List<@MinDuration(value = 30, unit = TimeUnit.SECONDS) Duration> minDurs = + ImmutableList.of(Duration.milliseconds(100)); + + @Valid + private List<@DurationRange(min = 10, max = 30, unit = TimeUnit.MINUTES) Duration> rangeDurs = + ImmutableList.of(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; + } + public void setMaxDurs(List maxDurs) { + this.maxDurs = maxDurs; + } + public void setMinDurs(List minDurs) { + this.minDurs = minDurs; + } + public void setRangeDurs(List rangeDurs) { + this.rangeDurs = rangeDurs; + } + } + + private final Validator validator = BaseValidator.newValidator(); + + @Test + public void returnsASetOfErrorsForAnObject() throws Exception { + if ("en".equals(Locale.getDefault().getLanguage())) { + final ImmutableList errors = + ConstraintViolations.format(validator.validate(new Example())); + + assertThat(errors) + .containsOnly( + "outOfRange must be between 10 MINUTES and 30 MINUTES", + "tooBig must be less than or equal to 30 SECONDS", + "tooSmall must be greater than or equal to 30 SECONDS", + "maxDurs[0] must be less than or equal to 30 SECONDS", + "minDurs[0] must be greater than or equal to 30 SECONDS", + "rangeDurs[0] must be between 10 MINUTES and 30 MINUTES"); + } + } + + @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)); + example.setMaxDurs(ImmutableList.of(Duration.seconds(10))); + example.setMinDurs(ImmutableList.of(Duration.seconds(100))); + example.setRangeDurs(ImmutableList.of(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 00000000000..a374887e36b --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/MethodValidatorTest.java @@ -0,0 +1,46 @@ +package io.dropwizard.validation; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.Validator; + +import static org.assertj.core.api.Assertions.assertThat; + +@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 = BaseValidator.newValidator(); + + @Test + public void complainsAboutMethodsWhichReturnFalse() throws Exception { + final ImmutableList errors = + ConstraintViolations.format(validator.validate(new Example())); + + assertThat(errors) + .containsOnly("must have a false thing", + "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 00000000000..e921e4a2d99 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/OneOfValidatorTest.java @@ -0,0 +1,76 @@ +package io.dropwizard.validation; + +import com.google.common.collect.ImmutableList; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.Validator; +import java.util.List; +import java.util.Locale; + +import static io.dropwizard.validation.ConstraintViolations.format; +import static org.assertj.core.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"; + + @Valid + private List<@OneOf({"one", "two", "three"}) String> basicList = ImmutableList.of("one"); + } + + private final Validator validator = BaseValidator.newValidator(); + + @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]"); + } + + @Test + public void doesNotAllowBadElementsInList() { + final Example example = new Example(); + example.basicList = ImmutableList.of("four"); + + assertThat(format(validator.validate(example))) + .containsOnly("basicList[0] must be one of [one, two, three]"); + } + + @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 00000000000..a6c2f53b3f6 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/PortRangeValidatorTest.java @@ -0,0 +1,84 @@ +package io.dropwizard.validation; + +import com.google.common.collect.ImmutableList; +import org.junit.Before; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.Validator; +import java.util.List; +import java.util.Locale; + +import static org.assertj.core.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; + + @Valid + List<@PortRange Integer> ports = ImmutableList.of(); + } + + + private final Validator validator = BaseValidator.newValidator(); + 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"); + } + + @Test + public void allowsForCustomMinimumPorts() throws Exception { + example.otherPort = 8080; + + assertThat(ConstraintViolations.format(validator.validate(example))) + .containsOnly("otherPort must be between 10000 and 15000"); + } + + @Test + public void allowsForCustomMaximumPorts() throws Exception { + example.otherPort = 16000; + + assertThat(ConstraintViolations.format(validator.validate(example))) + .containsOnly("otherPort must be between 10000 and 15000"); + } + + @Test + public void rejectsInvalidPortsInList() { + example.ports = ImmutableList.of(-1); + assertThat(ConstraintViolations.format(validator.validate(example))) + .containsOnly("ports[0] must be between 1 and 65535"); + } +} 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 00000000000..975e4226c04 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/SizeValidatorTest.java @@ -0,0 +1,87 @@ +package io.dropwizard.validation; + +import com.google.common.collect.ImmutableList; +import io.dropwizard.util.Size; +import io.dropwizard.util.SizeUnit; +import org.junit.Test; + +import javax.validation.Valid; +import javax.validation.Validator; +import java.util.List; +import java.util.Locale; + +import static org.assertj.core.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); + + @Valid + private List<@MaxSize(value = 30, unit = SizeUnit.KILOBYTES) Size> maxSize = + ImmutableList.of(Size.gigabytes(2)); + + @Valid + private List<@MinSize(value = 30, unit = SizeUnit.KILOBYTES) Size> minSize = + ImmutableList.of(Size.bytes(100)); + + @Valid + private List<@SizeRange(min = 10, max = 100, unit = SizeUnit.KILOBYTES) Size> rangeSize = + ImmutableList.of(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; + } + public void setMaxSize(List maxSize) { + this.maxSize = maxSize; + } + public void setMinSize(List minSize) { + this.minSize = minSize; + } + public void setRangeSize(List rangeSize) { + this.rangeSize = rangeSize; + } + } + + private final Validator validator = BaseValidator.newValidator(); + + @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", + "tooBig must be less than or equal to 30 KILOBYTES", + "tooSmall must be greater than or equal to 30 KILOBYTES", + "maxSize[0] must be less than or equal to 30 KILOBYTES", + "minSize[0] must be greater than or equal to 30 KILOBYTES", + "rangeSize[0] must be between 10 KILOBYTES and 100 KILOBYTES"); + } + } + + @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)); + example.setMaxSize(ImmutableList.of(Size.bytes(10))); + example.setMinSize(ImmutableList.of(Size.megabytes(10))); + example.setRangeSize(ImmutableList.of(Size.kilobytes(64))); + + assertThat(validator.validate(example)) + .isEmpty(); + } +} diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/GuavaOptionalValidatedValueUnwrapperTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/GuavaOptionalValidatedValueUnwrapperTest.java new file mode 100644 index 00000000000..89e212fc17c --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/GuavaOptionalValidatedValueUnwrapperTest.java @@ -0,0 +1,66 @@ +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.assertj.core.api.Assertions.assertThat; + +public class GuavaOptionalValidatedValueUnwrapperTest { + + public static class Example { + + @Min(3) + public Optional three = Optional.absent(); + + @NotNull + @UnwrapValidatedValue + public Optional notNull = Optional.of(123); + } + + private final Validator validator = Validation + .byProvider(HibernateValidator.class) + .configure() + .addValidatedValueHandler(new GuavaOptionalValidatedValueUnwrapper()) + .buildValidatorFactory() + .getValidator(); + + @Test + public void succeedsWhenAbsent() { + Example example = new Example(); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void failsWhenFailingConstraint() { + Example example = new Example(); + example.three = Optional.of(2); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } + + @Test + public void succeedsWhenConstraintsMet() { + Example example = new Example(); + example.three = Optional.of(10); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void notNullFailsWhenAbsent() { + Example example = new Example(); + example.notNull = Optional.absent(); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } +} diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalDoubleValidatedValueUnwrapperTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalDoubleValidatedValueUnwrapperTest.java new file mode 100644 index 00000000000..eefcfb1a13b --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalDoubleValidatedValueUnwrapperTest.java @@ -0,0 +1,66 @@ +package io.dropwizard.validation.valuehandling; + +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.OptionalDouble; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalDoubleValidatedValueUnwrapperTest { + + public static class Example { + @Min(3) + @UnwrapValidatedValue + public OptionalDouble three = OptionalDouble.empty(); + + @NotNull + @UnwrapValidatedValue + public OptionalDouble notNull = OptionalDouble.of(123.456D); + } + + private final Validator validator = Validation + .byProvider(HibernateValidator.class) + .configure() + .addValidatedValueHandler(new OptionalDoubleValidatedValueUnwrapper()) + .buildValidatorFactory() + .getValidator(); + + @Test + public void succeedsWhenAbsent() { + Example example = new Example(); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void failsWhenFailingConstraint() { + Example example = new Example(); + example.three = OptionalDouble.of(2); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } + + @Test + public void succeedsWhenConstraintsMet() { + Example example = new Example(); + example.three = OptionalDouble.of(10); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void notNullFailsWhenAbsent() { + Example example = new Example(); + example.notNull = OptionalDouble.empty(); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } +} diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalIntValidatedValueUnwrapperTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalIntValidatedValueUnwrapperTest.java new file mode 100644 index 00000000000..a7dd005b3b4 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalIntValidatedValueUnwrapperTest.java @@ -0,0 +1,66 @@ +package io.dropwizard.validation.valuehandling; + +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.OptionalInt; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalIntValidatedValueUnwrapperTest { + + public static class Example { + @Min(3) + @UnwrapValidatedValue + public OptionalInt three = OptionalInt.empty(); + + @NotNull + @UnwrapValidatedValue + public OptionalInt notNull = OptionalInt.of(123); + } + + private final Validator validator = Validation + .byProvider(HibernateValidator.class) + .configure() + .addValidatedValueHandler(new OptionalIntValidatedValueUnwrapper()) + .buildValidatorFactory() + .getValidator(); + + @Test + public void succeedsWhenAbsent() { + Example example = new Example(); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void failsWhenFailingConstraint() { + Example example = new Example(); + example.three = OptionalInt.of(2); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } + + @Test + public void succeedsWhenConstraintsMet() { + Example example = new Example(); + example.three = OptionalInt.of(10); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void notNullFailsWhenAbsent() { + Example example = new Example(); + example.notNull = OptionalInt.empty(); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } +} diff --git a/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalLongValidatedValueUnwrapperTest.java b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalLongValidatedValueUnwrapperTest.java new file mode 100644 index 00000000000..c538a6369a0 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalLongValidatedValueUnwrapperTest.java @@ -0,0 +1,66 @@ +package io.dropwizard.validation.valuehandling; + +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.OptionalLong; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptionalLongValidatedValueUnwrapperTest { + + public static class Example { + @Min(3) + @UnwrapValidatedValue + public OptionalLong three = OptionalLong.empty(); + + @NotNull + @UnwrapValidatedValue + public OptionalLong notNull = OptionalLong.of(123456789L); + } + + private final Validator validator = Validation + .byProvider(HibernateValidator.class) + .configure() + .addValidatedValueHandler(new OptionalLongValidatedValueUnwrapper()) + .buildValidatorFactory() + .getValidator(); + + @Test + public void succeedsWhenAbsent() { + Example example = new Example(); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void failsWhenFailingConstraint() { + Example example = new Example(); + example.three = OptionalLong.of(2); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } + + @Test + public void succeedsWhenConstraintsMet() { + Example example = new Example(); + example.three = OptionalLong.of(10); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void notNullFailsWhenAbsent() { + Example example = new Example(); + example.notNull = OptionalLong.empty(); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } +} 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 00000000000..8abb3951cf6 --- /dev/null +++ b/dropwizard-validation/src/test/java/io/dropwizard/validation/valuehandling/OptionalValidatedValueUnwrapperTest.java @@ -0,0 +1,69 @@ +package io.dropwizard.validation.valuehandling; + +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.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +// Dropwizard used to supply its own Java 8 optional validator but since +// Hibernate Validator 5.2, it's built in, so the class was removed but +// the test class stays to ensure behavior remains +public class OptionalValidatedValueUnwrapperTest { + + public static class Example { + + @Min(3) + @UnwrapValidatedValue + public Optional three = Optional.empty(); + + @NotNull + @UnwrapValidatedValue + public Optional notNull = Optional.of(123); + } + + private final Validator validator = Validation + .byProvider(HibernateValidator.class) + .configure() + .buildValidatorFactory() + .getValidator(); + + @Test + public void succeedsWhenAbsent() { + Example example = new Example(); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void failsWhenFailingConstraint() { + Example example = new Example(); + example.three = Optional.of(2); + Set> violations = validator.validate(example); + assertThat(violations).hasSize(1); + } + + @Test + public void succeedsWhenConstraintsMet() { + Example example = new Example(); + example.three = Optional.of(10); + Set> violations = validator.validate(example); + assertThat(violations).isEmpty(); + } + + @Test + public void notNullFailsWhenAbsent() { + Example example = new Example(); + example.notNull = Optional.empty(); + Set> 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 00000000000..f12a06b22ff --- /dev/null +++ b/dropwizard-views-freemarker/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-views-freemarker + Dropwizard Freemarker Views + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-views + + + org.freemarker + freemarker + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + test + + + 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 00000000000..356db2d6318 --- /dev/null +++ b/dropwizard-views-freemarker/src/main/java/io/dropwizard/views/freemarker/FreemarkerViewRenderer.java @@ -0,0 +1,88 @@ +package io.dropwizard.views.freemarker; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import com.google.common.collect.ImmutableMap; +import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; +import freemarker.template.Template; +import freemarker.template.TemplateException; +import freemarker.template.Version; +import io.dropwizard.views.View; +import io.dropwizard.views.ViewRenderer; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; + +/** + * A {@link ViewRenderer} which renders Freemarker ({@code .ftl}) templates. + */ +public class FreemarkerViewRenderer implements ViewRenderer { + + private static final Version FREEMARKER_VERSION = Configuration.getVersion(); + private final TemplateLoader loader; + + private static class TemplateLoader extends CacheLoader, Configuration> { + private Map baseConfig = ImmutableMap.of(); + @Override + public Configuration load(Class key) throws Exception { + final Configuration configuration = new Configuration(FREEMARKER_VERSION); + configuration.setObjectWrapper(new DefaultObjectWrapperBuilder(FREEMARKER_VERSION).build()); + configuration.loadBuiltInEncodingMap(); + configuration.setDefaultEncoding(StandardCharsets.UTF_8.name()); + configuration.setClassForTemplateLoading(key, "/"); + for (Map.Entry entry : baseConfig.entrySet()) { + configuration.setSetting(entry.getKey(), entry.getValue()); + } + return configuration; + } + + void setBaseConfig(Map baseConfig) { + this.baseConfig = baseConfig; + } + } + + private final LoadingCache, Configuration> configurationCache; + + public FreemarkerViewRenderer() { + this.loader = new TemplateLoader(); + this.configurationCache = CacheBuilder.newBuilder() + .concurrencyLevel(128) + .build(loader); + } + + @Override + public boolean isRenderable(View view) { + return view.getTemplateName().endsWith(getSuffix()); + } + + @Override + public void render(View view, + Locale locale, + OutputStream output) throws IOException { + try { + final Configuration configuration = configurationCache.getUnchecked(view.getClass()); + final Charset charset = view.getCharset().orElseGet(() -> 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 RuntimeException(e); + } + } + + @Override + public void configure(Map baseConfig) { + this.loader.setBaseConfig(baseConfig); + } + + @Override + public String getSuffix() { + return ".ftl"; + } +} 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 00000000000..7db7e8df558 --- /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 00000000000..288b4c267fa --- /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 00000000000..20a23c9fae1 --- /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/ErrorView.java b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/ErrorView.java new file mode 100644 index 00000000000..9152db5018e --- /dev/null +++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/ErrorView.java @@ -0,0 +1,9 @@ +package io.dropwizard.views.freemarker; + +import io.dropwizard.views.View; + +public class ErrorView extends View { + protected ErrorView() { + super("/example-error.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 00000000000..25a962974c6 --- /dev/null +++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/FreemarkerViewRendererTest.java @@ -0,0 +1,107 @@ +package io.dropwizard.views.freemarker; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.views.ViewMessageBodyWriter; +import io.dropwizard.views.ViewRenderer; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class FreemarkerViewRendererTest extends JerseyTest { + static { + BootstrapLogging.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(); + } + + @GET + @Path("/error") + public ErrorView showError() { + return new ErrorView(); + } + } + + @Override + protected Application configure() { + ResourceConfig config = new ResourceConfig(); + final ViewRenderer renderer = new FreemarkerViewRenderer(); + config.register(new ViewMessageBodyWriter(new MetricRegistry(), ImmutableList.of(renderer))); + config.register(new ExampleResource()); + return config; + } + + @Test + public void rendersViewsWithAbsoluteTemplatePaths() throws Exception { + final String response = target("/test/absolute") + .request().get(String.class); + assertThat(response).isEqualTo("Woop woop. yay\n"); + } + + @Test + public void rendersViewsWithRelativeTemplatePaths() throws Exception { + final String response = target("/test/relative") + .request().get(String.class); + assertThat(response).isEqualTo("Ok.\n"); + } + + @Test + public void returnsA500ForViewsWithBadTemplatePaths() throws Exception { + try { + target("/test/bad") + .request().get(String.class); + + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(500); + + assertThat(e.getResponse().readEntity(String.class)) + .isEqualTo(ViewMessageBodyWriter.TEMPLATE_ERROR_MSG); + } + } + + @Test + public void returnsA500ForViewsThatCantCompile() throws Exception { + try { + target("/test/error").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(500); + + assertThat(e.getResponse().readEntity(String.class)) + .isEqualTo(ViewMessageBodyWriter.TEMPLATE_ERROR_MSG); + } + } +} diff --git a/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/MultipleContentTypeTest.java b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/MultipleContentTypeTest.java new file mode 100644 index 00000000000..70f692b67c3 --- /dev/null +++ b/dropwizard-views-freemarker/src/test/java/io/dropwizard/views/freemarker/MultipleContentTypeTest.java @@ -0,0 +1,150 @@ +package io.dropwizard.views.freemarker; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import io.dropwizard.jackson.Jackson; +import io.dropwizard.jersey.DropwizardResourceConfig; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.views.View; +import io.dropwizard.views.ViewMessageBodyWriter; +import io.dropwizard.views.ViewRenderer; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MultipleContentTypeTest extends JerseyTest { + static { + BootstrapLogging.bootstrap(); + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + final ViewRenderer renderer = new FreemarkerViewRenderer(); + return DropwizardResourceConfig.forTesting(new MetricRegistry()) + .register(new ViewMessageBodyWriter(new MetricRegistry(), ImmutableList.of(renderer))) + .register(new InfoMessageBodyWriter()) + .register(new ExampleResource()); + } + + @Test + public void testJsonContentType() { + final Response response = target("/").request().accept(MediaType.APPLICATION_JSON_TYPE).get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(String.class)).isEqualTo("{\"title\":\"Title#TEST\",\"content\":\"Content#TEST\"}"); + } + + @Test + public void testHtmlContentType() { + final Response response = target("/").request().accept(MediaType.TEXT_HTML_TYPE).get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(String.class)) + .contains("Breaking news") + .contains("

    Title#TEST

    ") + .contains("

    Content#TEST

    "); + } + + @Test + public void testOnlyJsonContentType() { + final Response response = target("/json").request().accept(MediaType.APPLICATION_JSON_TYPE).get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(String.class)).isEqualTo("{\"title\":\"Title#TEST\",\"content\":\"Content#TEST\"}"); + } + + @Test + public void testOnlyHtmlContentType() { + final Response response = target("/html").request().accept(MediaType.TEXT_HTML_TYPE).get(); + + assertThat(response.getStatus()).isEqualTo(200); + assertThat(response.readEntity(String.class)) + .contains("Breaking news") + .contains("

    Title#TEST

    ") + .contains("

    Content#TEST

    "); + } + + @Path("/") + public static class ExampleResource { + @GET + @Produces({MediaType.APPLICATION_JSON, MediaType.TEXT_HTML}) + public Response getInfoCombined() { + final Info info = new Info("Title#TEST", "Content#TEST"); + return Response.ok(info).build(); + } + + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("json") + public Response getInfoJson() { + final Info info = new Info("Title#TEST", "Content#TEST"); + return Response.ok(info).build(); + } + + @GET + @Produces(MediaType.TEXT_HTML) + @Path("html") + public Response getInfoHtml() { + final Info info = new Info("Title#TEST", "Content#TEST"); + return Response.ok(info).build(); + } + } + + public static class Info extends View { + private final String title; + private final String content; + + public Info(String title, String content) { + super("/issue627.ftl"); + this.title = title; + this.content = content; + } + + public String getTitle() { + return title; + } + + public String getContent() { + return content; + } + } + + @Provider + @Produces(MediaType.APPLICATION_JSON) + public class InfoMessageBodyWriter implements MessageBodyWriter { + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Info.class.isAssignableFrom(type); + } + + @Override + public long getSize(Info info, Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return -1; + } + + @Override + public void writeTo(Info info, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + Jackson.newObjectMapper().writeValue(entityStream, info); + } + } +} 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 00000000000..709fd4e7dbb --- /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-error.ftl b/dropwizard-views-freemarker/src/test/resources/example-error.ftl new file mode 100644 index 00000000000..fa1a84a9f82 --- /dev/null +++ b/dropwizard-views-freemarker/src/test/resources/example-error.ftl @@ -0,0 +1,5 @@ +<#macro script> + + +<@script j=""/> + 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 00000000000..d8d6a80fd59 --- /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 00000000000..f205466db6c --- /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/issue627.ftl b/dropwizard-views-freemarker/src/test/resources/issue627.ftl new file mode 100644 index 00000000000..6a313dcf643 --- /dev/null +++ b/dropwizard-views-freemarker/src/test/resources/issue627.ftl @@ -0,0 +1,10 @@ + + + +Breaking news + + +

    ${title}

    +

    ${content}

    + + \ No newline at end of file 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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-views-freemarker/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-views-mustache/pom.xml b/dropwizard-views-mustache/pom.xml new file mode 100644 index 00000000000..292c7df1fc7 --- /dev/null +++ b/dropwizard-views-mustache/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-views-mustache + Dropwizard Mustache Views + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-views + + + com.github.spullara.mustache.java + compiler + + + org.glassfish.jersey.test-framework.providers + jersey-test-framework-provider-inmemory + test + + + 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 00000000000..d570396785b --- /dev/null +++ b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/MustacheViewRenderer.java @@ -0,0 +1,64 @@ +package io.dropwizard.views.mustache; + +import com.github.mustachejava.DefaultMustacheFactory; +import com.github.mustachejava.Mustache; +import com.github.mustachejava.MustacheFactory; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; +import io.dropwizard.views.View; +import io.dropwizard.views.ViewRenderer; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.Map; + +/** + * A {@link ViewRenderer} which renders Mustache ({@code .mustache}) templates. + */ +public class MustacheViewRenderer implements ViewRenderer { + private final LoadingCache, MustacheFactory> factories; + + public MustacheViewRenderer() { + this.factories = CacheBuilder.newBuilder() + .build(new CacheLoader, MustacheFactory>() { + @Override + public MustacheFactory load(Class key) throws Exception { + return new DefaultMustacheFactory(new PerClassMustacheResolver(key)); + } + }); + } + + @Override + public boolean isRenderable(View view) { + return view.getTemplateName().endsWith(getSuffix()); + } + + @Override + public void render(View view, Locale locale, OutputStream output) throws IOException { + try { + final Mustache template = factories.get(view.getClass()) + .compile(view.getTemplateName()); + final Charset charset = view.getCharset().orElse(StandardCharsets.UTF_8); + try (OutputStreamWriter writer = new OutputStreamWriter(output, charset)) { + template.execute(writer, view); + } + } catch (Throwable e) { + throw new RuntimeException("Mustache template error: " + view.getTemplateName(), e); + } + } + + @Override + public void configure(Map options) { + + } + + @Override + public String getSuffix() { + return ".mustache"; + } +} diff --git a/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/PerClassMustacheResolver.java b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/PerClassMustacheResolver.java new file mode 100644 index 00000000000..be536e06b0b --- /dev/null +++ b/dropwizard-views-mustache/src/main/java/io/dropwizard/views/mustache/PerClassMustacheResolver.java @@ -0,0 +1,31 @@ +package io.dropwizard.views.mustache; + +import com.github.mustachejava.MustacheResolver; +import io.dropwizard.views.View; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; + +/** + * {@link MustacheResolver} implementation that resolves mustache + * files from the classpath relatively from a provided class. + */ +class PerClassMustacheResolver implements MustacheResolver { + private final Class klass; + + PerClassMustacheResolver(Class klass) { + this.klass = klass; + } + + @Override + public Reader getReader(String resourceName) { + final InputStream is = klass.getResourceAsStream(resourceName); + if (is == null) { + return null; + } + return new BufferedReader(new InputStreamReader(is, StandardCharsets.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 00000000000..cdcd5830cbb --- /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 00000000000..7855345c64b --- /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 00000000000..07492418b09 --- /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/ErrorView.java b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/ErrorView.java new file mode 100644 index 00000000000..aea91ee78d6 --- /dev/null +++ b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/ErrorView.java @@ -0,0 +1,9 @@ +package io.dropwizard.views.mustache; + +import io.dropwizard.views.View; + +public class ErrorView extends View { + protected ErrorView() { + super("/example-error.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 00000000000..9e081686662 --- /dev/null +++ b/dropwizard-views-mustache/src/test/java/io/dropwizard/views/mustache/MustacheViewRendererTest.java @@ -0,0 +1,105 @@ +package io.dropwizard.views.mustache; + +import com.codahale.metrics.MetricRegistry; +import com.google.common.collect.ImmutableList; +import io.dropwizard.logging.BootstrapLogging; +import io.dropwizard.views.ViewMessageBodyWriter; +import io.dropwizard.views.ViewRenderer; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.Test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.failBecauseExceptionWasNotThrown; + +public class MustacheViewRendererTest extends JerseyTest { + static { + BootstrapLogging.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(); + } + + @GET + @Path("/error") + public ErrorView showError() { + return new ErrorView(); + } + } + + @Override + protected Application configure() { + forceSet(TestProperties.CONTAINER_PORT, "0"); + ResourceConfig config = new ResourceConfig(); + final ViewRenderer renderer = new MustacheViewRenderer(); + config.register(new ViewMessageBodyWriter(new MetricRegistry(), ImmutableList.of(renderer))); + config.register(new ExampleResource()); + return config; + } + + @Test + public void rendersViewsWithAbsoluteTemplatePaths() throws Exception { + final String response = target("/test/absolute").request().get(String.class); + assertThat(response).isEqualTo("Woop woop. yay\n"); + } + + @Test + public void rendersViewsWithRelativeTemplatePaths() throws Exception { + final String response = target("/test/relative").request().get(String.class); + assertThat(response).isEqualTo("Ok.\n"); + } + + @Test + public void returnsA500ForViewsWithBadTemplatePaths() throws Exception { + try { + target("/test/bad").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(500); + + assertThat(e.getResponse().readEntity(String.class)) + .isEqualTo(ViewMessageBodyWriter.TEMPLATE_ERROR_MSG); + } + } + + @Test + public void returnsA500ForViewsThatCantCompile() throws Exception { + try { + target("/test/error").request().get(String.class); + failBecauseExceptionWasNotThrown(WebApplicationException.class); + } catch (WebApplicationException e) { + assertThat(e.getResponse().getStatus()) + .isEqualTo(500); + + assertThat(e.getResponse().readEntity(String.class)) + .isEqualTo(ViewMessageBodyWriter.TEMPLATE_ERROR_MSG); + } + } +} 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 00000000000..4b09997ff9c --- /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-error.mustache b/dropwizard-views-mustache/src/test/resources/example-error.mustache new file mode 100644 index 00000000000..797e47ef1f6 --- /dev/null +++ b/dropwizard-views-mustache/src/test/resources/example-error.mustache @@ -0,0 +1 @@ +Woop woop. {{/}} 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 00000000000..cb60981e5b3 --- /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 00000000000..587579af915 --- /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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-views-mustache/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard-views/pom.xml b/dropwizard-views/pom.xml new file mode 100644 index 00000000000..b630fc6bc87 --- /dev/null +++ b/dropwizard-views/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + + io.dropwizard + dropwizard-parent + 1.0.1-SNAPSHOT + + + dropwizard-views + Dropwizard Views + + + + + io.dropwizard + dropwizard-bom + ${project.version} + pom + import + + + + + + + io.dropwizard + dropwizard-core + + + 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 00000000000..84c032ff72c --- /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 java.nio.charset.Charset; +import java.util.Optional; + +/** + * 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 getCharset() { + return Optional.ofNullable(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 00000000000..245e6dbe14f --- /dev/null +++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewBundle.java @@ -0,0 +1,122 @@ +package io.dropwizard.views; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import io.dropwizard.Bundle; +import io.dropwizard.Configuration; +import io.dropwizard.ConfiguredBundle; +import io.dropwizard.setup.Bootstrap; +import io.dropwizard.setup.Environment; + +import java.util.Collections; +import java.util.Map; +import java.util.ServiceLoader; + +import static com.google.common.base.MoreObjects.firstNonNull; + +/** + * A {@link Bundle}, which by default, enables the rendering of FreeMarker & Mustache views by your application. + * + *

    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}:

    + * + *
    
    + * new ViewBundle(ImmutableList.of(myViewRenderer))
    + * 
    + * + *

    A view combines a Freemarker or Mustache template with a set of Java objects:

    + * + *
    
    + * 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;
    + *     }
    + * }
    + * 
    + * + *

    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. + * + *

    A resource method with a view would looks something like this:

    + * + *
    
    + * \@GET
    + * public PersonView getPerson(\@PathParam("id") String id) {
    + *     return new PersonView(dao.find(id));
    + * }
    + * 
    + * + *

    Freemarker templates look something like this:

    + * + *
    {@code
    + * <#-- @ftlvariable name="" type="com.example.application.PersonView" -->
    + * 
    + *     
    + *         

    Hello, ${person.name?html}!

    + * + * + * }
    + * + *

    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 type-safety in your templates.

    + * + * @see FreeMarker Manual + * + *

    Mustache templates look something like this:

    + * + *
    {@code
    + * 
    + *     
    + *         

    Hello, {{person.name}}!

    + * + * + * }
    + * + *

    In this template, {@code {{person.name}}} calls {@code getPerson().getName()}.

    + * + * @see Mustache Manual + */ +public class ViewBundle implements ConfiguredBundle, ViewConfigurable { + private final Iterable viewRenderers; + + public ViewBundle() { + this(ServiceLoader.load(ViewRenderer.class)); + } + + public ViewBundle(Iterable viewRenderers) { + this.viewRenderers = ImmutableSet.copyOf(viewRenderers); + } + + @Override + public Map> getViewConfiguration(T configuration) { + return ImmutableMap.of(); + } + + @Override + public void run(T configuration, Environment environment) throws Exception { + final Map> options = getViewConfiguration(configuration); + for (ViewRenderer viewRenderer : viewRenderers) { + final Map viewOptions = options.get(viewRenderer.getSuffix()); + viewRenderer.configure(firstNonNull(viewOptions, Collections.emptyMap())); + } + environment.jersey().register(new ViewMessageBodyWriter(environment.metrics(), viewRenderers)); + } + + @Override + public void initialize(Bootstrap bootstrap) { + // nothing doing + } +} diff --git a/dropwizard-views/src/main/java/io/dropwizard/views/ViewConfigurable.java b/dropwizard-views/src/main/java/io/dropwizard/views/ViewConfigurable.java new file mode 100644 index 00000000000..4b24325634c --- /dev/null +++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewConfigurable.java @@ -0,0 +1,9 @@ +package io.dropwizard.views; + +import io.dropwizard.Configuration; + +import java.util.Map; + +public interface ViewConfigurable { + Map> getViewConfiguration(T configuration); +} 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 00000000000..906fcac3d8a --- /dev/null +++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewMessageBodyWriter.java @@ -0,0 +1,104 @@ +package io.dropwizard.views; + +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.Timer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Provider; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.List; +import java.util.Locale; +import java.util.ServiceLoader; + +import static com.codahale.metrics.MetricRegistry.name; + +@Provider +@Produces({ MediaType.TEXT_HTML, MediaType.APPLICATION_XHTML_XML }) +public class ViewMessageBodyWriter implements MessageBodyWriter { + private static final Logger LOGGER = LoggerFactory.getLogger(MessageBodyWriter.class); + public static final String TEMPLATE_ERROR_MSG = + "" + + "Template Error" + + "

    Template Error

    Something went wrong rendering the page

    " + + ""; + + @Context + private HttpHeaders headers; + + private final Iterable renderers; + private final MetricRegistry metricRegistry; + + @Deprecated + public ViewMessageBodyWriter(MetricRegistry metricRegistry) { + this(metricRegistry, ServiceLoader.load(ViewRenderer.class)); + } + + public ViewMessageBodyWriter(MetricRegistry metricRegistry, Iterable 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 httpHeaders, + OutputStream entityStream) throws IOException { + 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 (Exception e) { + LOGGER.error("Template Error", e); + throw new WebApplicationException(Response.serverError() + .type(MediaType.TEXT_HTML_TYPE) + .entity(TEMPLATE_ERROR_MSG) + .build()); + } finally { + context.stop(); + } + } + + private Locale detectLocale(HttpHeaders headers) { + final List 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 00000000000..7093cc06004 --- /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 00000000000..90ecbcf6081 --- /dev/null +++ b/dropwizard-views/src/main/java/io/dropwizard/views/ViewRenderer.java @@ -0,0 +1,45 @@ +package io.dropwizard.views; + +import javax.ws.rs.WebApplicationException; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Locale; +import java.util.Map; + +/** + * 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; + + /** + * options for configuring the view renderer + * @param options + */ + void configure(Map options); + + /** + * @return the suffix of the template type, e.g '.ftl', '.mustache' + */ + String getSuffix(); +} 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 00000000000..efc3b0d1dfa --- /dev/null +++ b/dropwizard-views/src/test/java/io/dropwizard/views/ViewBundleTest.java @@ -0,0 +1,120 @@ +package io.dropwizard.views; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import io.dropwizard.Configuration; +import io.dropwizard.jersey.setup.JerseyEnvironment; +import io.dropwizard.setup.Environment; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +import javax.validation.constraints.NotNull; +import javax.ws.rs.WebApplicationException; +import java.io.IOException; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class ViewBundleTest { + private final JerseyEnvironment jerseyEnvironment = mock(JerseyEnvironment.class); + private final Environment environment = mock(Environment.class); + private static class MyConfiguration extends Configuration { + @NotNull + private Map> viewRendererConfiguration = Collections.emptyMap(); + + @JsonProperty("viewRendererConfiguration") + public Map> getViewRendererConfiguration() { + return viewRendererConfiguration; + } + + @JsonProperty("viewRendererConfiguration") + public void setViewRendererConfiguration(Map> viewRendererConfiguration) { + ImmutableMap.Builder> builder = ImmutableMap.builder(); + for (Map.Entry> entry : viewRendererConfiguration.entrySet()) { + builder.put(entry.getKey(), ImmutableMap.copyOf(entry.getValue())); + } + this.viewRendererConfiguration = builder.build(); + } + } + + @Before + public void setUp() throws Exception { + when(environment.jersey()).thenReturn(jerseyEnvironment); + } + + @Test + public void addsTheViewMessageBodyWriterToTheEnvironment() throws Exception { + new ViewBundle<>().run(null, environment); + + verify(jerseyEnvironment).register(any(ViewMessageBodyWriter.class)); + } + + @Test + @SuppressWarnings("unchecked") + public void addsTheViewMessageBodyWriterWithSingleViewRendererToTheEnvironment() throws Exception { + final String viewSuffix = ".ftl"; + final String testKey = "testKey"; + final Map> viewRendererConfig = new HashMap<>(); + final Map freeMarkerConfig = new HashMap<>(); + freeMarkerConfig.put(testKey, "yes"); + viewRendererConfig.put(viewSuffix, freeMarkerConfig); + + MyConfiguration myConfiguration = new MyConfiguration(); + myConfiguration.setViewRendererConfiguration(viewRendererConfig); + + 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 + } + + @Override + public void configure(Map options) { + assertThat("should contain the testKey", Boolean.TRUE, is(options.containsKey(testKey))); + } + + @Override + public String getSuffix() { + return viewSuffix; + } + }; + + new ViewBundle(ImmutableList.of(renderer)) { + @Override + public Map> getViewConfiguration(MyConfiguration configuration) { + return configuration.getViewRendererConfiguration(); + } + }.run(myConfiguration, environment); + + verify(jerseyEnvironment).register(any(ViewMessageBodyWriter.class)); + + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(ViewMessageBodyWriter.class); + verify(jerseyEnvironment).register(argumentCaptor.capture()); + + Field renderers = ViewMessageBodyWriter.class.getDeclaredField("renderers"); + renderers.setAccessible(true); + List configuredRenderers = ImmutableList.copyOf((Iterable) 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 00000000000..f9c57422ff0 --- /dev/null +++ b/dropwizard-views/src/test/java/io/dropwizard/views/ViewTest.java @@ -0,0 +1,16 @@ +package io.dropwizard.views; + +import org.junit.Test; + +import static org.assertj.core.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 00000000000..a167d4b7ff8 --- /dev/null +++ b/dropwizard-views/src/test/resources/logback-test.xml @@ -0,0 +1,11 @@ + + + + false + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + diff --git a/dropwizard/pom.xml b/dropwizard/pom.xml deleted file mode 100644 index 8b2559eb1ab..00000000000 --- a/dropwizard/pom.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - 4.0.0 - - - com.yammer - dropwizard-parent - 0.1.0-SNAPSHOT - - - com.yammer - dropwizard - Dropwizard - - - 1.10 - 7.5.4.v20111024 - 1.9.2 - 1.6.4 - - - - - com.sun.jersey - jersey-core - ${jersey.version} - - - com.sun.jersey - jersey-server - ${jersey.version} - - - com.sun.jersey - jersey-servlet - ${jersey.version} - - - com.yammer.metrics - metrics-core - ${metrics.version} - - - com.yammer.metrics - metrics-servlet - ${metrics.version} - - - com.yammer.metrics - metrics-jetty - ${metrics.version} - - - com.yammer.metrics - metrics-log4j - ${metrics.version} - - - org.codehaus.jackson - jackson-core-asl - ${jackson.version} - - - org.codehaus.jackson - jackson-mapper-asl - ${jackson.version} - - - commons-cli - commons-cli - 1.2 - - - org.slf4j - slf4j-api - ${slf4j.version} - - - org.slf4j - slf4j-log4j12 - ${slf4j.version} - - - org.slf4j - jul-to-slf4j - ${slf4j.version} - - - org.eclipse.jetty - jetty-server - ${jetty.version} - - - org.eclipse.jetty - jetty-servlet - ${jetty.version} - - - org.eclipse.jetty - jetty-http - ${jetty.version} - - - com.google.guava - guava - 10.0.1 - - - org.hibernate - hibernate-validator - 4.2.0.Final - - - org.yaml - snakeyaml - 1.9 - - - diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaDeserializers.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaDeserializers.java deleted file mode 100644 index 84af148d074..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaDeserializers.java +++ /dev/null @@ -1,174 +0,0 @@ -package com.fasterxml.jackson.module.guava; - -import com.fasterxml.jackson.module.guava.deser.*; -import com.google.common.base.Optional; -import com.google.common.collect.*; -import org.codehaus.jackson.map.*; -import org.codehaus.jackson.map.type.CollectionType; -import org.codehaus.jackson.map.type.MapType; -import org.codehaus.jackson.type.JavaType; - -/** - * Custom deserializers module offers. - * - * @author tsaloranta - */ -public class GuavaDeserializers - extends Deserializers.Base -{ - @Override - public JsonDeserializer findBeanDeserializer(JavaType type, - DeserializationConfig config, - DeserializerProvider provider, - BeanDescription beanDesc, - BeanProperty property) throws JsonMappingException { - if (Optional.class.isAssignableFrom(type.getRawClass())) { - final JavaType elementType = type.containedType(0); - return new OptionalDeserializer( - provider.findTypedValueDeserializer(config, elementType, property)); - } - return super.findBeanDeserializer(type, config, provider, beanDesc, property); - } - - /** - * Concrete implementation class to use for properties declared as - * {@link Multiset}s. - * Defaults to using - */ -// protected Class> _cfgDefaultMultiset; - -// protected Class> _cfgDefaultMultimap; - - /* - * No bean types to support yet; may need to add? - */ - /* - public JsonDeserializer findBeanDeserializer(JavaType type, - DeserializationConfig config, DeserializerProvider provider, - BeanDescription beanDesc) { - return null; - } - */ - - @Override - public JsonDeserializer findCollectionDeserializer(CollectionType type, - DeserializationConfig config, DeserializerProvider provider, - BeanDescription beanDesc, BeanProperty property, - TypeDeserializer elementTypeDeser, JsonDeserializer elementDeser) - throws JsonMappingException - { - Class raw = type.getRawClass(); - - // Multi-xxx collections? - if (Multiset.class.isAssignableFrom(raw)) { - // Quite a few variations... - if (LinkedHashMultiset.class.isAssignableFrom(raw)) { - // !!! TODO - } - if (HashMultiset.class.isAssignableFrom(raw)) { - return new HashMultisetDeserializer(type, elementTypeDeser, - _verifyElementDeserializer(elementDeser, type, config, provider)); - } - if (ImmutableMultiset.class.isAssignableFrom(raw)) { - // !!! TODO - } - if (EnumMultiset.class.isAssignableFrom(raw)) { - // !!! TODO - } - if (TreeMultiset.class.isAssignableFrom(raw)) { - // !!! TODO - } - - // TODO: make configurable (for now just default blindly) - return new HashMultisetDeserializer(type, elementTypeDeser, - _verifyElementDeserializer(elementDeser, type, config, provider)); - } - - // ImmutableXxx types? - if (ImmutableCollection.class.isAssignableFrom(raw)) { - if (ImmutableList.class.isAssignableFrom(raw)) { - return new ImmutableListDeserializer(type, elementTypeDeser, - _verifyElementDeserializer(elementDeser, type, config, provider)); - } - if (ImmutableSet.class.isAssignableFrom(raw)) { - // sorted one? - if (ImmutableSortedSet.class.isAssignableFrom(raw)) { - /* 28-Nov-2010, tatu: With some more work would be able to use other things - * than natural ordering; but that'll have to do for now... - */ - Class elemType = type.getContentType().getRawClass(); - if (!Comparable.class.isAssignableFrom(elemType)) { - throw new IllegalArgumentException("Can not handle ImmutableSortedSet with elements that are not Comparable (" - +raw.getName()+")"); - } - return new ImmutableSortedSetDeserializer(type, elementTypeDeser, - _verifyElementDeserializer(elementDeser, type, config, provider)); - } - // nah, just regular one - return new ImmutableSetDeserializer(type, elementTypeDeser, - _verifyElementDeserializer(elementDeser, type, config, provider)); - } - } - return null; - } - - /** - * A few Map types to support. - */ - @Override - public JsonDeserializer findMapDeserializer(MapType type, - DeserializationConfig config, DeserializerProvider provider, - BeanDescription beanDesc, BeanProperty property, KeyDeserializer keyDeser, - TypeDeserializer elementTypeDeser, JsonDeserializer elementDeser) - throws JsonMappingException - { - Class raw = type.getRawClass(); - // ImmutableXxxMap types? - if (ImmutableMap.class.isAssignableFrom(raw)) { - if (ImmutableSortedMap.class.isAssignableFrom(raw)) { - // !!! TODO - } - if (ImmutableBiMap.class.isAssignableFrom(raw)) { - // !!! TODO - } - // Otherwise, plain old ImmutableMap... - return new ImmutableMapDeserializer(type, keyDeser, elementTypeDeser, - _verifyElementDeserializer(elementDeser, type, config, provider)); - } - // Multimaps? - if (Multimap.class.isAssignableFrom(raw)) { - if (ListMultimap.class.isAssignableFrom(raw)) { - // !!! TODO - } - if (SetMultimap.class.isAssignableFrom(raw)) { - // !!! TODO - } - if (SortedSetMultimap.class.isAssignableFrom(raw)) { - // !!! TODO - } - } - return null; - } - - /* - /********************************************************************** - /* Helper methods - /********************************************************************** - */ - - /** - * Helper method used to ensure that we have a deserializer for elements - * of collection being deserialized. - */ - JsonDeserializer _verifyElementDeserializer(JsonDeserializer deser, - JavaType type, - DeserializationConfig config, DeserializerProvider provider) - throws JsonMappingException - { - if (deser == null) { - // 'null' -> collections have no referring fields - deser = provider.findValueDeserializer(config, type.getContentType(), null); - } - return deser; - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaModule.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaModule.java deleted file mode 100644 index 42d1fb89622..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaModule.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.fasterxml.jackson.module.guava; - -import org.codehaus.jackson.Version; -import org.codehaus.jackson.map.*; - -public class GuavaModule extends Module // can't use just SimpleModule, due to generic types -{ - private static final String NAME = "GuavaModule"; - - // Should externalize this, probably... - private final static Version VERSION = new Version(0, 1, 0, null); // 0.1.0 - - public GuavaModule() - { - - } - - @Override public String getModuleName() { return NAME; } - @Override public Version version() { return VERSION; } - - @Override - public void setupModule(SetupContext context) - { - context.addDeserializers(new GuavaDeserializers()); - context.addSerializers(new GuavaSerializers()); - } - -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaSerializers.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaSerializers.java deleted file mode 100644 index 008e75969b7..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/GuavaSerializers.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.fasterxml.jackson.module.guava; - -import com.fasterxml.jackson.module.guava.ser.OptionalSerializer; -import com.google.common.base.Optional; -import org.codehaus.jackson.map.*; -import org.codehaus.jackson.type.JavaType; - -public class GuavaSerializers extends Serializers.Base { - @Override - public JsonSerializer findSerializer(SerializationConfig config, - JavaType type, - BeanDescription beanDesc, - BeanProperty property) { - if (Optional.class.isAssignableFrom(type.getRawClass())) { - return new OptionalSerializer(); - } - return super.findSerializer(config, type, beanDesc, property); - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/GuavaCollectionDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/GuavaCollectionDeserializer.java deleted file mode 100644 index dd4c8880d33..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/GuavaCollectionDeserializer.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import java.io.IOException; - -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.type.CollectionType; - -public abstract class GuavaCollectionDeserializer extends JsonDeserializer -{ - protected final CollectionType _containerType; - - /** - * Deserializer used for values contained in collection being deserialized; - * null if it can not be dynamically determined. - */ - protected final JsonDeserializer _valueDeserializer; - - /** - * If value instances have polymorphic type information, this - * is the type deserializer that can deserialize required type - * information - */ - protected final TypeDeserializer _typeDeserializerForValue; - - protected GuavaCollectionDeserializer(CollectionType type, - TypeDeserializer typeDeser, JsonDeserializer deser) - { - _containerType = type; - _typeDeserializerForValue = typeDeser; - _valueDeserializer = deser; - } - - /** - * Base implementation that does not assume specific type - * inclusion mechanism. Sub-classes are expected to override - * this method if they are to handle type information. - */ - @Override - public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, - TypeDeserializer typeDeserializer) - throws IOException, JsonProcessingException - { - return typeDeserializer.deserializeTypedFromArray(jp, ctxt); - } - - @Override - public T deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - // Ok: must point to START_ARRAY - if (jp.getCurrentToken() != JsonToken.START_ARRAY) { - throw ctxt.mappingException(_containerType.getRawClass()); - } - return _deserializeContents(jp, ctxt); - } - - /* - /********************************************************************** - /* Abstract methods for impl classes - /********************************************************************** - */ - - protected abstract T _deserializeContents(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException; - - /* - /********************************************************************** - /* Helper methods - /********************************************************************** - */ -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/GuavaMapDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/GuavaMapDeserializer.java deleted file mode 100644 index 0e6308da325..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/GuavaMapDeserializer.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import java.io.IOException; - -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.KeyDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.type.MapType; - -public abstract class GuavaMapDeserializer extends JsonDeserializer -{ - protected final MapType _mapType; - - /** - * Key deserializer used, if not null. If null, String from JSON - * content is used as is. - */ - protected final KeyDeserializer _keyDeserializer; - - /** - * Value deserializer. - */ - protected final JsonDeserializer _valueDeserializer; - - /** - * If value instances have polymorphic type information, this - * is the type deserializer that can handle it - */ - protected final TypeDeserializer _typeDeserializerForValue; - - protected GuavaMapDeserializer(MapType type, KeyDeserializer keyDeser, - TypeDeserializer typeDeser, JsonDeserializer deser) - { - _mapType = type; - _keyDeserializer = keyDeser; - _typeDeserializerForValue = typeDeser; - _valueDeserializer = deser; - } - - /** - * Base implementation that does not assume specific type - * inclusion mechanism. Sub-classes are expected to override - * this method if they are to handle type information. - */ - @Override - public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, - TypeDeserializer typeDeserializer) - throws IOException, JsonProcessingException - { - return typeDeserializer.deserializeTypedFromArray(jp, ctxt); - } - - @Override - public T deserialize(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - // Ok: must point to START_OBJECT or FIELD_NAME - JsonToken t = jp.getCurrentToken(); - if (t == JsonToken.START_OBJECT) { // If START_OBJECT, move to next; may also be END_OBJECT - t = jp.nextToken(); - if (t != JsonToken.FIELD_NAME && t != JsonToken.END_OBJECT) { - throw ctxt.mappingException(_mapType.getRawClass()); - } - } else if (t != JsonToken.FIELD_NAME) { - throw ctxt.mappingException(_mapType.getRawClass()); - } - return _deserializeEntries(jp, ctxt); - } - - /* - /********************************************************************** - /* Abstract methods for impl classes - /********************************************************************** - */ - - protected abstract T _deserializeEntries(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException; - - /* - /********************************************************************** - /* Helper methods - /********************************************************************** - */ - -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/HashMultisetDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/HashMultisetDeserializer.java deleted file mode 100644 index 6d7068068b3..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/HashMultisetDeserializer.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import com.google.common.collect.HashMultiset; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.annotate.JsonCachable; -import org.codehaus.jackson.map.type.CollectionType; - -import java.io.IOException; - -@JsonCachable -public class HashMultisetDeserializer extends GuavaCollectionDeserializer> -{ - public HashMultisetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) - { - super(type, typeDeser, deser); - } - - @Override - protected HashMultiset _deserializeContents(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - JsonDeserializer valueDes = _valueDeserializer; - JsonToken t; - final TypeDeserializer typeDeser = _typeDeserializerForValue; - HashMultiset set = HashMultiset.create(); - - while ((t = jp.nextToken()) != JsonToken.END_ARRAY) { - Object value; - - if (t == JsonToken.VALUE_NULL) { - value = null; - } else if (typeDeser == null) { - value = valueDes.deserialize(jp, ctxt); - } else { - value = valueDes.deserializeWithType(jp, ctxt, typeDeser); - } - set.add(value); - } - return set; - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableListDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableListDeserializer.java deleted file mode 100644 index 778d66d387e..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableListDeserializer.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import com.google.common.collect.ImmutableList; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.annotate.JsonCachable; -import org.codehaus.jackson.map.type.CollectionType; - -import java.io.IOException; - -@JsonCachable -public class ImmutableListDeserializer extends GuavaCollectionDeserializer> -{ - public ImmutableListDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) - { - super(type, typeDeser, deser); - } - - @Override - protected ImmutableList _deserializeContents(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - JsonDeserializer valueDes = _valueDeserializer; - JsonToken t; - final TypeDeserializer typeDeser = _typeDeserializerForValue; - // No way to pass actual type parameter; but does not matter, just compiler-time fluff: - ImmutableList.Builder builder = ImmutableList.builder(); - - while ((t = jp.nextToken()) != JsonToken.END_ARRAY) { - Object value; - - if (t == JsonToken.VALUE_NULL) { - value = null; - } else if (typeDeser == null) { - value = valueDes.deserialize(jp, ctxt); - } else { - value = valueDes.deserializeWithType(jp, ctxt, typeDeser); - } - builder.add(value); - } - return builder.build(); - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableMapDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableMapDeserializer.java deleted file mode 100644 index 4a1c85a4673..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableMapDeserializer.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import com.google.common.collect.ImmutableMap; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.KeyDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.annotate.JsonCachable; -import org.codehaus.jackson.map.type.MapType; - -import java.io.IOException; - -@JsonCachable -public class ImmutableMapDeserializer extends GuavaMapDeserializer> -{ - public ImmutableMapDeserializer(MapType type, KeyDeserializer keyDeser, - TypeDeserializer typeDeser, JsonDeserializer deser) - { - super(type, keyDeser, typeDeser, deser); - } - - @Override - protected ImmutableMap _deserializeEntries(JsonParser jp, - DeserializationContext ctxt) throws IOException, JsonProcessingException - { - final KeyDeserializer keyDes = _keyDeserializer; - final JsonDeserializer valueDes = _valueDeserializer; - final TypeDeserializer typeDeser = _typeDeserializerForValue; - - ImmutableMap.Builder builder = ImmutableMap.builder(); - for (; jp.getCurrentToken() == JsonToken.FIELD_NAME; jp.nextToken()) { - // Must point to field name now - String fieldName = jp.getCurrentName(); - Object key = (keyDes == null) ? fieldName : keyDes.deserializeKey(fieldName, ctxt); - // And then the value... - JsonToken t = jp.nextToken(); - // 28-Nov-2010, tatu: Should probably support "ignorable properties" in future... - Object value; - if (t == JsonToken.VALUE_NULL) { - value = null; - } else if (typeDeser == null) { - value = valueDes.deserialize(jp, ctxt); - } else { - value = valueDes.deserializeWithType(jp, ctxt, typeDeser); - } - builder.put(key, value); - } - return builder.build(); - } - -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableSetDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableSetDeserializer.java deleted file mode 100644 index 112c2a193a1..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableSetDeserializer.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import com.google.common.collect.ImmutableSet; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.annotate.JsonCachable; -import org.codehaus.jackson.map.type.CollectionType; - -import java.io.IOException; - -@JsonCachable -public class ImmutableSetDeserializer extends GuavaCollectionDeserializer> -{ - public ImmutableSetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) - { - super(type, typeDeser, deser); - } - - @Override - protected ImmutableSet _deserializeContents(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - JsonDeserializer valueDes = _valueDeserializer; - JsonToken t; - final TypeDeserializer typeDeser = _typeDeserializerForValue; - // No way to pass actual type parameter; but does not matter, just compiler-time fluff: - ImmutableSet.Builder builder = ImmutableSet.builder(); - - while ((t = jp.nextToken()) != JsonToken.END_ARRAY) { - Object value; - - if (t == JsonToken.VALUE_NULL) { - value = null; - } else if (typeDeser == null) { - value = valueDes.deserialize(jp, ctxt); - } else { - value = valueDes.deserializeWithType(jp, ctxt, typeDeser); - } - builder.add(value); - } - return builder.build(); - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableSortedSetDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableSortedSetDeserializer.java deleted file mode 100644 index fabddb5ef40..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/ImmutableSortedSetDeserializer.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import com.google.common.collect.ImmutableSortedSet; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.TypeDeserializer; -import org.codehaus.jackson.map.annotate.JsonCachable; -import org.codehaus.jackson.map.type.CollectionType; - -import java.io.IOException; - -@JsonCachable -public class ImmutableSortedSetDeserializer extends GuavaCollectionDeserializer> -{ - public ImmutableSortedSetDeserializer(CollectionType type, TypeDeserializer typeDeser, JsonDeserializer deser) - { - super(type, typeDeser, deser); - } - - @Override - protected ImmutableSortedSet _deserializeContents(JsonParser jp, DeserializationContext ctxt) - throws IOException, JsonProcessingException - { - JsonDeserializer valueDes = _valueDeserializer; - JsonToken t; - final TypeDeserializer typeDeser = _typeDeserializerForValue; - /* Not quite sure what to do with sorting/ordering; may require better support either - * via annotations, or via custom serialization (bean style that includes ordering - * aspects) - */ - @SuppressWarnings("unchecked") - ImmutableSortedSet.Builder builderComp = ImmutableSortedSet.naturalOrder(); - @SuppressWarnings("unchecked") - ImmutableSortedSet.Builder builder = (ImmutableSortedSet.Builder) builderComp; - - while ((t = jp.nextToken()) != JsonToken.END_ARRAY) { - Object value; - - if (t == JsonToken.VALUE_NULL) { - value = null; - } else if (typeDeser == null) { - value = valueDes.deserialize(jp, ctxt); - } else { - value = valueDes.deserializeWithType(jp, ctxt, typeDeser); - } - builder.add(value); - } - return builder.build(); - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/OptionalDeserializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/OptionalDeserializer.java deleted file mode 100644 index 7a8284d34c9..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/deser/OptionalDeserializer.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.fasterxml.jackson.module.guava.deser; - -import com.google.common.base.Optional; -import org.codehaus.jackson.JsonParser; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.JsonToken; -import org.codehaus.jackson.map.DeserializationContext; -import org.codehaus.jackson.map.JsonDeserializer; -import org.codehaus.jackson.map.annotate.JsonCachable; - -import java.io.IOException; - -@JsonCachable -public class OptionalDeserializer extends JsonDeserializer> { - private final JsonDeserializer elementDeserializer; - - public OptionalDeserializer(JsonDeserializer elementDeserializer) { - this.elementDeserializer = elementDeserializer; - } - - @Override - public Optional deserialize(JsonParser jp, - DeserializationContext ctxt) throws IOException, JsonProcessingException { - if (jp.getCurrentToken() == JsonToken.VALUE_NULL) { - return Optional.absent(); - } - return Optional.fromNullable(elementDeserializer.deserialize(jp, ctxt)); - } - - @Override - public Optional getNullValue() { - return Optional.absent(); - } - - @Override - public Optional getEmptyValue() { - return Optional.absent(); - } -} diff --git a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/ser/OptionalSerializer.java b/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/ser/OptionalSerializer.java deleted file mode 100644 index 9d907f8937d..00000000000 --- a/dropwizard/src/main/java/com/fasterxml/jackson/module/guava/ser/OptionalSerializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.fasterxml.jackson.module.guava.ser; - -import com.google.common.base.Optional; -import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.JsonProcessingException; -import org.codehaus.jackson.map.JsonSerializer; -import org.codehaus.jackson.map.SerializerProvider; -import org.codehaus.jackson.map.annotate.JsonCachable; - -import java.io.IOException; - -@JsonCachable -public class OptionalSerializer extends JsonSerializer> { - @Override - public void serialize(Optional value, - JsonGenerator jgen, - SerializerProvider provider) throws IOException, JsonProcessingException { - if (value.isPresent()) { - jgen.writeObject(value.get()); - } else { - jgen.writeNull(); - } - } -} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/AbstractService.java b/dropwizard/src/main/java/com/yammer/dropwizard/AbstractService.java deleted file mode 100644 index 316634725a5..00000000000 --- a/dropwizard/src/main/java/com/yammer/dropwizard/AbstractService.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.yammer.dropwizard; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.yammer.dropwizard.cli.Command; -import com.yammer.dropwizard.cli.ConfiguredCommand; -import com.yammer.dropwizard.cli.ServerCommand; -import com.yammer.dropwizard.cli.UsagePrinter; -import com.yammer.dropwizard.config.Configuration; -import com.yammer.dropwizard.config.Environment; -import com.yammer.dropwizard.config.LoggingFactory; - -import java.lang.reflect.ParameterizedType; -import java.util.Arrays; -import java.util.List; -import java.util.SortedMap; - -/** - * The base class for both Java and Scala services. - * - * @param the type of configuration class for this service - */ -public abstract class AbstractService { - static { - // make sure spinning up Hibernate Validator doesn't yell at us - LoggingFactory.bootstrap(); - } - - private final String name; - private final List modules; - private final SortedMap commands; - private String banner = null; - - protected AbstractService(String name) { - this.name = name; - this.modules = Lists.newArrayList(); - this.commands = Maps.newTreeMap(); - addCommand(new ServerCommand(getConfigurationClass())); - } - - /** - * A simple reminder that this particular class isn't meant to be extended by non-DW classes. - */ - @SuppressWarnings("UnusedDeclaration") - protected abstract void subclassServiceInsteadOfThis(); - - public final String getName() { - return name; - } - - /** - * Returns the {@link Class} of the configuration class type parameter. - * - * @return the configuration class - * @see Super Type Tokens - */ - @SuppressWarnings("unchecked") - public final Class getConfigurationClass() { - return (Class) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0]; - } - - public final ImmutableList getModules() { - return ImmutableList.copyOf(modules); - } - - protected final void addModule(Module module) { - modules.add(module); - } - - public final ImmutableList getCommands() { - return ImmutableList.copyOf(commands.values()); - } - - protected final void addCommand(Command command) { - commands.put(command.getName(), command); - } - - protected final void addCommand(ConfiguredCommand command) { - commands.put(command.getName(), command); - } - - public final boolean hasBanner() { - return banner != null; - } - - public final String getBanner() { - return banner; - } - - protected final void setBanner(String banner) { - this.banner = banner; - } - - public abstract void initialize(T configuration, Environment environment); - - public void initializeWithModules(T configuration, Environment environment) { - for (Module module : modules) { - module.initialize(environment); - } - initialize(configuration, environment); - } - - public final void run(String[] arguments) throws Exception { - if (isHelp(arguments)) { - UsagePrinter.printRootHelp(this); - } else { - final Command cmd = commands.get(arguments[0]); - if (cmd != null) { - cmd.run(this, Arrays.copyOfRange(arguments, 1, arguments.length)); - } else { - UsagePrinter.printRootHelp(this); - } - } - } - - private static boolean isHelp(String[] arguments) { - return (arguments.length == 0) || - ((arguments.length == 1) && - ("-h".equals(arguments[0]) || - "--help".equals(arguments[0]))); - } -} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/BearerToken.java b/dropwizard/src/main/java/com/yammer/dropwizard/BearerToken.java deleted file mode 100644 index 070eb45f3ed..00000000000 --- a/dropwizard/src/main/java/com/yammer/dropwizard/BearerToken.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.yammer.dropwizard; - -import java.lang.annotation.*; - -/** - * A method parameter of type {@code Option[String]} will be populated with the - * OAuth2 Bearer Token, if one is provided by the client. - */ -@Target(ElementType.PARAMETER) -@Retention(RetentionPolicy.RUNTIME) -@Documented -public @interface BearerToken { - String value() default "Bearer"; -} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/Module.java b/dropwizard/src/main/java/com/yammer/dropwizard/Module.java deleted file mode 100644 index 973d45c8162..00000000000 --- a/dropwizard/src/main/java/com/yammer/dropwizard/Module.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.yammer.dropwizard; - -import com.yammer.dropwizard.config.Environment; - -/** - * A reusable module, used to define blocks of service behavior. - */ -public interface Module { - /** - * Initializes the environment. - * - * @param environment the service's {@link Environment} - */ - public void initialize(Environment environment); -} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/Service.java b/dropwizard/src/main/java/com/yammer/dropwizard/Service.java deleted file mode 100644 index 5e61183455a..00000000000 --- a/dropwizard/src/main/java/com/yammer/dropwizard/Service.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.yammer.dropwizard; - -import com.yammer.dropwizard.config.Configuration; -import com.yammer.dropwizard.modules.JavaModule; - -public abstract class Service extends AbstractService { - protected Service(String name) { - super(name); - addModule(new JavaModule()); - } - - @Override - protected final void subclassServiceInsteadOfThis() { - - } -} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/Validated.java b/dropwizard/src/main/java/com/yammer/dropwizard/Validated.java deleted file mode 100644 index 7f1f54bd132..00000000000 --- a/dropwizard/src/main/java/com/yammer/dropwizard/Validated.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.yammer.dropwizard; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * An annotation for classes being parsed by - * {@link com.yammer.dropwizard.jersey.JacksonMessageBodyProvider}. If present, the class will be - * validated using {@link com.yammer.dropwizard.util.Validator}. - */ -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -public @interface Validated { - // TODO: 11/10/11 -- add support for Validated for Scala -} diff --git a/dropwizard/src/main/java/com/yammer/dropwizard/cli/Command.java b/dropwizard/src/main/java/com/yammer/dropwizard/cli/Command.java deleted file mode 100644 index 6d8fcc1af0a..00000000000 --- a/dropwizard/src/main/java/com/yammer/dropwizard/cli/Command.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.yammer.dropwizard.cli; - -import com.yammer.dropwizard.AbstractService; -import com.yammer.dropwizard.util.JarLocation; -import org.apache.commons.cli.*; - -import java.util.Collection; - -import static com.google.common.base.Preconditions.checkNotNull; -import static java.lang.String.format; - -/** - * A basic CLI command. - */ -public abstract class Command { - private final String name; - private final String description; - - /** - * Create a new {@link Command} instance. - * - * @param name the command name (must be unique for the service) - * @param description the description of the command - */ - protected Command(String name, - String description) { - this.name = checkNotNull(name); - this.description = checkNotNull(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; - } - - /** - * Returns an empty {@link Options} instance. Override this to allow your commands to parse - * command line arguments. - * - * @return an empty {@link Options} instance - */ - public Options getOptions() { - return new Options(); - } - - @SuppressWarnings("unchecked") - final Options getOptionsWithHelp() { - final Options options = new Options(); - final OptionGroup group = new OptionGroup(); - for (Option option : (Collection