1   /*
2    * Copyright 2017 LINE Corporation
3    *
4    * LINE Corporation licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package com.linecorp.centraldogma.server.internal.api;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static com.google.common.base.Strings.nullToEmpty;
21  import static java.util.Objects.requireNonNull;
22  
23  import java.util.Locale;
24  import java.util.concurrent.CompletionStage;
25  import java.util.function.BiFunction;
26  import java.util.function.Function;
27  import java.util.function.Supplier;
28  
29  import javax.annotation.Nullable;
30  
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  
34  import com.fasterxml.jackson.core.JsonProcessingException;
35  import com.fasterxml.jackson.databind.JsonNode;
36  import com.fasterxml.jackson.databind.node.JsonNodeFactory;
37  import com.fasterxml.jackson.databind.node.ObjectNode;
38  import com.google.common.collect.ImmutableList;
39  import com.google.common.collect.ImmutableMap;
40  
41  import com.linecorp.armeria.common.HttpResponse;
42  import com.linecorp.armeria.common.HttpStatus;
43  import com.linecorp.armeria.common.MediaType;
44  import com.linecorp.armeria.common.RequestContext;
45  import com.linecorp.armeria.common.logging.LogLevel;
46  import com.linecorp.armeria.common.util.Exceptions;
47  import com.linecorp.armeria.server.HttpResponseException;
48  import com.linecorp.centraldogma.common.ShuttingDownException;
49  import com.linecorp.centraldogma.internal.Jackson;
50  
51  /**
52   * A utility class which provides common functions for HTTP API.
53   */
54  //TODO(minwoox) change this class to package-local when the admin API is integrated with HTTP API
55  public final class HttpApiUtil {
56  
57      private static final Logger logger = LoggerFactory.getLogger(HttpApiUtil.class);
58      private static final String ERROR_MESSAGE_FORMAT = "{} Returning a {} response: {}";
59  
60      static final JsonNode unremoveRequest = Jackson.valueToTree(
61              ImmutableList.of(
62                      ImmutableMap.of("op", "replace",
63                                      "path", "/status",
64                                      "value", "active")));
65  
66      /**
67       * Throws a newly created {@link HttpResponseException} with the specified {@link HttpStatus} and
68       * {@code message}.
69       */
70      public static <T> T throwResponse(RequestContext ctx, HttpStatus status, String message) {
71          throw HttpResponseException.of(newResponse(ctx, status, message));
72      }
73  
74      /**
75       * Throws a newly created {@link HttpResponseException} with the specified {@link HttpStatus} and
76       * the formatted message.
77       */
78      public static <T> T throwResponse(RequestContext ctx, HttpStatus status, String format, Object... args) {
79          throw HttpResponseException.of(newResponse(ctx, status, format, args));
80      }
81  
82      /**
83       * Throws a newly created {@link HttpResponseException} with the specified {@link HttpStatus},
84       * {@code cause} and {@code message}.
85       */
86      public static <T> T throwResponse(RequestContext ctx, HttpStatus status, Throwable cause, String message) {
87          throw HttpResponseException.of(newResponse(ctx, status, cause, message));
88      }
89  
90      /**
91       * Throws a newly created {@link HttpResponseException} with the specified {@link HttpStatus},
92       * {@code cause} and the formatted message.
93       */
94      public static <T> T throwResponse(RequestContext ctx, HttpStatus status, Throwable cause,
95                                        String format, Object... args) {
96          throw HttpResponseException.of(newResponse(ctx, status, cause, format, args));
97      }
98  
99      /**
100      * Returns a newly created {@link HttpResponse} with the specified {@link HttpStatus} and the formatted
101      * message.
102      */
103     public static HttpResponse newResponse(RequestContext ctx, HttpStatus status,
104                                            String format, Object... args) {
105         requireNonNull(ctx, "ctx");
106         requireNonNull(status, "status");
107         requireNonNull(format, "format");
108         requireNonNull(args, "args");
109         return newResponse(ctx, status, String.format(Locale.ENGLISH, format, args));
110     }
111 
112     /**
113      * Returns a newly created {@link HttpResponse} with the specified {@link HttpStatus} and {@code message}.
114      */
115     public static HttpResponse newResponse(RequestContext ctx, HttpStatus status, String message) {
116         requireNonNull(ctx, "ctx");
117         requireNonNull(status, "status");
118         requireNonNull(message, "message");
119         return newResponse0(ctx, status, null, message);
120     }
121 
122     /**
123      * Returns a newly created {@link HttpResponse} with the specified {@link HttpStatus} and {@code cause}.
124      */
125     public static HttpResponse newResponse(RequestContext ctx, HttpStatus status, Throwable cause) {
126         requireNonNull(ctx, "ctx");
127         requireNonNull(status, "status");
128         requireNonNull(cause, "cause");
129         return newResponse0(ctx, status, cause, null);
130     }
131 
132     /**
133      * Returns a newly created {@link HttpResponse} with the specified {@link HttpStatus}, {@code cause} and
134      * the formatted message.
135      */
136     public static HttpResponse newResponse(RequestContext ctx, HttpStatus status, Throwable cause,
137                                            String format, Object... args) {
138         requireNonNull(ctx, "ctx");
139         requireNonNull(status, "status");
140         requireNonNull(cause, "cause");
141         requireNonNull(format, "format");
142         requireNonNull(args, "args");
143 
144         return newResponse(ctx, status, cause, String.format(Locale.ENGLISH, format, args));
145     }
146 
147     /**
148      * Returns a newly created {@link HttpResponse} with the specified {@link HttpStatus}, {@code cause} and
149      * {@code message}.
150      */
151     public static HttpResponse newResponse(RequestContext ctx, HttpStatus status,
152                                            Throwable cause, String message) {
153         requireNonNull(ctx, "ctx");
154         requireNonNull(status, "status");
155         requireNonNull(cause, "cause");
156         requireNonNull(message, "message");
157 
158         return newResponse0(ctx, status, cause, message);
159     }
160 
161     private static HttpResponse newResponse0(RequestContext ctx, HttpStatus status,
162                                              @Nullable Throwable cause, @Nullable String message) {
163         checkArgument(!status.isContentAlwaysEmpty(),
164                       "status: %s (expected: a status with non-empty content)", status);
165 
166         final ObjectNode node = JsonNodeFactory.instance.objectNode();
167         if (cause != null) {
168             cause = Exceptions.peel(cause);
169             node.put("exception", cause.getClass().getName());
170             if (message == null) {
171                 message = cause.getMessage();
172             }
173         }
174 
175         final String m = nullToEmpty(message);
176         node.put("message", m);
177 
178         final LogLevel logLevel;
179         switch (status.codeClass()) {
180             case SERVER_ERROR:
181                 if (cause != null) {
182                     if (!(Exceptions.isStreamCancelling(cause) ||
183                           cause instanceof ShuttingDownException)) {
184                         logLevel = LogLevel.WARN;
185                     } else {
186                         logLevel = null;
187                     }
188                 } else {
189                     logLevel = LogLevel.WARN;
190                 }
191                 break;
192             case CLIENT_ERROR:
193                 logLevel = LogLevel.DEBUG;
194                 break;
195             case UNKNOWN:
196                 logLevel = LogLevel.WARN;
197                 break;
198             default:
199                 logLevel = null;
200         }
201 
202         // TODO(trustin): Use LogLevel.OFF instead of null and logLevel.log()
203         //                once we add LogLevel.OFF and LogLevel.log() with more args.
204         if (logLevel != null) {
205             if (logLevel == LogLevel.WARN) {
206                 if (cause != null) {
207                     logger.warn(ERROR_MESSAGE_FORMAT, ctx, status, m, cause);
208                 } else {
209                     logger.warn(ERROR_MESSAGE_FORMAT, ctx, status, m);
210                 }
211             } else {
212                 if (cause != null) {
213                     logger.debug(ERROR_MESSAGE_FORMAT, ctx, status, m, cause);
214                 } else {
215                     logger.debug(ERROR_MESSAGE_FORMAT, ctx, status, m);
216                 }
217             }
218         }
219 
220         // TODO(hyangtack) Need to introduce a new field such as 'stackTrace' in order to return
221         //                 the stack trace of the cause to the trusted client.
222         try {
223             return HttpResponse.of(status, MediaType.JSON_UTF_8, Jackson.writeValueAsBytes(node));
224         } catch (JsonProcessingException e) {
225             // should not reach here
226             throw new Error(e);
227         }
228     }
229 
230     /**
231      * Ensures the specified {@link JsonNode} equals to {@link HttpApiUtil#unremoveRequest}.
232      *
233      * @throws IllegalArgumentException if the {@code node} does not equal to
234      *                                  {@link HttpApiUtil#unremoveRequest}.
235      */
236     static void checkUnremoveArgument(JsonNode node) {
237         checkArgument(unremoveRequest.equals(node),
238                       "Unsupported JSON patch: " + node +
239                       " (expected: " + unremoveRequest + ')');
240     }
241 
242     /**
243      * Ensures the specified {@code status} is valid.
244      *
245      * @throws IllegalArgumentException if the {@code status} is invalid.
246      */
247     static void checkStatusArgument(String status) {
248         checkArgument("removed".equalsIgnoreCase(status),
249                       "invalid status: " + status + " (expected: removed)");
250     }
251 
252     /**
253      * Throws the specified {@link Throwable} if it is not {@code null}. This is a special form to be used
254      * for {@link CompletionStage#handle(BiFunction)}.
255      */
256     @SuppressWarnings("unused")
257     static Void throwUnsafelyIfNonNull(@Nullable Object unused,
258                                        @Nullable Throwable cause) {
259         throwUnsafelyIfNonNull(cause);
260         return null;
261     }
262 
263     /**
264      * Throws the specified {@link Throwable} if it is not {@code null}.
265      */
266     public static void throwUnsafelyIfNonNull(@Nullable Throwable cause) {
267         if (cause != null) {
268             Exceptions.throwUnsafely(cause);
269         }
270     }
271 
272     /**
273      * Returns a {@link BiFunction} for a {@link CompletionStage#handle(BiFunction)} which returns an object
274      * if the specified {@link Throwable} is {@code null}. Otherwise, it throws the {@code cause}.
275      */
276     @SuppressWarnings("unchecked")
277     static <T, U> BiFunction<? super T, Throwable, ? extends U> returnOrThrow(Supplier<? super U> supplier) {
278         return (unused, cause) -> {
279             throwUnsafelyIfNonNull(cause);
280             return (U) supplier.get();
281         };
282     }
283 
284     /**
285      * Returns a {@link BiFunction} for a {@link CompletionStage#handle(BiFunction)} which returns an object
286      * applied by the specified {@code function} if the specified {@link Throwable} is {@code null}.
287      * Otherwise, it throws the {@code cause}.
288      */
289     static <T, U> BiFunction<? super T, Throwable, ? extends U> returnOrThrow(
290             Function<? super T, ? extends U> function) {
291         return (result, cause) -> {
292             throwUnsafelyIfNonNull(cause);
293             return function.apply(result);
294         };
295     }
296 
297     private HttpApiUtil() {}
298 }