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