View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.client.lib;
20  
21  import jakarta.ws.rs.BadRequestException;
22  import jakarta.ws.rs.ForbiddenException;
23  import jakarta.ws.rs.NotAuthorizedException;
24  import jakarta.ws.rs.NotFoundException;
25  import jakarta.ws.rs.WebApplicationException;
26  import jakarta.ws.rs.core.GenericType;
27  import jakarta.ws.rs.core.Response;
28  import jakarta.ws.rs.ext.Provider;
29  import java.util.HashSet;
30  import java.util.List;
31  import java.util.Set;
32  import org.apache.commons.lang3.StringUtils;
33  import org.apache.cxf.jaxrs.client.ResponseExceptionMapper;
34  import org.apache.syncope.common.lib.SyncopeClientCompositeException;
35  import org.apache.syncope.common.lib.SyncopeClientException;
36  import org.apache.syncope.common.lib.to.ErrorTO;
37  import org.apache.syncope.common.lib.types.ClientExceptionType;
38  import org.apache.syncope.common.rest.api.RESTHeaders;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  
42  @Provider
43  public class RestClientExceptionMapper implements ResponseExceptionMapper<Exception> {
44  
45      private static final Logger LOG = LoggerFactory.getLogger(RestClientExceptionMapper.class);
46  
47      @Override
48      public Exception fromResponse(final Response response) {
49          int statusCode = response.getStatus();
50          String message = response.getHeaderString(RESTHeaders.ERROR_INFO);
51  
52          Exception ex;
53          SyncopeClientCompositeException scce = checkSyncopeClientCompositeException(response);
54          if (scce != null) {
55              // 1. Check for client (possibly composite) exception in HTTP header
56              ex = scce.getExceptions().size() == 1
57                      ? scce.getExceptions().iterator().next()
58                      : scce;
59          } else if (statusCode == Response.Status.UNAUTHORIZED.getStatusCode()) {
60              // 2. Map SC_UNAUTHORIZED
61              ex = new NotAuthorizedException(StringUtils.isBlank(message)
62                      ? "Remote unauthorized exception"
63                      : message,
64                      response);
65          } else if (statusCode == Response.Status.FORBIDDEN.getStatusCode()) {
66              // 3. Map SC_FORBIDDEN
67              ex = new ForbiddenException(StringUtils.isBlank(message)
68                      ? "Remote forbidden exception"
69                      : message,
70                      response);
71          } else if (statusCode == Response.Status.BAD_REQUEST.getStatusCode()) {
72              // 4. Map SC_BAD_REQUEST
73              ex = StringUtils.isBlank(message)
74                      ? new BadRequestException(response)
75                      : new BadRequestException(message, response);
76          } else {
77              // 5. All other codes are mapped to runtime exception with HTTP code information
78              ex = new WebApplicationException("Remote exception with status code: %s".
79                      formatted(Response.Status.fromStatusCode(statusCode).name()),
80                      response);
81          }
82  
83          if (ex instanceof NotFoundException
84                  || (ex instanceof SyncopeClientException sce && sce.getType() == ClientExceptionType.NotFound)) {
85  
86              if (LOG.isDebugEnabled()) {
87                  LOG.debug("Exception thrown", ex);
88              } else {
89                  LOG.warn("{} thrown: {}", NotFoundException.class.getSimpleName(), ex.getMessage());
90              }
91          } else {
92              LOG.error("Exception thrown", ex);
93          }
94  
95          return ex;
96      }
97  
98      private static SyncopeClientCompositeException checkSyncopeClientCompositeException(final Response response) {
99          SyncopeClientCompositeException compException = SyncopeClientException.buildComposite();
100 
101         // Attempts to read ErrorTO or List<ErrorTO> as entity...
102         List<ErrorTO> errors = null;
103         try {
104             ErrorTO error = response.readEntity(ErrorTO.class);
105             if (error != null) {
106                 errors = List.of(error);
107             }
108         } catch (Exception e) {
109             LOG.debug("Could not read {}, attempting to read composite...", ErrorTO.class.getName(), e);
110         }
111         if (errors == null) {
112             try {
113                 errors = response.readEntity(new GenericType<>() {
114                 });
115             } catch (Exception e) {
116                 LOG.debug("Could not read {} list, attempting to read headers...", ErrorTO.class.getName(), e);
117             }
118         }
119 
120         // ...if not possible, attempts to parse response headers
121         if (errors == null) {
122             List<String> exTypesInHeaders = response.getStringHeaders().get(RESTHeaders.ERROR_CODE);
123             if (exTypesInHeaders == null) {
124                 LOG.debug("No " + RESTHeaders.ERROR_CODE + " provided");
125                 return null;
126             }
127             List<String> exInfos = response.getStringHeaders().get(RESTHeaders.ERROR_INFO);
128 
129             Set<String> handledExceptions = new HashSet<>();
130             exTypesInHeaders.forEach(exTypeAsString -> {
131                 ClientExceptionType exceptionType = null;
132                 try {
133                     exceptionType = ClientExceptionType.fromHeaderValue(exTypeAsString);
134                 } catch (IllegalArgumentException e) {
135                     LOG.error("Unexpected value of " + RESTHeaders.ERROR_CODE + ": {}", exTypeAsString, e);
136                 }
137                 if (exceptionType != null) {
138                     handledExceptions.add(exTypeAsString);
139 
140                     SyncopeClientException clientException = SyncopeClientException.build(exceptionType);
141                     if (exInfos != null && !exInfos.isEmpty()) {
142                         for (String element : exInfos) {
143                             if (element.startsWith(exceptionType.name())) {
144                                 clientException.getElements().add(StringUtils.substringAfter(element, ":"));
145                             }
146                         }
147                     }
148                     compException.addException(clientException);
149                 }
150             });
151 
152             exTypesInHeaders.removeAll(handledExceptions);
153             if (!exTypesInHeaders.isEmpty()) {
154                 LOG.error("Unmanaged exceptions: {}", exTypesInHeaders);
155             }
156         } else {
157             for (ErrorTO error : errors) {
158                 SyncopeClientException clientException = SyncopeClientException.build(error.getType());
159                 clientException.getElements().addAll(error.getElements());
160                 compException.addException(clientException);
161             }
162         }
163 
164         if (compException.hasExceptions()) {
165             return compException;
166         }
167 
168         return null;
169     }
170 }