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.core.rest.cxf;
20  
21  import java.sql.SQLException;
22  import java.util.HashMap;
23  import java.util.List;
24  import java.util.Map;
25  import java.util.Optional;
26  import java.util.Set;
27  import java.util.stream.Collectors;
28  import javax.persistence.EntityExistsException;
29  import javax.persistence.PersistenceException;
30  import javax.persistence.RollbackException;
31  import javax.validation.ValidationException;
32  import javax.ws.rs.WebApplicationException;
33  import javax.ws.rs.core.HttpHeaders;
34  import javax.ws.rs.core.Response;
35  import javax.ws.rs.core.Response.ResponseBuilder;
36  import javax.ws.rs.ext.ExceptionMapper;
37  import javax.ws.rs.ext.Provider;
38  import org.apache.commons.lang3.exception.ExceptionUtils;
39  import org.apache.cxf.jaxrs.utils.JAXRSUtils;
40  import org.apache.cxf.jaxrs.validation.ValidationExceptionMapper;
41  import org.apache.syncope.common.lib.SyncopeClientCompositeException;
42  import org.apache.syncope.common.lib.SyncopeClientException;
43  import org.apache.syncope.common.lib.to.ErrorTO;
44  import org.apache.syncope.common.lib.types.ClientExceptionType;
45  import org.apache.syncope.common.lib.types.EntityViolationType;
46  import org.apache.syncope.common.rest.api.RESTHeaders;
47  import org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException;
48  import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException;
49  import org.apache.syncope.core.persistence.api.dao.DuplicateException;
50  import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
51  import org.apache.syncope.core.persistence.api.dao.NotFoundException;
52  import org.apache.syncope.core.persistence.api.entity.PlainAttr;
53  import org.apache.syncope.core.spring.security.DelegatedAdministrationException;
54  import org.apache.syncope.core.workflow.api.WorkflowException;
55  import org.identityconnectors.framework.common.exceptions.ConfigurationException;
56  import org.identityconnectors.framework.common.exceptions.ConnectorException;
57  import org.slf4j.Logger;
58  import org.slf4j.LoggerFactory;
59  import org.springframework.core.env.Environment;
60  import org.springframework.dao.DataIntegrityViolationException;
61  import org.springframework.dao.UncategorizedDataAccessException;
62  import org.springframework.security.access.AccessDeniedException;
63  import org.springframework.transaction.TransactionSystemException;
64  
65  @Provider
66  public class RestServiceExceptionMapper implements ExceptionMapper<Exception> {
67  
68      private static final Logger LOG = LoggerFactory.getLogger(RestServiceExceptionMapper.class);
69  
70      private final ValidationExceptionMapper validationEM = new ValidationExceptionMapper();
71  
72      private static final String UNIQUE_MSG_KEY = "UniqueConstraintViolation";
73  
74      private static final Map<String, String> EXCEPTION_CODE_MAP = new HashMap<>() {
75  
76          private static final long serialVersionUID = -7688359318035249200L;
77  
78          {
79              put("23000", UNIQUE_MSG_KEY);
80              put("23505", UNIQUE_MSG_KEY);
81          }
82      };
83  
84      protected final Environment env;
85  
86      public RestServiceExceptionMapper(final Environment env) {
87          this.env = env;
88      }
89  
90      @Override
91      public Response toResponse(final Exception ex) {
92          LOG.error("Exception thrown", ex);
93  
94          ResponseBuilder builder;
95  
96          if (ex instanceof AccessDeniedException) {
97              // leaves the default exception processing to Spring Security
98              builder = null;
99          } else if (ex instanceof SyncopeClientException) {
100             SyncopeClientException sce = (SyncopeClientException) ex;
101             builder = sce.isComposite()
102                     ? getSyncopeClientCompositeExceptionResponse(sce.asComposite())
103                     : getSyncopeClientExceptionResponse(sce);
104         } else if (ex instanceof DelegatedAdministrationException
105                 || ExceptionUtils.getRootCause(ex) instanceof DelegatedAdministrationException) {
106 
107             builder = builder(ClientExceptionType.DelegatedAdministration, ExceptionUtils.getRootCauseMessage(ex));
108         } else if (ex instanceof EntityExistsException || ex instanceof DuplicateException
109                 || ((ex instanceof PersistenceException || ex instanceof DataIntegrityViolationException)
110                 && ex.getCause() instanceof EntityExistsException)) {
111 
112             builder = builder(ClientExceptionType.EntityExists,
113                     getPersistenceErrorMessage(
114                             ex instanceof PersistenceException || ex instanceof DataIntegrityViolationException
115                                     ? ex.getCause() : ex));
116         } else if (ex instanceof DataIntegrityViolationException || ex instanceof UncategorizedDataAccessException) {
117             builder = builder(ClientExceptionType.DataIntegrityViolation, getPersistenceErrorMessage(ex));
118         } else if (ex instanceof ConnectorException) {
119             builder = builder(ClientExceptionType.ConnectorException, ExceptionUtils.getRootCauseMessage(ex));
120         } else if (ex instanceof NotFoundException) {
121             builder = builder(ClientExceptionType.NotFound, ExceptionUtils.getRootCauseMessage(ex));
122         } else {
123             builder = processInvalidEntityExceptions(ex);
124             if (builder == null) {
125                 builder = processBadRequestExceptions(ex);
126             }
127             // process JAX-RS validation errors
128             if (builder == null && ex instanceof ValidationException) {
129                 builder = builder(validationEM.toResponse((ValidationException) ex)).
130                         header(RESTHeaders.ERROR_CODE, ClientExceptionType.RESTValidation.name()).
131                         header(RESTHeaders.ERROR_INFO, ClientExceptionType.RESTValidation.getInfoHeaderValue(
132                                 ExceptionUtils.getRootCauseMessage(ex)));
133             }
134             // process web application exceptions
135             if (builder == null && ex instanceof WebApplicationException) {
136                 builder = builder(((WebApplicationException) ex).getResponse()).
137                         header(RESTHeaders.ERROR_CODE, ClientExceptionType.Unknown.name()).
138                         header(RESTHeaders.ERROR_INFO, ClientExceptionType.Unknown.getInfoHeaderValue(
139                                 ExceptionUtils.getRootCauseMessage(ex)));
140             }
141             // ...or just report as InternalServerError
142             if (builder == null) {
143                 builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR).
144                         header(RESTHeaders.ERROR_CODE, ClientExceptionType.Unknown.name()).
145                         header(RESTHeaders.ERROR_INFO, ClientExceptionType.Unknown.getInfoHeaderValue(
146                                 ExceptionUtils.getRootCauseMessage(ex)));
147 
148                 ErrorTO error = new ErrorTO();
149                 error.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode());
150                 error.setType(ClientExceptionType.Unknown);
151                 error.getElements().add(ExceptionUtils.getRootCauseMessage(ex));
152 
153                 builder.entity(error);
154             }
155         }
156 
157         return Optional.ofNullable(builder).map(ResponseBuilder::build).orElse(null);
158     }
159 
160     private static ResponseBuilder getSyncopeClientExceptionResponse(final SyncopeClientException ex) {
161         ResponseBuilder builder = Response.status(ex.getType().getResponseStatus());
162         builder.header(RESTHeaders.ERROR_CODE, ex.getType().name());
163 
164         ErrorTO error = new ErrorTO();
165         error.setStatus(ex.getType().getResponseStatus().getStatusCode());
166         error.setType(ex.getType());
167 
168         ex.getElements().forEach(element -> {
169             builder.header(RESTHeaders.ERROR_INFO, ex.getType().getInfoHeaderValue(element));
170             error.getElements().add(element);
171         });
172 
173         return builder.entity(error);
174     }
175 
176     private static ResponseBuilder getSyncopeClientCompositeExceptionResponse(
177             final SyncopeClientCompositeException ex) {
178         if (ex.getExceptions().size() == 1) {
179             return getSyncopeClientExceptionResponse(ex.getExceptions().iterator().next());
180         }
181 
182         ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
183 
184         List<ErrorTO> errors = ex.getExceptions().stream().map(sce -> {
185             builder.header(RESTHeaders.ERROR_CODE, sce.getType().name());
186 
187             ErrorTO error = new ErrorTO();
188             error.setStatus(sce.getType().getResponseStatus().getStatusCode());
189             error.setType(sce.getType());
190 
191             sce.getElements().forEach(element -> {
192                 builder.header(RESTHeaders.ERROR_INFO, sce.getType().getInfoHeaderValue(element));
193                 error.getElements().add(element);
194             });
195 
196             return error;
197         }).collect(Collectors.toList());
198 
199         return builder.entity(errors);
200     }
201 
202     private static ResponseBuilder processInvalidEntityExceptions(final Exception ex) {
203         InvalidEntityException iee = null;
204 
205         if (ex instanceof InvalidEntityException) {
206             iee = (InvalidEntityException) ex;
207         }
208         if (ex instanceof TransactionSystemException && ex.getCause() instanceof RollbackException
209                 && ex.getCause().getCause() instanceof InvalidEntityException) {
210 
211             iee = (InvalidEntityException) ex.getCause().getCause();
212         }
213 
214         if (iee != null) {
215             ClientExceptionType exType;
216             if (iee.getEntityClassSimpleName().endsWith("Policy")) {
217                 exType = ClientExceptionType.InvalidPolicy;
218             } else if (iee.getEntityClassSimpleName().equals(PlainAttr.class.getSimpleName())) {
219                 exType = ClientExceptionType.InvalidValues;
220             } else {
221                 try {
222                     exType = ClientExceptionType.valueOf("Invalid" + iee.getEntityClassSimpleName());
223                 } catch (IllegalArgumentException e) {
224                     // ignore
225                     exType = ClientExceptionType.InvalidEntity;
226                 }
227             }
228 
229             ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST);
230             builder.header(RESTHeaders.ERROR_CODE, exType.name());
231 
232             ErrorTO error = new ErrorTO();
233             error.setStatus(exType.getResponseStatus().getStatusCode());
234             error.setType(exType);
235 
236             for (Map.Entry<Class<?>, Set<EntityViolationType>> violation : iee.getViolations().entrySet()) {
237                 for (EntityViolationType violationType : violation.getValue()) {
238                     builder.header(RESTHeaders.ERROR_INFO,
239                             exType.getInfoHeaderValue(violationType.name() + ": " + violationType.getMessage()));
240                     error.getElements().add(violationType.name() + ": " + violationType.getMessage());
241                 }
242             }
243 
244             return builder;
245         }
246 
247         return null;
248     }
249 
250     private static ResponseBuilder processBadRequestExceptions(final Exception ex) {
251         // This exception might be raised by Flowable (if enabled)
252         Class<?> ibatisPersistenceException = null;
253         try {
254             ibatisPersistenceException = Class.forName("org.apache.ibatis.exceptions.PersistenceException");
255         } catch (ClassNotFoundException e) {
256             // ignore
257         }
258 
259         if (ex instanceof WorkflowException) {
260             return builder(ClientExceptionType.Workflow, ExceptionUtils.getRootCauseMessage(ex));
261         } else if (ex instanceof PersistenceException) {
262             return builder(ClientExceptionType.GenericPersistence, ExceptionUtils.getRootCauseMessage(ex));
263         } else if (ibatisPersistenceException != null && ibatisPersistenceException.isAssignableFrom(ex.getClass())) {
264             return builder(ClientExceptionType.Workflow, "Currently unavailable. Please try later.");
265         } else if (ex instanceof UncategorizedDataAccessException) {
266             return builder(ClientExceptionType.DataIntegrityViolation, ExceptionUtils.getRootCauseMessage(ex));
267         } else if (ex instanceof ConfigurationException) {
268             return builder(ClientExceptionType.InvalidConnIdConf, ExceptionUtils.getRootCauseMessage(ex));
269         } else if (ex instanceof ParsingValidationException) {
270             return builder(ClientExceptionType.InvalidValues, ExceptionUtils.getRootCauseMessage(ex));
271         } else if (ex instanceof MalformedPathException) {
272             return builder(ClientExceptionType.InvalidPath, ExceptionUtils.getRootCauseMessage(ex));
273         }
274 
275         return null;
276     }
277 
278     private static ResponseBuilder builder(final ClientExceptionType hType, final String msg) {
279         ResponseBuilder builder = Response.status(hType.getResponseStatus()).
280                 header(RESTHeaders.ERROR_CODE, hType.name()).
281                 header(RESTHeaders.ERROR_INFO, hType.getInfoHeaderValue(msg));
282 
283         ErrorTO error = new ErrorTO();
284         error.setStatus(hType.getResponseStatus().getStatusCode());
285         error.setType(hType);
286         error.getElements().add(msg);
287 
288         return builder.entity(error);
289     }
290 
291     /**
292      * Overriding {@link JAXRSUtils#fromResponse(javax.ws.rs.core.Response)} in order to avoid setting
293      * {@code Content-Type} from original {@code response}.
294      *
295      * @param response model to construct {@link ResponseBuilder} from
296      * @return new {@link ResponseBuilder} instance initialized from given response
297      */
298     private static ResponseBuilder builder(final Response response) {
299         ResponseBuilder builder = JAXRSUtils.toResponseBuilder(response.getStatus());
300         builder.entity(response.getEntity());
301         response.getMetadata().forEach((key, value) -> {
302             if (!HttpHeaders.CONTENT_TYPE.equals(key)) {
303                 value.forEach(headerValue -> builder.header(key, headerValue));
304             }
305         });
306 
307         return builder;
308     }
309 
310     private String getPersistenceErrorMessage(final Throwable ex) {
311         Throwable throwable = ExceptionUtils.getRootCause(ex);
312 
313         String message = null;
314         if (throwable instanceof SQLException) {
315             String messageKey = EXCEPTION_CODE_MAP.get(((SQLException) throwable).getSQLState());
316             if (messageKey != null) {
317                 message = env.getProperty("errMessage." + messageKey);
318             }
319         } else if (throwable instanceof EntityExistsException || throwable instanceof DuplicateException) {
320             message = env.getProperty("errMessage." + UNIQUE_MSG_KEY);
321         }
322 
323         return Optional.ofNullable(message).
324                 orElseGet(() -> Optional.ofNullable(ex.getCause()).
325                 map(Throwable::getMessage).
326                 orElseGet(() -> ex.getMessage()));
327     }
328 }