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.MoreObjects.firstNonNull;
20  import static com.linecorp.centraldogma.server.internal.api.HttpApiUtil.newResponse;
21  
22  import java.util.Map;
23  
24  import com.google.common.collect.ImmutableMap;
25  
26  import com.linecorp.armeria.common.HttpRequest;
27  import com.linecorp.armeria.common.HttpResponse;
28  import com.linecorp.armeria.common.HttpStatus;
29  import com.linecorp.armeria.common.util.Exceptions;
30  import com.linecorp.armeria.server.HttpResponseException;
31  import com.linecorp.armeria.server.HttpStatusException;
32  import com.linecorp.armeria.server.ServiceRequestContext;
33  import com.linecorp.armeria.server.annotation.ExceptionHandlerFunction;
34  import com.linecorp.centraldogma.common.ChangeConflictException;
35  import com.linecorp.centraldogma.common.EntryNoContentException;
36  import com.linecorp.centraldogma.common.EntryNotFoundException;
37  import com.linecorp.centraldogma.common.InvalidPushException;
38  import com.linecorp.centraldogma.common.ProjectExistsException;
39  import com.linecorp.centraldogma.common.ProjectNotFoundException;
40  import com.linecorp.centraldogma.common.QueryExecutionException;
41  import com.linecorp.centraldogma.common.ReadOnlyException;
42  import com.linecorp.centraldogma.common.RedundantChangeException;
43  import com.linecorp.centraldogma.common.RepositoryExistsException;
44  import com.linecorp.centraldogma.common.RepositoryNotFoundException;
45  import com.linecorp.centraldogma.common.RevisionNotFoundException;
46  import com.linecorp.centraldogma.common.TooManyRequestsException;
47  import com.linecorp.centraldogma.server.internal.admin.service.TokenNotFoundException;
48  import com.linecorp.centraldogma.server.internal.storage.RequestAlreadyTimedOutException;
49  import com.linecorp.centraldogma.server.internal.storage.repository.RepositoryMetadataException;
50  
51  /**
52   * A default {@link ExceptionHandlerFunction} of HTTP API.
53   */
54  public final class HttpApiExceptionHandler implements ExceptionHandlerFunction {
55  
56      /**
57       * A map of exception handler functions for well known exceptions.
58       */
59      private static final Map<Class<?>, ExceptionHandlerFunction> exceptionHandlers;
60  
61      static {
62          final ImmutableMap.Builder<Class<?>,
63                  ExceptionHandlerFunction> builder = ImmutableMap.builder();
64  
65          builder.put(ChangeConflictException.class,
66                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
67                                                       "The given changeset or revision has a conflict."))
68                 .put(EntryNotFoundException.class,
69                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
70                                                       "Entry '%s' does not exist.", cause.getMessage()))
71                 .put(EntryNoContentException.class,
72                      (ctx, req, cause) -> HttpResponse.of(HttpStatus.NO_CONTENT))
73                 .put(ProjectExistsException.class,
74                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
75                                                       "Project '%s' exists already.", cause.getMessage()))
76                 .put(ProjectNotFoundException.class,
77                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
78                                                       "Project '%s' does not exist.", cause.getMessage()))
79                 .put(RedundantChangeException.class,
80                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
81                                                       "The given changeset does not change anything."))
82                 .put(RepositoryExistsException.class,
83                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.CONFLICT, cause,
84                                                       "Repository '%s' exists already.", cause.getMessage()))
85                 .put(RepositoryMetadataException.class,
86                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, cause))
87                 .put(RepositoryNotFoundException.class,
88                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
89                                                       "Repository '%s' does not exist.", cause.getMessage()))
90                 .put(RevisionNotFoundException.class,
91                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
92                                                       "Revision %s does not exist.", cause.getMessage()))
93                 .put(TokenNotFoundException.class,
94                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.NOT_FOUND, cause,
95                                                       "Token '%s' does not exist.", cause.getMessage()))
96                 .put(QueryExecutionException.class,
97                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.BAD_REQUEST, cause))
98                 .put(UnsupportedOperationException.class,
99                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.BAD_REQUEST, cause))
100                .put(TooManyRequestsException.class,
101                     (ctx, req, cause) -> {
102                         final TooManyRequestsException cast = (TooManyRequestsException) cause;
103                         final Object type = firstNonNull(cast.type(), "requests");
104                         return newResponse(ctx, HttpStatus.TOO_MANY_REQUESTS, cast,
105                                            "Too many %s are sent to %s", type, cause.getMessage());
106                     })
107                .put(InvalidPushException.class,
108                     (ctx, req, cause) -> newResponse(ctx, HttpStatus.BAD_REQUEST, cause))
109                 .put(ReadOnlyException.class,
110                      (ctx, req, cause) -> newResponse(ctx, HttpStatus.SERVICE_UNAVAILABLE, cause));
111 
112         exceptionHandlers = builder.build();
113     }
114 
115     @Override
116     public HttpResponse handleException(ServiceRequestContext ctx, HttpRequest req, Throwable cause) {
117         final Throwable peeledCause = Exceptions.peel(cause);
118 
119         if (peeledCause instanceof HttpStatusException ||
120             peeledCause instanceof HttpResponseException) {
121             return ExceptionHandlerFunction.fallthrough();
122         }
123 
124         // Use precomputed map if the cause is instance of CentralDogmaException to access in a faster way.
125         final ExceptionHandlerFunction func = exceptionHandlers.get(peeledCause.getClass());
126         if (func != null) {
127             return func.handleException(ctx, req, peeledCause);
128         }
129 
130         if (peeledCause instanceof IllegalArgumentException) {
131             return newResponse(ctx, HttpStatus.BAD_REQUEST, peeledCause);
132         }
133 
134         if (peeledCause instanceof RequestAlreadyTimedOutException) {
135             return newResponse(ctx, HttpStatus.SERVICE_UNAVAILABLE, peeledCause);
136         }
137 
138         return newResponse(ctx, HttpStatus.INTERNAL_SERVER_ERROR, peeledCause);
139     }
140 }