-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathResponse.java
More file actions
159 lines (130 loc) · 5.73 KB
/
Copy pathResponse.java
File metadata and controls
159 lines (130 loc) · 5.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package com.retailsvc.http;
import static java.net.HttpURLConnection.HTTP_ACCEPTED;
import static java.net.HttpURLConnection.HTTP_CREATED;
import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
import static java.net.HttpURLConnection.HTTP_OK;
import com.retailsvc.http.internal.BodyWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* The value returned by every {@link RequestHandler}. Carries status, optional body, optional
* content type, and headers. The framework renders it to the underlying {@code HttpExchange} after
* any registered {@link ResponseDecorator}s have transformed it.
*
* <p>Body handling:
*
* <ul>
* <li>{@code null} body → no response body (status only).
* <li>{@code byte[]} body → written verbatim with the supplied content type.
* <li>Streaming body (via {@link #stream(int, String, StreamingBody)} / sized variant) → written
* incrementally.
* <li>Any other object → serialised by the {@link TypeMapper} registered for the response's
* content type (default {@code application/json}).
* </ul>
*/
public record Response(int status, Object body, String contentType, Map<String, String> headers) {
public Response {
headers = headers == null ? Map.of() : Map.copyOf(headers);
}
// -- one-shot, no-body --
/** {@code 204 No Content} with no body. */
public static Response empty() {
return new Response(HTTP_NO_CONTENT, null, null, Map.of());
}
/** Given status, no body. Use for {@code 200 OK} no body, {@code 404}, {@code 405}, etc. */
public static Response status(int status) {
return new Response(status, null, null, Map.of());
}
// -- one-shot, JSON body --
/** {@code 200 OK} with {@code body} serialised as JSON. */
public static Response ok(Object body) {
return new Response(HTTP_OK, body, null, Map.of());
}
/**
* {@code 201 Created} with {@code body} serialised as JSON. Add a {@code Location} header for the
* new resource via {@link #withLocation(String)}.
*/
public static Response created(Object body) {
return new Response(HTTP_CREATED, body, null, Map.of());
}
/** {@code 202 Accepted} with no body. Use for fire-and-forget async work. */
public static Response accepted() {
return new Response(HTTP_ACCEPTED, null, null, Map.of());
}
/** {@code 202 Accepted} with {@code body} serialised as JSON (typically a job/poll URL). */
public static Response accepted(Object body) {
return new Response(HTTP_ACCEPTED, body, null, Map.of());
}
/** {@code 404 Not Found} with no body. */
public static Response notFound() {
return new Response(HTTP_NOT_FOUND, null, null, Map.of());
}
/** {@code 404 Not Found} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
public static Response notFound(Object body) {
return new Response(HTTP_NOT_FOUND, body, null, Map.of());
}
/** {@code 501 Not Implemented} with no body. */
public static Response notImplemented() {
return new Response(HTTP_NOT_IMPLEMENTED, null, null, Map.of());
}
/** {@code status} with {@code body} serialised by the content-type's {@link TypeMapper}. */
public static Response of(int status, Object body) {
return new Response(status, body, null, Map.of());
}
// -- one-shot, text / raw bytes --
/** {@code status} with {@code body} written as UTF-8 with {@code Content-Type: text/plain}. */
public static Response text(int status, String body) {
return new Response(
status, body.getBytes(StandardCharsets.UTF_8), "text/plain; charset=UTF-8", Map.of());
}
/**
* {@code status} with pre-serialised {@code bytes} written verbatim under {@code contentType}.
*/
public static Response bytes(int status, byte[] bytes, String contentType) {
return new Response(status, bytes, contentType, Map.of());
}
// -- streaming --
/** Streaming response with unknown length (chunked transfer encoding). */
public static Response stream(int status, String contentType, StreamingBody writer) {
return new Response(status, new BodyWriter.Chunked(writer::writeTo), contentType, Map.of());
}
/** Streaming response with a known content length. */
public static Response stream(int status, long length, String contentType, StreamingBody writer) {
if (length < 0) {
throw new IllegalArgumentException("length must be non-negative");
}
return new Response(
status, new BodyWriter.Sized(length, writer::writeTo), contentType, Map.of());
}
// -- non-destructive mutators --
public Response withStatus(int newStatus) {
return new Response(newStatus, body, contentType, headers);
}
public Response withContentType(String newContentType) {
return new Response(status, body, newContentType, headers);
}
public Response withHeader(String name, String value) {
LinkedHashMap<String, String> merged = new LinkedHashMap<>(headers);
merged.put(name, value);
return new Response(status, body, contentType, merged);
}
/** Sets the {@code Location} header, typically the URI of a newly {@link #created} resource. */
public Response withLocation(String location) {
return withHeader("Location", location);
}
public Response withHeaders(Map<String, String> additional) {
LinkedHashMap<String, String> merged = new LinkedHashMap<>(headers);
merged.putAll(additional);
return new Response(status, body, contentType, merged);
}
/** Writer signature for {@link #stream(int, String, StreamingBody)}. */
@FunctionalInterface
public interface StreamingBody {
void writeTo(OutputStream out) throws IOException;
}
}