| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.syncope.core.rest.cxf; |
| |
| import java.sql.SQLException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.stream.Collectors; |
| import javax.persistence.EntityExistsException; |
| import javax.persistence.PersistenceException; |
| import javax.persistence.RollbackException; |
| import javax.validation.ValidationException; |
| import javax.ws.rs.core.HttpHeaders; |
| import javax.ws.rs.core.Response; |
| import javax.ws.rs.core.Response.ResponseBuilder; |
| import javax.ws.rs.ext.ExceptionMapper; |
| import javax.ws.rs.ext.Provider; |
| import org.apache.commons.lang3.exception.ExceptionUtils; |
| import org.apache.cxf.jaxrs.utils.JAXRSUtils; |
| import org.apache.cxf.jaxrs.validation.ValidationExceptionMapper; |
| import org.apache.syncope.common.lib.SyncopeClientCompositeException; |
| import org.apache.syncope.common.lib.SyncopeClientException; |
| import org.apache.syncope.common.lib.to.ErrorTO; |
| import org.apache.syncope.common.lib.types.ClientExceptionType; |
| import org.apache.syncope.common.lib.types.EntityViolationType; |
| import org.apache.syncope.common.rest.api.RESTHeaders; |
| import org.apache.syncope.core.spring.security.DelegatedAdministrationException; |
| import org.apache.syncope.core.persistence.api.attrvalue.validation.InvalidEntityException; |
| import org.apache.syncope.core.persistence.api.attrvalue.validation.ParsingValidationException; |
| import org.apache.syncope.core.persistence.api.dao.DuplicateException; |
| import org.apache.syncope.core.persistence.api.dao.MalformedPathException; |
| import org.apache.syncope.core.persistence.api.dao.NotFoundException; |
| import org.apache.syncope.core.persistence.api.entity.PlainAttr; |
| import org.apache.syncope.core.workflow.api.WorkflowException; |
| import org.identityconnectors.framework.common.exceptions.ConfigurationException; |
| import org.identityconnectors.framework.common.exceptions.ConnectorException; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.springframework.beans.factory.annotation.Autowired; |
| import org.springframework.core.env.Environment; |
| import org.springframework.dao.DataIntegrityViolationException; |
| import org.springframework.dao.UncategorizedDataAccessException; |
| import org.springframework.security.access.AccessDeniedException; |
| import org.springframework.transaction.TransactionSystemException; |
| |
| @Provider |
| public class RestServiceExceptionMapper implements ExceptionMapper<Exception> { |
| |
| private static final Logger LOG = LoggerFactory.getLogger(RestServiceExceptionMapper.class); |
| |
| private final ValidationExceptionMapper validationEM = new ValidationExceptionMapper(); |
| |
| @Autowired |
| private Environment env; |
| |
| private static final String UNIQUE_MSG_KEY = "UniqueConstraintViolation"; |
| |
| private static final Map<String, String> EXCEPTION_CODE_MAP = new HashMap<>() { |
| |
| private static final long serialVersionUID = -7688359318035249200L; |
| |
| { |
| put("23000", UNIQUE_MSG_KEY); |
| put("23505", UNIQUE_MSG_KEY); |
| } |
| }; |
| |
| @Override |
| public Response toResponse(final Exception ex) { |
| LOG.error("Exception thrown", ex); |
| |
| ResponseBuilder builder; |
| |
| if (ex instanceof AccessDeniedException) { |
| // leaves the default exception processing to Spring Security |
| builder = null; |
| } else if (ex instanceof SyncopeClientException) { |
| SyncopeClientException sce = (SyncopeClientException) ex; |
| builder = sce.isComposite() |
| ? getSyncopeClientCompositeExceptionResponse(sce.asComposite()) |
| : getSyncopeClientExceptionResponse(sce); |
| } else if (ex instanceof DelegatedAdministrationException |
| || ExceptionUtils.getRootCause(ex) instanceof DelegatedAdministrationException) { |
| |
| builder = builder(ClientExceptionType.DelegatedAdministration, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ex instanceof EntityExistsException || ex instanceof DuplicateException |
| || ((ex instanceof PersistenceException || ex instanceof DataIntegrityViolationException) |
| && ex.getCause() instanceof EntityExistsException)) { |
| |
| builder = builder(ClientExceptionType.EntityExists, |
| getPersistenceErrorMessage( |
| ex instanceof PersistenceException || ex instanceof DataIntegrityViolationException |
| ? ex.getCause() : ex)); |
| } else if (ex instanceof DataIntegrityViolationException || ex instanceof UncategorizedDataAccessException) { |
| builder = builder(ClientExceptionType.DataIntegrityViolation, getPersistenceErrorMessage(ex)); |
| } else if (ex instanceof ConnectorException) { |
| builder = builder(ClientExceptionType.ConnectorException, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ex instanceof NotFoundException) { |
| builder = builder(ClientExceptionType.NotFound, ExceptionUtils.getRootCauseMessage(ex)); |
| } else { |
| builder = processInvalidEntityExceptions(ex); |
| if (builder == null) { |
| builder = processBadRequestExceptions(ex); |
| } |
| // process JAX-RS validation errors |
| if (builder == null && ex instanceof ValidationException) { |
| builder = builder(validationEM.toResponse((ValidationException) ex)). |
| header(RESTHeaders.ERROR_CODE, ClientExceptionType.RESTValidation.name()). |
| header(RESTHeaders.ERROR_INFO, ClientExceptionType.RESTValidation.getInfoHeaderValue( |
| ExceptionUtils.getRootCauseMessage(ex))); |
| |
| ErrorTO error = new ErrorTO(); |
| error.setStatus(ClientExceptionType.RESTValidation.getResponseStatus().getStatusCode()); |
| error.setType(ClientExceptionType.RESTValidation); |
| error.getElements().add(ExceptionUtils.getRootCauseMessage(ex)); |
| |
| builder.entity(error); |
| } |
| // ...or just report as InternalServerError |
| if (builder == null) { |
| builder = Response.status(Response.Status.INTERNAL_SERVER_ERROR). |
| header(RESTHeaders.ERROR_INFO, ClientExceptionType.Unknown.getInfoHeaderValue( |
| ExceptionUtils.getRootCauseMessage(ex))); |
| |
| ErrorTO error = new ErrorTO(); |
| error.setStatus(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); |
| error.setType(ClientExceptionType.Unknown); |
| error.getElements().add(ExceptionUtils.getRootCauseMessage(ex)); |
| |
| builder.entity(error); |
| } |
| } |
| |
| return Optional.ofNullable(builder).map(ResponseBuilder::build).orElse(null); |
| } |
| |
| private static ResponseBuilder getSyncopeClientExceptionResponse(final SyncopeClientException ex) { |
| ResponseBuilder builder = Response.status(ex.getType().getResponseStatus()); |
| builder.header(RESTHeaders.ERROR_CODE, ex.getType().name()); |
| |
| ErrorTO error = new ErrorTO(); |
| error.setStatus(ex.getType().getResponseStatus().getStatusCode()); |
| error.setType(ex.getType()); |
| |
| ex.getElements().forEach(element -> { |
| builder.header(RESTHeaders.ERROR_INFO, ex.getType().getInfoHeaderValue(element)); |
| error.getElements().add(element); |
| }); |
| |
| return builder.entity(error); |
| } |
| |
| private static ResponseBuilder getSyncopeClientCompositeExceptionResponse( |
| final SyncopeClientCompositeException ex) { |
| if (ex.getExceptions().size() == 1) { |
| return getSyncopeClientExceptionResponse(ex.getExceptions().iterator().next()); |
| } |
| |
| ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST); |
| |
| List<ErrorTO> errors = ex.getExceptions().stream().map(sce -> { |
| builder.header(RESTHeaders.ERROR_CODE, sce.getType().name()); |
| |
| ErrorTO error = new ErrorTO(); |
| error.setStatus(sce.getType().getResponseStatus().getStatusCode()); |
| error.setType(sce.getType()); |
| |
| sce.getElements().forEach(element -> { |
| builder.header(RESTHeaders.ERROR_INFO, sce.getType().getInfoHeaderValue(element)); |
| error.getElements().add(element); |
| }); |
| |
| return error; |
| }).collect(Collectors.toList()); |
| |
| return builder.entity(errors); |
| } |
| |
| private static ResponseBuilder processInvalidEntityExceptions(final Exception ex) { |
| InvalidEntityException iee = null; |
| |
| if (ex instanceof InvalidEntityException) { |
| iee = (InvalidEntityException) ex; |
| } |
| if (ex instanceof TransactionSystemException && ex.getCause() instanceof RollbackException |
| && ex.getCause().getCause() instanceof InvalidEntityException) { |
| |
| iee = (InvalidEntityException) ex.getCause().getCause(); |
| } |
| |
| if (iee != null) { |
| ClientExceptionType exType; |
| if (iee.getEntityClassSimpleName().endsWith("Policy")) { |
| exType = ClientExceptionType.InvalidPolicy; |
| } else if (iee.getEntityClassSimpleName().equals(PlainAttr.class.getSimpleName())) { |
| exType = ClientExceptionType.InvalidValues; |
| } else { |
| try { |
| exType = ClientExceptionType.valueOf("Invalid" + iee.getEntityClassSimpleName()); |
| } catch (IllegalArgumentException e) { |
| // ignore |
| exType = ClientExceptionType.InvalidEntity; |
| } |
| } |
| |
| ResponseBuilder builder = Response.status(Response.Status.BAD_REQUEST); |
| builder.header(RESTHeaders.ERROR_CODE, exType.name()); |
| |
| ErrorTO error = new ErrorTO(); |
| error.setStatus(exType.getResponseStatus().getStatusCode()); |
| error.setType(exType); |
| |
| for (Map.Entry<Class<?>, Set<EntityViolationType>> violation : iee.getViolations().entrySet()) { |
| for (EntityViolationType violationType : violation.getValue()) { |
| builder.header(RESTHeaders.ERROR_INFO, |
| exType.getInfoHeaderValue(violationType.name() + ": " + violationType.getMessage())); |
| error.getElements().add(violationType.name() + ": " + violationType.getMessage()); |
| } |
| } |
| |
| return builder; |
| } |
| |
| return null; |
| } |
| |
| private static ResponseBuilder processBadRequestExceptions(final Exception ex) { |
| // This exception might be raised by Flowable (if enabled) |
| Class<?> ibatisPersistenceException = null; |
| try { |
| ibatisPersistenceException = Class.forName("org.apache.ibatis.exceptions.PersistenceException"); |
| } catch (ClassNotFoundException e) { |
| // ignore |
| } |
| |
| if (ex instanceof WorkflowException) { |
| return builder(ClientExceptionType.Workflow, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ex instanceof PersistenceException) { |
| return builder(ClientExceptionType.GenericPersistence, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ibatisPersistenceException != null && ibatisPersistenceException.isAssignableFrom(ex.getClass())) { |
| return builder(ClientExceptionType.Workflow, "Currently unavailable. Please try later."); |
| } else if (ex instanceof UncategorizedDataAccessException) { |
| return builder(ClientExceptionType.DataIntegrityViolation, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ex instanceof ConfigurationException) { |
| return builder(ClientExceptionType.InvalidConnIdConf, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ex instanceof ParsingValidationException) { |
| return builder(ClientExceptionType.InvalidValues, ExceptionUtils.getRootCauseMessage(ex)); |
| } else if (ex instanceof MalformedPathException) { |
| return builder(ClientExceptionType.InvalidPath, ExceptionUtils.getRootCauseMessage(ex)); |
| } |
| |
| return null; |
| } |
| |
| private static ResponseBuilder builder(final ClientExceptionType hType, final String msg) { |
| ResponseBuilder builder = Response.status(hType.getResponseStatus()). |
| header(RESTHeaders.ERROR_CODE, hType.name()). |
| header(RESTHeaders.ERROR_INFO, hType.getInfoHeaderValue(msg)); |
| |
| ErrorTO error = new ErrorTO(); |
| error.setStatus(hType.getResponseStatus().getStatusCode()); |
| error.setType(hType); |
| error.getElements().add(msg); |
| |
| return builder.entity(error); |
| } |
| |
| /** |
| * Overriding {@link JAXRSUtils#fromResponse(javax.ws.rs.core.Response)} in order to avoid setting |
| * {@code Content-Type} from original {@code response}. |
| * |
| * @param response model to construct {@link ResponseBuilder} from |
| * @return new {@link ResponseBuilder} instance initialized from given response |
| */ |
| private static ResponseBuilder builder(final Response response) { |
| ResponseBuilder builder = JAXRSUtils.toResponseBuilder(response.getStatus()); |
| builder.entity(response.getEntity()); |
| response.getMetadata().forEach((key, value) -> { |
| if (!HttpHeaders.CONTENT_TYPE.equals(key)) { |
| value.forEach(headerValue -> builder.header(key, headerValue)); |
| } |
| }); |
| |
| return builder; |
| } |
| |
| private String getPersistenceErrorMessage(final Throwable ex) { |
| Throwable throwable = ExceptionUtils.getRootCause(ex); |
| |
| String message = null; |
| if (throwable instanceof SQLException) { |
| String messageKey = EXCEPTION_CODE_MAP.get(((SQLException) throwable).getSQLState()); |
| if (messageKey != null) { |
| message = env.getProperty("errMessage." + messageKey); |
| } |
| } else if (throwable instanceof EntityExistsException || throwable instanceof DuplicateException) { |
| message = env.getProperty("errMessage." + UNIQUE_MSG_KEY); |
| } |
| |
| return Optional.ofNullable(message) |
| .orElseGet(() -> (ex.getCause() == null) ? ex.getMessage() : ex.getCause().getMessage()); |
| } |
| } |