Skip to content

Commit

Permalink
release: 2.0.0
Browse files Browse the repository at this point in the history
- Introduces support for inline URI parameters
- Rewritten path processing
- Fixes websocket channels broadcast and close
  • Loading branch information
1zun4 committed Sep 4, 2024
1 parent d8c458e commit 2818977
Show file tree
Hide file tree
Showing 15 changed files with 717 additions and 166 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Netty HttpServer is a Kotlin-based library for building web REST APIs on top of
To include Netty HttpServer in your project, add the following dependency to your `build.gradle` file:

```gradle
implementation 'com.github.CCBlueX:netty-httpserver:1.0.0'
implementation 'com.github.CCBlueX:netty-httpserver:2.0.0'
```

### Basic Usage
Expand Down
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ val authorName = "ccbluex"
val projectUrl = "https://github.com/ccbluex/netty-httpserver"

group = "net.ccbluex"
version = "1.0.0"
version = "2.0.0"

repositories {
mavenCentral()
Expand All @@ -37,8 +37,8 @@ dependencies {
implementation("org.apache.tika:tika-core:2.9.2")

testImplementation(kotlin("test"))
// https://mvnrepository.com/artifact/org.mockito/mockito-core
testImplementation("org.mockito:mockito-core:5.13.0")
testImplementation("com.squareup.retrofit2:retrofit:2.9.0")
testImplementation("com.squareup.retrofit2:converter-gson:2.9.0")
}

tasks.test {
Expand Down
10 changes: 7 additions & 3 deletions examples/echo-server/src/main/kotlin/EchoServerExample.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import com.google.gson.JsonObject
import io.netty.handler.codec.http.FullHttpResponse
import net.ccbluex.netty.http.HttpServer
import net.ccbluex.netty.http.model.RequestObject
import net.ccbluex.netty.http.util.httpOk

fun main() {
val server = HttpServer()

server.routeController.apply {
post("/echo") { request ->
httpOk(request.asJson<JsonObject>())
}
post("/echo", ::postEcho) // /echo
}

server.start(8080) // Start the server on port 8080
}

fun postEcho(requestObject: RequestObject): FullHttpResponse {
return httpOk(requestObject.asJson<JsonObject>())
}
1 change: 1 addition & 0 deletions examples/file-server/files/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
hello!
File renamed without changes.
69 changes: 66 additions & 3 deletions examples/file-server/src/main/kotlin/FileServerExample.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,78 @@
import com.google.gson.JsonObject
import io.netty.handler.codec.http.FullHttpResponse
import net.ccbluex.netty.http.HttpServer
import net.ccbluex.netty.http.model.RequestObject
import net.ccbluex.netty.http.util.httpBadRequest
import net.ccbluex.netty.http.util.httpOk
import java.io.File

const val FOLDER_NAME = "files"
val folder = File(FOLDER_NAME)

fun main() {
val server = HttpServer()
val folder = File("files")

println("Serving files from: ${folder.absolutePath}")

server.routeController.apply {
// Serve files from the "files" directory
file("/files", folder)
get("/", ::getRoot)
get("/conflicting", ::getConflictingPath)
get("/a/b/c", ::getConflictingPath)
get("/file/:name", ::getFileInformation)
post("/file/:name", ::postFile)

// register file serving at the bottom of the routing tree
// to avoid overwriting other routes
file("/", folder)
}

server.start(8080) // Start the server on port 8080
}

@Suppress("UNUSED_PARAMETER")
fun getRoot(requestObject: RequestObject): FullHttpResponse {
// Count the number of files in the folder
// Walk the folder and count the number of files
return httpOk(JsonObject().apply {
addProperty("path", folder.absolutePath)
addProperty("files", folder.walk().count())
})
}

@Suppress("UNUSED_PARAMETER")
fun getConflictingPath(requestObject: RequestObject): FullHttpResponse {
return httpOk(JsonObject().apply {
addProperty("message", "This is a conflicting path")
})
}

@Suppress("UNUSED_PARAMETER")
fun getFileInformation(requestObject: RequestObject): FullHttpResponse {
val name = requestObject.params["name"] ?: return httpBadRequest("Missing name parameter")
val file = File(folder, name)

if (!file.exists()) {
return httpBadRequest("File not found")
}

return httpOk(JsonObject().apply {
addProperty("name", file.name)
addProperty("size", file.length())
addProperty("lastModified", file.lastModified())
})
}

@Suppress("UNUSED_PARAMETER")
fun postFile(requestObject: RequestObject): FullHttpResponse {
val name = requestObject.params["name"] ?: return httpBadRequest("Missing name parameter")
val file = File(folder, name)

if (file.exists()) {
return httpBadRequest("File already exists")
}

file.writeText(requestObject.body)
return httpOk(JsonObject().apply {
addProperty("message", "File written")
})
}
34 changes: 29 additions & 5 deletions examples/hello-world/src/main/kotlin/HelloWorldExample.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
import com.google.gson.JsonObject
import io.netty.handler.codec.http.FullHttpResponse
import net.ccbluex.netty.http.HttpServer
import net.ccbluex.netty.http.model.RequestObject
import net.ccbluex.netty.http.util.httpOk

fun main() {
val server = HttpServer()

server.routeController.apply {
get("/hello") {
httpOk(JsonObject().apply {
addProperty("message", "Hello, World!")
})
}
get("/", ::getRoot)
get("/hello", ::getHello) // /hello?name=World
get("/hello/:name", ::getHello) // /hello/World
get("/hello/:name/:age", ::getHelloWithAge) // /hello/World/20
}

server.start(8080) // Start the server on port 8080
}

@Suppress("UNUSED_PARAMETER")
fun getRoot(requestObject: RequestObject): FullHttpResponse {
return httpOk(JsonObject().apply {
addProperty("root", true)
})
}

fun getHello(requestObject: RequestObject): FullHttpResponse {
val name = requestObject.params["name"] ?: requestObject.queryParams["name"] ?: "World"
return httpOk(JsonObject().apply {
addProperty("message", "Hello, $name!")
})
}

fun getHelloWithAge(requestObject: RequestObject): FullHttpResponse {
val name = requestObject.params["name"] ?: "World"
val age = requestObject.params["age"] ?: "0"
return httpOk(JsonObject().apply {
addProperty("message", "Hello, $name! You are $age years old.")
})
}

2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ rootProject.name = "netty-httpserver"

include(":examples:hello-world")
include(":examples:echo-server")
include(":examples:file-server")
include(":examples:file-server")
42 changes: 23 additions & 19 deletions src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package net.ccbluex.netty.http
import io.netty.buffer.Unpooled
import io.netty.handler.codec.http.*
import net.ccbluex.netty.http.HttpServer.Companion.logger
import net.ccbluex.netty.http.model.RequestContext
import net.ccbluex.netty.http.util.httpBadRequest
import net.ccbluex.netty.http.util.httpInternalServerError
import net.ccbluex.netty.http.util.httpNotFound
Expand All @@ -30,20 +31,20 @@ import net.ccbluex.netty.http.model.RequestObject
internal class HttpConductor(private val server: HttpServer) {

/**
* Processes the incoming request object and returns the response.
* Processes the incoming request context and returns the response.
*
* @param requestObject The incoming request object.
* @param context The request context to process.
* @return The response to the request.
*/
fun processRequestObject(requestObject: RequestObject) = runCatching {
val context = requestObject.context
fun processRequestContext(context: RequestContext) = runCatching {
val content = context.contentBuffer.toString()
val method = context.httpMethod

logger.debug("Request {}", requestObject)
logger.debug("Request {}", context)

if (!context.headers["content-length"].isNullOrEmpty() &&
context.headers["content-length"]?.toInt() != requestObject.content.toByteArray(Charsets.UTF_8).size) {
logger.warn("Received incomplete request: $requestObject")
context.headers["content-length"]?.toInt() != content.toByteArray(Charsets.UTF_8).size) {
logger.warn("Received incomplete request: $context")
return@runCatching httpBadRequest("Incomplete request")
}

Expand All @@ -63,21 +64,24 @@ internal class HttpConductor(private val server: HttpServer) {
return@runCatching response
}

server.routeController.findRoute(context.path, method)?.let { route ->
logger.debug("Found route {}", route)
return@runCatching route.handler(requestObject)
}
val (node, params, remaining) = server.routeController.processPath(context.path, method) ?:
return@runCatching httpNotFound(context.path, "Route not found")

if (method == HttpMethod.GET) {
server.routeController.findFileServant(context.path)?.let { (fileServant, path) ->
logger.debug("Found file servant {}", fileServant)
return@runCatching fileServant.handleFileRequest(path)
}
}
logger.debug("Found destination {}", node)
val requestObject = RequestObject(
uri = context.uri,
path = context.path,
remainingPath = remaining,
method = method,
body = content,
params = params,
queryParams = context.params,
headers = context.headers
)

httpNotFound(context.path, "Route not found")
return@runCatching node.handleRequest(requestObject)
}.getOrElse {
logger.error("Error while processing request object: $requestObject", it)
logger.error("Error while processing request object: $context", it)
httpInternalServerError(it.message ?: "Unknown error")
}

Expand Down
4 changes: 1 addition & 3 deletions src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.codec.http.websocketx.WebSocketServerHandshakerFactory
import net.ccbluex.netty.http.HttpServer.Companion.logger
import net.ccbluex.netty.http.model.RequestContext
import net.ccbluex.netty.http.model.RequestObject
import net.ccbluex.netty.http.websocket.WebSocketHandler
import java.net.URLDecoder

Expand Down Expand Up @@ -109,11 +108,10 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun

// If this is the last content, process the request
if (msg is LastHttpContent) {
val requestObject = RequestObject(requestContext)
localRequestContext.remove()

val httpConductor = HttpConductor(server)
val response = httpConductor.processRequestObject(requestObject)
val response = httpConductor.processRequestContext(requestContext)
ctx.writeAndFlush(response)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ private fun getUriParams(uri: String): Map<String, String> {
{ v1: String?, v2: String -> v2 })
)
}
return HashMap()

return emptyMap()
}
35 changes: 30 additions & 5 deletions src/main/kotlin/net/ccbluex/netty/http/model/RequestObject.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,38 @@
package net.ccbluex.netty.http.model

import com.google.gson.Gson
import io.netty.handler.codec.http.HttpMethod

data class RequestObject(var context: RequestContext) {
val content = context.contentBuffer.toString()
val params = context.params
/**
* Represents an HTTP request object.
*
* @property uri The full URI of the request.
* @property path The path of the request.
* @property remainingPath The ending part of the path which was not matched by the route.
* @property method The HTTP method of the request.
* @property body The body of the request.
* @property params The inline URI parameters of the request.
* @property queryParams The query parameters of the request.
* @property headers The headers of the request.
*/
data class RequestObject(
val uri: String,
val path: String,
val remainingPath: String,
val method: HttpMethod,
val body: String,
val params: Map<String, String>,
val queryParams: Map<String, String>,
val headers: Map<String, String>
) {

/**
* Converts the body of the request to a JSON object of the specified type.
*
* @return The JSON object of the specified type.
*/
inline fun <reified T> asJson(): T {
return Gson().fromJson(content, T::class.java)
return Gson().fromJson(body, T::class.java)
}

}
}
Loading

0 comments on commit 2818977

Please sign in to comment.