From 2629b93c23e7a8f60570e6e354cba74b0223977e Mon Sep 17 00:00:00 2001
From: tursom <tursom@foxmail.com>
Date: Fri, 22 Nov 2019 09:47:54 +0800
Subject: [PATCH] =?UTF-8?q?=E7=8B=AC=E7=AB=8B=E5=87=BA=E8=AF=B7=E6=B1=82?=
 =?UTF-8?q?=E4=B8=8E=E5=93=8D=E5=BA=94=E7=9A=84=20HeaderAdapter?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 build.gradle                                  |  1 -
 .../tursom/web/netty/NettyExceptionContent.kt | 67 ++++++++++++++-----
 .../cn/tursom/web/netty/NettyHttpContent.kt   | 48 +++----------
 .../cn/tursom/web/netty/NettyHttpServer.kt    |  3 +-
 .../web/netty/NettyResponseHeaderAdapter.kt   | 47 +++++++++++++
 .../kotlin/cn/tursom/web/ExceptionContent.kt  | 18 ++---
 .../main/kotlin/cn/tursom/web/HttpContent.kt  | 38 +----------
 .../main/kotlin/cn/tursom/web/HttpHandler.kt  |  1 +
 .../cn/tursom/web/RequestHeaderAdapter.kt     |  9 +++
 .../cn/tursom/web/ResponseHeaderAdapter.kt    | 43 ++++++++++++
 10 files changed, 169 insertions(+), 106 deletions(-)
 create mode 100644 web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyResponseHeaderAdapter.kt
 create mode 100644 web/src/main/kotlin/cn/tursom/web/RequestHeaderAdapter.kt
 create mode 100644 web/src/main/kotlin/cn/tursom/web/ResponseHeaderAdapter.kt

diff --git a/build.gradle b/build.gradle
index 8b77081..e2f2c44 100644
--- a/build.gradle
+++ b/build.gradle
@@ -53,7 +53,6 @@ allprojects {
                 name = "GitHubPackages"
                 url = uri("https://maven.pkg.github.com/tursom/TursomServer")
                 credentials {
-                    println project.findProperty("gpr.user")
                     //username = project.findProperty("gpr.user") ?: System.getenv("USERNAME")
                     username = "tursom"
                     password = project.findProperty("gpr.key") ?: System.getenv("PASSWORD")
diff --git a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyExceptionContent.kt b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyExceptionContent.kt
index 5aab353..c8db82e 100644
--- a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyExceptionContent.kt
+++ b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyExceptionContent.kt
@@ -2,29 +2,60 @@ package cn.tursom.web.netty
 
 import cn.tursom.core.buffer.ByteBuffer
 import cn.tursom.web.ExceptionContent
+import io.netty.buffer.ByteBuf
+import io.netty.buffer.CompositeByteBuf
 import io.netty.buffer.Unpooled
 import io.netty.channel.ChannelHandlerContext
+import io.netty.handler.codec.http.*
 
+@Suppress("MemberVisibilityCanBePrivate")
 class NettyExceptionContent(
-	val ctx: ChannelHandlerContext,
-	override val cause: Throwable
-) : ExceptionContent {
-	override fun write(message: String) {
-		ctx.write(Unpooled.wrappedBuffer(message.toByteArray()))
-	}
+  val ctx: ChannelHandlerContext,
+  override val cause: Throwable
+) : ExceptionContent, NettyResponseHeaderAdapter() {
+  val responseBodyBuf: CompositeByteBuf = ctx.alloc().compositeBuffer()!!
+  var responseStatus: HttpResponseStatus = HttpResponseStatus.INTERNAL_SERVER_ERROR
+  override var responseCode: Int
+    get() = responseStatus.code()
+    set(value) {
+      responseStatus = HttpResponseStatus.valueOf(value)
+    }
 
-	override fun write(bytes: ByteArray, offset: Int, length: Int) {
-		ctx.write(Unpooled.wrappedBuffer(bytes, offset, length))
-	}
+  override fun write(message: String) {
+    responseBodyBuf.addComponent(Unpooled.wrappedBuffer(message.toByteArray()))
+  }
 
-	override fun write(buffer: ByteBuffer) {
-		when (buffer) {
-			is NettyByteBuffer -> ctx.write(buffer.byteBuf)
-			else -> write(buffer.getBytes())
-		}
-	}
+  override fun write(bytes: ByteArray, offset: Int, length: Int) {
+    responseBodyBuf.addComponent(Unpooled.wrappedBuffer(bytes, offset, length))
+  }
 
-	override fun finish() {
-		ctx.flush()
-	}
+  override fun write(buffer: ByteBuffer) {
+    when (buffer) {
+      is NettyByteBuffer -> responseBodyBuf.addComponent(buffer.byteBuf)
+      else -> write(buffer.getBytes())
+    }
+  }
+
+  override fun finish() {
+    finish(responseBodyBuf)
+  }
+
+  fun finish(buf: ByteBuf) = finish(buf, responseStatus)
+  fun finish(buf: ByteBuf, responseCode: HttpResponseStatus) {
+    val response = DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseCode, buf)
+    finish(response)
+  }
+
+  fun finish(response: FullHttpResponse) {
+    val heads = response.headers()
+    addHeaders(
+      heads,
+      mapOf(
+        HttpHeaderNames.CONTENT_TYPE to "${HttpHeaderValues.TEXT_PLAIN}; charset=UTF-8",
+        HttpHeaderNames.CONTENT_LENGTH to response.content().readableBytes(),
+        HttpHeaderNames.CONNECTION to HttpHeaderValues.KEEP_ALIVE
+      )
+    )
+    ctx.writeAndFlush(response)
+  }
 }
\ No newline at end of file
diff --git a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpContent.kt b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpContent.kt
index bb72cb1..ecc1cc2 100644
--- a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpContent.kt
+++ b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpContent.kt
@@ -11,17 +11,13 @@ import io.netty.handler.codec.http.*
 import io.netty.handler.stream.ChunkedFile
 import java.io.File
 import java.io.RandomAccessFile
-import java.util.*
-import kotlin.collections.ArrayList
-import kotlin.collections.component1
-import kotlin.collections.component2
 import kotlin.collections.set
 
 @Suppress("MemberVisibilityCanBePrivate", "unused")
 open class NettyHttpContent(
   val ctx: ChannelHandlerContext,
   val msg: FullHttpRequest
-) : AdvanceHttpContent {
+) : AdvanceHttpContent, NettyResponseHeaderAdapter() {
   override val uri: String by lazy {
     var uri = msg.uri()
     while (uri.contains("//")) {
@@ -38,10 +34,13 @@ open class NettyHttpContent(
   override val cookieMap by lazy { getHeader("Cookie")?.let { decodeCookie(it) } ?: mapOf() }
   override val body = msg.content()?.let { NettyByteBuffer(it) }
 
-  val responseMap = HashMap<String, Any>()
-  val responseListMap = HashMap<String, ArrayList<Any>>()
   //override val responseBody = ByteArrayOutputStream()
-  override var responseCode: Int = 200
+  var responseStatus: HttpResponseStatus = HttpResponseStatus.INTERNAL_SERVER_ERROR
+  override var responseCode: Int
+    get() = responseStatus.code()
+    set(value) {
+      responseStatus = HttpResponseStatus.valueOf(value)
+    }
   override var responseMessage: String? = null
   override val method: String get() = httpMethod.name()
   val chunkedList = ArrayList<ByteBuffer>()
@@ -74,19 +73,6 @@ open class NettyHttpContent(
     (paramMap[key] as ArrayList).add(value)
   }
 
-  override fun setResponseHeader(name: String, value: Any) {
-    responseMap[name] = value
-  }
-
-  override fun addResponseHeader(name: String, value: Any) {
-    val list = responseListMap[name] ?: run {
-      val newList = ArrayList<Any>()
-      responseListMap[name] = newList
-      newList
-    }
-    list.add(value)
-  }
-
   override fun write(message: String) {
     responseBodyBuf.addComponent(Unpooled.wrappedBuffer(message.toByteArray()))
     //responseBody.write(message.toByteArray())
@@ -134,7 +120,7 @@ open class NettyHttpContent(
     }
   }
 
-  fun finish(buf: ByteBuf) = finish(buf, HttpResponseStatus.valueOf(responseCode))
+  fun finish(buf: ByteBuf) = finish(buf, responseStatus)
   fun finish(buf: ByteBuf, responseCode: HttpResponseStatus) {
     val response = DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseCode, buf)
     finish(response)
@@ -153,26 +139,10 @@ open class NettyHttpContent(
     ctx.writeAndFlush(response)
   }
 
-  fun addHeaders(heads: HttpHeaders, defaultHeaders: Map<out CharSequence, Any>) {
-    responseListMap.forEach { (t, u) ->
-      u.forEach {
-        heads.add(t, it)
-      }
-    }
-
-    defaultHeaders.forEach { (t, u) ->
-      heads.set(t, u)
-    }
-
-    responseMap.forEach { (t, u) ->
-      heads.set(t, u)
-    }
-  }
-
   override fun writeChunkedHeader() {
     val response = DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK)
     response.status = if (responseMessage != null) HttpResponseStatus(responseCode, responseMessage)
-    else HttpResponseStatus.valueOf(responseCode)
+    else responseStatus
     val heads = response.headers()
     addHeaders(
       heads, mapOf(
diff --git a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpServer.kt b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpServer.kt
index d73a4dd..9a3b556 100644
--- a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpServer.kt
+++ b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyHttpServer.kt
@@ -51,14 +51,13 @@ class NettyHttpServer(
     .option(ChannelOption.SO_BACKLOG, 1024) // determining the number of connections queued
     .option(ChannelOption.SO_REUSEADDR, true)
     .childOption(ChannelOption.SO_KEEPALIVE, java.lang.Boolean.TRUE)
-  private lateinit var future: ChannelFuture
+  private val future: ChannelFuture = b.bind(port)
 
   init {
     if (autoRun) run()
   }
 
   override fun run() {
-    future = b.bind(port)
     future.sync()
   }
 
diff --git a/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyResponseHeaderAdapter.kt b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyResponseHeaderAdapter.kt
new file mode 100644
index 0000000..b3338b8
--- /dev/null
+++ b/web/netty-web/src/main/kotlin/cn/tursom/web/netty/NettyResponseHeaderAdapter.kt
@@ -0,0 +1,47 @@
+package cn.tursom.web.netty
+
+import cn.tursom.web.ResponseHeaderAdapter
+import io.netty.handler.codec.http.HttpHeaders
+import java.util.HashMap
+
+@Suppress("MemberVisibilityCanBePrivate")
+open class NettyResponseHeaderAdapter : ResponseHeaderAdapter {
+  val responseMap = HashMap<String, Any>()
+  val responseListMap = HashMap<String, ArrayList<Any>>()
+
+  override fun setResponseHeader(name: String, value: Any) {
+    responseMap[name] = value
+    responseListMap.remove(name)
+  }
+
+  override fun addResponseHeader(name: String, value: Any) {
+    val list = responseListMap[name] ?: run {
+      val newList = ArrayList<Any>()
+      responseListMap[name] = newList
+      newList
+    }
+    responseMap[name]?.let {
+      responseMap.remove(name)
+      list.add(it)
+    }
+    list.add(value)
+  }
+
+  protected fun addHeaders(heads: HttpHeaders, defaultHeaders: Map<out CharSequence, Any>) {
+    responseListMap.forEach { (t, u) ->
+      u.forEach {
+        heads.add(t, it)
+      }
+    }
+
+    responseMap.forEach { (t, u) ->
+      heads.set(t, u)
+    }
+
+    defaultHeaders.forEach { (t, u) ->
+      if (!heads.contains(t)) {
+        heads.set(t, u)
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/cn/tursom/web/ExceptionContent.kt b/web/src/main/kotlin/cn/tursom/web/ExceptionContent.kt
index b3457a3..7bc8d62 100644
--- a/web/src/main/kotlin/cn/tursom/web/ExceptionContent.kt
+++ b/web/src/main/kotlin/cn/tursom/web/ExceptionContent.kt
@@ -3,14 +3,14 @@ package cn.tursom.web
 import cn.tursom.core.buffer.ByteBuffer
 
 
-interface ExceptionContent {
-	val cause: Throwable
+interface ExceptionContent : ResponseHeaderAdapter {
+  val cause: Throwable
+  var responseCode: Int
+  fun write(message: String)
+  fun write(bytes: ByteArray, offset: Int = 0, length: Int = bytes.size - offset)
+  fun write(buffer: ByteBuffer) {
+    write(buffer.getBytes())
+  }
 
-	fun write(message: String)
-	fun write(bytes: ByteArray, offset: Int = 0, length: Int = bytes.size - offset)
-	fun write(buffer: ByteBuffer) {
-		write(buffer.getBytes())
-	}
-
-	fun finish()
+  fun finish()
 }
\ No newline at end of file
diff --git a/web/src/main/kotlin/cn/tursom/web/HttpContent.kt b/web/src/main/kotlin/cn/tursom/web/HttpContent.kt
index fa2b8d7..a84a14a 100644
--- a/web/src/main/kotlin/cn/tursom/web/HttpContent.kt
+++ b/web/src/main/kotlin/cn/tursom/web/HttpContent.kt
@@ -10,7 +10,7 @@ import java.io.File
 import java.io.RandomAccessFile
 import java.net.SocketAddress
 
-interface HttpContent {
+interface HttpContent : ResponseHeaderAdapter, RequestHeaderAdapter {
   val uri: String
   var responseCode: Int
   var responseMessage: String?
@@ -24,17 +24,12 @@ interface HttpContent {
       str.substring(1, str.indexOf(':').let { if (it < 1) str.length else it - 1 })
     }
 
-  fun getHeader(header: String): String?
-  fun getHeaders(): List<Map.Entry<String, String>>
-
   fun getParam(param: String): String?
   fun getParams(): Map<String, List<String>>
   fun getParams(param: String): List<String>?
 
   operator fun get(name: String) = (getHeader(name) ?: getParam(name))?.urlDecode
 
-  fun setResponseHeader(name: String, value: Any)
-  fun addResponseHeader(name: String, value: Any)
   operator fun set(name: String, value: Any) = setResponseHeader(name, value)
 
   fun write(message: String)
@@ -115,39 +110,8 @@ interface HttpContent {
 
   fun usingCache() = finish(304)
 
-  fun setCacheTag(tag: Any) = setResponseHeader("Etag", tag)
-  fun getCacheTag(): String? = getHeader("If-None-Match")
-
-  fun cacheControl(
-    cacheControl: CacheControl,
-    maxAge: Int? = null,
-    mustRevalidate: Boolean = false
-  ) = setResponseHeader(
-    "Cache-Control", "$cacheControl${
-  if (maxAge != null && maxAge > 0) ", max-age=$maxAge" else ""}${
-  if (mustRevalidate) ", must-revalidate" else ""
-  }"
-  )
-
   fun getCookie(name: String): Cookie? = cookieMap[name]
 
-  fun addCookie(
-    name: String,
-    value: Any,
-    maxAge: Int = 0,
-    domain: String? = null,
-    path: String? = null,
-    sameSite: SameSite? = null
-  ) = addResponseHeader(
-    "Set-Cookie",
-    "$name=$value${
-    if (maxAge > 0) "; Max-Age=$maxAge" else ""}${
-    if (domain != null) "; Domain=$domain" else ""}${
-    if (path != null) "; Path=$path" else ""}${
-    if (sameSite != null) ": SameSite=$sameSite" else ""
-    }"
-  )
-
   fun setCookie(
     name: String,
     value: Any,
diff --git a/web/src/main/kotlin/cn/tursom/web/HttpHandler.kt b/web/src/main/kotlin/cn/tursom/web/HttpHandler.kt
index f9f4963..d9115c1 100644
--- a/web/src/main/kotlin/cn/tursom/web/HttpHandler.kt
+++ b/web/src/main/kotlin/cn/tursom/web/HttpHandler.kt
@@ -5,6 +5,7 @@ interface HttpHandler<in T : HttpContent, in E : ExceptionContent> {
 
 	fun exception(e: E) {
 		e.cause.printStackTrace()
+		e.finish()
 	}
 
 	operator fun invoke(content: T) {
diff --git a/web/src/main/kotlin/cn/tursom/web/RequestHeaderAdapter.kt b/web/src/main/kotlin/cn/tursom/web/RequestHeaderAdapter.kt
new file mode 100644
index 0000000..75a1a66
--- /dev/null
+++ b/web/src/main/kotlin/cn/tursom/web/RequestHeaderAdapter.kt
@@ -0,0 +1,9 @@
+package cn.tursom.web
+
+interface RequestHeaderAdapter {
+  val requestHost: String? get() = getHeader("Host")
+  fun getHeader(header: String): String?
+  fun getHeaders(): List<Map.Entry<String, String>>
+
+  fun getCacheTag(): String? = getHeader("If-None-Match")
+}
\ No newline at end of file
diff --git a/web/src/main/kotlin/cn/tursom/web/ResponseHeaderAdapter.kt b/web/src/main/kotlin/cn/tursom/web/ResponseHeaderAdapter.kt
new file mode 100644
index 0000000..1cbe068
--- /dev/null
+++ b/web/src/main/kotlin/cn/tursom/web/ResponseHeaderAdapter.kt
@@ -0,0 +1,43 @@
+package cn.tursom.web
+
+import cn.tursom.web.utils.CacheControl
+import cn.tursom.web.utils.SameSite
+
+interface ResponseHeaderAdapter {
+  fun setResponseHeader(name: String, value: Any)
+  fun addResponseHeader(name: String, value: Any)
+
+  fun setCacheTag(tag: Any) = setResponseHeader("Etag", tag)
+
+  fun cacheControl(
+    cacheControl: CacheControl,
+    maxAge: Int? = null,
+    mustRevalidate: Boolean = false
+  ) = setResponseHeader(
+    "Cache-Control", "$cacheControl${
+  if (maxAge != null && maxAge > 0) ", max-age=$maxAge" else ""}${
+  if (mustRevalidate) ", must-revalidate" else ""
+  }"
+  )
+
+  fun addCookie(
+    name: String,
+    value: Any,
+    maxAge: Int = 0,
+    domain: String? = null,
+    path: String? = null,
+    sameSite: SameSite? = null
+  ) = addResponseHeader(
+    "Set-Cookie",
+    "$name=$value${
+    if (maxAge > 0) "; Max-Age=$maxAge" else ""}${
+    if (domain != null) "; Domain=$domain" else ""}${
+    if (path != null) "; Path=$path" else ""}${
+    if (sameSite != null) ": SameSite=$sameSite" else ""
+    }"
+  )
+
+  fun setLanguage(language: String) {
+    setResponseHeader("Content-Language", language)
+  }
+}
\ No newline at end of file