11package com .retailsvc .http ;
22
33import static java .net .HttpURLConnection .HTTP_ACCEPTED ;
4+ import static java .net .HttpURLConnection .HTTP_BAD_METHOD ;
5+ import static java .net .HttpURLConnection .HTTP_BAD_REQUEST ;
6+ import static java .net .HttpURLConnection .HTTP_CONFLICT ;
47import static java .net .HttpURLConnection .HTTP_CREATED ;
8+ import static java .net .HttpURLConnection .HTTP_FORBIDDEN ;
9+ import static java .net .HttpURLConnection .HTTP_INTERNAL_ERROR ;
510import static java .net .HttpURLConnection .HTTP_NOT_FOUND ;
611import static java .net .HttpURLConnection .HTTP_NOT_IMPLEMENTED ;
12+ import static java .net .HttpURLConnection .HTTP_NOT_MODIFIED ;
713import static java .net .HttpURLConnection .HTTP_NO_CONTENT ;
814import static java .net .HttpURLConnection .HTTP_OK ;
15+ import static java .net .HttpURLConnection .HTTP_UNAUTHORIZED ;
916
1017import com .retailsvc .http .internal .BodyWriter ;
18+ import com .retailsvc .http .spec .HttpMethod ;
1119import java .io .IOException ;
1220import java .io .OutputStream ;
1321import java .nio .charset .StandardCharsets ;
22+ import java .util .Collection ;
1423import java .util .LinkedHashMap ;
24+ import java .util .List ;
1525import java .util .Map ;
26+ import java .util .stream .Collectors ;
1627
1728/**
1829 * The value returned by every {@link RequestHandler}. Carries status, optional body, optional
3243 */
3344public record Response (int status , Object body , String contentType , Map <String , String > headers ) {
3445
46+ /** Not defined by {@link java.net.HttpURLConnection}. */
47+ private static final int HTTP_UNPROCESSABLE_CONTENT = 422 ;
48+
3549 public Response {
3650 headers = headers == null ? Map .of () : Map .copyOf (headers );
3751 }
3852
3953 // -- one-shot, no-body --
4054
41- /** {@code 204 No Content} with no body. */
55+ /** {@code 204 No Content} with no body. Same as {@link #noContent()}. */
4256 public static Response empty () {
43- return new Response ( HTTP_NO_CONTENT , null , null , Map . of () );
57+ return noContent ( );
4458 }
4559
46- /** Given status, no body. Use for {@code 200 OK} no body, {@code 404}, {@code 405}, etc . */
60+ /** Given status, no body. Prefer a named factory such as {@link #ok()} when one exists . */
4761 public static Response status (int status ) {
4862 return new Response (status , null , null , Map .of ());
4963 }
5064
65+ /** {@code 200 OK} with no body. */
66+ public static Response ok () {
67+ return status (HTTP_OK );
68+ }
69+
70+ /** {@code 204 No Content} with no body. */
71+ public static Response noContent () {
72+ return status (HTTP_NO_CONTENT );
73+ }
74+
75+ /** {@code 304 Not Modified} with no body. */
76+ public static Response notModified () {
77+ return status (HTTP_NOT_MODIFIED );
78+ }
79+
80+ /** {@code 400 Bad Request} with no body. */
81+ public static Response badRequest () {
82+ return status (HTTP_BAD_REQUEST );
83+ }
84+
85+ /** {@code 401 Unauthorized} with no body. Add a {@code WWW-Authenticate} header as needed. */
86+ public static Response unauthorized () {
87+ return status (HTTP_UNAUTHORIZED );
88+ }
89+
90+ /** {@code 403 Forbidden} with no body. */
91+ public static Response forbidden () {
92+ return status (HTTP_FORBIDDEN );
93+ }
94+
95+ /**
96+ * {@code 405 Method Not Allowed} with no body and an {@code Allow} header listing {@code
97+ * allowed}.
98+ */
99+ public static Response methodNotAllowed (HttpMethod ... allowed ) {
100+ return methodNotAllowed (List .of (allowed ));
101+ }
102+
103+ /**
104+ * {@code 405 Method Not Allowed} with no body and an {@code Allow} header listing {@code allowed}
105+ * in {@link HttpMethod} declaration order.
106+ */
107+ public static Response methodNotAllowed (Collection <HttpMethod > allowed ) {
108+ String allow =
109+ allowed .stream ().sorted ().distinct ().map (Enum ::name ).collect (Collectors .joining (", " ));
110+ return status (HTTP_BAD_METHOD ).withHeader ("Allow" , allow );
111+ }
112+
113+ /** {@code 409 Conflict} with no body. */
114+ public static Response conflict () {
115+ return status (HTTP_CONFLICT );
116+ }
117+
118+ /** {@code 500 Internal Server Error} with no body. */
119+ public static Response internalServerError () {
120+ return status (HTTP_INTERNAL_ERROR );
121+ }
122+
51123 // -- one-shot, JSON body --
52124
53125 /** {@code 200 OK} with {@code body} serialised as JSON. */
@@ -73,6 +145,16 @@ public static Response accepted(Object body) {
73145 return new Response (HTTP_ACCEPTED , body , null , Map .of ());
74146 }
75147
148+ /** {@code 400 Bad Request} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
149+ public static Response badRequest (Object body ) {
150+ return new Response (HTTP_BAD_REQUEST , body , null , Map .of ());
151+ }
152+
153+ /** {@code 403 Forbidden} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
154+ public static Response forbidden (Object body ) {
155+ return new Response (HTTP_FORBIDDEN , body , null , Map .of ());
156+ }
157+
76158 /** {@code 404 Not Found} with no body. */
77159 public static Response notFound () {
78160 return new Response (HTTP_NOT_FOUND , null , null , Map .of ());
@@ -83,6 +165,19 @@ public static Response notFound(Object body) {
83165 return new Response (HTTP_NOT_FOUND , body , null , Map .of ());
84166 }
85167
168+ /** {@code 409 Conflict} with {@code body} serialised as JSON (e.g. a ProblemDetail). */
169+ public static Response conflict (Object body ) {
170+ return new Response (HTTP_CONFLICT , body , null , Map .of ());
171+ }
172+
173+ /**
174+ * {@code 422 Unprocessable Content} with {@code body} serialised as JSON (e.g. a ProblemDetail).
175+ * Use when the request is well-formed but breaks a business rule.
176+ */
177+ public static Response unprocessableContent (Object body ) {
178+ return new Response (HTTP_UNPROCESSABLE_CONTENT , body , null , Map .of ());
179+ }
180+
86181 /** {@code 501 Not Implemented} with no body. */
87182 public static Response notImplemented () {
88183 return new Response (HTTP_NOT_IMPLEMENTED , null , null , Map .of ());
0 commit comments