Repository: tipsy/javalin (kotlin)

Issue:
javalin-openapi requires Jetty to run
First of all, thank you for this wonderful framework. I absolutely love how grok-able this framework is.

**Actual behavior (the bug)**
`javalin-openapi` depends on code from `jetty` to compile/run, the `org.eclipse.jetty.http.HttpStatus` class in particular.

**Expected behavior**
Since there is a documented way to run javalin in other servlet containers, being able to run openapi there as well. Since openapi and jetty does not really have that much to do with each other, I'd consider this a bug. But I guess you could also consider it a feature request.

**To Reproduce**
Try running the app in tomcat as described in https://javalin.io/2018/11/15/javalin-embedded-example.html, excluding jetty dependencies.

**Additional context**

The `HttpStatus` class seems to *only* be used for finding the message (or reason phrase) for a numeric status code. I tried looking at other ways of doing this - but wanted to raise this issue for discussion since there was no obvious candidate. 

One possible solution is to just create a custom function from it, copying in all the status codes from a suitable source. This would incur some additional code. The good news is that it would only need maintenance when http status codes are added, changed or removed, i.e. approximately never.

Pull request:
- Adds new HttpCode class in javalin which can translate status codes to
messages.
- Excludes Jetty dependencies is javalin-openapi.
- Uses new HttpCode in DocumentedResponse

Fixes #1229

Patch:
diff --git a/javalin-openapi/pom.xml b/javalin-openapi/pom.xml
index a8e63224c..bdbd975ca 100644
--- a/javalin-openapi/pom.xml
+++ b/javalin-openapi/pom.xml
@@ -15,6 +15,16 @@
             <groupId>io.javalin</groupId>
             <artifactId>javalin</artifactId>
             <version>${project.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.eclipse.jetty</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.eclipse.jetty.websocket</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <!-- BEGIN Plugin dependencies -->
diff --git a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt
index 84617ad43..91c83ad7e 100644
--- a/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt
+++ b/javalin-openapi/src/main/java/io/javalin/plugin/openapi/dsl/DocumentedResponse.kt
@@ -1,19 +1,19 @@
 package io.javalin.plugin.openapi.dsl
 
+import io.javalin.http.HttpCode
 import io.javalin.plugin.openapi.external.findSchema
 import io.javalin.plugin.openapi.external.mediaTypeRef
 import io.swagger.v3.oas.models.Components
 import io.swagger.v3.oas.models.media.ArraySchema
 import io.swagger.v3.oas.models.media.ComposedSchema
 import io.swagger.v3.oas.models.responses.ApiResponse
-import org.eclipse.jetty.http.HttpStatus
 
 class DocumentedResponse(
         val status: String,
         val content: List<DocumentedContent>
 )
 
-fun DocumentedResponse.getStatusMessage() = status.toIntOrNull()?.let { HttpStatus.getMessage(it) } ?: ""
+fun DocumentedResponse.getStatusMessage() = status.toIntOrNull()?.let { HttpCode.messageFor(it) } ?: ""
 
 fun ApiResponse.applyDocumentedResponse(documentedResponse: DocumentedResponse) {
     description = description ?: documentedResponse.getStatusMessage()
diff --git a/javalin/src/main/java/io/javalin/http/HttpCode.kt b/javalin/src/main/java/io/javalin/http/HttpCode.kt
new file mode 100644
index 000000000..f944deba6
--- /dev/null
+++ b/javalin/src/main/java/io/javalin/http/HttpCode.kt
@@ -0,0 +1,86 @@
+package io.javalin.http
+
+enum class HttpCode(
+        val status: Int,
+        val message: String
+) {
+
+    CONTINUE(100, "Continue"),
+    SWITCHING_PROTOCOLS(101, "Switching Protocols"),
+    PROCESSING(102, "Processing"),
+    OK(200, "OK"),
+    CREATED(201, "Created"),
+    ACCEPTED(202, "Accepted"),
+    NON_AUTHORITATIVE_INFORMATION(203, "Non Authoritative Information"),
+    NO_CONTENT(204, "No Content"),
+    RESET_CONTENT(205, "Reset Content"),
+    PARTIAL_CONTENT(206, "Partial Content"),
+    MULTI_STATUS(207, "Multi-Status"),
+    MULTIPLE_CHOICES(300, "Multiple Choices"),
+    MOVED_PERMANENTLY(301, "Moved Permanently"),
+    FOUND(302, "Found"),
+    SEE_OTHER(303, "See Other"),
+    NOT_MODIFIED(304, "Not Modified"),
+    USE_PROXY(305, "Use Proxy"),
+    TEMPORARY_REDIRECT(307, "Temporary Redirect"),
+    PERMANENT_REDIRECT(308, "Permanent Redirect"),
+    BAD_REQUEST(400, "Bad Request"),
+    UNAUTHORIZED(401, "Unauthorized"),
+    PAYMENT_REQUIRED(402, "Payment Required"),
+    FORBIDDEN(403, "Forbidden"),
+    NOT_FOUND(404, "Not Found"),
+    METHOD_NOT_ALLOWED(405, "Method Not Allowed"),
+    NOT_ACCEPTABLE(406, "Not Acceptable"),
+    PROXY_AUTHENTICATION_REQUIRED(407, "Proxy Authentication Required"),
+    REQUEST_TIMEOUT(408, "Request Timeout"),
+    CONFLICT(409, "Conflict"),
+    GONE(410, "Gone"),
+    LENGTH_REQUIRED(411, "Length Required"),
+    PRECONDITION_FAILED(412, "Precondition Failed"),
+    PAYLOAD_TOO_LARGE(413, "Payload Too Large"),
+    URI_TOO_LONG(414, "URI Too Long"),
+    UNSUPPORTED_MEDIA_TYPE(415, "Unsupported Media Type"),
+    RANGE_NOT_SATISFIABLE(416, "Range Not Satisfiable"),
+    EXPECTATION_FAILED(417, "Expectation Failed"),
+    IM_A_TEAPOT(418, "I'm a Teapot"),
+    ENHANCE_YOUR_CALM(420, "Enhance your Calm"),
+    MISDIRECTED_REQUEST(421, "Misdirected Request"),
+    UNPROCESSABLE_ENTITY(422, "Unprocessable Entity"),
+    LOCKED(423, "Locked"),
+    FAILED_DEPENDENCY(424, "Failed Dependency"),
+    UPGRADE_REQUIRED(426, "Upgrade Required"),
+    PRECONDITION_REQUIRED(428, "Precondition Required"),
+    TOO_MANY_REQUESTS(429, "Too Many Requests"),
+    REQUEST_HEADER_FIELDS_TOO_LARGE(431, "Request Header Fields Too Large"),
+    UNAVAILABLE_FOR_LEGAL_REASONS(451, "Unavailable for Legal Reason"),
+    INTERNAL_SERVER_ERROR(500, "Server Error"),
+    NOT_IMPLEMENTED(501, "Not Implemented"),
+    BAD_GATEWAY(502, "Bad Gateway"),
+    SERVICE_UNAVAILABLE(503, "Service Unavailable"),
+    GATEWAY_TIMEOUT(504, "Gateway Timeout"),
+    HTTP_VERSION_NOT_SUPPORTED(505, "HTTP Version Not Supported"),
+    INSUFFICIENT_STORAGE(507, "Insufficient Storage"),
+    LOOP_DETECTED(508, "Loop Detected"),
+    NOT_EXTENDED(510, "Not Extended"),
+    NETWORK_AUTHENTICATION_REQUIRED(511, "Network Authentication Required");
+
+    companion object {
+        private val codes = Array<HttpCode?>(512) { null }
+
+        init {
+            values().forEach { code ->
+                codes[code.status] = code
+            }
+        }
+
+        fun codeFor(status: Int): HttpCode? =
+                if (status < 0 || status >= codes.size) {
+                    null
+                } else {
+                    codes[status]
+                }
+
+        fun messageFor(status: Int) = codeFor(status)?.message
+
+    }
+}


Test patch:
diff --git a/javalin/src/test/java/io/javalin/http/HttpCodeTest.kt b/javalin/src/test/java/io/javalin/http/HttpCodeTest.kt
new file mode 100644
index 000000000..12921a58f
--- /dev/null
+++ b/javalin/src/test/java/io/javalin/http/HttpCodeTest.kt
@@ -0,0 +1,22 @@
+package io.javalin.http
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+
+class HttpCodeTest {
+
+    @Test
+    fun `fetching http codes by code should get the right code`() {
+        HttpCode.values().forEach {
+            assertThat(HttpCode.codeFor(it.status)).isEqualTo(it)
+        }
+    }
+
+    @Test
+    fun `fetching http codes by code outside of range should not throw errors`() {
+        assertThat(HttpCode.codeFor(512)).isNull()
+        assertThat(HttpCode.codeFor(-1)).isNull()
+        assertThat(HttpCode.codeFor(542345)).isNull()
+    }
+
+}