diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..2ebee9a
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,14 @@
+version: 2
+updates:
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ timezone: "Asia/Jerusalem"
+ - package-ecosystem: "gradle"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ timezone: "Asia/Jerusalem"
+ reviewers:
+ - "muliyul"
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..c04dca2
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,37 @@
+name: Dropwizard Swagger CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up JDK 1.8
+ uses: actions/setup-java@v1
+ with:
+ java-version: 1.8
+
+ - name: Cache Gradle packages
+ uses: actions/cache@v2
+ with:
+ path: ~/.gradle/caches
+ key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}-uidist-{{ hashFiles('src/main/resources/static/swagger-ui/*.*') }}
+ restore-keys: ${{ runner.os }}-gradle
+
+ - name: Clone Swagger UI distro
+ run: sh ./update-swagger-ui.sh
+
+ - name: Build
+ uses: eskatos/gradle-command-action@v1
+ with:
+ gradle-version: 6.7
+ arguments: build
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..211cef5
--- /dev/null
+++ b/README.md
@@ -0,0 +1,81 @@
+dropwizard-swagger
+==================
+
+A Dropwizard bundle inspired by the original [dropwizard-swagger](https://github.com/federecio/dropwizard-swagger) and its popular [fork](https://github.com/smoketurner/dropwizard-swagger)
+that serves [Swagger UI](https://github.com/swagger-api/swagger-ui) and loads [OpenApi](https://github.com/OAI/OpenAPI-Specification) 3.0 (or Swagger2) endpoints.
+
+#### Notable improvements:
+
+- Spec can be defined in standard locations (see [this](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#known-locations)). `config.yaml` takes precedence.
+- UI is configurable (programmatically or via `config.yaml`).
+- If `dropwizard-auth` is included, then by default your `@Auth` annotated parameters will be marked as `@Hidden` so they won't interfere with request body.
+
+### Installation
+
+#### Maven
+```xml
+
+com.muliyul
+dropwizard-swagger
+0.0.1
+
+```
+
+#### Gradle
+```groovy
+implementation('com.muliyul:dropwizard-swagger:0.0.1')
+```
+
+### Usage
+
+See [example](src/test/java/com/muliyul/dropwizard/swagger/TestJavaConfiguration.java)
+```java
+class AppConfiguration extends Configuration implements com.muliyul.dropwizard.swagger.SwaggerConfiguration {
+ @JsonProperty("swagger")
+ private SwaggerConfiguration swaggerConfiguration = null;
+ @JsonProperty("swagger-ui")
+ private SwaggerUiConfiguration swaggerUiConfiguration = null;
+
+ public SwaggerConfiguration getSwaggerConfiguration() {
+ return swaggerConfiguration;
+ }
+
+ public SwaggerUiConfiguration getSwaggerUiConfiguration() {
+ return swaggerUiConfiguration;
+ }
+}
+```
+
+#### In your Application class:
+```java
+@Override
+public void initialize(Bootstrap bootstrap) {
+ bootstrap.addBundle(new SwaggerBundle<>());
+
+ // or pass the packages to scan
+ // bootstrap.addBundle(new SwaggerBundle("com.example.resources", "com.example.resources2"));
+ // or specify them in config.yaml
+ // or specify them in one of the known OpenApi spec locations
+ // the choice is yours!
+}
+```
+
+That's it!
+
+The bundle will scan your classpath for any resources and expose them to swagger-ui via `/swagger`.
+
+You can access the complete definitions in `/openapi`, `/openapi.json`, `/openapi.yaml` [see this](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Integration-and-Configuration#openapiresource).
+
+#### Configuring Swagger-UI
+
+See [SwaggerUiConfiguration](src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/SwaggerUiConfiguration.kt)
+
+---
+Check out OpenApi spec file/definitions [here]().
+
+Check out the available Swagger-UI options [here]().
+
+---
+## Development
+
+Clone the project and run `TestJavaApplication` or `TestKotlinApplication`.
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..9cf38ce
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,58 @@
+plugins {
+ id 'org.jetbrains.kotlin.jvm' version '1.4.21'
+}
+
+group 'com.muliyul'
+version '0.0.1'
+
+repositories {
+ mavenCentral()
+}
+
+
+java {
+ registerFeature('auth') {
+ usingSourceSet(sourceSets.main)
+ }
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
+
+ implementation platform("io.dropwizard:dropwizard-bom:2.0.17")
+ implementation("io.dropwizard:dropwizard-core")
+ implementation("io.dropwizard:dropwizard-assets")
+ authImplementation("io.dropwizard:dropwizard-auth")
+// implementation("io.dropwizard:dropwizard-client")
+
+ implementation("io.swagger.core.v3:swagger-jaxrs2:2.1.2")
+
+ // Test
+ testImplementation("io.dropwizard:dropwizard-testing")
+
+ testImplementation('org.seleniumhq.selenium:selenium-java:3.141.59')
+ testImplementation('io.github.bonigarcia:selenium-jupiter:3.3.5')
+
+ testImplementation platform("org.glassfish.jersey:jersey-bom:2.33")
+ testImplementation("org.glassfish.jersey.ext:jersey-proxy-client")
+ testImplementation("io.dropwizard:dropwizard-auth")
+
+
+ // JUnit
+ testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
+ testImplementation platform("org.junit:junit-bom:5.7.0")
+ testImplementation("org.junit.jupiter:junit-jupiter-api")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+}
+
+compileKotlin {
+ kotlinOptions.jvmTarget = "1.8"
+}
+compileTestKotlin {
+ kotlinOptions.jvmTarget = "1.8"
+}
+
+
+test {
+ useJUnitPlatform()
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..29e08e8
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+kotlin.code.style=official
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..be52383
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100644
index 0000000..4f906e0
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 the original author or authors.
+#
+# 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
+#
+# https://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.
+#
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+ echo "$*"
+}
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+ NONSTOP* )
+ nonstop=true
+ ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=`expr $i + 1`
+ done
+ case $i in
+ 0) set -- ;;
+ 1) set -- "$args0" ;;
+ 2) set -- "$args0" "$args1" ;;
+ 3) set -- "$args0" "$args1" "$args2" ;;
+ 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Escape application args
+save () {
+ for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+ echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..107acd3
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,89 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..0db3279
--- /dev/null
+++ b/package.json
@@ -0,0 +1,3 @@
+{
+
+}
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..0a1de7b
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,2 @@
+rootProject.name = 'dropwizard-swagger'
+
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/DropwizardCompatibleReader.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/DropwizardCompatibleReader.kt
new file mode 100644
index 0000000..890f19a
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/DropwizardCompatibleReader.kt
@@ -0,0 +1,39 @@
+package com.muliyul.dropwizard.swagger
+
+import com.fasterxml.jackson.annotation.*
+import io.dropwizard.auth.*
+import io.swagger.v3.jaxrs2.*
+import io.swagger.v3.oas.models.*
+import io.swagger.v3.oas.models.security.*
+import java.lang.reflect.*
+import javax.ws.rs.*
+
+/**
+ * This class instructs Swagger to ignore [Auth]
+ */
+open class DropwizardCompatibleReader : Reader() {
+
+ override fun getParameters(
+ type: Type?,
+ annotations: MutableList?,
+ operation: Operation,
+ classConsumes: Consumes?,
+ methodConsumes: Consumes?,
+ jsonViewAnnotation: JsonView?
+ ): ResolvedParameter {
+ val isAuthParam = annotations?.any { it.annotationClass == Auth::class } == true
+
+ return super.getParameters(
+ type,
+ annotations,
+ operation,
+ classConsumes,
+ methodConsumes,
+ jsonViewAnnotation
+ ).apply {
+ // instructs Swagger to ignore @Auth as body param
+ if (isAuthParam) requestBody = null
+ }
+ }
+
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/DropwizardCompatibleScanner.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/DropwizardCompatibleScanner.kt
new file mode 100644
index 0000000..83ae32a
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/DropwizardCompatibleScanner.kt
@@ -0,0 +1,15 @@
+package com.muliyul.dropwizard.swagger
+
+import io.swagger.v3.jaxrs2.integration.*
+import io.swagger.v3.oas.integration.api.*
+
+private val ignoredPackages = setOf("com.papertrail.profiler", "org.glassfish.jersey")
+
+private val delegate = JaxrsApplicationAndAnnotationScanner()
+
+open class DropwizardCompatibleScanner : OpenApiScanner by delegate {
+ override fun classes(): MutableSet> =
+ delegate.classes()
+ .filter { c -> ignoredPackages.none { p -> c.name.startsWith(p, ignoreCase = true) } }
+ .toMutableSet()
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/SwaggerBundle.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/SwaggerBundle.kt
new file mode 100644
index 0000000..0a564bd
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/SwaggerBundle.kt
@@ -0,0 +1,55 @@
+package com.muliyul.dropwizard.swagger
+
+import com.muliyul.dropwizard.swagger.ui.*
+import com.muliyul.dropwizard.swagger.ui.configuration.*
+import io.dropwizard.*
+import io.dropwizard.Configuration
+import io.dropwizard.assets.*
+import io.dropwizard.setup.*
+import io.swagger.v3.jaxrs2.integration.*
+import io.swagger.v3.oas.integration.*
+import com.muliyul.dropwizard.swagger.resource.*
+
+
+class SwaggerBundle @JvmOverloads constructor(
+ private val swaggerUiConfiguration: SwaggerUiConfiguration = SwaggerUiConfiguration()
+) : ConfiguredBundle where C : Configuration, C : SwaggerBundleConfiguration {
+
+ override fun initialize(bootstrap: Bootstrap<*>) {
+ bootstrap.addBundle(AssetsBundle("/static/swagger-ui/", "/swagger-ui/", null, "swagger"))
+ }
+
+ override fun run(configuration: C, environment: Environment) {
+ val swaggerUiConfiguration = configuration.swaggerUiConfiguration ?: swaggerUiConfiguration
+
+ val resourceConfig = environment.jersey().resourceConfig
+ val ctx = JaxrsOpenApiContextBuilder>()
+ .application(resourceConfig)
+ .buildContext(false)
+ .apply {
+ setOpenApiScanner(DropwizardCompatibleScanner())
+ setOpenApiReader(DropwizardCompatibleReader())
+ }
+ .init()
+
+ val resources = listOf(
+ DropwizardAcceptHeaderOpenApiResource(),
+ DropwizardOpenApiResource()
+ )
+
+ environment.jersey().apply {
+ resources.forEach { resource ->
+ val swaggerConfiguration = ctx.openApiConfiguration as SwaggerConfiguration
+ resource.openApiConfiguration = swaggerConfiguration.apply {
+ scannerClass = scannerClass ?: DropwizardCompatibleScanner::class.qualifiedName
+ readerClass = readerClass ?: DropwizardCompatibleReader::class.qualifiedName
+ }
+ register(resource)
+ }
+ if (!swaggerUiConfiguration.disabled) {
+ register(SwaggerResource(environment.objectMapper, swaggerUiConfiguration))
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/SwaggerBundleConfiguration.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/SwaggerBundleConfiguration.kt
new file mode 100644
index 0000000..5bf2f7c
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/SwaggerBundleConfiguration.kt
@@ -0,0 +1,7 @@
+package com.muliyul.dropwizard.swagger
+
+import com.muliyul.dropwizard.swagger.ui.configuration.*
+
+interface SwaggerBundleConfiguration {
+ val swaggerUiConfiguration: SwaggerUiConfiguration?
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/resource/DropwizardAcceptHeaderOpenApiResource.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/resource/DropwizardAcceptHeaderOpenApiResource.kt
new file mode 100644
index 0000000..0d241e7
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/resource/DropwizardAcceptHeaderOpenApiResource.kt
@@ -0,0 +1,38 @@
+package com.muliyul.dropwizard.swagger.resource
+
+import io.swagger.v3.jaxrs2.integration.resources.*
+import io.swagger.v3.oas.annotations.*
+import javax.servlet.*
+import javax.ws.rs.*
+import javax.ws.rs.core.*
+
+@Path("/openapi")
+class DropwizardAcceptHeaderOpenApiResource : BaseOpenApiResource() {
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(hidden = true)
+ fun getOpenApiJson(
+ @Context headers: HttpHeaders,
+ @Context uriInfo: UriInfo,
+ @Context config: ServletConfig,
+ @Context app: Application
+ ): Response {
+ return super.getOpenApi(headers, config, app, uriInfo, "json")
+ }
+
+ @GET
+ @Consumes(MediaType.WILDCARD)
+ @Produces("application/yaml")
+ @Operation(hidden = true)
+ fun getOpenApiYaml(
+ @Context headers: HttpHeaders,
+ @Context uriInfo: UriInfo,
+ @Context config: ServletConfig,
+ @Context app: Application
+ ): Response {
+ return super.getOpenApi(headers, config, app, uriInfo, "yaml")
+ }
+
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/resource/DropwizardOpenApiResource.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/resource/DropwizardOpenApiResource.kt
new file mode 100644
index 0000000..bcc6efa
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/resource/DropwizardOpenApiResource.kt
@@ -0,0 +1,25 @@
+package com.muliyul.dropwizard.swagger.resource
+
+import io.swagger.v3.jaxrs2.integration.resources.*
+import io.swagger.v3.oas.annotations.*
+import javax.servlet.*
+import javax.ws.rs.*
+import javax.ws.rs.core.*
+
+@Path("/openapi.{type:json|yaml}")
+class DropwizardOpenApiResource : BaseOpenApiResource() {
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON, "application/yaml")
+ @Operation(hidden = true)
+ fun getOpenApi(
+ @Context headers: HttpHeaders,
+ @Context uriInfo: UriInfo,
+ @PathParam("type") type: String,
+ @Context config: ServletConfig,
+ @Context app: Application
+ ): Response {
+ return super.getOpenApi(headers, config, app, uriInfo, type)
+ }
+
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/Aliases.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/Aliases.kt
new file mode 100644
index 0000000..875d82d
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/Aliases.kt
@@ -0,0 +1,8 @@
+package com.muliyul.dropwizard.swagger.ui
+
+// JavaScript concatenated string (window.location.origin + '/something') or plain string
+typealias JsStringOrString = String
+// function() {} or () => {}
+typealias JsFunction = String
+// [{}, GlobalObject...]
+typealias JsArrayOfObjects = String
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/SwaggerUiResource.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/SwaggerUiResource.kt
new file mode 100644
index 0000000..e4fea5b
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/SwaggerUiResource.kt
@@ -0,0 +1,36 @@
+package com.muliyul.dropwizard.swagger.ui
+
+import com.fasterxml.jackson.databind.*
+import com.muliyul.dropwizard.swagger.ui.configuration.*
+import io.swagger.v3.oas.annotations.*
+import javax.ws.rs.*
+import javax.ws.rs.core.*
+
+@Path("/swagger")
+class SwaggerResource(
+ private val mapper: ObjectMapper,
+ private val swaggerUiConfiguration: SwaggerUiConfiguration
+) {
+
+ @get:GET
+ @get:Consumes(MediaType.WILDCARD)
+ @get:Produces(MediaType.TEXT_HTML)
+ @get:Operation(hidden = true)
+ val index by lazy {
+ val originalIndexInputStream = ClassLoader.getSystemClassLoader()
+ .getResourceAsStream("static/swagger-ui/index.html") ?: error("Could not find index.html in the classpath.")
+
+ val originalIndex = originalIndexInputStream
+ .reader()
+ .readText()
+ .replace("./", "./swagger-ui/")
+
+ val swaggerUiConfigurationRegex = """.*\((?\{(.*\s*)*})\).*""".toRegex()
+ val (before, after) = swaggerUiConfigurationRegex.split(originalIndex, 2)
+
+ val prettyPrinter = mapper.writer().withDefaultPrettyPrinter()
+ val configurationJson = prettyPrinter.writeValueAsString(swaggerUiConfiguration)
+ "${before}\r\nvar ui = SwaggerUIBundle($configurationJson);\r\n$after"
+ }
+
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/SyntaxHighlight.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/SyntaxHighlight.kt
new file mode 100644
index 0000000..66ca7ea
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/SyntaxHighlight.kt
@@ -0,0 +1,6 @@
+package com.muliyul.dropwizard.swagger.ui
+
+data class SyntaxHighlight(
+ val activate: Boolean = true,
+ val theme: Theme = Theme.Agate
+)
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/Theme.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/Theme.kt
new file mode 100644
index 0000000..6bb9f89
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/Theme.kt
@@ -0,0 +1,16 @@
+package com.muliyul.dropwizard.swagger.ui
+
+import com.fasterxml.jackson.annotation.*
+
+@Suppress("unused")
+enum class Theme {
+ Agate,
+ Arta,
+ Monokai,
+ Nord,
+ Obsidian,
+ Tomorrow_Night;
+
+ @JsonValue
+ private fun toJson() = this.name.toLowerCase().replace('_', '-')
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/Builder.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/Builder.kt
new file mode 100644
index 0000000..9fd8c24
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/Builder.kt
@@ -0,0 +1,174 @@
+package com.muliyul.dropwizard.swagger.ui.configuration
+
+import com.muliyul.dropwizard.swagger.ui.*
+
+// Mainly for java users
+class Builder {
+ private var disabled: Boolean = false
+ private var configUrl: String? = null
+ private var spec: Map? = null
+ private var url: JsStringOrString = """window.location.origin + '/openapi'"""
+ private var urls: List? = null
+
+ // Display
+ private var deepLinking: Boolean? = null
+ private var displayOperationId: Boolean? = null
+ private var defaultModelsExpandDepth: Int? = null
+ private var defaultModelExpandDepth: Int? = null
+ private var defaultModelRendering: String? = null
+ private var displayRequestDuration: Boolean? = null
+ private var docExpansion: String? = null
+ private var filter: Boolean? = null
+ private var maxDisplayedTags: Int? = null
+ private var operationsSorter: JsFunction? = null
+ private var tagsSorter: JsFunction? = null
+ private var showExtensions: Boolean? = null
+ private var showCommonExtensions: Boolean? = null
+ private var useUnsafeMarkdown: Boolean? = null
+ private var syntaxHighlight: SyntaxHighlight? = null
+
+ // Network
+ private var oauth2RedirectUrl: String? = null
+ private var requestInterceptor: JsFunction? = null
+
+ // TODO: these break the UI for some reason. not supported in the meantime.
+// @field:JsonProperty("request.curlOptions")
+// @field:JsonRawValue
+// var curlOptions: JsOrString = JS_UNDEFINED,
+ private var responseInterceptor: JsFunction? = null
+ private var showMutatedRequest: Boolean? = null
+ private var supportedSubmitMethods: List? = null
+ private var validatorUrl: String? = null
+ private var withCredentials: Boolean? = null
+
+ // Macros
+ private var modelPropertyMacro: JsFunction? = null
+ private var parameterMacro: JsFunction? = null
+
+ // Authorization
+ private var persistAuthorization: Boolean? = null
+
+ //Plugin system
+ private var layout: String = "StandaloneLayout"
+ private var presets: JsArrayOfObjects = """[
+ SwaggerUIBundle.presets.apis,
+ SwaggerUIStandalonePreset
+ ]"""
+ private var plugins: JsArrayOfObjects? = null
+
+ fun disabled() = apply {
+ this.disabled = true
+ }
+
+ fun configUrl(configUrl: String) = apply {
+ this.configUrl = configUrl
+ }
+
+ fun spec(spec: Map) = apply {
+ this.spec = spec
+ }
+
+ fun url(url: String) = apply {
+ this.url = url
+ }
+
+ fun urls(vararg urls: String) = apply {
+ this.urls = urls.toList()
+ }
+
+ fun deepLinking(deepLinking: Boolean) = apply {
+ this.deepLinking = deepLinking
+ }
+
+ fun displayOperationId(displayOperationId: Boolean) = apply {
+ this.displayOperationId = displayOperationId
+ }
+
+ fun defaultModelsExpandDepth(defaultModelsExpandDepth: Int) = apply {
+ this.defaultModelsExpandDepth = defaultModelsExpandDepth
+ }
+
+ fun defaultModelExpandDepth(defaultModelExpandDepth: Int) = apply {
+ this.defaultModelExpandDepth = defaultModelExpandDepth
+ }
+
+ fun defaultModelRendering(defaultModelRendering: String) = apply {
+ this.defaultModelRendering = defaultModelRendering
+ }
+
+ fun displayRequestDuration(displayRequestDuration: Boolean) = apply {
+ this.displayRequestDuration = displayRequestDuration
+ }
+
+ fun docExpansion(docExpansion: String) = apply {
+ this.docExpansion = docExpansion
+ }
+
+ fun filter(filter: Boolean) = apply {
+ this.filter = filter
+ }
+
+ fun maxDisplayedTags(maxDisplayedTags: Int) = apply {
+ this.maxDisplayedTags = maxDisplayedTags
+ }
+
+ fun operationsSorter(operationsSorter: JsFunction) = apply {
+ this.operationsSorter = operationsSorter
+ }
+
+ fun tagsSorter(tagsSorter: JsFunction) = apply {
+ this.tagsSorter = tagsSorter
+ }
+
+ fun showExtensions(showExtensions: Boolean) = apply {
+ this.showExtensions = showExtensions
+ }
+
+ fun showCommonExtensions(showCommonExtensions: Boolean) = apply {
+ this.showCommonExtensions = showCommonExtensions
+ }
+
+ fun useUnsafeMarkdown(useUnsafeMarkdown: Boolean) = apply {
+ this.useUnsafeMarkdown = useUnsafeMarkdown
+ }
+
+ fun syntaxHighlight(syntaxHighlight: SyntaxHighlight) = apply {
+ this.syntaxHighlight = syntaxHighlight
+ }
+
+ fun build() = SwaggerUiConfiguration(
+ disabled = disabled,
+ configUrl = configUrl,
+ spec = spec,
+ url = url,
+ urls = urls,
+ deepLinking = deepLinking,
+ displayOperationId = displayOperationId,
+ defaultModelsExpandDepth = defaultModelsExpandDepth,
+ defaultModelExpandDepth = defaultModelExpandDepth,
+ defaultModelRendering = defaultModelRendering,
+ displayRequestDuration = displayRequestDuration,
+ docExpansion = docExpansion,
+ filter = filter,
+ maxDisplayedTags = maxDisplayedTags,
+ operationsSorter = operationsSorter,
+ tagsSorter = tagsSorter,
+ showExtensions = showExtensions,
+ showCommonExtensions = showCommonExtensions,
+ useUnsafeMarkdown = useUnsafeMarkdown,
+ syntaxHighlight = syntaxHighlight,
+ oauth2RedirectUrl = oauth2RedirectUrl,
+ requestInterceptor = requestInterceptor,
+ responseInterceptor = responseInterceptor,
+ showMutatedRequest = showMutatedRequest,
+ supportedSubmitMethods = supportedSubmitMethods,
+ validatorUrl = validatorUrl,
+ withCredentials = withCredentials,
+ modelPropertyMacro = modelPropertyMacro,
+ parameterMacro = parameterMacro,
+ persistAuthorization = persistAuthorization,
+ layout = layout,
+ presets = presets,
+ plugins = plugins
+ )
+}
diff --git a/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/SwaggerUiConfiguration.kt b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/SwaggerUiConfiguration.kt
new file mode 100644
index 0000000..682d633
--- /dev/null
+++ b/src/main/kotlin/com/muliyul/dropwizard/swagger/ui/configuration/SwaggerUiConfiguration.kt
@@ -0,0 +1,91 @@
+package com.muliyul.dropwizard.swagger.ui.configuration
+
+import com.fasterxml.jackson.annotation.*
+import com.muliyul.dropwizard.swagger.ui.*
+
+/**
+ * @see Swagger-UI configuration
+ */
+@JsonInclude(value = JsonInclude.Include.NON_NULL)
+class SwaggerUiConfiguration(
+ val disabled: Boolean = false,
+
+ // Core
+ val configUrl: String? = null,
+ val spec: Map? = null,
+ url: JsStringOrString = """window.location.origin + '/openapi'""",
+ val urls: List? = null,
+
+ // Display
+ val deepLinking: Boolean? = null,
+ val displayOperationId: Boolean? = null,
+ val defaultModelsExpandDepth: Int? = null,
+ val defaultModelExpandDepth: Int? = null,
+ val defaultModelRendering: String? = null,
+ val displayRequestDuration: Boolean? = null,
+ val docExpansion: String? = null,
+ val filter: Boolean? = null,
+ val maxDisplayedTags: Int? = null,
+ @field:JsonRawValue
+ val operationsSorter: JsFunction? = null,
+ @field:JsonRawValue
+ val tagsSorter: JsFunction? = null,
+ val showExtensions: Boolean? = null,
+ val showCommonExtensions: Boolean? = null,
+ val useUnsafeMarkdown: Boolean? = null,
+ val syntaxHighlight: SyntaxHighlight? = null,
+
+ // Network
+ val oauth2RedirectUrl: String? = null,
+ @field:JsonRawValue
+ val requestInterceptor: JsFunction? = null,
+// TODO: these break the UI for some reason. not supported in the meantime.
+// @field:JsonProperty("request.curlOptions")
+// @field:JsonRawValue
+// val curlOptions: JsOrString = JS_UNDEFINED,
+ @field:JsonRawValue
+ val responseInterceptor: JsFunction? = null,
+ val showMutatedRequest: Boolean? = null,
+ val supportedSubmitMethods: List? = null,
+ val validatorUrl: String? = null,
+ val withCredentials: Boolean? = null,
+
+ // Macros
+ @field:JsonRawValue
+ val modelPropertyMacro: JsFunction? = null,
+ @field:JsonRawValue
+ val parameterMacro: JsFunction? = null,
+
+ // Authorization
+ val persistAuthorization: Boolean? = null,
+
+ //Plugin system
+ val layout: String = "StandaloneLayout",
+ @field:JsonRawValue
+ val presets: JsArrayOfObjects = """[
+ SwaggerUIBundle.presets.apis,
+ SwaggerUIStandalonePreset
+ ]""",
+ @field:JsonRawValue
+ val plugins: JsArrayOfObjects? = null
+// = """[
+// SwaggerUIBundle.plugins.DownloadUrl
+// ]"""
+) {
+ @Suppress("unused")
+ @field:JsonProperty("dom_id")
+ private val domId: String = "#swagger-ui"
+
+ @field:JsonRawValue
+ val url = if (url.contains("""["']""".toRegex())) {
+ //probably raw js. use as is.
+ url
+ }
+ // otherwise probably a path/url. wrap in quotes.
+ else "'$url'"
+
+ companion object {
+ @JvmStatic
+ fun builder() = Builder()
+ }
+}
diff --git a/src/test/java/com/muliyul/dropwizard/swagger/TestJavaApplication.java b/src/test/java/com/muliyul/dropwizard/swagger/TestJavaApplication.java
new file mode 100644
index 0000000..d699156
--- /dev/null
+++ b/src/test/java/com/muliyul/dropwizard/swagger/TestJavaApplication.java
@@ -0,0 +1,24 @@
+package com.muliyul.dropwizard.swagger;
+
+import com.muliyul.dropwizard.swagger.bundles.auth.AuthBundle;
+import io.dropwizard.Application;
+import io.dropwizard.configuration.ResourceConfigurationSourceProvider;
+import io.dropwizard.setup.Bootstrap;
+import io.dropwizard.setup.Environment;
+
+public class TestJavaApplication extends Application {
+ public static void main(String[] args) throws Exception {
+ new TestKotlinApplication().run(args);
+ }
+
+ @Override
+ public void initialize(Bootstrap bootstrap) {
+ bootstrap.setConfigurationSourceProvider(new ResourceConfigurationSourceProvider());
+ bootstrap.addBundle(new SwaggerBundle<>());
+ bootstrap.addBundle(new AuthBundle<>());
+ }
+
+ @Override
+ public void run(TestJavaConfiguration configuration, Environment environment) {
+ }
+}
diff --git a/src/test/java/com/muliyul/dropwizard/swagger/TestJavaConfiguration.java b/src/test/java/com/muliyul/dropwizard/swagger/TestJavaConfiguration.java
new file mode 100644
index 0000000..a031a97
--- /dev/null
+++ b/src/test/java/com/muliyul/dropwizard/swagger/TestJavaConfiguration.java
@@ -0,0 +1,17 @@
+package com.muliyul.dropwizard.swagger;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.muliyul.dropwizard.swagger.ui.configuration.SwaggerUiConfiguration;
+import io.dropwizard.Configuration;
+import org.jetbrains.annotations.Nullable;
+
+public class TestJavaConfiguration extends Configuration implements SwaggerBundleConfiguration {
+ @JsonProperty("swagger-ui")
+ private SwaggerUiConfiguration swaggerUiConfiguration = null;
+
+ @Nullable
+ @Override
+ public SwaggerUiConfiguration getSwaggerUiConfiguration() {
+ return swaggerUiConfiguration;
+ }
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/TestKotlinApplication.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/TestKotlinApplication.kt
new file mode 100644
index 0000000..7b101ab
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/TestKotlinApplication.kt
@@ -0,0 +1,24 @@
+package com.muliyul.dropwizard.swagger
+
+import com.muliyul.dropwizard.swagger.bundles.auth.*
+import io.dropwizard.*
+import io.dropwizard.configuration.*
+import io.dropwizard.setup.*
+
+class TestKotlinApplication : Application() {
+ override fun initialize(bootstrap: Bootstrap) {
+ bootstrap.configurationSourceProvider = ResourceConfigurationSourceProvider()
+ bootstrap.addBundle(SwaggerBundle())
+ bootstrap.addBundle(AuthBundle())
+ }
+
+ override fun run(configuration: TestKotlinConfiguration, environment: Environment) {
+ environment.jersey().register(TestResource())
+ }
+
+ companion object {
+ @JvmStatic
+ fun main(args: Array) = TestKotlinApplication().run(*args)
+ }
+}
+
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/TestKotlinConfiguration.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/TestKotlinConfiguration.kt
new file mode 100644
index 0000000..082c666
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/TestKotlinConfiguration.kt
@@ -0,0 +1,12 @@
+package com.muliyul.dropwizard.swagger
+
+import com.fasterxml.jackson.annotation.*
+import com.muliyul.dropwizard.swagger.ui.configuration.*
+import io.dropwizard.*
+
+class TestKotlinConfiguration(
+// @field:JsonProperty("swagger")
+// override val swaggerConfiguration: SwaggerConfiguration? = null,
+ @field:JsonProperty("swagger-ui")
+ override val swaggerUiConfiguration: SwaggerUiConfiguration? = null
+) : Configuration(), SwaggerBundleConfiguration
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/TestResource.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/TestResource.kt
new file mode 100644
index 0000000..2596424
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/TestResource.kt
@@ -0,0 +1,23 @@
+package com.muliyul.dropwizard.swagger
+
+import io.dropwizard.auth.*
+import io.swagger.v3.oas.annotations.security.*
+import javax.ws.rs.*
+import javax.ws.rs.core.*
+
+@Path("/")
+class TestResource {
+ @GET
+ @Path("/sanity")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ fun sanity() = "Sanity check!"
+
+ @GET
+ @Path("/greeting")
+ @Consumes(MediaType.WILDCARD)
+ @Produces(MediaType.TEXT_PLAIN)
+ @SecurityRequirement(name = "api_key")
+ fun greeting(@Auth user: User) = "Hello ${user.name}!"
+}
+
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/User.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/User.kt
new file mode 100644
index 0000000..a5907c3
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/User.kt
@@ -0,0 +1,9 @@
+package com.muliyul.dropwizard.swagger
+
+import java.security.*
+
+class User(
+ private val username: String
+) : Principal {
+ override fun getName(): String = username
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/AbstractAuthFilter.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/AbstractAuthFilter.kt
new file mode 100644
index 0000000..dc707bf
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/AbstractAuthFilter.kt
@@ -0,0 +1,31 @@
+package com.muliyul.dropwizard.swagger.bundles.auth
+
+import io.dropwizard.auth.*
+import org.slf4j.*
+import java.security.*
+import javax.ws.rs.*
+import javax.ws.rs.container.*
+
+@Suppress("unused")
+abstract class AbstractAuthFilter(
+ private val scheme: String
+) : AuthFilter(), CredentialsProvider {
+
+ private val logger = LoggerFactory.getLogger(AbstractAuthFilter::class.java)
+
+ protected fun throwUnauthorizedException(): Nothing =
+ throw WebApplicationException(unauthorizedHandler.buildResponse(prefix, realm))
+
+ override fun filter(requestContext: ContainerRequestContext) {
+ val credentials = try {
+ retrieveCredentials(requestContext)
+ } catch (e: Throwable) {
+ logger.error("Exception thrown while trying to retrieve credentials!", e)
+ throwUnauthorizedException()
+ }
+ if (!authenticate(requestContext, credentials, scheme)) {
+ throwUnauthorizedException()
+ }
+ }
+
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/ApiKeyCredentialAuthFilter.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/ApiKeyCredentialAuthFilter.kt
new file mode 100644
index 0000000..d472b20
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/ApiKeyCredentialAuthFilter.kt
@@ -0,0 +1,15 @@
+package com.muliyul.dropwizard.swagger.bundles.auth
+
+import com.muliyul.dropwizard.swagger.*
+import javax.ws.rs.container.*
+
+class ApiKeyCredentialAuthFilter : AbstractAuthFilter("apiKey") {
+
+ override fun retrieveCredentials(requestContext: ContainerRequestContext): String =
+ requestContext.getHeaderString("api_key") ?: throwUnauthorizedException()
+
+ class Builder : GenericAuthFilterBuilder() {
+ override fun newInstance(): ApiKeyCredentialAuthFilter = ApiKeyCredentialAuthFilter()
+ }
+
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/AuthBundle.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/AuthBundle.kt
new file mode 100644
index 0000000..25c6602
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/AuthBundle.kt
@@ -0,0 +1,65 @@
+package com.muliyul.dropwizard.swagger.bundles.auth
+
+import com.muliyul.dropwizard.swagger.*
+import com.muliyul.dropwizard.swagger.ext.*
+import io.dropwizard.*
+import io.dropwizard.auth.*
+import io.dropwizard.auth.basic.*
+import io.dropwizard.auth.chained.*
+import io.dropwizard.auth.oauth.*
+import io.dropwizard.setup.*
+import org.glassfish.jersey.server.filter.*
+
+
+class AuthBundle : ConfiguredBundle {
+
+ override fun run(configuration: C, environment: Environment) {
+ environment.jersey().run {
+ val basicAuthFilter = BasicCredentialAuthFilter.Builder()
+ .setAuthenticator {
+ when {
+ it.password.contains("secret") -> User("basic-user-${it.username}")
+ else -> null
+ }.toOptional()
+ }
+ .setPrefix("Basic")
+ .buildAuthFilter()
+
+ val oauthAuthFilter = OAuthCredentialAuthFilter.Builder()
+ .setAuthenticator {
+ when {
+ it.contains("secret") -> User("oauth-user-$it")
+ else -> null
+ }.toOptional()
+ }
+ .setPrefix("Bearer")
+ .buildAuthFilter()
+
+ val apiKeyAuthFilter = ApiKeyCredentialAuthFilter.Builder()
+ .setAuthenticator {
+ when {
+ it.contains("secret") -> User("api_key-user-$it")
+ else -> null
+ }.toOptional()
+ }
+ .setPrefix("Api")
+ .buildAuthFilter()
+
+
+ register(RolesAllowedDynamicFeature::class.java)
+ register(
+ AuthDynamicFeature(
+ ChainedAuthFilter(
+ listOf(
+ apiKeyAuthFilter,
+ basicAuthFilter,
+ oauthAuthFilter
+ )
+ )
+ )
+ )
+ register(AuthValueFactoryProvider.Binder(User::class.java))
+ }
+ }
+
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/CredentialsProvider.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/CredentialsProvider.kt
new file mode 100644
index 0000000..734143c
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/CredentialsProvider.kt
@@ -0,0 +1,7 @@
+package com.muliyul.dropwizard.swagger.bundles.auth
+
+import javax.ws.rs.container.*
+
+fun interface CredentialsProvider {
+ fun retrieveCredentials(requestContext: ContainerRequestContext): C
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/GenericAuthFilterBuilder.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/GenericAuthFilterBuilder.kt
new file mode 100644
index 0000000..82da366
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/bundles/auth/GenericAuthFilterBuilder.kt
@@ -0,0 +1,7 @@
+package com.muliyul.dropwizard.swagger.bundles.auth
+
+import io.dropwizard.auth.*
+import java.security.*
+
+abstract class GenericAuthFilterBuilder> :
+ AuthFilter.AuthFilterBuilder()
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/ext/Optional.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/ext/Optional.kt
new file mode 100644
index 0000000..dc9dbcb
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/ext/Optional.kt
@@ -0,0 +1,5 @@
+package com.muliyul.dropwizard.swagger.ext
+
+import java.util.*
+
+fun T?.toOptional() = Optional.ofNullable(this)
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/ext/Selenium.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/ext/Selenium.kt
new file mode 100644
index 0000000..849feb3
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/ext/Selenium.kt
@@ -0,0 +1,17 @@
+package com.muliyul.dropwizard.swagger.ext
+
+import org.openqa.selenium.*
+import org.openqa.selenium.support.ui.*
+
+fun WebDriver.wait(timeoutInSeconds: Long) = WebDriverWait(this, timeoutInSeconds)
+
+fun WebDriverWait.untilError(block: (WebDriver) -> T): T = try {
+ until { block(it) }
+} catch (e: Throwable) {
+ throw Error(e)
+}
+
+val WebDriver.origin
+ get() = currentUrl.substring(0, currentUrl.indexOf("/", "https://".length))
+
+fun WebDriver.relativeGet(path: String) = get(currentUrl + path.removePrefix("/"))
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/integration/SwaggerIntegrationTest.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/integration/SwaggerIntegrationTest.kt
new file mode 100644
index 0000000..7884364
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/integration/SwaggerIntegrationTest.kt
@@ -0,0 +1,41 @@
+package com.muliyul.dropwizard.swagger.integration
+
+import com.muliyul.dropwizard.swagger.*
+import io.dropwizard.testing.*
+import io.dropwizard.testing.junit5.*
+import io.github.bonigarcia.seljup.*
+import org.junit.jupiter.api.*
+import org.junit.jupiter.api.extension.*
+import org.junit.jupiter.api.extension.Extensions
+import org.openqa.selenium.chrome.*
+import kotlin.test.Test
+
+@Extensions(
+ ExtendWith(DropwizardExtensionsSupport::class),
+ ExtendWith(SeleniumJupiter::class)
+)
+@Disabled
+class SwaggerIntegrationTest(
+ @DockerBrowser(type = BrowserType.CHROME)
+ private val webDriver: ChromeDriver
+) {
+ companion object {
+ @JvmStatic
+ private val ext = DropwizardAppExtension(
+ TestKotlinApplication::class.java,
+ "config-test.yaml",
+ ConfigOverride.randomPorts()
+ )
+ }
+
+ @BeforeEach
+ fun beforeEach() {
+ webDriver.get("http://localhost:${ext.localPort}")
+ }
+
+ @Test
+ fun `should expose default endpoint`() {
+ SwaggerUiPage(webDriver)
+ .expandSanity()
+ }
+}
diff --git a/src/test/kotlin/com/muliyul/dropwizard/swagger/integration/SwaggerUiPage.kt b/src/test/kotlin/com/muliyul/dropwizard/swagger/integration/SwaggerUiPage.kt
new file mode 100644
index 0000000..74adb91
--- /dev/null
+++ b/src/test/kotlin/com/muliyul/dropwizard/swagger/integration/SwaggerUiPage.kt
@@ -0,0 +1,32 @@
+package com.muliyul.dropwizard.swagger.integration
+
+import com.muliyul.dropwizard.swagger.ext.*
+import org.openqa.selenium.*
+import org.openqa.selenium.remote.*
+import org.openqa.selenium.support.*
+import org.openqa.selenium.support.ui.*
+
+
+class SwaggerUiPage(
+ private val webDriver: RemoteWebDriver
+) : LoadableComponent() {
+ @FindBy(id = "operations-default-sanity")
+ private lateinit var sanityResource: WebElement
+
+ init {
+ PageFactory.initElements(webDriver, this)
+ get()
+ }
+
+ override fun load() {
+ webDriver.relativeGet("/swagger")
+ }
+
+ override fun isLoaded() {
+ webDriver.wait(5).untilError { sanityResource.isDisplayed }
+ }
+
+ fun expandSanity() {
+ sanityResource.click()
+ }
+}
diff --git a/src/test/resources/config-test.yaml b/src/test/resources/config-test.yaml
new file mode 100644
index 0000000..4cd975b
--- /dev/null
+++ b/src/test/resources/config-test.yaml
@@ -0,0 +1,39 @@
+server:
+ applicationConnectors:
+ - type: http
+ port: 8080
+ adminConnectors:
+ - type: http
+ port: 8081
+
+#swagger:
+# prettyPrint: true
+# cacheTTL: 0
+# openAPI:
+# info:
+# version: '1.0'
+# title: Swagger Pet Sample App Config File
+# description: 'This is a sample server Petstore server. You can find out more
+# about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net,
+# #swagger](http://swagger.io/irc/). For this sample, you can use the api key
+# `special-key` to test the authorization filters.'
+# termsOfService: http://swagger.io/terms/
+# contact:
+# email: apiteam@swagger.io
+# license:
+# name: Apache 2.0
+# url: http://www.apache.org/licenses/LICENSE-2.0.html
+# components:
+# securitySchemes:
+# petstore_auth:
+# type: oauth2
+# flows:
+# implicit:
+# authorizationUrl: http://petstore.swagger.io/oauth/dialog
+# scopes:
+# write:pets: modify pets in your account
+# read:pets: read your pets
+# api_key:
+# type: apiKey
+# name: api_key
+# in: header
diff --git a/src/test/resources/junit-platform.properties b/src/test/resources/junit-platform.properties
new file mode 100644
index 0000000..dd62250
--- /dev/null
+++ b/src/test/resources/junit-platform.properties
@@ -0,0 +1,2 @@
+junit.jupiter.execution.parallel.enabled=true
+junit.jupiter.execution.parallel.mode.default=concurrent
diff --git a/src/test/resources/openapi.yaml b/src/test/resources/openapi.yaml
new file mode 100644
index 0000000..aa35eed
--- /dev/null
+++ b/src/test/resources/openapi.yaml
@@ -0,0 +1,37 @@
+prettyPrint: true
+cacheTTL: 30000
+openAPI:
+ info:
+ title: Swagger Petstore - OpenAPI 3.0
+ description: "This is a sample Pet Store Server based on the OpenAPI 3.0 specification.\
+ \ You can find out more about\nSwagger at [http://swagger.io](http://swagger.io).\
+ \ In the third iteration of the pet store, we've switched to the design first\
+ \ approach!\nYou can now help us improve the API whether it's by making changes\
+ \ to the definition itself or to the code.\nThat way, with time, we can improve\
+ \ the API in general, and expose some of the new features in OAS3.\n\nSome useful\
+ \ links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n\
+ - [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/com.muliyul.dropwizard.swagger.resources/openapi.yaml)"
+ termsOfService: http://swagger.io/terms/
+ contact:
+ email: apiteam@swagger.io
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+ version: 1.0.5
+ externalDocs:
+ description: Find out more about Swagger
+ url: http://swagger.io
+ components:
+ securitySchemes:
+ petstore_auth:
+ type: oauth2
+ flows:
+ implicit:
+ authorizationUrl: https://petstore3.swagger.io/oauth/authorize
+ scopes:
+ write:pets: modify pets in your account
+ read:pets: read your pets
+ api_key:
+ type: apiKey
+ name: api_key
+ in: header
diff --git a/src/test/resources/selenium-jupiter.properties b/src/test/resources/selenium-jupiter.properties
new file mode 100644
index 0000000..cd157b0
--- /dev/null
+++ b/src/test/resources/selenium-jupiter.properties
@@ -0,0 +1,98 @@
+sel.jup.vnc=false
+sel.jup.vnc.screen.resolution=1920x1080x24
+sel.jup.vnc.create.redirect.html.page=false
+sel.jup.vnc.export=vnc.session.url
+sel.jup.recording=false
+sel.jup.recording.when.failure=false
+sel.jup.recording.video.screen.size=1024x768
+sel.jup.recording.video.frame.rate=12
+sel.jup.recording.image=selenoid/video-recorder:latest-release
+sel.jup.output.folder=.
+sel.jup.screenshot.at.the.end.of.tests=false
+sel.jup.screenshot.format=base64
+sel.jup.exception.when.no.driver=true
+sel.jup.browser.template.json.file=classpath:browsers.json
+sel.jup.default.browser=chrome-in-docker
+sel.jup.default.version=latest
+sel.jup.default.browser.fallback=chrome,firefox,safari,edge,phantomjs
+sel.jup.default.browser.fallback.version=latest,latest,latest,latest,latest
+sel.jup.remote.webdriver.wait.timeout.sec=20
+sel.jup.remote.webdriver.poll.time.sec=2
+sel.jup.ttl.sec=86400
+sel.jup.wdm.use.preferences=true
+
+sel.jup.browser.list.from.docker.hub=true
+sel.jup.browser.session.timeout.duration=1m0s
+sel.jup.browser.list.in.parallel=true
+sel.jup.selenoid.image=aerokube/selenoid:1.10.0
+sel.jup.selenoid.port=4444
+sel.jup.selenoid.vnc.password=selenoid
+sel.jup.selenoid.tmpfs.size=128m
+sel.jup.novnc.image=psharkey/novnc:3.3-t6
+sel.jup.novnc.port=8080
+sel.jup.chrome.image.format=selenoid/vnc:chrome_%s
+sel.jup.chrome.first.version=48.0
+sel.jup.chrome.latest.version=79.0
+sel.jup.chrome.path=/
+sel.jup.chrome.beta.image=elastestbrowsers/chrome:beta
+sel.jup.chrome.beta.path=/wd/hub
+sel.jup.chrome.unstable.image=elastestbrowsers/chrome:unstable
+sel.jup.chrome.unstable.path=/wd/hub
+sel.jup.firefox.image.format=selenoid/vnc:firefox_%s
+sel.jup.firefox.first.version=3.6
+sel.jup.firefox.latest.version=71.0
+sel.jup.firefox.path=/wd/hub
+sel.jup.firefox.beta.image=elastestbrowsers/firefox:beta
+sel.jup.firefox.beta.path=/wd/hub
+sel.jup.firefox.unstable.image=elastestbrowsers/firefox:nightly
+sel.jup.firefox.unstable.path=/wd/hub
+sel.jup.opera.image.format=selenoid/vnc:opera_%s
+sel.jup.opera.first.version=33.0
+sel.jup.opera.latest.version=65.0
+sel.jup.opera.path=/
+sel.jup.opera.binary.path.linux=/usr/bin/opera
+sel.jup.opera.binary.path.win=C:\\Program Files\\Opera\\launcher.exe
+sel.jup.opera.binary.path.mac=/Applications/Opera.app/Contents/MacOS/Opera
+sel.jup.edge.image=windows/edge:%s
+sel.jup.edge.path=/
+sel.jup.edge.latest.version=18
+sel.jup.iexplorer.image=windows/ie:%s
+sel.jup.iexplorer.path=/
+sel.jup.iexplorer.latest.version=11
+
+sel.jup.android.default.version=9.0
+sel.jup.android.image.5.0.1=butomo1989/docker-android-x86-5.0.1:1.5-p6
+sel.jup.android.image.5.1.1=butomo1989/docker-android-x86-5.1.1:1.5-p6
+sel.jup.android.image.6.0=butomo1989/docker-android-x86-6.0:1.5-p6
+sel.jup.android.image.7.0.1=butomo1989/docker-android-x86-7.0:1.5-p6
+sel.jup.android.image.7.1.1=butomo1989/docker-android-x86-7.1.1:1.5-p6
+sel.jup.android.image.8.0=butomo1989/docker-android-x86-8.0:1.5-p6
+sel.jup.android.image.8.1=butomo1989/docker-android-x86-8.1:1.5-p6
+sel.jup.android.image.9.0=butomo1989/docker-android-x86-9.0:1.5-p6
+sel.jup.android.image.genymotion=budtmo/docker-android-genymotion:1.7-p0
+sel.jup.android.novnc.port=6080
+sel.jup.android.appium.port=4723
+sel.jup.android.device.name=Samsung Galaxy S6
+sel.jup.android.device.timeout.sec=200
+sel.jup.android.device.startup.timeout.sec=75
+sel.jup.android.appium.ping.period.sec=10
+sel.jup.android.logging=false
+sel.jup.android.logs.folder=androidLogs
+sel.jup.android.screen.width=1900
+sel.jup.android.screen.height=900
+sel.jup.android.screen.depth=24+32
+
+sel.jup.docker.wait.timeout.sec=20
+sel.jup.docker.poll.time.ms=200
+sel.jup.docker.default.socket=/var/run/docker.sock
+sel.jup.docker.hub.url=https://hub.docker.com/
+sel.jup.docker.stop.timeout.sec=5
+sel.jup.docker.api.version=1.35
+sel.jup.docker.network=bridge
+sel.jup.docker.timezone=Europe/Madrid
+sel.jup.docker.lang=en
+sel.jup.docker.startup.timeout.duration=3m
+
+sel.jup.server.port=4042
+sel.jup.server.path=/wd/hub
+sel.jup.server.timeout.sec=180
diff --git a/update-swagger-ui.sh b/update-swagger-ui.sh
new file mode 100644
index 0000000..d12d4a4
--- /dev/null
+++ b/update-swagger-ui.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env sh
+
+rm -rf tmp
+git clone https://github.com/swagger-api/swagger-ui.git --no-checkout tmp --depth=1
+cd tmp || exit 1
+git config core.sparseCheckout true
+git sparse-checkout init --cone # to fetch only root files
+git sparse-checkout set dist # etc, to list sub-folders to checkout
+git read-tree -mu HEAD
+cd ..
+mkdir -p src/main/resources/static/swagger-ui
+mv tmp/dist/*.png src/main/resources/static/swagger-ui/ || exit 1
+mv tmp/dist/*.css src/main/resources/static/swagger-ui/ || exit 1
+mv tmp/dist/*.html src/main/resources/static/swagger-ui/ || exit 1
+mv tmp/dist/swagger-ui-bundle.* src/main/resources/static/swagger-ui/ || exit 1
+mv tmp/dist/swagger-ui-standalone-preset.* src/main/resources/static/swagger-ui/ || exit 1
+rm -rf tmp