1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.linecorp.centraldogma.server.internal.api;
18
19 import static com.google.common.base.MoreObjects.firstNonNull;
20 import static com.google.common.base.Preconditions.checkArgument;
21 import static com.google.common.base.Strings.nullToEmpty;
22 import static java.util.Objects.requireNonNull;
23
24 import java.util.Locale;
25 import java.util.concurrent.CompletionStage;
26 import java.util.function.BiFunction;
27 import java.util.function.Function;
28 import java.util.function.Supplier;
29
30 import javax.annotation.Nullable;
31
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34
35 import com.fasterxml.jackson.core.JsonProcessingException;
36 import com.fasterxml.jackson.databind.JsonNode;
37 import com.fasterxml.jackson.databind.node.JsonNodeFactory;
38 import com.fasterxml.jackson.databind.node.ObjectNode;
39 import com.google.common.collect.ImmutableList;
40 import com.google.common.collect.ImmutableMap;
41
42 import com.linecorp.armeria.common.Flags;
43 import com.linecorp.armeria.common.HttpResponse;
44 import com.linecorp.armeria.common.HttpStatus;
45 import com.linecorp.armeria.common.MediaType;
46 import com.linecorp.armeria.common.logging.LogLevel;
47 import com.linecorp.armeria.common.util.Exceptions;
48 import com.linecorp.armeria.server.HttpResponseException;
49 import com.linecorp.armeria.server.ServiceRequestContext;
50 import com.linecorp.centraldogma.common.ShuttingDownException;
51 import com.linecorp.centraldogma.internal.Jackson;
52 import com.linecorp.centraldogma.server.metadata.User;
53
54 import io.netty.util.AttributeKey;
55
56
57
58
59
60 public final class HttpApiUtil {
61
62 private static final AttributeKey<Boolean> VERBOSE_RESPONSES =
63 AttributeKey.valueOf(HttpApiUtil.class, "VERBOSE_RESPONSES");
64
65 private static final Logger logger = LoggerFactory.getLogger(HttpApiUtil.class);
66 private static final String ERROR_MESSAGE_FORMAT = "{} Returning a {} response: {}";
67
68 static final JsonNode unremoveRequest = Jackson.valueToTree(
69 ImmutableList.of(
70 ImmutableMap.of("op", "replace",
71 "path", "/status",
72 "value", "active")));
73
74
75
76
77
78 public static <T> T throwResponse(ServiceRequestContext ctx, HttpStatus status, String message) {
79 throw HttpResponseException.of(newResponse(ctx, status, message));
80 }
81
82
83
84
85
86 public static <T> T throwResponse(ServiceRequestContext ctx, HttpStatus status, String format,
87 Object... args) {
88 throw HttpResponseException.of(newResponse(ctx, status, format, args));
89 }
90
91
92
93
94
95 public static <T> T throwResponse(ServiceRequestContext ctx, HttpStatus status, Throwable cause,
96 String message) {
97 throw HttpResponseException.of(newResponse(ctx, status, cause, message));
98 }
99
100
101
102
103
104 public static <T> T throwResponse(ServiceRequestContext ctx, HttpStatus status, Throwable cause,
105 String format, Object... args) {
106 throw HttpResponseException.of(newResponse(ctx, status, cause, format, args));
107 }
108
109
110
111
112
113 public static HttpResponse newResponse(ServiceRequestContext ctx, HttpStatus status,
114 String format, Object... args) {
115 requireNonNull(ctx, "ctx");
116 requireNonNull(status, "status");
117 requireNonNull(format, "format");
118 requireNonNull(args, "args");
119 return newResponse(ctx, status, String.format(Locale.ENGLISH, format, args));
120 }
121
122
123
124
125 public static HttpResponse newResponse(ServiceRequestContext ctx, HttpStatus status, String message) {
126 requireNonNull(ctx, "ctx");
127 requireNonNull(status, "status");
128 requireNonNull(message, "message");
129 return newResponse0(ctx, status, null, message);
130 }
131
132
133
134
135 public static HttpResponse newResponse(ServiceRequestContext ctx, HttpStatus status, Throwable cause) {
136 requireNonNull(ctx, "ctx");
137 requireNonNull(status, "status");
138 requireNonNull(cause, "cause");
139 return newResponse0(ctx, status, cause, null);
140 }
141
142
143
144
145
146 public static HttpResponse newResponse(ServiceRequestContext ctx, HttpStatus status, Throwable cause,
147 String format, Object... args) {
148 requireNonNull(ctx, "ctx");
149 requireNonNull(status, "status");
150 requireNonNull(cause, "cause");
151 requireNonNull(format, "format");
152 requireNonNull(args, "args");
153
154 return newResponse(ctx, status, cause, String.format(Locale.ENGLISH, format, args));
155 }
156
157
158
159
160
161 public static HttpResponse newResponse(ServiceRequestContext ctx, HttpStatus status,
162 Throwable cause, String message) {
163 requireNonNull(ctx, "ctx");
164 requireNonNull(status, "status");
165 requireNonNull(cause, "cause");
166 requireNonNull(message, "message");
167
168 return newResponse0(ctx, status, cause, message);
169 }
170
171 private static HttpResponse newResponse0(ServiceRequestContext ctx, HttpStatus status,
172 @Nullable Throwable cause, @Nullable String message) {
173 checkArgument(!status.isContentAlwaysEmpty(),
174 "status: %s (expected: a status with non-empty content)", status);
175
176 final ObjectNode node = JsonNodeFactory.instance.objectNode();
177 if (cause != null) {
178 cause = Exceptions.peel(cause);
179 node.put("exception", cause.getClass().getName());
180 if (message == null) {
181 message = cause.getMessage();
182 }
183 }
184
185 final String m = nullToEmpty(message);
186 node.put("message", m);
187
188 if (cause != null && isVerboseResponse(ctx)) {
189 node.put("detail", Exceptions.traceText(cause));
190 }
191
192 final LogLevel logLevel;
193 switch (status.codeClass()) {
194 case SERVER_ERROR:
195 if (cause != null) {
196 if (!(Exceptions.isStreamCancelling(cause) ||
197 cause instanceof ShuttingDownException)) {
198 logLevel = LogLevel.WARN;
199 } else {
200 logLevel = null;
201 }
202 } else {
203 logLevel = LogLevel.WARN;
204 }
205 break;
206 case CLIENT_ERROR:
207 logLevel = LogLevel.DEBUG;
208 break;
209 case UNKNOWN:
210 logLevel = LogLevel.WARN;
211 break;
212 default:
213 logLevel = null;
214 }
215
216
217
218 if (logLevel != null) {
219 if (logLevel == LogLevel.WARN) {
220 if (cause != null) {
221 logger.warn(ERROR_MESSAGE_FORMAT, ctx, status, m, cause);
222 } else {
223 logger.warn(ERROR_MESSAGE_FORMAT, ctx, status, m);
224 }
225 } else {
226 if (cause != null) {
227 logger.debug(ERROR_MESSAGE_FORMAT, ctx, status, m, cause);
228 } else {
229 logger.debug(ERROR_MESSAGE_FORMAT, ctx, status, m);
230 }
231 }
232 }
233
234
235
236 try {
237 return HttpResponse.of(status, MediaType.JSON_UTF_8, Jackson.writeValueAsBytes(node));
238 } catch (JsonProcessingException e) {
239
240 throw new Error(e);
241 }
242 }
243
244
245
246
247
248
249
250 static void checkUnremoveArgument(JsonNode node) {
251 checkArgument(unremoveRequest.equals(node),
252 "Unsupported JSON patch: " + node +
253 " (expected: " + unremoveRequest + ')');
254 }
255
256
257
258
259
260
261 static void checkStatusArgument(String status) {
262 checkArgument("removed".equalsIgnoreCase(status),
263 "invalid status: " + status + " (expected: removed)");
264 }
265
266
267
268
269
270 @SuppressWarnings("unused")
271 static Void throwUnsafelyIfNonNull(@Nullable Object unused,
272 @Nullable Throwable cause) {
273 throwUnsafelyIfNonNull(cause);
274 return null;
275 }
276
277
278
279
280 public static void throwUnsafelyIfNonNull(@Nullable Throwable cause) {
281 if (cause != null) {
282 Exceptions.throwUnsafely(cause);
283 }
284 }
285
286
287
288
289
290 @SuppressWarnings("unchecked")
291 static <T, U> BiFunction<? super T, Throwable, ? extends U> returnOrThrow(Supplier<? super U> supplier) {
292 return (unused, cause) -> {
293 throwUnsafelyIfNonNull(cause);
294 return (U) supplier.get();
295 };
296 }
297
298
299
300
301
302
303 static <T, U> BiFunction<? super T, Throwable, ? extends U> returnOrThrow(
304 Function<? super T, ? extends U> function) {
305 return (result, cause) -> {
306 throwUnsafelyIfNonNull(cause);
307 return function.apply(result);
308 };
309 }
310
311 public static void setVerboseResponses(ServiceRequestContext ctx, User user) {
312 ctx.setAttr(VERBOSE_RESPONSES, Flags.verboseResponses() || user.isSystemAdmin());
313 }
314
315 private static boolean isVerboseResponse(ServiceRequestContext ctx) {
316 final Boolean verboseResponses = ctx.attr(VERBOSE_RESPONSES);
317 return firstNonNull(verboseResponses, false);
318 }
319
320 private HttpApiUtil() {}
321 }