Skip to content

Commit 1ca2436

Browse files
refactor: SPA app improvements (#10)
* Add functional interface RequestHandler * RequestContext.contentBuffer to ByteArrayOutputStream (avoid multiple Charset actions) * improve ZipServant response creation
1 parent c01d543 commit 1ca2436

10 files changed

Lines changed: 97 additions & 114 deletions

File tree

src/main/kotlin/net/ccbluex/netty/http/HttpConductor.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ import net.ccbluex.netty.http.util.httpNoContent
3535
* @return The response to the request.
3636
*/
3737
internal fun HttpServer.processRequestContext(context: RequestContext) = runCatching {
38-
val content = context.contentBuffer.toString()
38+
val content = context.contentBuffer.toByteArray()
3939
val method = context.httpMethod
4040

4141
logger.debug("Request {}", context)
4242

4343
if (!context.headers["content-length"].isNullOrEmpty() &&
44-
context.headers["content-length"]?.toInt() != content.toByteArray(Charsets.UTF_8).size) {
44+
context.headers["content-length"]?.toInt() != content.size) {
4545
logger.warn("Received incomplete request: $context")
4646
return@runCatching httpBadRequest("Incomplete request")
4747
}
@@ -59,13 +59,13 @@ internal fun HttpServer.processRequestContext(context: RequestContext) = runCatc
5959
path = context.path,
6060
remainingPath = remaining,
6161
method = method,
62-
body = content,
62+
body = content.toString(Charsets.UTF_8),
6363
params = params,
6464
queryParams = context.params,
6565
headers = context.headers
6666
)
6767

68-
return@runCatching node.handleRequest(requestObject)
68+
return@runCatching node.handle(requestObject)
6969
}.getOrElse {
7070
logger.error("Error while processing request object: $context", it)
7171
httpInternalServerError(it.message ?: "Unknown error")

src/main/kotlin/net/ccbluex/netty/http/HttpServerHandler.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,7 @@ internal class HttpServerHandler(private val server: HttpServer) : ChannelInboun
111111
}
112112

113113
// Append content to the buffer
114-
requestContext
115-
.contentBuffer
116-
.append(msg.content().toString(Charsets.UTF_8))
114+
msg.content().readBytes(requestContext.contentBuffer, msg.content().readableBytes())
117115

118116
// If this is the last content, process the request
119117
if (msg is LastHttpContent) {

src/main/kotlin/net/ccbluex/netty/http/model/RequestContext.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ package net.ccbluex.netty.http.model
2121

2222
import io.netty.handler.codec.http.HttpHeaders
2323
import io.netty.handler.codec.http.HttpMethod
24+
import java.io.ByteArrayOutputStream
2425

2526
data class RequestContext(var httpMethod: HttpMethod, var uri: String, var headers: HttpHeaders) {
26-
val contentBuffer = StringBuilder()
27+
val contentBuffer = ByteArrayOutputStream()
2728
val path = uri.substringBefore('?', uri)
2829
val params = getUriParams(uri)
2930
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package net.ccbluex.netty.http.model
2+
3+
import io.netty.handler.codec.http.FullHttpResponse
4+
5+
fun interface RequestHandler {
6+
fun handle(request: RequestObject): FullHttpResponse
7+
}

src/main/kotlin/net/ccbluex/netty/http/rest/FileServant.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ class FileServant(part: String, private val baseFolder: File) : Node(part) {
3737

3838
override val isExecutable = true
3939

40-
override fun handleRequest(requestObject: RequestObject): FullHttpResponse {
41-
val path = requestObject.remainingPath
40+
override fun handle(request: RequestObject): FullHttpResponse {
41+
val path = request.remainingPath
4242
val sanitizedPath = path.replace("..", "")
4343
val file = baseFolder.resolve(sanitizedPath)
4444

src/main/kotlin/net/ccbluex/netty/http/rest/Node.kt

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,7 @@ package net.ccbluex.netty.http.rest
2121

2222
import io.netty.handler.codec.http.FullHttpResponse
2323
import io.netty.handler.codec.http.HttpMethod
24-
import net.ccbluex.netty.http.util.httpFile
25-
import net.ccbluex.netty.http.util.httpForbidden
26-
import net.ccbluex.netty.http.util.httpNotFound
24+
import net.ccbluex.netty.http.model.RequestHandler
2725
import net.ccbluex.netty.http.model.RequestObject
2826
import java.io.File
2927
import java.io.InputStream
@@ -34,7 +32,7 @@ import java.io.InputStream
3432
* @property part The part of the path this node represents.
3533
*/
3634
@Suppress("TooManyFunctions")
37-
open class Node(val part: String) {
35+
open class Node(val part: String) : RequestHandler {
3836

3937
open val isRoot = part.isEmpty()
4038
open val isExecutable = false
@@ -66,7 +64,7 @@ open class Node(val part: String) {
6664
* @param handler The handler function for the route.
6765
* @return The node representing the route.
6866
*/
69-
fun route(path: String, method: HttpMethod, handler: (RequestObject) -> FullHttpResponse) =
67+
fun route(path: String, method: HttpMethod, handler: RequestHandler) =
7068
chain({ Route(it, method, handler) }, *path.asPathArray())
7169

7270
/**
@@ -89,28 +87,28 @@ open class Node(val part: String) {
8987
fun zip(path: String, zipInputStream: InputStream) =
9088
chain({ ZipServant(it, zipInputStream) }, *path.asPathArray())
9189

92-
fun get(path: String, handler: (RequestObject) -> FullHttpResponse)
90+
fun get(path: String, handler: RequestHandler)
9391
= route(path, HttpMethod.GET, handler)
9492

95-
fun post(path: String, handler: (RequestObject) -> FullHttpResponse)
93+
fun post(path: String, handler: RequestHandler)
9694
= route(path, HttpMethod.POST, handler)
9795

98-
fun put(path: String, handler: (RequestObject) -> FullHttpResponse)
96+
fun put(path: String, handler: RequestHandler)
9997
= route(path, HttpMethod.PUT, handler)
10098

101-
fun delete(path: String, handler: (RequestObject) -> FullHttpResponse)
99+
fun delete(path: String, handler: RequestHandler)
102100
= route(path, HttpMethod.DELETE, handler)
103101

104-
fun patch(path: String, handler: (RequestObject) -> FullHttpResponse)
102+
fun patch(path: String, handler: RequestHandler)
105103
= route(path, HttpMethod.PATCH, handler)
106104

107-
fun head(path: String, handler: (RequestObject) -> FullHttpResponse)
105+
fun head(path: String, handler: RequestHandler)
108106
= route(path, HttpMethod.HEAD, handler)
109107

110-
fun options(path: String, handler: (RequestObject) -> FullHttpResponse)
108+
fun options(path: String, handler: RequestHandler)
111109
= route(path, HttpMethod.OPTIONS, handler)
112110

113-
fun trace(path: String, handler: (RequestObject) -> FullHttpResponse)
111+
fun trace(path: String, handler: RequestHandler)
114112
= route(path, HttpMethod.TRACE, handler)
115113

116114
/**
@@ -134,18 +132,15 @@ open class Node(val part: String) {
134132
/**
135133
* Handles an HTTP request.
136134
*
137-
* @param requestObject The request object.
135+
* @param request The request object.
138136
* @return The HTTP response.
139137
*/
140-
open fun handleRequest(requestObject: RequestObject): FullHttpResponse {
141-
error("Node does not implement handleRequest")
142-
}
138+
override fun handle(request: RequestObject): FullHttpResponse = throw NotImplementedError()
143139

144140
/**
145141
* Checks if the node matches a part of the path and HTTP method.
146142
*
147143
* @param part The part of the path.
148-
* @param method The HTTP method.
149144
* @return True if the node matches, false otherwise.
150145
*/
151146
open fun matches(index: Int, part: String) =

src/main/kotlin/net/ccbluex/netty/http/rest/Route.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,9 @@
1919
*/
2020
package net.ccbluex.netty.http.rest
2121

22-
import io.netty.handler.codec.http.FullHttpResponse
2322
import io.netty.handler.codec.http.HttpMethod
24-
import net.ccbluex.netty.http.util.httpFile
25-
import net.ccbluex.netty.http.util.httpForbidden
26-
import net.ccbluex.netty.http.util.httpNotFound
23+
import net.ccbluex.netty.http.model.RequestHandler
2724
import net.ccbluex.netty.http.model.RequestObject
28-
import java.io.File
2925

3026
/**
3127
* Represents a route in the routing tree.
@@ -34,10 +30,10 @@ import java.io.File
3430
* @property method The HTTP method of the route.
3531
* @property handler The handler function for the route.
3632
*/
37-
open class Route(name: String, private val method: HttpMethod, val handler: (RequestObject) -> FullHttpResponse)
33+
open class Route(name: String, private val method: HttpMethod, val handler: RequestHandler)
3834
: Node(name) {
3935
override val isExecutable = true
40-
override fun handleRequest(requestObject: RequestObject) = handler(requestObject)
36+
override fun handle(request: RequestObject) = handler.handle(request)
4137
override fun matchesMethod(method: HttpMethod) =
4238
this.method == method && super.matchesMethod(method)
4339

src/main/kotlin/net/ccbluex/netty/http/rest/ZipServant.kt

Lines changed: 35 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,24 @@
1919
*/
2020
package net.ccbluex.netty.http.rest
2121

22+
import io.netty.buffer.ByteBuf
23+
import io.netty.buffer.Unpooled
24+
import io.netty.handler.codec.http.DefaultFullHttpResponse
25+
import io.netty.handler.codec.http.EmptyHttpHeaders
2226
import io.netty.handler.codec.http.FullHttpResponse
23-
import net.ccbluex.netty.http.util.httpFileStream
27+
import io.netty.handler.codec.http.HttpHeaderNames
28+
import io.netty.handler.codec.http.HttpResponseStatus
29+
import io.netty.handler.codec.http.HttpVersion
30+
import io.netty.handler.codec.http.ReadOnlyHttpHeaders
2431
import net.ccbluex.netty.http.util.httpNotFound
2532
import net.ccbluex.netty.http.model.RequestObject
2633
import org.apache.tika.Tika
27-
import java.io.ByteArrayInputStream
2834
import java.io.InputStream
2935
import java.util.zip.ZipEntry
3036
import java.util.zip.ZipInputStream
3137

38+
private val tika = Tika()
39+
3240
/**
3341
* Represents a zip servant in the routing tree that serves files from a zip archive kept in memory.
3442
*
@@ -48,32 +56,28 @@ class ZipServant(part: String, zipInputStream: InputStream) : Node(part) {
4856
*/
4957
private data class ZipFileEntry(
5058
val name: String,
51-
val data: ByteArray,
52-
val isDirectory: Boolean
59+
val data: ByteBuf,
60+
val isDirectory: Boolean,
5361
) {
54-
override fun equals(other: Any?): Boolean {
55-
if (this === other) return true
56-
if (javaClass != other?.javaClass) return false
57-
58-
other as ZipFileEntry
59-
60-
if (name != other.name) return false
61-
if (!data.contentEquals(other.data)) return false
62-
if (isDirectory != other.isDirectory) return false
63-
64-
return true
65-
}
62+
private val headers =
63+
ReadOnlyHttpHeaders(
64+
false,
65+
HttpHeaderNames.CONTENT_TYPE, tika.detect(name),
66+
HttpHeaderNames.CONTENT_LENGTH, data.readableBytes().toString(),
67+
)
6668

67-
override fun hashCode(): Int {
68-
var result = name.hashCode()
69-
result = 31 * result + data.contentHashCode()
70-
result = 31 * result + isDirectory.hashCode()
71-
return result
69+
fun toResponse(): FullHttpResponse {
70+
return DefaultFullHttpResponse(
71+
HttpVersion.HTTP_1_1,
72+
HttpResponseStatus.OK,
73+
data.duplicate(),
74+
headers,
75+
EmptyHttpHeaders.INSTANCE,
76+
)
7277
}
7378
}
7479

7580
private val zipFiles: Map<String, ZipFileEntry>
76-
private val tika = Tika()
7781

7882
init {
7983
zipFiles = loadZipData(zipInputStream)
@@ -96,10 +100,10 @@ class ZipServant(part: String, zipInputStream: InputStream) : Node(part) {
96100
val isDirectory = entry.isDirectory
97101

98102
if (isDirectory) {
99-
files[name] = ZipFileEntry(name, ByteArray(0), true)
103+
files[name] = ZipFileEntry(name, Unpooled.EMPTY_BUFFER, true)
100104
} else {
101105
val data = zis.readBytes()
102-
files[name] = ZipFileEntry(name, data, false)
106+
files[name] = ZipFileEntry(name, Unpooled.wrappedBuffer(data), false)
103107
}
104108

105109
zis.closeEntry()
@@ -110,8 +114,8 @@ class ZipServant(part: String, zipInputStream: InputStream) : Node(part) {
110114
return files
111115
}
112116

113-
override fun handleRequest(requestObject: RequestObject): FullHttpResponse {
114-
val path = requestObject.remainingPath.removePrefix("/")
117+
override fun handle(request: RequestObject): FullHttpResponse {
118+
val path = request.remainingPath.removePrefix("/")
115119
val cleanPath = path.substringBefore("?")
116120
val sanitizedPath = cleanPath.replace("..", "")
117121

@@ -141,11 +145,7 @@ class ZipServant(part: String, zipInputStream: InputStream) : Node(part) {
141145
// Try to find exact file match first (non-directory)
142146
val exactMatch = findFile(sanitizedPath)
143147
if (exactMatch != null && !exactMatch.isDirectory) {
144-
return httpFileStream(
145-
stream = ByteArrayInputStream(exactMatch.data),
146-
contentType = tika.detect(exactMatch.name),
147-
contentLength = exactMatch.data.size
148-
)
148+
return exactMatch.toResponse()
149149
}
150150

151151
// Handle directory requests or SPA routes
@@ -154,47 +154,31 @@ class ZipServant(part: String, zipInputStream: InputStream) : Node(part) {
154154
sanitizedPath.isEmpty() -> {
155155
val indexEntry = findIndexInDirectory("")
156156
if (indexEntry != null && !indexEntry.isDirectory) {
157-
return httpFileStream(
158-
stream = ByteArrayInputStream(indexEntry.data),
159-
contentType = tika.detect(indexEntry.name),
160-
contentLength = indexEntry.data.size
161-
)
157+
return indexEntry.toResponse()
162158
}
163159
}
164160

165161
// Case 2: Path ends with "/" - explicit directory request
166162
sanitizedPath.endsWith("/") -> {
167163
val indexEntry = findIndexInDirectory(directoryPath)
168164
if (indexEntry != null && !indexEntry.isDirectory) {
169-
return httpFileStream(
170-
stream = ByteArrayInputStream(indexEntry.data),
171-
contentType = tika.detect(indexEntry.name),
172-
contentLength = indexEntry.data.size
173-
)
165+
return indexEntry.toResponse()
174166
}
175167
}
176168

177169
// Case 3: Path contains "#" - SPA route with fragment
178170
fragmentIndex != -1 -> {
179171
val indexEntry = findIndexInDirectory(directoryPath)
180172
if (indexEntry != null && !indexEntry.isDirectory) {
181-
return httpFileStream(
182-
stream = ByteArrayInputStream(indexEntry.data),
183-
contentType = tika.detect(indexEntry.name),
184-
contentLength = indexEntry.data.size
185-
)
173+
return indexEntry.toResponse()
186174
}
187175
}
188176

189177
// Case 4: Check if path is an implicit directory and has index.html
190178
isImplicitDirectory(sanitizedPath) -> {
191179
val indexEntry = findIndexInDirectory(sanitizedPath)
192180
if (indexEntry != null && !indexEntry.isDirectory) {
193-
return httpFileStream(
194-
stream = ByteArrayInputStream(indexEntry.data),
195-
contentType = tika.detect(indexEntry.name),
196-
contentLength = indexEntry.data.size
197-
)
181+
return indexEntry.toResponse()
198182
}
199183
}
200184
}

0 commit comments

Comments
 (0)