|
2 | 2 |
|
3 | 3 | import com.retailsvc.http.validate.ValidationError; |
4 | 4 |
|
| 5 | +/** |
| 6 | + * Renders a {@link ValidationError} as an RFC 7807 {@code application/problem+json} document. |
| 7 | + * |
| 8 | + * <p>Hand-rolled to avoid pulling in a JSON library; only six fixed fields are emitted, all with |
| 9 | + * known shapes, so a writer-from-scratch is safer than a generic encoder. |
| 10 | + */ |
5 | 11 | public final class ProblemDetailRenderer { |
| 12 | + |
| 13 | + private static final String PROBLEM_TYPE = "about:blank"; |
| 14 | + private static final String PROBLEM_TITLE = "Bad Request"; |
| 15 | + private static final int PROBLEM_STATUS = 400; |
| 16 | + |
| 17 | + /** Codepoints below this value are control characters and must be unicode-escaped in JSON. */ |
| 18 | + private static final int FIRST_PRINTABLE_ASCII = 0x20; |
| 19 | + |
6 | 20 | private ProblemDetailRenderer() {} |
7 | 21 |
|
8 | 22 | public static String render(ValidationError error) { |
9 | | - return "{" |
10 | | - + "\"type\":\"about:blank\"," |
11 | | - + "\"title\":\"Bad Request\"," |
12 | | - + "\"status\":400," |
13 | | - + "\"detail\":\"" |
14 | | - + escape(error.message()) |
15 | | - + "\"," |
16 | | - + "\"pointer\":\"" |
17 | | - + escape(error.pointer()) |
18 | | - + "\"," |
19 | | - + "\"keyword\":\"" |
20 | | - + escape(error.keyword()) |
21 | | - + "\"" |
22 | | - + "}"; |
| 23 | + StringBuilder out = new StringBuilder(128); |
| 24 | + out.append('{'); |
| 25 | + appendStringField(out, "type", PROBLEM_TYPE); |
| 26 | + out.append(','); |
| 27 | + appendStringField(out, "title", PROBLEM_TITLE); |
| 28 | + out.append(','); |
| 29 | + appendIntField(out, "status", PROBLEM_STATUS); |
| 30 | + out.append(','); |
| 31 | + appendStringField(out, "detail", error.message()); |
| 32 | + out.append(','); |
| 33 | + appendStringField(out, "pointer", error.pointer()); |
| 34 | + out.append(','); |
| 35 | + appendStringField(out, "keyword", error.keyword()); |
| 36 | + out.append('}'); |
| 37 | + return out.toString(); |
| 38 | + } |
| 39 | + |
| 40 | + private static void appendStringField(StringBuilder out, String name, String value) { |
| 41 | + out.append('"').append(name).append("\":\""); |
| 42 | + appendEscaped(out, value); |
| 43 | + out.append('"'); |
23 | 44 | } |
24 | 45 |
|
25 | | - private static String escape(String s) { |
26 | | - StringBuilder b = new StringBuilder(s.length() + 8); |
27 | | - for (int i = 0; i < s.length(); i++) { |
28 | | - char c = s.charAt(i); |
| 46 | + private static void appendIntField(StringBuilder out, String name, int value) { |
| 47 | + out.append('"').append(name).append("\":").append(value); |
| 48 | + } |
| 49 | + |
| 50 | + /** |
| 51 | + * Appends {@code value} to {@code out} with JSON-string escaping applied. Handles the six |
| 52 | + * mandatory escape sequences and emits {@code \uXXXX} for control characters below {@link |
| 53 | + * #FIRST_PRINTABLE_ASCII}. |
| 54 | + */ |
| 55 | + private static void appendEscaped(StringBuilder out, String value) { |
| 56 | + for (int i = 0; i < value.length(); i++) { |
| 57 | + char c = value.charAt(i); |
29 | 58 | switch (c) { |
30 | | - case '\\' -> b.append("\\\\"); |
31 | | - case '"' -> b.append("\\\""); |
32 | | - case '\n' -> b.append("\\n"); |
33 | | - case '\r' -> b.append("\\r"); |
34 | | - case '\t' -> b.append("\\t"); |
35 | | - default -> { |
36 | | - if (c < 0x20) b.append(String.format("\\u%04x", (int) c)); |
37 | | - else b.append(c); |
38 | | - } |
| 59 | + case '\\' -> out.append("\\\\"); |
| 60 | + case '"' -> out.append("\\\""); |
| 61 | + case '\n' -> out.append("\\n"); |
| 62 | + case '\r' -> out.append("\\r"); |
| 63 | + case '\t' -> out.append("\\t"); |
| 64 | + default -> appendUnicodeOrLiteral(out, c); |
39 | 65 | } |
40 | 66 | } |
41 | | - return b.toString(); |
| 67 | + } |
| 68 | + |
| 69 | + private static void appendUnicodeOrLiteral(StringBuilder out, char c) { |
| 70 | + if (c < FIRST_PRINTABLE_ASCII) { |
| 71 | + out.append(String.format("\\u%04x", (int) c)); |
| 72 | + } else { |
| 73 | + out.append(c); |
| 74 | + } |
42 | 75 | } |
43 | 76 | } |
0 commit comments