Skip to content

Commit 2f55b12

Browse files
committed
feat: Migrate Handlers factories to RequestHandler
1 parent f9c8b1c commit 2f55b12

7 files changed

Lines changed: 198 additions & 261 deletions

File tree

src/main/java/com/retailsvc/http/Handlers.java

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
package com.retailsvc.http;
22

3+
import static com.retailsvc.http.spec.HttpMethod.GET;
4+
import static com.retailsvc.http.spec.HttpMethod.HEAD;
35
import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
46
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;
57
import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
68
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
7-
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
89
import static java.net.HttpURLConnection.HTTP_OK;
910
import static java.net.HttpURLConnection.HTTP_UNAVAILABLE;
1011

1112
import com.retailsvc.http.internal.ClasspathResourceHandler;
12-
import com.retailsvc.http.internal.MethodLimitedHandler;
1313
import com.retailsvc.http.internal.ProblemDetail;
1414
import com.sun.net.httpserver.HttpHandler;
1515
import java.util.List;
@@ -61,13 +61,12 @@ public static HttpHandler notFoundHandler() {
6161
}
6262

6363
/** Returns 204 No Content on GET/HEAD; 405 with {@code Allow: GET, HEAD} otherwise. */
64-
public static HttpHandler aliveHandler() {
65-
return new MethodLimitedHandler(
66-
exchange -> {
67-
try (exchange) {
68-
exchange.sendResponseHeaders(HTTP_NO_CONTENT, -1);
69-
}
70-
});
64+
public static RequestHandler aliveHandler() {
65+
return req ->
66+
switch (req.method()) {
67+
case GET, HEAD -> Response.empty();
68+
default -> Response.status(HTTP_BAD_METHOD).withHeader("Allow", "GET, HEAD");
69+
};
7170
}
7271

7372
/**
@@ -91,26 +90,24 @@ public static HttpHandler aliveHandler() {
9190
* @param jsonMapper used to encode the wire-shape DTO to bytes
9291
* @param probe supplier of the current {@link HealthOutcome}
9392
*/
94-
public static HttpHandler healthHandler(TypeMapper jsonMapper, Supplier<HealthOutcome> probe) {
93+
public static RequestHandler healthHandler(TypeMapper jsonMapper, Supplier<HealthOutcome> probe) {
9594
Objects.requireNonNull(jsonMapper, "jsonMapper");
9695
Objects.requireNonNull(probe, "probe");
97-
return new MethodLimitedHandler(
98-
exchange -> {
99-
try (exchange) {
100-
HealthOutcome outcome;
101-
try {
102-
outcome = Objects.requireNonNull(probe.get(), "Health probe returned null");
103-
} catch (RuntimeException e) {
104-
LOG.warn("Health probe failed", e);
105-
outcome = new HealthOutcome(false, List.of());
106-
}
107-
byte[] body = jsonMapper.writeTo(toWireShape(outcome));
108-
int status = outcome.up() ? HTTP_OK : HTTP_UNAVAILABLE;
109-
exchange.getResponseHeaders().add("Content-Type", "application/json");
110-
exchange.sendResponseHeaders(status, body.length);
111-
exchange.getResponseBody().write(body);
112-
}
113-
});
96+
return req -> {
97+
if (req.method() != GET && req.method() != HEAD) {
98+
return Response.status(HTTP_BAD_METHOD).withHeader("Allow", "GET, HEAD");
99+
}
100+
HealthOutcome outcome;
101+
try {
102+
outcome = Objects.requireNonNull(probe.get(), "Health probe returned null");
103+
} catch (RuntimeException e) {
104+
LOG.warn("Health probe failed", e);
105+
outcome = new HealthOutcome(false, List.of());
106+
}
107+
byte[] body = jsonMapper.writeTo(toWireShape(outcome));
108+
int status = outcome.up() ? HTTP_OK : HTTP_UNAVAILABLE;
109+
return Response.bytes(status, body, "application/json");
110+
};
114111
}
115112

116113
private static HealthBody toWireShape(HealthOutcome outcome) {
@@ -137,7 +134,18 @@ private record DependencyBody(String id, String status) {}
137134
*
138135
* @param classpathResource absolute classpath path, e.g. {@code /schemas/v1/openapi.yaml}
139136
*/
140-
public static HttpHandler specHandler(String classpathResource) {
141-
return new MethodLimitedHandler(new ClasspathResourceHandler(classpathResource));
137+
public static RequestHandler specHandler(String classpathResource) {
138+
ClasspathResourceHandler resource = new ClasspathResourceHandler(classpathResource);
139+
byte[] bytes = resource.bytes();
140+
String contentType = resource.contentType();
141+
return req ->
142+
switch (req.method()) {
143+
case GET -> Response.bytes(HTTP_OK, bytes, contentType);
144+
case HEAD ->
145+
Response.status(HTTP_OK)
146+
.withContentType(contentType)
147+
.withHeader("Content-Length", String.valueOf(bytes.length));
148+
default -> Response.status(HTTP_BAD_METHOD).withHeader("Allow", "GET, HEAD");
149+
};
142150
}
143151
}

src/main/java/com/retailsvc/http/internal/ClasspathResourceHandler.java

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,14 @@
11
package com.retailsvc.http.internal;
22

3-
import static java.net.HttpURLConnection.HTTP_OK;
4-
5-
import com.sun.net.httpserver.HttpExchange;
6-
import com.sun.net.httpserver.HttpHandler;
73
import java.io.IOException;
84
import java.io.InputStream;
95
import java.util.Locale;
106

117
/**
12-
* Serves bytes loaded eagerly from a classpath resource. Content-Type is inferred from the file
13-
* extension. Throws {@link IllegalArgumentException} if the resource is missing.
8+
* Eagerly-loaded bytes for a classpath resource. Content-Type is inferred from the file extension.
9+
* Throws {@link IllegalArgumentException} if the resource is missing.
1410
*/
15-
public final class ClasspathResourceHandler implements HttpHandler {
11+
public final class ClasspathResourceHandler {
1612

1713
private final byte[] bytes;
1814
private final String contentType;
@@ -30,18 +26,12 @@ public ClasspathResourceHandler(String classpathResource) {
3026
this.contentType = contentTypeFor(classpathResource);
3127
}
3228

33-
@Override
34-
public void handle(HttpExchange exchange) throws IOException {
35-
try (exchange) {
36-
exchange.getResponseHeaders().add("Content-Type", contentType);
37-
if ("HEAD".equals(exchange.getRequestMethod())) {
38-
exchange.getResponseHeaders().add("Content-Length", String.valueOf(bytes.length));
39-
exchange.sendResponseHeaders(HTTP_OK, -1);
40-
return;
41-
}
42-
exchange.sendResponseHeaders(HTTP_OK, bytes.length);
43-
exchange.getResponseBody().write(bytes);
44-
}
29+
public byte[] bytes() {
30+
return bytes;
31+
}
32+
33+
public String contentType() {
34+
return contentType;
4535
}
4636

4737
private static String contentTypeFor(String path) {

src/test/java/com/retailsvc/http/ExtraHandlersIT.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,16 @@
1111
class ExtraHandlersIT extends ServerBaseTest {
1212

1313
@Test
14+
// MIGRATED-IN-TASK-6: re-enable Handlers.aliveHandler() once extraRoute accepts RequestHandler
1415
void aliveExtraReturns204AndBypassesValidation() throws Exception {
16+
com.sun.net.httpserver.HttpHandler alive =
17+
ex -> {
18+
try (ex) {
19+
ex.sendResponseHeaders(204, -1);
20+
}
21+
};
1522
try (var s =
16-
newBuilder()
17-
.spec(spec)
18-
.handlers(Map.of())
19-
.port(0)
20-
.extraRoute("/alive", Handlers.aliveHandler())
21-
.build();
23+
newBuilder().spec(spec).handlers(Map.of()).port(0).extraRoute("/alive", alive).build();
2224
var client = httpClient()) {
2325

2426
var req =
@@ -34,13 +36,23 @@ void aliveExtraReturns204AndBypassesValidation() throws Exception {
3436
}
3537

3638
@Test
39+
// MIGRATED-IN-TASK-6: re-enable Handlers.specHandler() once extraRoute accepts RequestHandler
3740
void specHandlerServesClasspathResource() throws Exception {
41+
byte[] yamlBytes = ExtraHandlersIT.class.getResourceAsStream("/openapi.yaml").readAllBytes();
42+
com.sun.net.httpserver.HttpHandler serveYaml =
43+
ex -> {
44+
try (ex) {
45+
ex.getResponseHeaders().add("Content-Type", "application/yaml");
46+
ex.sendResponseHeaders(200, yamlBytes.length);
47+
ex.getResponseBody().write(yamlBytes);
48+
}
49+
};
3850
try (var s =
3951
newBuilder()
4052
.spec(spec)
4153
.handlers(Map.of())
4254
.port(0)
43-
.extraRoute("/openapi.yaml", Handlers.specHandler("/openapi.yaml"))
55+
.extraRoute("/openapi.yaml", serveYaml)
4456
.build();
4557
var client = httpClient()) {
4658

Lines changed: 49 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,62 @@
11
package com.retailsvc.http;
22

3+
import static com.retailsvc.http.spec.HttpMethod.GET;
4+
import static com.retailsvc.http.spec.HttpMethod.HEAD;
5+
import static com.retailsvc.http.spec.HttpMethod.POST;
36
import static org.assertj.core.api.Assertions.assertThat;
47
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5-
import static org.mockito.Mockito.mock;
6-
import static org.mockito.Mockito.verify;
7-
import static org.mockito.Mockito.when;
8-
9-
import com.sun.net.httpserver.Headers;
10-
import com.sun.net.httpserver.HttpExchange;
11-
import java.io.ByteArrayOutputStream;
12-
import java.io.IOException;
8+
9+
import com.retailsvc.http.spec.HttpMethod;
10+
import java.util.Map;
11+
import java.util.function.UnaryOperator;
1312
import org.junit.jupiter.api.Test;
1413

1514
class HandlersTest {
1615

17-
@Test
18-
void aliveHandlerReturns204OnGet() throws IOException {
19-
HttpExchange ex = newExchange("GET");
20-
Handlers.aliveHandler().handle(ex);
21-
verify(ex).sendResponseHeaders(204, -1);
16+
private static final UnaryOperator<String> NO_HEADERS = name -> null;
17+
18+
private static Request request(HttpMethod method) {
19+
return new Request(new byte[0], null, null, null, Map.of(), null, NO_HEADERS, Map.of(), method);
2220
}
2321

2422
@Test
25-
void aliveHandlerReturns204OnHead() throws IOException {
26-
HttpExchange ex = newExchange("HEAD");
27-
Handlers.aliveHandler().handle(ex);
28-
verify(ex).sendResponseHeaders(204, -1);
23+
void aliveHandlerReturns204OnGet() {
24+
Response resp = Handlers.aliveHandler().handle(request(GET));
25+
26+
assertThat(resp.status()).isEqualTo(204);
27+
assertThat(resp.body()).isNull();
2928
}
3029

3130
@Test
32-
void aliveHandlerReturns405OnPost() throws IOException {
33-
HttpExchange ex = newExchange("POST");
34-
Headers headers = new Headers();
35-
when(ex.getResponseHeaders()).thenReturn(headers);
36-
Handlers.aliveHandler().handle(ex);
37-
verify(ex).sendResponseHeaders(405, -1);
38-
assertThat(headers.getFirst("Allow")).isEqualTo("GET, HEAD");
31+
void aliveHandlerReturns204OnHead() {
32+
Response resp = Handlers.aliveHandler().handle(request(HEAD));
33+
34+
assertThat(resp.status()).isEqualTo(204);
3935
}
4036

4137
@Test
42-
void specHandlerServesYamlWithInferredContentType() throws IOException {
43-
HttpExchange ex = newExchange("GET");
44-
Headers responseHeaders = new Headers();
45-
when(ex.getResponseHeaders()).thenReturn(responseHeaders);
46-
ByteArrayOutputStream body = new ByteArrayOutputStream();
47-
when(ex.getResponseBody()).thenReturn(body);
48-
49-
Handlers.specHandler("/openapi.yaml").handle(ex);
50-
51-
assertThat(responseHeaders.getFirst("Content-Type")).isEqualTo("application/yaml");
52-
verify(ex)
53-
.sendResponseHeaders(
54-
org.mockito.ArgumentMatchers.eq(200),
55-
org.mockito.ArgumentMatchers.longThat(n -> n > 0));
56-
assertThat(body.toByteArray()).isNotEmpty();
38+
void aliveHandlerReturns405OnPost() {
39+
Response resp = Handlers.aliveHandler().handle(request(POST));
40+
41+
assertThat(resp.status()).isEqualTo(405);
42+
assertThat(resp.headers()).containsEntry("Allow", "GET, HEAD");
5743
}
5844

5945
@Test
60-
void specHandlerInfersJsonContentType() throws IOException {
61-
HttpExchange ex = newExchange("GET");
62-
Headers responseHeaders = new Headers();
63-
when(ex.getResponseHeaders()).thenReturn(responseHeaders);
64-
when(ex.getResponseBody()).thenReturn(new ByteArrayOutputStream());
46+
void specHandlerServesYamlBytesWithInferredContentType() {
47+
Response resp = Handlers.specHandler("/openapi.yaml").handle(request(GET));
6548

66-
Handlers.specHandler("/openapi.json").handle(ex);
49+
assertThat(resp.status()).isEqualTo(200);
50+
assertThat(resp.contentType()).isEqualTo("application/yaml");
51+
assertThat(resp.body()).isInstanceOf(byte[].class);
52+
assertThat((byte[]) resp.body()).isNotEmpty();
53+
}
54+
55+
@Test
56+
void specHandlerInfersJsonContentType() {
57+
Response resp = Handlers.specHandler("/openapi.json").handle(request(GET));
6758

68-
assertThat(responseHeaders.getFirst("Content-Type")).isEqualTo("application/json");
59+
assertThat(resp.contentType()).isEqualTo("application/json");
6960
}
7061

7162
@Test
@@ -76,21 +67,20 @@ void specHandlerThrowsAtConstructionForMissingResource() {
7667
}
7768

7869
@Test
79-
void specHandlerReturns405OnPost() throws IOException {
80-
HttpExchange ex = newExchange("POST");
81-
Headers headers = new Headers();
82-
when(ex.getResponseHeaders()).thenReturn(headers);
70+
void specHandlerReturns405OnPost() {
71+
Response resp = Handlers.specHandler("/openapi.yaml").handle(request(POST));
8372

84-
Handlers.specHandler("/openapi.yaml").handle(ex);
85-
86-
verify(ex).sendResponseHeaders(405, -1);
87-
assertThat(headers.getFirst("Allow")).isEqualTo("GET, HEAD");
73+
assertThat(resp.status()).isEqualTo(405);
74+
assertThat(resp.headers()).containsEntry("Allow", "GET, HEAD");
8875
}
8976

90-
private static HttpExchange newExchange(String method) {
91-
HttpExchange ex = mock(HttpExchange.class);
92-
when(ex.getRequestMethod()).thenReturn(method);
93-
when(ex.getResponseHeaders()).thenReturn(new Headers());
94-
return ex;
77+
@Test
78+
void specHandlerHeadReturnsContentLengthWithoutBody() {
79+
Response resp = Handlers.specHandler("/openapi.yaml").handle(request(HEAD));
80+
81+
assertThat(resp.status()).isEqualTo(200);
82+
assertThat(resp.body()).isNull();
83+
assertThat(resp.headers()).containsKey("Content-Length");
84+
assertThat(Integer.parseInt(resp.headers().get("Content-Length"))).isGreaterThan(0);
9585
}
9686
}

0 commit comments

Comments
 (0)