Fix/exception mapper (#3083)

* Added mapping for OptimisticLockingFailureException in ExceptionMapper for proper propagation

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>

* Added ExceptionMapperTest

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>

---------

Signed-off-by: vasilchev <vasil.ilchev@bosch.com>
This commit is contained in:
Vasil Ilchev
2026-05-18 09:22:49 +03:00
committed by GitHub
parent 60f006cfa1
commit 2a4c12fd29
3 changed files with 119 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.exception.GenericSpServerException;
import org.eclipse.hawkbit.ql.QueryException;
import org.eclipse.hawkbit.repository.exception.ConcurrentModificationException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
@@ -36,7 +37,7 @@ import org.springframework.transaction.TransactionSystemException;
@Slf4j
public class ExceptionMapper {
private static final Map<String, String> EXCEPTION_MAPPING = HashMap.newHashMap(4);
private static final Map<String, String> EXCEPTION_MAPPING = HashMap.newHashMap(5);
// this is required to enable a certain order of exception and to select the most specific mappable exception according to the type
// hierarchy of the exception
@@ -48,6 +49,7 @@ public class ExceptionMapper {
MAPPED_EXCEPTION_ORDER.add(AccessDeniedException.class);
EXCEPTION_MAPPING.put(DuplicateKeyException.class.getName(), EntityAlreadyExistsException.class.getName());
EXCEPTION_MAPPING.put(OptimisticLockingFailureException.class.getName(), ConcurrentModificationException.class.getName());
EXCEPTION_MAPPING.put(AccessDeniedException.class.getName(), InsufficientPermissionException.class.getName());
EXCEPTION_MAPPING.put("org.hibernate.exception.ConstraintViolationException", EntityAlreadyExistsException.class.getName());

View File

@@ -0,0 +1,84 @@
/**
* Copyright (c) 2026 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.utils;
import static org.assertj.core.api.Assertions.assertThat;
import jakarta.persistence.OptimisticLockException;
import org.eclipse.hawkbit.exception.GenericSpServerException;
import org.eclipse.hawkbit.ql.QueryException;
import org.eclipse.hawkbit.repository.exception.ConcurrentModificationException;
import org.eclipse.hawkbit.repository.exception.EntityAlreadyExistsException;
import org.eclipse.hawkbit.repository.exception.InsufficientPermissionException;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.orm.jpa.JpaOptimisticLockingFailureException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.security.authorization.AuthorizationResult;
class ExceptionMapperTest {
@Test
void duplicateKeyMappedToEntityAlreadyExists() {
final DuplicateKeyException cause = new DuplicateKeyException("dup");
assertMappedTo(ExceptionMapper.mapRe(cause), cause, EntityAlreadyExistsException.class);
}
@Test
void optimisticLockMappedToConcurrentModification() {
final OptimisticLockingFailureException cause = new OptimisticLockingFailureException("conflict");
assertMappedTo(ExceptionMapper.mapRe(cause), cause, ConcurrentModificationException.class);
}
@Test
void jpaOptimisticLockSubclassMappedToConcurrentModification() {
final JpaOptimisticLockingFailureException cause = new JpaOptimisticLockingFailureException(new OptimisticLockException("conflict"));
assertMappedTo(ExceptionMapper.mapRe(cause), cause, ConcurrentModificationException.class);
}
@Test
void accessDeniedMappedToInsufficientPermission() {
final var cause = new AccessDeniedException("denied");
assertMappedTo(ExceptionMapper.mapRe(cause), cause, InsufficientPermissionException.class);
}
@Test
void authorizationDeniedMappedToInsufficientPermission() {
final var cause = new AuthorizationDeniedException("denied", (AuthorizationResult) () -> false);
assertMappedTo(ExceptionMapper.mapRe(cause), cause, InsufficientPermissionException.class);
}
@Test
void queryExceptionGenericMappedToGenericSpServerException() {
final QueryException qe = new QueryException(QueryException.ErrorCode.GENERIC, "generic error");
assertMappedTo(ExceptionMapper.map(qe), qe, GenericSpServerException.class);
}
@Test
void unknownExceptionReturnedUnchanged() {
final var unknown = new IllegalStateException("unexpected");
assertThat(ExceptionMapper.mapRe(unknown)).isSameAs(unknown);
}
private static void assertMappedTo(final Exception actualResult, final Exception expectedCause, final Class<?> expectedType) {
assertThat(actualResult)
.isInstanceOf(expectedType)
.hasCause(expectedCause);
}
}