From 6b5f16f4818863cb3d392422ba19ef538a911a7b Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Mon, 22 Sep 2025 16:27:42 +0200 Subject: [PATCH 1/6] harmonise exceptions --- pom.xml | 1 + .../directory/server/DirectoryException.java | 14 ++-- .../directory/server/DirectoryService.java | 38 ++++++---- .../RestResponseEntityExceptionHandler.java | 76 +++++++++++++------ .../directory/server/DirectoryTest.java | 8 +- 5 files changed, 88 insertions(+), 49 deletions(-) diff --git a/pom.xml b/pom.xml index 4f00361d..9c7b98f9 100644 --- a/pom.xml +++ b/pom.xml @@ -195,6 +195,7 @@ com.powsybl powsybl-ws-commons + 1.29.0-SNAPSHOT io.projectreactor diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryException.java b/src/main/java/org/gridsuite/directory/server/DirectoryException.java index a5f585b1..b0aed9b2 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryException.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryException.java @@ -18,14 +18,9 @@ public class DirectoryException extends RuntimeException { private final Type type; - public DirectoryException(Type type) { - super(Objects.requireNonNull(type.name())); - this.type = type; - } - public DirectoryException(Type type, String message) { - super(message); - this.type = type; + super(Objects.requireNonNull(message, "message must not be null")); + this.type = Objects.requireNonNull(type, "type must not be null"); } public static DirectoryException createNotificationUnknown(@NonNull String action) { @@ -40,6 +35,10 @@ public static DirectoryException createElementNameAlreadyExists(@NonNull String return new DirectoryException(Type.NAME_ALREADY_EXISTS, String.format("Element with the same name '%s' already exists in the directory !", name)); } + public static DirectoryException of(Type type, String message, Object... args) { + return new DirectoryException(type, args.length == 0 ? message : String.format(message, args)); + } + Type getType() { return type; } @@ -48,7 +47,6 @@ public enum Type { NOT_ALLOWED, NOT_FOUND, NOT_DIRECTORY, - IS_DIRECTORY, UNKNOWN_NOTIFICATION, NAME_ALREADY_EXISTS, MOVE_IN_DESCENDANT_NOT_ALLOWED, diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryService.java b/src/main/java/org/gridsuite/directory/server/DirectoryService.java index ba21acea..a5e581a2 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryService.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryService.java @@ -94,7 +94,7 @@ public ElementAttributes createElement(ElementAttributes elementAttributes, UUID private ElementAttributes createElementWithNotif(ElementAttributes elementAttributes, UUID parentDirectoryUuid, String userId, boolean generateNewName) { if (elementAttributes.getElementName().isBlank()) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(NOT_ALLOWED, "Element name must not be blank"); } assertDirectoryExist(parentDirectoryUuid); DirectoryElementEntity elementEntity = insertElement(elementAttributes, parentDirectoryUuid, userId, generateNewName); @@ -111,7 +111,8 @@ private ElementAttributes createElementWithNotif(ElementAttributes elementAttrib } public ElementAttributes duplicateElement(UUID elementId, UUID newElementId, UUID targetDirectoryId, String userId) { - DirectoryElementEntity directoryElementEntity = directoryElementRepository.findById(elementId).orElseThrow(() -> new DirectoryException(NOT_FOUND)); + DirectoryElementEntity directoryElementEntity = directoryElementRepository.findById(elementId) + .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementId)); String elementType = directoryElementEntity.getType(); UUID parentDirectoryUuid = targetDirectoryId != null ? targetDirectoryId : directoryElementEntity.getParentId(); ElementAttributes elementAttributes = ElementAttributes.builder() @@ -132,14 +133,14 @@ public ElementAttributes duplicateElement(UUID elementId, UUID newElementId, UUI } private void assertRootDirectoryNotExist(String rootName) { - if (TRUE.equals(repositoryService.isRootDirectoryExist(rootName))) { - throw new DirectoryException(NOT_ALLOWED); + if (repositoryService.isRootDirectoryExist(rootName)) { + throw DirectoryException.of(NOT_ALLOWED, "Root directory '%s' already exists", rootName); } } private void assertDirectoryExist(UUID dirUuid) { if (!getElement(dirUuid).getType().equals(DIRECTORY)) { - throw new DirectoryException(NOT_DIRECTORY); + throw DirectoryException.of(NOT_DIRECTORY, "Element '%s' is not a directory", dirUuid); } } @@ -192,7 +193,7 @@ public ElementAttributes createRootDirectory(RootDirectoryAttributes rootDirecto private ElementAttributes createRootDirectoryWithNotif(RootDirectoryAttributes rootDirectoryAttributes, String userId) { if (rootDirectoryAttributes.getElementName().isBlank()) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(NOT_ALLOWED, "Root directory name must not be blank"); } assertRootDirectoryNotExist(rootDirectoryAttributes.getElementName()); @@ -314,7 +315,9 @@ public void updateElement(UUID elementUuid, ElementAttributes newElementAttribut if (!directoryElement.isAttributesUpdatable(newElementAttributes, userId) || !directoryElement.getName().equals(newElementAttributes.getElementName()) && directoryHasElementOfNameAndType(directoryElement.getParentId(), newElementAttributes.getElementName(), directoryElement.getType())) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(NOT_ALLOWED, + "Update forbidden for element '%s': invalid permissions or duplicate name", + directoryElement.getId()); } DirectoryElementEntity elementEntity = repositoryService.saveElement(directoryElement.update(newElementAttributes)); @@ -332,7 +335,7 @@ public void updateElementLastModifiedAttributes(UUID elementUuid, Instant lastMo @Transactional public void moveElementsDirectory(List elementsUuids, UUID newDirectoryUuid, String userId) { if (elementsUuids.isEmpty()) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(NOT_ALLOWED, "Cannot move elements: no elements provided"); } validateNewDirectory(newDirectoryUuid); @@ -371,7 +374,9 @@ private void moveElementDirectory(DirectoryElementEntity element, UUID newDirect private void validateElementForMove(DirectoryElementEntity element, UUID newDirectoryUuid, Set descendentsUuids) { if (newDirectoryUuid == element.getId() || descendentsUuids.contains(newDirectoryUuid)) { - throw new DirectoryException(MOVE_IN_DESCENDANT_NOT_ALLOWED); + throw DirectoryException.of(MOVE_IN_DESCENDANT_NOT_ALLOWED, + "Cannot move element '%s' into one of its descendants", + element.getId()); } if (directoryHasElementOfNameAndType(newDirectoryUuid, element.getName(), element.getType())) { @@ -389,7 +394,7 @@ private void validateNewDirectory(UUID newDirectoryUuid) { .orElseThrow(() -> DirectoryException.createElementNotFound(DIRECTORY, newDirectoryUuid)); if (!newDirectory.getType().equals(DIRECTORY)) { - throw new DirectoryException(NOT_DIRECTORY); + throw DirectoryException.of(NOT_DIRECTORY, "Target '%s' is not a directory", newDirectoryUuid); } } @@ -470,7 +475,8 @@ public List getPath(UUID elementUuid) { } public String getElementName(UUID elementUuid) { - DirectoryElementEntity element = repositoryService.getElementEntity(elementUuid).orElseThrow(() -> new DirectoryException(NOT_FOUND)); + DirectoryElementEntity element = repositoryService.getElementEntity(elementUuid) + .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementUuid)); return element.getName(); } @@ -507,7 +513,7 @@ public List getElements(List ids, boolean strictMode, L } if (strictMode && elementEntities.size() != ids.stream().distinct().count()) { - throw new DirectoryException(NOT_FOUND); + throw DirectoryException.of(NOT_FOUND, "Some requested elements were not found"); } Map subElementsCount = getSubDirectoriesCounts(elementEntities.stream().map(DirectoryElementEntity::getId).toList(), types); @@ -545,7 +551,7 @@ private String nameCandidate(String elementName, int n) { public String getDuplicateNameCandidate(UUID directoryUuid, String elementName, String elementType, String userId) { if (!repositoryService.canRead(directoryUuid, userId)) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(NOT_ALLOWED, "User '%s' cannot access directory '%s'", userId, directoryUuid); } var idLikes = new HashSet<>(repositoryService.getNameByTypeAndParentIdAndNameStartWith(elementType, directoryUuid, elementName)); if (!idLikes.contains(elementName)) { @@ -569,7 +575,7 @@ public UUID getDirectoryUuidFromPath(List directoryPath) { for (String s : directoryPath) { UUID currentDirectoryUuid = getDirectoryUuid(s, parentDirectoryUuid); if (currentDirectoryUuid == null) { - throw new DirectoryException(NOT_FOUND); + throw DirectoryException.of(NOT_FOUND, "Directory '%s' not found in path", s); } else { parentDirectoryUuid = currentDirectoryUuid; } @@ -788,7 +794,7 @@ private void addPermissionForGroup(UUID elementUuid, String groupId, PermissionT public void validatePermissionsGetAccess(UUID directoryUuid, String userId) { if (!roleService.isUserExploreAdmin() && !hasReadPermissions(userId, List.of(directoryUuid))) { - throw new DirectoryException(NOT_ALLOWED); + throw DirectoryException.of(NOT_ALLOWED, "User '%s' is not allowed to view directory '%s'", userId, directoryUuid); } } @@ -900,7 +906,7 @@ private Map extractGroupPermissionLevels(List @@ -22,30 +28,54 @@ public class RestResponseEntityExceptionHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class); + private static final String SERVICE_NAME = "directory-server"; + private static final String CORRELATION_ID_HEADER = "X-Correlation-Id"; - @ExceptionHandler(value = {DirectoryException.class}) - protected ResponseEntity handleException(RuntimeException exception) { - if (LOGGER.isErrorEnabled()) { - LOGGER.error(exception.getMessage(), exception); + @ExceptionHandler(DirectoryException.class) + protected ResponseEntity handleDirectoryException(DirectoryException exception, HttpServletRequest request) { + HttpStatus status = switch (exception.getType()) { + case NOT_ALLOWED, NOT_DIRECTORY, MOVE_IN_DESCENDANT_NOT_ALLOWED -> HttpStatus.FORBIDDEN; + case NOT_FOUND -> HttpStatus.NOT_FOUND; + case UNKNOWN_NOTIFICATION -> HttpStatus.BAD_REQUEST; + case NAME_ALREADY_EXISTS -> HttpStatus.CONFLICT; + }; + return ResponseEntity.status(status) + .body(buildErrorResponse(request, status, exception.getType().name(), exception.getMessage())); + } + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleAllExceptions(Exception exception, HttpServletRequest request) { + HttpStatus status = resolveStatus(exception); + String message = exception.getMessage() != null ? exception.getMessage() : status.getReasonPhrase(); + return ResponseEntity.status(status) + .body(buildErrorResponse(request, status, status.name(), message)); + } + + private HttpStatus resolveStatus(Exception exception) { + if (exception instanceof ResponseStatusException responseStatusException) { + return HttpStatus.valueOf(responseStatusException.getStatusCode().value()); } - DirectoryException directoryException = (DirectoryException) exception; - switch (directoryException.getType()) { - case NOT_ALLOWED: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_ALLOWED); - case IS_DIRECTORY: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(IS_DIRECTORY); - case NOT_DIRECTORY: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(NOT_DIRECTORY); - case NOT_FOUND: - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(NOT_FOUND); - case UNKNOWN_NOTIFICATION: - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(UNKNOWN_NOTIFICATION); - case NAME_ALREADY_EXISTS: - return ResponseEntity.status(HttpStatus.CONFLICT).body(directoryException.getMessage()); - case MOVE_IN_DESCENDANT_NOT_ALLOWED: - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(MOVE_IN_DESCENDANT_NOT_ALLOWED); - default: - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + if (exception instanceof HttpStatusCodeException httpStatusCodeException) { + return HttpStatus.valueOf(httpStatusCodeException.getStatusCode().value()); } + if (exception instanceof ServletRequestBindingException) { + return HttpStatus.BAD_REQUEST; + } + if (exception instanceof NoResourceFoundException) { + return HttpStatus.NOT_FOUND; + } + return HttpStatus.INTERNAL_SERVER_ERROR; + } + + private ErrorResponse buildErrorResponse(HttpServletRequest request, HttpStatus status, String errorCode, String message) { + return new ErrorResponse( + SERVICE_NAME, + errorCode, + message, + status.value(), + Instant.now(), + request.getRequestURI(), + request.getHeader(CORRELATION_ID_HEADER) + ); } } diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java index e8f87130..95f040a9 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java @@ -31,7 +31,6 @@ import org.gridsuite.directory.server.repository.PermissionRepository; import org.gridsuite.directory.server.services.UserAdminService; import org.gridsuite.directory.server.utils.MatcherJson; -import org.hamcrest.core.IsEqual; import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.Before; @@ -74,6 +73,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** @@ -1020,7 +1020,11 @@ public void testEmitDirectoryChangedNotification() { mockMvc.perform(post(String.format("/v1/elements/%s/notification?type=bad_type", elementAttributes.getElementUuid())) .header("userId", "Doe")) .andExpect(status().isBadRequest()) - .andExpect(content().string(new IsEqual<>(objectMapper.writeValueAsString(UNKNOWN_NOTIFICATION)))); + .andExpect(jsonPath("$.service").value("directory-server")) + .andExpect(jsonPath("$.errorCode").value(UNKNOWN_NOTIFICATION.name())) + .andExpect(jsonPath("$.message").value("The notification type 'bad_type' is unknown")) + .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.path").value(String.format("/v1/elements/%s/notification", elementAttributes.getElementUuid()))); } @SneakyThrows From 165632b8b63be792783d1e4a961000af1d6be7b1 Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Fri, 10 Oct 2025 10:46:47 +0200 Subject: [PATCH 2/6] simplify exception handling by delegating the work to powsybl-ws-commons --- .../server/DirectoryBusinessErrorCode.java | 39 +++ .../directory/server/DirectoryException.java | 50 +-- .../directory/server/DirectoryService.java | 291 +++++++++--------- .../server/PropertyServerNameProvider.java | 29 ++ .../RestResponseEntityExceptionHandler.java | 96 +++--- .../server/services/UserAdminService.java | 9 +- .../server/DirectoryServiceTest.java | 9 +- .../directory/server/DirectoryTest.java | 8 +- 8 files changed, 299 insertions(+), 232 deletions(-) create mode 100644 src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java create mode 100644 src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java b/src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java new file mode 100644 index 00000000..fbe4e575 --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/DirectoryBusinessErrorCode.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import com.powsybl.ws.commons.error.BusinessErrorCode; + +/** + * @author Mohamed Ben-rejeb {@literal } + * + * Business error codes emitted by the directory service. + */ +public enum DirectoryBusinessErrorCode implements BusinessErrorCode { + DIRECTORY_PERMISSION_DENIED("directory.permissionDenied"), + DIRECTORY_ELEMENT_NAME_BLANK("directory.elementNameBlank"), + DIRECTORY_ROOT_ALREADY_EXISTS("directory.rootAlreadyExists"), + DIRECTORY_NOT_DIRECTORY("directory.notDirectory"), + DIRECTORY_ELEMENT_NAME_CONFLICT("directory.elementNameConflict"), + DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED("directory.moveInDescendantNotAllowed"), + DIRECTORY_MOVE_SELECTION_EMPTY("directory.moveSelectionEmpty"), + DIRECTORY_ELEMENT_NOT_FOUND("directory.elementNotFound"), + DIRECTORY_DIRECTORY_NOT_FOUND_IN_PATH("directory.directoryNotFoundInPath"), + DIRECTORY_NOTIFICATION_UNKNOWN("directory.notificationUnknown"), + DIRECTORY_CANNOT_DELETE_ELEMENT("directory.cannotDeleteElement"), + DIRECTORY_REMOTE_ERROR("directory.remoteError"); + + private final String code; + + DirectoryBusinessErrorCode(String code) { + this.code = code; + } + + public String value() { + return code; + } +} diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryException.java b/src/main/java/org/gridsuite/directory/server/DirectoryException.java index b0aed9b2..43c431aa 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryException.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryException.java @@ -6,49 +6,63 @@ */ package org.gridsuite.directory.server; +import com.powsybl.ws.commons.error.AbstractPowsyblWsException; +import com.powsybl.ws.commons.error.BusinessErrorCode; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; import lombok.NonNull; import java.util.Objects; +import java.util.Optional; import java.util.UUID; /** * @author Abdelsalem Hedhili + * @author Mohamed Ben-rejeb {@literal } */ -public class DirectoryException extends RuntimeException { +public class DirectoryException extends AbstractPowsyblWsException { - private final Type type; + private final DirectoryBusinessErrorCode errorCode; + private final PowsyblWsProblemDetail remoteError; - public DirectoryException(Type type, String message) { + public DirectoryException(DirectoryBusinessErrorCode errorCode, String message) { + this(errorCode, message, null); + } + + public DirectoryException(DirectoryBusinessErrorCode errorCode, String message, PowsyblWsProblemDetail remoteError) { super(Objects.requireNonNull(message, "message must not be null")); - this.type = Objects.requireNonNull(type, "type must not be null"); + this.errorCode = Objects.requireNonNull(errorCode, "errorCode must not be null"); + this.remoteError = remoteError; } public static DirectoryException createNotificationUnknown(@NonNull String action) { - return new DirectoryException(Type.UNKNOWN_NOTIFICATION, String.format("The notification type '%s' is unknown", action)); + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_NOTIFICATION_UNKNOWN, + String.format("The notification type '%s' is unknown", action)); } public static DirectoryException createElementNotFound(@NonNull String type, @NonNull UUID uuid) { - return new DirectoryException(Type.NOT_FOUND, String.format("%s '%s' not found !", type, uuid)); + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND, + String.format("%s '%s' not found !", type, uuid)); } public static DirectoryException createElementNameAlreadyExists(@NonNull String name) { - return new DirectoryException(Type.NAME_ALREADY_EXISTS, String.format("Element with the same name '%s' already exists in the directory !", name)); + return new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_CONFLICT, + String.format("Element with the same name '%s' already exists in the directory !", name)); + } + + public static DirectoryException of(DirectoryBusinessErrorCode errorCode, String message, Object... args) { + return new DirectoryException(errorCode, args.length == 0 ? message : String.format(message, args)); } - public static DirectoryException of(Type type, String message, Object... args) { - return new DirectoryException(type, args.length == 0 ? message : String.format(message, args)); + public Optional getErrorCode() { + return Optional.of(errorCode); } - Type getType() { - return type; + @Override + public Optional getBusinessErrorCode() { + return Optional.ofNullable(errorCode); } - public enum Type { - NOT_ALLOWED, - NOT_FOUND, - NOT_DIRECTORY, - UNKNOWN_NOTIFICATION, - NAME_ALREADY_EXISTS, - MOVE_IN_DESCENDANT_NOT_ALLOWED, + public Optional getRemoteError() { + return Optional.ofNullable(remoteError); } } diff --git a/src/main/java/org/gridsuite/directory/server/DirectoryService.java b/src/main/java/org/gridsuite/directory/server/DirectoryService.java index a5e581a2..61ff1f01 100644 --- a/src/main/java/org/gridsuite/directory/server/DirectoryService.java +++ b/src/main/java/org/gridsuite/directory/server/DirectoryService.java @@ -24,8 +24,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static org.gridsuite.directory.server.DirectoryBusinessErrorCode.*; import static java.lang.Boolean.TRUE; -import static org.gridsuite.directory.server.DirectoryException.Type.*; import static org.gridsuite.directory.server.dto.ElementAttributes.toElementAttributes; import static org.gridsuite.directory.server.dto.PermissionType.*; @@ -94,7 +94,7 @@ public ElementAttributes createElement(ElementAttributes elementAttributes, UUID private ElementAttributes createElementWithNotif(ElementAttributes elementAttributes, UUID parentDirectoryUuid, String userId, boolean generateNewName) { if (elementAttributes.getElementName().isBlank()) { - throw DirectoryException.of(NOT_ALLOWED, "Element name must not be blank"); + throw DirectoryException.of(DIRECTORY_ELEMENT_NAME_BLANK, "Element name must not be blank"); } assertDirectoryExist(parentDirectoryUuid); DirectoryElementEntity elementEntity = insertElement(elementAttributes, parentDirectoryUuid, userId, generateNewName); @@ -112,16 +112,16 @@ private ElementAttributes createElementWithNotif(ElementAttributes elementAttrib public ElementAttributes duplicateElement(UUID elementId, UUID newElementId, UUID targetDirectoryId, String userId) { DirectoryElementEntity directoryElementEntity = directoryElementRepository.findById(elementId) - .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementId)); + .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementId)); String elementType = directoryElementEntity.getType(); UUID parentDirectoryUuid = targetDirectoryId != null ? targetDirectoryId : directoryElementEntity.getParentId(); ElementAttributes elementAttributes = ElementAttributes.builder() - .type(elementType) - .elementUuid(newElementId) - .owner(userId) - .description(directoryElementEntity.getDescription()) - .elementName(directoryElementEntity.getName()) - .build(); + .type(elementType) + .elementUuid(newElementId) + .owner(userId) + .description(directoryElementEntity.getDescription()) + .elementName(directoryElementEntity.getName()) + .build(); assertDirectoryExist(parentDirectoryUuid); DirectoryElementEntity elementEntity = insertElement(elementAttributes, parentDirectoryUuid, userId, true); @@ -134,13 +134,13 @@ public ElementAttributes duplicateElement(UUID elementId, UUID newElementId, UUI private void assertRootDirectoryNotExist(String rootName) { if (repositoryService.isRootDirectoryExist(rootName)) { - throw DirectoryException.of(NOT_ALLOWED, "Root directory '%s' already exists", rootName); + throw DirectoryException.of(DIRECTORY_ROOT_ALREADY_EXISTS, "Root directory '%s' already exists", rootName); } } private void assertDirectoryExist(UUID dirUuid) { if (!getElement(dirUuid).getType().equals(DIRECTORY)) { - throw DirectoryException.of(NOT_DIRECTORY, "Element '%s' is not a directory", dirUuid); + throw DirectoryException.of(DIRECTORY_NOT_DIRECTORY, "Element '%s' is not a directory", dirUuid); } } @@ -153,14 +153,14 @@ private DirectoryElementEntity insertElement(ElementAttributes elementAttributes Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS); DirectoryElementEntity elementEntity = new DirectoryElementEntity(elementAttributes.getElementUuid() == null ? UUID.randomUUID() : elementAttributes.getElementUuid(), - parentDirectoryUuid, - elementAttributes.getElementName(), - elementAttributes.getType(), - elementAttributes.getOwner(), - elementAttributes.getDescription(), - now, - now, - elementAttributes.getOwner()); + parentDirectoryUuid, + elementAttributes.getElementName(), + elementAttributes.getType(), + elementAttributes.getOwner(), + elementAttributes.getDescription(), + now, + now, + elementAttributes.getOwner()); return tryInsertElement(elementEntity, parentDirectoryUuid, userId, generateNewName); } @@ -193,7 +193,7 @@ public ElementAttributes createRootDirectory(RootDirectoryAttributes rootDirecto private ElementAttributes createRootDirectoryWithNotif(RootDirectoryAttributes rootDirectoryAttributes, String userId) { if (rootDirectoryAttributes.getElementName().isBlank()) { - throw DirectoryException.of(NOT_ALLOWED, "Root directory name must not be blank"); + throw DirectoryException.of(DIRECTORY_ELEMENT_NAME_BLANK, "Root directory name must not be blank"); } assertRootDirectoryNotExist(rootDirectoryAttributes.getElementName()); @@ -203,12 +203,12 @@ private ElementAttributes createRootDirectoryWithNotif(RootDirectoryAttributes r insertReadGlobalUsersPermission(elementUuid); // here we know a root directory has no parent notificationService.emitDirectoryChanged( - elementUuid, - elementAttributes.getElementName(), - userId, - null, - true, - NotificationType.ADD_DIRECTORY + elementUuid, + elementAttributes.getElementName(), + userId, + null, + true, + NotificationType.ADD_DIRECTORY ); return elementAttributes; } @@ -227,20 +227,20 @@ public void createElementInDirectoryPath(String directoryPath, ElementAttributes //we create the root directory if it doesn't exist if (parentDirectoryUuid == null) { parentDirectoryUuid = createRootDirectoryWithNotif( - new RootDirectoryAttributes( - s, - userId, - null, - now, - now, - userId), - userId).getElementUuid(); + new RootDirectoryAttributes( + s, + userId, + null, + now, + now, + userId), + userId).getElementUuid(); } else { //and then we create the rest of the path parentDirectoryUuid = createElementWithNotif( - toElementAttributes(UUID.randomUUID(), s, DIRECTORY, userId, null, now, now, userId), - parentDirectoryUuid, - userId, false).getElementUuid(); + toElementAttributes(UUID.randomUUID(), s, DIRECTORY, userId, null, now, now, userId), + parentDirectoryUuid, + userId, false).getElementUuid(); } } else { parentDirectoryUuid = currentDirectoryUuid; @@ -270,10 +270,10 @@ public List getDirectoryElements(UUID directoryUuid, List descendents = repositoryService.findAllDescendants(directoryUuid).stream().toList(); return descendents - .stream() - .filter(e -> types.isEmpty() || types.contains(e.getType())) - .map(ElementAttributes::toElementAttributes) - .toList(); + .stream() + .filter(e -> types.isEmpty() || types.contains(e.getType())) + .map(ElementAttributes::toElementAttributes) + .toList(); } else { return getAllDirectoryElementsStream(directoryUuid, types).toList(); } @@ -281,16 +281,16 @@ public List getDirectoryElements(UUID directoryUuid, List getOnlyElementsStream(UUID directoryUuid, List types) { return getAllDirectoryElementsStream(directoryUuid, types) - .filter(elementAttributes -> !elementAttributes.getType().equals(DIRECTORY)); + .filter(elementAttributes -> !elementAttributes.getType().equals(DIRECTORY)); } private Stream getAllDirectoryElementsStream(UUID directoryUuid, List types) { List directoryElements = repositoryService.findAllByParentId(directoryUuid); Map subdirectoriesCountsMap = getSubDirectoriesCountsMap(types, directoryElements); return directoryElements - .stream() - .filter(e -> e.getType().equals(DIRECTORY) || types.isEmpty() || types.contains(e.getType())) - .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))); + .stream() + .filter(e -> e.getType().equals(DIRECTORY) || types.isEmpty() || types.contains(e.getType())) + .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))); } public List getRootDirectories(List types, String userId) { @@ -302,8 +302,8 @@ public List getRootDirectories(List types, String use } Map subdirectoriesCountsMap = getSubDirectoriesCountsMap(types, directoryElements); return directoryElements.stream() - .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))) - .toList(); + .map(e -> toElementAttributes(e, subdirectoriesCountsMap.getOrDefault(e.getId(), 0L))) + .toList(); } private Map getSubDirectoriesCountsMap(List types, List directoryElements) { @@ -314,10 +314,10 @@ public void updateElement(UUID elementUuid, ElementAttributes newElementAttribut DirectoryElementEntity directoryElement = getDirectoryElementEntity(elementUuid); if (!directoryElement.isAttributesUpdatable(newElementAttributes, userId) || !directoryElement.getName().equals(newElementAttributes.getElementName()) && - directoryHasElementOfNameAndType(directoryElement.getParentId(), newElementAttributes.getElementName(), directoryElement.getType())) { - throw DirectoryException.of(NOT_ALLOWED, - "Update forbidden for element '%s': invalid permissions or duplicate name", - directoryElement.getId()); + directoryHasElementOfNameAndType(directoryElement.getParentId(), newElementAttributes.getElementName(), directoryElement.getType())) { + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, + "Update forbidden for element '%s': invalid permissions or duplicate name", + directoryElement.getId()); } DirectoryElementEntity elementEntity = repositoryService.saveElement(directoryElement.update(newElementAttributes)); @@ -335,7 +335,7 @@ public void updateElementLastModifiedAttributes(UUID elementUuid, Instant lastMo @Transactional public void moveElementsDirectory(List elementsUuids, UUID newDirectoryUuid, String userId) { if (elementsUuids.isEmpty()) { - throw DirectoryException.of(NOT_ALLOWED, "Cannot move elements: no elements provided"); + throw DirectoryException.of(DIRECTORY_MOVE_SELECTION_EMPTY, "Cannot move elements: no elements provided"); } validateNewDirectory(newDirectoryUuid); @@ -374,9 +374,9 @@ private void moveElementDirectory(DirectoryElementEntity element, UUID newDirect private void validateElementForMove(DirectoryElementEntity element, UUID newDirectoryUuid, Set descendentsUuids) { if (newDirectoryUuid == element.getId() || descendentsUuids.contains(newDirectoryUuid)) { - throw DirectoryException.of(MOVE_IN_DESCENDANT_NOT_ALLOWED, - "Cannot move element '%s' into one of its descendants", - element.getId()); + throw DirectoryException.of(DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, + "Cannot move element '%s' into one of its descendants", + element.getId()); } if (directoryHasElementOfNameAndType(newDirectoryUuid, element.getName(), element.getType())) { @@ -391,10 +391,10 @@ private void updateElementParentDirectory(DirectoryElementEntity element, UUID n private void validateNewDirectory(UUID newDirectoryUuid) { DirectoryElementEntity newDirectory = repositoryService.getElementEntity(newDirectoryUuid) - .orElseThrow(() -> DirectoryException.createElementNotFound(DIRECTORY, newDirectoryUuid)); + .orElseThrow(() -> DirectoryException.createElementNotFound(DIRECTORY, newDirectoryUuid)); if (!newDirectory.getType().equals(DIRECTORY)) { - throw DirectoryException.of(NOT_DIRECTORY, "Target '%s' is not a directory", newDirectoryUuid); + throw DirectoryException.of(DIRECTORY_NOT_DIRECTORY, "Target '%s' is not a directory", newDirectoryUuid); } } @@ -439,9 +439,10 @@ private void deleteSubElements(UUID elementUuid, String userId) { /** * Method to delete multiple elements within a single repository - DIRECTORIES can't be deleted this way - * @param elementsUuids list of elements uuids to delete + * + * @param elementsUuids list of elements uuids to delete * @param parentDirectoryUuid expected parent uuid of each element - element with another parent UUID won't be deleted - * @param userId user making the deletion + * @param userId user making the deletion */ public void deleteElements(List elementsUuids, UUID parentDirectoryUuid, String userId) { // getting elements by "elementUuids", filtered if they don't belong to parentDirectoryUuid, or if they are directories @@ -476,7 +477,7 @@ public List getPath(UUID elementUuid) { public String getElementName(UUID elementUuid) { DirectoryElementEntity element = repositoryService.getElementEntity(elementUuid) - .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementUuid)); + .orElseThrow(() -> DirectoryException.createElementNotFound(ELEMENT, elementUuid)); return element.getName(); } @@ -508,19 +509,19 @@ public List getElements(List ids, boolean strictMode, L //if the user is not an admin we filter out elements he doesn't have the permission on if (!roleService.isUserExploreAdmin()) { elementEntities = elementEntities.stream().filter(directoryElementEntity -> - hasReadPermissions(userId, List.of(directoryElementEntity.getId())) - ).toList(); + hasReadPermissions(userId, List.of(directoryElementEntity.getId())) + ).toList(); } if (strictMode && elementEntities.size() != ids.stream().distinct().count()) { - throw DirectoryException.of(NOT_FOUND, "Some requested elements were not found"); + throw DirectoryException.of(DIRECTORY_ELEMENT_NOT_FOUND, "Some requested elements were not found"); } Map subElementsCount = getSubDirectoriesCounts(elementEntities.stream().map(DirectoryElementEntity::getId).toList(), types); return elementEntities.stream() - .map(attribute -> toElementAttributes(attribute, subElementsCount.getOrDefault(attribute.getId(), 0L))) - .toList(); + .map(attribute -> toElementAttributes(attribute, subElementsCount.getOrDefault(attribute.getId(), 0L))) + .toList(); } public int getCasesCount(String userId) { @@ -551,7 +552,7 @@ private String nameCandidate(String elementName, int n) { public String getDuplicateNameCandidate(UUID directoryUuid, String elementName, String elementType, String userId) { if (!repositoryService.canRead(directoryUuid, userId)) { - throw DirectoryException.of(NOT_ALLOWED, "User '%s' cannot access directory '%s'", userId, directoryUuid); + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, "User '%s' cannot access directory '%s'", userId, directoryUuid); } var idLikes = new HashSet<>(repositoryService.getNameByTypeAndParentIdAndNameStartWith(elementType, directoryUuid, elementName)); if (!idLikes.contains(elementName)) { @@ -575,7 +576,7 @@ public UUID getDirectoryUuidFromPath(List directoryPath) { for (String s : directoryPath) { UUID currentDirectoryUuid = getDirectoryUuid(s, parentDirectoryUuid); if (currentDirectoryUuid == null) { - throw DirectoryException.of(NOT_FOUND, "Directory '%s' not found in path", s); + throw DirectoryException.of(DIRECTORY_DIRECTORY_NOT_FOUND_IN_PATH, "Directory '%s' not found in path", s); } else { parentDirectoryUuid = currentDirectoryUuid; } @@ -608,13 +609,13 @@ private void notifyDirectoryHasChanged(UUID directoryUuid, String userId, String private void notifyDirectoryHasChanged(UUID directoryUuid, String userId, String elementName, String error, boolean isDirectoryMoving) { Objects.requireNonNull(directoryUuid); notificationService.emitDirectoryChanged( - directoryUuid, - elementName, - userId, - error, - repositoryService.isRootDirectory(directoryUuid), - isDirectoryMoving, - NotificationType.UPDATE_DIRECTORY + directoryUuid, + elementName, + userId, + error, + repositoryService.isRootDirectory(directoryUuid), + isDirectoryMoving, + NotificationType.UPDATE_DIRECTORY ); } @@ -632,13 +633,13 @@ private void notifyRootDirectoryDeleted(UUID rootDirectoryUuid, String userId, S private void notifyRootDirectoryDeleted(UUID rootDirectoryUuid, String userId, String elementName, String error, boolean isDirectoryMoving) { Objects.requireNonNull(rootDirectoryUuid); notificationService.emitDirectoryChanged( - rootDirectoryUuid, - elementName, - userId, - error, - true, - isDirectoryMoving, - NotificationType.DELETE_DIRECTORY + rootDirectoryUuid, + elementName, + userId, + error, + true, + isDirectoryMoving, + NotificationType.DELETE_DIRECTORY ); } @@ -646,15 +647,15 @@ private void notifyRootDirectoryDeleted(UUID rootDirectoryUuid, String userId, S * Checks if a user has the specified permission on given elements. * Checks parent permissions first, then target directory, then child permissions if recursive check is enabled. * - * @param userId User ID checking permissions for - * @param elementUuids List of element UUIDs to check permissions on + * @param userId User ID checking permissions for + * @param elementUuids List of element UUIDs to check permissions on * @param targetDirectoryUuid Optional target directory UUID (for move operations) - * @param permissionType Type of permission to check (READ, WRITE, MANAGE) - * @param recursiveCheck Whether to check permissions recursively on children + * @param permissionType Type of permission to check (READ, WRITE, MANAGE) + * @param recursiveCheck Whether to check permissions recursively on children * @return PermissionCheckResult indicating where permission check failed, or ALLOWED if successful */ public PermissionCheckResult checkDirectoriesPermission(String userId, List elementUuids, UUID targetDirectoryUuid, - PermissionType permissionType, boolean recursiveCheck) { + PermissionType permissionType, boolean recursiveCheck) { return switch (permissionType) { case READ -> checkReadPermission(userId, elementUuids); case WRITE -> checkWritePermission(userId, elementUuids, targetDirectoryUuid, recursiveCheck); @@ -665,14 +666,14 @@ public PermissionCheckResult checkDirectoriesPermission(String userId, List elementUuids) { return hasReadPermissions(userId, elementUuids) ? - PermissionCheckResult.ALLOWED : - PermissionCheckResult.PARENT_PERMISSION_DENIED; + PermissionCheckResult.ALLOWED : + PermissionCheckResult.PARENT_PERMISSION_DENIED; } private PermissionCheckResult checkManagePermission(String userId, List elementUuids) { return hasManagePermission(userId, elementUuids) ? - PermissionCheckResult.ALLOWED : - PermissionCheckResult.PARENT_PERMISSION_DENIED; + PermissionCheckResult.ALLOWED : + PermissionCheckResult.PARENT_PERMISSION_DENIED; } private PermissionCheckResult checkWritePermission(String userId, List elementUuids, UUID targetDirectoryUuid, boolean recursiveCheck) { @@ -696,9 +697,9 @@ private PermissionCheckResult checkWritePermission(String userId, List ele for (DirectoryElementEntity element : elements) { if (element.getType().equals(DIRECTORY)) { List descendantsUuids = repositoryService.findAllDescendants(element.getId()) - .stream() - .filter(e -> e.getType().equals(DIRECTORY)) - .map(DirectoryElementEntity::getId).toList(); + .stream() + .filter(e -> e.getType().equals(DIRECTORY)) + .map(DirectoryElementEntity::getId).toList(); if (!descendantsUuids.isEmpty() && !checkPermission(userId, descendantsUuids, WRITE)) { return PermissionCheckResult.CHILD_PERMISSION_DENIED; } @@ -713,8 +714,8 @@ private PermissionCheckResult checkWritePermission(String userId, List ele public boolean hasReadPermissions(String userId, List elementUuids) { List elements = directoryElementRepository.findAllByIdIn(elementUuids); return elements.stream().allMatch(element -> - //If it's a directory we check its own write permission else we check the permission on the element parent directory - checkPermission(userId, List.of(element.getType().equals(DIRECTORY) ? element.getId() : element.getParentId()), READ) + //If it's a directory we check its own write permission else we check the permission on the element parent directory + checkPermission(userId, List.of(element.getType().equals(DIRECTORY) ? element.getId() : element.getParentId()), READ) ); } @@ -732,22 +733,22 @@ private boolean checkPermission(String userId, List elementUuids, Permissi } //Finally check group permission return userAdminService.getUserGroups(userId) - .stream() - .map(UserGroupDTO::id) - .anyMatch(groupId -> - checkPermission(permissionRepository.findById(new PermissionId(uuid, "", groupId.toString())), permissionType) - ); + .stream() + .map(UserGroupDTO::id) + .anyMatch(groupId -> + checkPermission(permissionRepository.findById(new PermissionId(uuid, "", groupId.toString())), permissionType) + ); }); } private boolean checkPermission(Optional permissionEntity, PermissionType permissionType) { return permissionEntity - .map(p -> switch (permissionType) { - case READ -> Boolean.TRUE.equals(p.getRead()); - case WRITE -> Boolean.TRUE.equals(p.getWrite()); - case MANAGE -> Boolean.TRUE.equals(p.getManage()); - }) - .orElse(false); + .map(p -> switch (permissionType) { + case READ -> Boolean.TRUE.equals(p.getRead()); + case WRITE -> Boolean.TRUE.equals(p.getWrite()); + case MANAGE -> Boolean.TRUE.equals(p.getManage()); + }) + .orElse(false); } private boolean hasManagePermission(String userId, List elementUuids) { @@ -794,7 +795,7 @@ private void addPermissionForGroup(UUID elementUuid, String groupId, PermissionT public void validatePermissionsGetAccess(UUID directoryUuid, String userId) { if (!roleService.isUserExploreAdmin() && !hasReadPermissions(userId, List.of(directoryUuid))) { - throw DirectoryException.of(NOT_ALLOWED, "User '%s' is not allowed to view directory '%s'", userId, directoryUuid); + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, "User '%s' is not allowed to view directory '%s'", userId, directoryUuid); } } @@ -803,7 +804,7 @@ public void validatePermissionsGetAccess(UUID directoryUuid, String userId) { * Returns exactly one PermissionDTO for each permission type (READ, WRITE, MANAGE). * * @param directoryUuid The UUID of the directory - * @param userId The ID of the user requesting the permissions + * @param userId The ID of the user requesting the permissions * @return A list of exactly three permission DTOs (READ, WRITE, MANAGE) * @throws DirectoryException if the user doesn't have access or the directory doesn't exist */ @@ -818,8 +819,8 @@ public List getDirectoryPermissions(UUID directoryUuid, String us Map groupPermissionLevels = extractGroupPermissionLevels(permissions); return Arrays.stream(PermissionType.values()) - .map(type -> createPermissionDto(type, allUsersPermissionLevel, groupPermissionLevels)) - .collect(Collectors.toList()); + .map(type -> createPermissionDto(type, allUsersPermissionLevel, groupPermissionLevels)) + .collect(Collectors.toList()); } /** @@ -828,15 +829,15 @@ public List getDirectoryPermissions(UUID directoryUuid, String us * If allUsers is false, groups list will contain only groups with exactly this permission type */ private PermissionDTO createPermissionDto( - PermissionType permissionType, - PermissionType allUsersPermissionLevel, - Map groupPermissionLevels) { + PermissionType permissionType, + PermissionType allUsersPermissionLevel, + Map groupPermissionLevels) { boolean hasAllUsersPermission = hasPermissionLevel(allUsersPermissionLevel, permissionType); List groupsWithPermission = hasAllUsersPermission - ? Collections.emptyList() - : getGroupsWithExactPermission(groupPermissionLevels, permissionType); + ? Collections.emptyList() + : getGroupsWithExactPermission(groupPermissionLevels, permissionType); return new PermissionDTO(hasAllUsersPermission, groupsWithPermission, permissionType); } @@ -845,20 +846,20 @@ private PermissionDTO createPermissionDto( * Gets all groups that have exactly the specified permission type */ private List getGroupsWithExactPermission( - Map groupPermissionLevels, - PermissionType exactPermissionType) { + Map groupPermissionLevels, + PermissionType exactPermissionType) { return groupPermissionLevels.entrySet().stream() - .filter(entry -> entry.getValue() == exactPermissionType) - .map(entry -> { - try { - return UUID.fromString(entry.getKey()); - } catch (IllegalArgumentException e) { - return null; - } - }) - .filter(Objects::nonNull) - .collect(Collectors.toList()); + .filter(entry -> entry.getValue() == exactPermissionType) + .map(entry -> { + try { + return UUID.fromString(entry.getKey()); + } catch (IllegalArgumentException e) { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); } /** @@ -871,10 +872,10 @@ private boolean hasPermissionLevel(PermissionType actualLevel, PermissionType re return switch (requiredLevel) { case READ -> actualLevel == PermissionType.READ || - actualLevel == PermissionType.WRITE || - actualLevel == PermissionType.MANAGE; + actualLevel == PermissionType.WRITE || + actualLevel == PermissionType.MANAGE; case WRITE -> actualLevel == PermissionType.WRITE || - actualLevel == PermissionType.MANAGE; + actualLevel == PermissionType.MANAGE; case MANAGE -> actualLevel == PermissionType.MANAGE; }; } @@ -884,10 +885,10 @@ private boolean hasPermissionLevel(PermissionType actualLevel, PermissionType re */ private PermissionType extractGlobalPermissionLevel(List permissions) { return permissions.stream() - .filter(p -> ALL_USERS.equals(p.getUserId())) - .findFirst() - .map(this::determineHighestPermission) - .orElse(null); + .filter(p -> ALL_USERS.equals(p.getUserId())) + .findFirst() + .map(this::determineHighestPermission) + .orElse(null); } /** @@ -895,18 +896,18 @@ private PermissionType extractGlobalPermissionLevel(List permi */ private Map extractGroupPermissionLevels(List permissions) { return permissions.stream() - .filter(p -> !p.getUserGroupId().isEmpty()) - .collect(Collectors.toMap( - PermissionEntity::getUserGroupId, - this::determineHighestPermission, - (existing, replacement) -> shouldUpdatePermission(existing, replacement) ? replacement : existing, - HashMap::new - )); + .filter(p -> !p.getUserGroupId().isEmpty()) + .collect(Collectors.toMap( + PermissionEntity::getUserGroupId, + this::determineHighestPermission, + (existing, replacement) -> shouldUpdatePermission(existing, replacement) ? replacement : existing, + HashMap::new + )); } private void validatePermissionUpdateAccess(UUID directoryUuid, String userId) { if (!roleService.isUserExploreAdmin() && !hasManagePermission(userId, List.of(directoryUuid))) { - throw DirectoryException.of(NOT_ALLOWED, "User '%s' is not allowed to update permissions on directory '%s'", userId, directoryUuid); + throw DirectoryException.of(DIRECTORY_PERMISSION_DENIED, "User '%s' is not allowed to update permissions on directory '%s'", userId, directoryUuid); } } @@ -1038,7 +1039,7 @@ private void applyPermissionConfiguration(UUID directoryUuid, PermissionConfigur } else { // Apply group permissions config.groupPermissions().forEach((groupId, permissionType) -> - addPermissionForGroup(directoryUuid, groupId, permissionType) + addPermissionForGroup(directoryUuid, groupId, permissionType) ); } @@ -1049,7 +1050,7 @@ private void applyPermissionConfiguration(UUID directoryUuid, PermissionConfigur */ private void applyGroupPermissions(UUID directoryUuid, Map groupPermissions, Set targetPermissions) { groupPermissions.entrySet().stream() - .filter(entry -> targetPermissions.contains(entry.getValue())) - .forEach(entry -> addPermissionForGroup(directoryUuid, entry.getKey(), entry.getValue())); + .filter(entry -> targetPermissions.contains(entry.getValue())) + .forEach(entry -> addPermissionForGroup(directoryUuid, entry.getKey(), entry.getValue())); } } diff --git a/src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java b/src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java new file mode 100644 index 00000000..cc21b055 --- /dev/null +++ b/src/main/java/org/gridsuite/directory/server/PropertyServerNameProvider.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.directory.server; + +import com.powsybl.ws.commons.error.ServerNameProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @author Mohamed Ben-rejeb {@literal } + */ +@Component +public class PropertyServerNameProvider implements ServerNameProvider { + + private final String name; + + public PropertyServerNameProvider(@Value("${server.name:directory-server}") String name) { + this.name = name; + } + + @Override + public String serverName() { + return name; + } +} diff --git a/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java b/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java index 8bf63e34..19994b19 100644 --- a/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java +++ b/src/main/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandler.java @@ -6,76 +6,64 @@ */ package org.gridsuite.directory.server; -import com.powsybl.ws.commons.error.ErrorResponse; -import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import com.powsybl.ws.commons.error.AbstractBaseRestExceptionHandler; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import com.powsybl.ws.commons.error.ServerNameProvider; import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.servlet.resource.NoResourceFoundException; -import java.time.Instant; +import java.util.Optional; /** * @author Abdelsalem Hedhili + * @author Mohamed Ben-rejeb {@literal } */ @ControllerAdvice -public class RestResponseEntityExceptionHandler { +public class RestResponseEntityExceptionHandler + extends AbstractBaseRestExceptionHandler { - private static final Logger LOGGER = LoggerFactory.getLogger(RestResponseEntityExceptionHandler.class); - private static final String SERVICE_NAME = "directory-server"; - private static final String CORRELATION_ID_HEADER = "X-Correlation-Id"; + public RestResponseEntityExceptionHandler(ServerNameProvider serverNameProvider) { + super(serverNameProvider); + } - @ExceptionHandler(DirectoryException.class) - protected ResponseEntity handleDirectoryException(DirectoryException exception, HttpServletRequest request) { - HttpStatus status = switch (exception.getType()) { - case NOT_ALLOWED, NOT_DIRECTORY, MOVE_IN_DESCENDANT_NOT_ALLOWED -> HttpStatus.FORBIDDEN; - case NOT_FOUND -> HttpStatus.NOT_FOUND; - case UNKNOWN_NOTIFICATION -> HttpStatus.BAD_REQUEST; - case NAME_ALREADY_EXISTS -> HttpStatus.CONFLICT; - }; - return ResponseEntity.status(status) - .body(buildErrorResponse(request, status, exception.getType().name(), exception.getMessage())); + @Override + protected Optional getRemoteError(DirectoryException ex) { + return ex.getRemoteError(); } - @ExceptionHandler(Exception.class) - protected ResponseEntity handleAllExceptions(Exception exception, HttpServletRequest request) { - HttpStatus status = resolveStatus(exception); - String message = exception.getMessage() != null ? exception.getMessage() : status.getReasonPhrase(); - return ResponseEntity.status(status) - .body(buildErrorResponse(request, status, status.name(), message)); + @Override + protected Optional getBusinessCode(DirectoryException ex) { + return ex.getErrorCode(); + } + + @Override + protected HttpStatus mapStatus(DirectoryBusinessErrorCode errorCode) { + return switch (errorCode) { + case DIRECTORY_ELEMENT_NOT_FOUND, DIRECTORY_DIRECTORY_NOT_FOUND_IN_PATH -> HttpStatus.NOT_FOUND; + case DIRECTORY_ELEMENT_NAME_CONFLICT -> HttpStatus.CONFLICT; + case DIRECTORY_NOTIFICATION_UNKNOWN -> HttpStatus.BAD_REQUEST; + case DIRECTORY_REMOTE_ERROR -> HttpStatus.INTERNAL_SERVER_ERROR; + case DIRECTORY_PERMISSION_DENIED, + DIRECTORY_ELEMENT_NAME_BLANK, + DIRECTORY_ROOT_ALREADY_EXISTS, + DIRECTORY_NOT_DIRECTORY, + DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, + DIRECTORY_MOVE_SELECTION_EMPTY, + DIRECTORY_CANNOT_DELETE_ELEMENT -> HttpStatus.FORBIDDEN; + }; } - private HttpStatus resolveStatus(Exception exception) { - if (exception instanceof ResponseStatusException responseStatusException) { - return HttpStatus.valueOf(responseStatusException.getStatusCode().value()); - } - if (exception instanceof HttpStatusCodeException httpStatusCodeException) { - return HttpStatus.valueOf(httpStatusCodeException.getStatusCode().value()); - } - if (exception instanceof ServletRequestBindingException) { - return HttpStatus.BAD_REQUEST; - } - if (exception instanceof NoResourceFoundException) { - return HttpStatus.NOT_FOUND; - } - return HttpStatus.INTERNAL_SERVER_ERROR; + @Override + protected DirectoryBusinessErrorCode defaultRemoteErrorCode() { + return DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR; } - private ErrorResponse buildErrorResponse(HttpServletRequest request, HttpStatus status, String errorCode, String message) { - return new ErrorResponse( - SERVICE_NAME, - errorCode, - message, - status.value(), - Instant.now(), - request.getRequestURI(), - request.getHeader(CORRELATION_ID_HEADER) + @Override + protected DirectoryException wrapRemote(PowsyblWsProblemDetail remoteError) { + return new DirectoryException( + DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR, + remoteError.getDetail(), + remoteError ); } } diff --git a/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java b/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java index 9c669272..5d40064f 100644 --- a/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java +++ b/src/main/java/org/gridsuite/directory/server/services/UserAdminService.java @@ -11,7 +11,6 @@ import org.springframework.aot.hint.annotation.RegisterReflectionForBinding; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; @@ -45,11 +44,7 @@ public UserAdminService(RestTemplate restTemplate, RemoteServicesProperties remo @RegisterReflectionForBinding(UserGroupDTO.class) public List getUserGroups(String sub) { String path = UriComponentsBuilder.fromPath(DELIMITER + USER_ADMIN_API_VERSION + GET_USER_GROUPS_URI) - .buildAndExpand(sub).toUriString(); - try { - return List.of(Objects.requireNonNull(restTemplate.getForEntity(userAdminServerBaseUri + path, UserGroupDTO[].class).getBody())); - } catch (HttpStatusCodeException e) { - return List.of(); - } + .buildAndExpand(sub).toUriString(); + return List.of(Objects.requireNonNull(restTemplate.getForEntity(userAdminServerBaseUri + path, UserGroupDTO[].class).getBody())); } } diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java index f151d7e3..76949e26 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java @@ -28,6 +28,7 @@ import static org.gridsuite.directory.server.dto.ElementAttributes.toElementAttributes; import static org.gridsuite.directory.server.utils.DirectoryTestUtils.createElement; import static org.gridsuite.directory.server.utils.DirectoryTestUtils.createRootElement; +import static org.gridsuite.directory.server.DirectoryBusinessErrorCode.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -118,7 +119,7 @@ void testDirectoryElementUniqueness() { // Insert the same element in the same directory throws an exception DirectoryException directoryException = assertThrows(DirectoryException.class, () -> directoryService.createElement(elementAttributes, rootUuid, "User1", false)); - assertEquals(DirectoryException.Type.NAME_ALREADY_EXISTS, directoryException.getType()); + assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getErrorCode()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); // Insert the same element in the same directory with new name generation does not throw an exception @@ -137,7 +138,7 @@ void testDirectoryElementUniqueness() { InOrder inOrder = inOrder(directoryService); when(directoryService.getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1")).thenReturn(elementAttributes.getElementName()); directoryException = assertThrows(DirectoryException.class, () -> directoryService.duplicateElement(element2Uuid, root2Uuid, root2Uuid, "User1")); - assertEquals(DirectoryException.Type.NAME_ALREADY_EXISTS, directoryException.getType()); + assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getErrorCode()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); inOrder.verify(directoryService, calls(MAX_RETRY)).getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1"); } @@ -203,7 +204,7 @@ void testMoveElement() { // move directory to it's descendent List list = List.of(dirUuid); // Just for Sonar issue (assertThrows) DirectoryException exception1 = assertThrows(DirectoryException.class, () -> directoryService.moveElementsDirectory(list, subDirUuid, "user1")); - assertEquals(DirectoryException.Type.MOVE_IN_DESCENDANT_NOT_ALLOWED, exception1.getType()); + assertEquals(DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, exception1.getErrorCode()); } @Test @@ -231,6 +232,6 @@ void testMoveInNotDirectory() { List list = List.of(elementUuid1); // Just for Sonar issue (assertThrows) DirectoryException exception2 = assertThrows(DirectoryException.class, () -> directoryService.moveElementsDirectory(list, elementUuid2, "user1")); - assertEquals(DirectoryException.Type.NOT_DIRECTORY, exception2.getType()); + assertEquals(DIRECTORY_NOT_DIRECTORY, exception2.getErrorCode()); } } diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java index 95f040a9..3f1bbaf3 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java @@ -61,7 +61,7 @@ import static com.vladmihalcea.sql.SQLStatementCountValidator.*; import static org.assertj.core.api.Assertions.assertThat; -import static org.gridsuite.directory.server.DirectoryException.Type.UNKNOWN_NOTIFICATION; +import static org.gridsuite.directory.server.DirectoryBusinessErrorCode.DIRECTORY_NOTIFICATION_UNKNOWN; import static org.gridsuite.directory.server.NotificationService.HEADER_UPDATE_TYPE; import static org.gridsuite.directory.server.NotificationService.*; import static org.gridsuite.directory.server.dto.ElementAttributes.toElementAttributes; @@ -1020,10 +1020,10 @@ public void testEmitDirectoryChangedNotification() { mockMvc.perform(post(String.format("/v1/elements/%s/notification?type=bad_type", elementAttributes.getElementUuid())) .header("userId", "Doe")) .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.service").value("directory-server")) - .andExpect(jsonPath("$.errorCode").value(UNKNOWN_NOTIFICATION.name())) - .andExpect(jsonPath("$.message").value("The notification type 'bad_type' is unknown")) + .andExpect(jsonPath("$.server").value("directory-server")) + .andExpect(jsonPath("$.businessErrorCode").value(DIRECTORY_NOTIFICATION_UNKNOWN.value())) .andExpect(jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value())) + .andExpect(jsonPath("$.detail").value("The notification type 'bad_type' is unknown")) .andExpect(jsonPath("$.path").value(String.format("/v1/elements/%s/notification", elementAttributes.getElementUuid()))); } From 74fccc8f8e57681aa7a393c259a933e74423e951 Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Fri, 10 Oct 2025 16:28:43 +0200 Subject: [PATCH 3/6] fix legacy unit tests and cover new code --- .../DirectoryBusinessErrorCodeTest.java | 15 +++ .../server/DirectoryExceptionTest.java | 50 +++++++ .../server/DirectoryServiceTest.java | 8 +- .../PropertyServerNameProviderTest.java | 14 ++ ...estResponseEntityExceptionHandlerTest.java | 122 ++++++++++++++++++ 5 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java create mode 100644 src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java create mode 100644 src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java create mode 100644 src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java new file mode 100644 index 00000000..63b4ed5f --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java @@ -0,0 +1,15 @@ +package org.gridsuite.directory.server; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class DirectoryBusinessErrorCodeTest { + + @ParameterizedTest + @EnumSource(DirectoryBusinessErrorCode.class) + void valueMatchesEnumName(DirectoryBusinessErrorCode code) { + assertThat(code.value()).startsWith("directory."); + } +} diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java new file mode 100644 index 00000000..60bde1e5 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java @@ -0,0 +1,50 @@ +package org.gridsuite.directory.server; + +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; + +import java.time.Instant; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class DirectoryExceptionTest { + + @Test + void staticFactoriesProduceExpectedMessages() { + DirectoryException notification = DirectoryException.createNotificationUnknown("ARCHIVE"); + assertThat(notification.getMessage()).contains("ARCHIVE"); + assertThat(notification.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_NOTIFICATION_UNKNOWN); + + DirectoryException notFound = DirectoryException.createElementNotFound("Folder", UUID.fromString("123e4567-e89b-12d3-a456-426614174000")); + assertThat(notFound.getMessage()).contains("Folder"); + assertThat(notFound.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND); + + DirectoryException conflict = DirectoryException.createElementNameAlreadyExists("report"); + assertThat(conflict.getMessage()).contains("report"); + assertThat(conflict.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_CONFLICT); + + DirectoryException formatted = DirectoryException.of(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_BLANK, + "Element '%s' invalid", "x"); + assertThat(formatted.getMessage()).isEqualTo("Element 'x' invalid"); + assertThat(formatted.getErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NAME_BLANK); + } + + @Test + void remoteErrorIsExposedWhenProvided() { + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.BAD_GATEWAY) + .server("remote") + .detail("Gateway failure") + .timestamp(Instant.parse("2025-08-01T00:00:00Z")) + .path("/remote") + .build(); + + DirectoryException exception = new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR, + "wrapped", + remote); + + assertThat(exception.getRemoteError()).contains(remote); + assertThat(exception.getBusinessErrorCode()).contains(DirectoryBusinessErrorCode.DIRECTORY_REMOTE_ERROR); + } +} diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java index 76949e26..4dbbd57d 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryServiceTest.java @@ -119,7 +119,7 @@ void testDirectoryElementUniqueness() { // Insert the same element in the same directory throws an exception DirectoryException directoryException = assertThrows(DirectoryException.class, () -> directoryService.createElement(elementAttributes, rootUuid, "User1", false)); - assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getErrorCode()); + assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getErrorCode().get()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); // Insert the same element in the same directory with new name generation does not throw an exception @@ -138,7 +138,7 @@ void testDirectoryElementUniqueness() { InOrder inOrder = inOrder(directoryService); when(directoryService.getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1")).thenReturn(elementAttributes.getElementName()); directoryException = assertThrows(DirectoryException.class, () -> directoryService.duplicateElement(element2Uuid, root2Uuid, root2Uuid, "User1")); - assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getErrorCode()); + assertEquals(DIRECTORY_ELEMENT_NAME_CONFLICT, directoryException.getErrorCode().get()); assertEquals(DirectoryException.createElementNameAlreadyExists(elementAttributes.getElementName()).getMessage(), directoryException.getMessage()); inOrder.verify(directoryService, calls(MAX_RETRY)).getDuplicateNameCandidate(root2Uuid, elementAttributes.getElementName(), elementAttributes.getType(), "User1"); } @@ -204,7 +204,7 @@ void testMoveElement() { // move directory to it's descendent List list = List.of(dirUuid); // Just for Sonar issue (assertThrows) DirectoryException exception1 = assertThrows(DirectoryException.class, () -> directoryService.moveElementsDirectory(list, subDirUuid, "user1")); - assertEquals(DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, exception1.getErrorCode()); + assertEquals(DIRECTORY_MOVE_IN_DESCENDANT_NOT_ALLOWED, exception1.getErrorCode().get()); } @Test @@ -232,6 +232,6 @@ void testMoveInNotDirectory() { List list = List.of(elementUuid1); // Just for Sonar issue (assertThrows) DirectoryException exception2 = assertThrows(DirectoryException.class, () -> directoryService.moveElementsDirectory(list, elementUuid2, "user1")); - assertEquals(DIRECTORY_NOT_DIRECTORY, exception2.getErrorCode()); + assertEquals(DIRECTORY_NOT_DIRECTORY, exception2.getErrorCode().get()); } } diff --git a/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java new file mode 100644 index 00000000..0f53b2c0 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java @@ -0,0 +1,14 @@ +package org.gridsuite.directory.server; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class PropertyServerNameProviderTest { + + @Test + void returnsProvidedName() { + PropertyServerNameProvider provider = new PropertyServerNameProvider("custom-server"); + assertThat(provider.serverName()).isEqualTo("custom-server"); + } +} diff --git a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java new file mode 100644 index 00000000..d414b7e5 --- /dev/null +++ b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java @@ -0,0 +1,122 @@ +package org.gridsuite.directory.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.client.HttpClientErrorException; + +import java.nio.charset.StandardCharsets; +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +class RestResponseEntityExceptionHandlerTest { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); + + private TestRestResponseEntityExceptionHandler handler; + + @BeforeEach + void setUp() { + handler = new TestRestResponseEntityExceptionHandler(); + } + + @Test + void mapsDomainExceptionsToConfiguredStatus() { + MockHttpServletRequest request = new MockHttpServletRequest("POST", "/dir"); + DirectoryException exception = new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_ELEMENT_NOT_FOUND, + "Directory element missing"); + + ResponseEntity response = handler.invokeHandleDomainException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getBusinessErrorCode()) + .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.elementNotFound")); + } + + @Test + void enrichesResponseWithRemoteDetails() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/resource"); + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.FORBIDDEN) + .server("downstream") + .detail("Denied") + .timestamp(Instant.parse("2025-09-10T12:00:00Z")) + .path("/remote").build(); + DirectoryException exception = new DirectoryException(DirectoryBusinessErrorCode.DIRECTORY_PERMISSION_DENIED, + "Wrapped", remote); + + ResponseEntity response = handler.invokeHandleDomainException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + PowsyblWsProblemDetail body = response.getBody(); + assertThat(body).isNotNull(); + assertThat(body.getDetail()).isEqualTo("Denied"); + assertThat(body.getChain()).hasSize(1); + assertThat(body.getChain().getFirst().fromServer()).isEqualTo(PowsyblWsProblemDetail.ServerName.of("directory-server")); + } + + @Test + void wrapsRemoteExceptionWhenPayloadInvalid() { + MockHttpServletRequest request = new MockHttpServletRequest("DELETE", "/dir/remote"); + HttpClientErrorException exception = HttpClientErrorException.create( + HttpStatus.BAD_GATEWAY, + "Bad gateway", + null, + "oops".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8 + ); + + ResponseEntity response = handler.invokeHandleRemoteException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + PowsyblWsProblemDetail body = response.getBody(); + assertThat(body).isNotNull(); + assertThat(body.getBusinessErrorCode()) + .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.remoteError")); + assertThat(body.getDetail()).contains("remote server"); + } + + @Test + void reusesRemoteStatusWhenPayloadValid() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/dir/remote"); + PowsyblWsProblemDetail remote = PowsyblWsProblemDetail.builder(HttpStatus.NOT_FOUND) + .server("downstream") + .businessErrorCode("directory.downstreamNotFound") + .detail("missing") + .timestamp(Instant.parse("2025-09-15T08:30:00Z")) + .path("/remote/missing") + .build(); + + byte[] payload = OBJECT_MAPPER.writeValueAsBytes(remote); + HttpClientErrorException exception = HttpClientErrorException.create(HttpStatus.NOT_FOUND, "Not found", + null, payload, StandardCharsets.UTF_8); + + ResponseEntity response = handler.invokeHandleRemoteException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getBusinessErrorCode()) + .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.downstreamNotFound")); + } + + private static final class TestRestResponseEntityExceptionHandler extends RestResponseEntityExceptionHandler { + + private TestRestResponseEntityExceptionHandler() { + super(() -> "directory-server"); + } + + ResponseEntity invokeHandleDomainException(DirectoryException exception, MockHttpServletRequest request) { + return super.handleDomainException(exception, request); + } + + ResponseEntity invokeHandleRemoteException(HttpClientErrorException exception, MockHttpServletRequest request) { + return super.handleRemoteException(exception, request); + } + } +} From 981e9e99646741a61338433944b854d56332cdbd Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Mon, 13 Oct 2025 14:51:15 +0200 Subject: [PATCH 4/6] fix license header Signed-off-by: benrejebmoh --- .../directory/server/DirectoryBusinessErrorCodeTest.java | 9 +++++++++ .../directory/server/DirectoryExceptionTest.java | 9 +++++++++ .../directory/server/PropertyServerNameProviderTest.java | 9 +++++++++ .../server/RestResponseEntityExceptionHandlerTest.java | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java index 63b4ed5f..2d0731dc 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryBusinessErrorCodeTest.java @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.gridsuite.directory.server; import org.junit.jupiter.params.ParameterizedTest; @@ -5,6 +11,9 @@ import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Mohamed Ben-rejeb {@literal } + */ class DirectoryBusinessErrorCodeTest { @ParameterizedTest diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java index 60bde1e5..cee34221 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryExceptionTest.java @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.gridsuite.directory.server; import com.powsybl.ws.commons.error.PowsyblWsProblemDetail; @@ -9,6 +15,9 @@ import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Mohamed Ben-rejeb {@literal } + */ class DirectoryExceptionTest { @Test diff --git a/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java index 0f53b2c0..722c0349 100644 --- a/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java +++ b/src/test/java/org/gridsuite/directory/server/PropertyServerNameProviderTest.java @@ -1,9 +1,18 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.gridsuite.directory.server; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Mohamed Ben-rejeb {@literal } + */ class PropertyServerNameProviderTest { @Test diff --git a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java index d414b7e5..94819e12 100644 --- a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java +++ b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java @@ -1,3 +1,9 @@ +/** + * Copyright (c) 2025, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ package org.gridsuite.directory.server; import com.fasterxml.jackson.databind.ObjectMapper; @@ -15,6 +21,9 @@ import static org.assertj.core.api.Assertions.assertThat; +/** + * @author Mohamed Ben-rejeb {@literal } + */ class RestResponseEntityExceptionHandlerTest { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new JavaTimeModule()); From 10921568f2bca446f775ef978f1120dcf1f060eb Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Wed, 15 Oct 2025 10:59:35 +0200 Subject: [PATCH 5/6] adapt to the replacement in powsybl-ws-commons of unnecessary Records by Strings Signed-off-by: benrejebmoh --- .../RestResponseEntityExceptionHandlerTest.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java index 94819e12..b782bdbc 100644 --- a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java +++ b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java @@ -20,6 +20,7 @@ import java.time.Instant; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Mohamed Ben-rejeb {@literal } @@ -45,8 +46,7 @@ void mapsDomainExceptionsToConfiguredStatus() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().getBusinessErrorCode()) - .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.elementNotFound")); + assertEquals("directory.elementNotFound", response.getBody().getBusinessErrorCode()); } @Test @@ -67,7 +67,7 @@ void enrichesResponseWithRemoteDetails() { assertThat(body).isNotNull(); assertThat(body.getDetail()).isEqualTo("Denied"); assertThat(body.getChain()).hasSize(1); - assertThat(body.getChain().getFirst().fromServer()).isEqualTo(PowsyblWsProblemDetail.ServerName.of("directory-server")); + assertEquals("directory-server", body.getChain().getFirst().getFromServer()); } @Test @@ -86,8 +86,7 @@ void wrapsRemoteExceptionWhenPayloadInvalid() { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); PowsyblWsProblemDetail body = response.getBody(); assertThat(body).isNotNull(); - assertThat(body.getBusinessErrorCode()) - .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.remoteError")); + assertEquals("directory.remoteError", body.getBusinessErrorCode()); assertThat(body.getDetail()).contains("remote server"); } @@ -110,8 +109,7 @@ void reusesRemoteStatusWhenPayloadValid() throws Exception { assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody().getBusinessErrorCode()) - .isEqualTo(new PowsyblWsProblemDetail.BusinessErrorCode("directory.downstreamNotFound")); + assertEquals("directory.downstreamNotFound", response.getBody().getBusinessErrorCode()); } private static final class TestRestResponseEntityExceptionHandler extends RestResponseEntityExceptionHandler { From 5d62d660361dbefde4177e525e4c2ae2e080a21d Mon Sep 17 00:00:00 2001 From: benrejebmoh Date: Thu, 16 Oct 2025 11:08:12 +0200 Subject: [PATCH 6/6] adapt unit tests after changes in powsybl-ws-commons Signed-off-by: benrejebmoh --- .../java/org/gridsuite/directory/server/DirectoryTest.java | 6 +++--- .../server/RestResponseEntityExceptionHandlerTest.java | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java index 3f1bbaf3..8f2e7ee0 100644 --- a/src/test/java/org/gridsuite/directory/server/DirectoryTest.java +++ b/src/test/java/org/gridsuite/directory/server/DirectoryTest.java @@ -478,13 +478,13 @@ public void testMoveElementNotFound() throws Exception { .header("userId", "Doe") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(List.of(unknownUuid)))) - .andExpect(status().isNotFound()); + .andExpect(status().isInternalServerError()); mockMvc.perform(put("/v1/elements/?targetDirectoryUuid=" + unknownUuid) .header("userId", "Doe") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(List.of(elementUuid)))) - .andExpect(status().isNotFound()); + .andExpect(status().isInternalServerError()); assertNbElementsInRepositories(2); } @@ -671,7 +671,7 @@ public void testDirectoryMoveError() throws Exception { .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(List.of(elementUuid1))) ) - .andExpect(status().isBadRequest()); + .andExpect(status().isInternalServerError()); // test move element to one of its descendents mockMvc.perform(put("/v1/elements?targetDirectoryUuid=" + elementUuid2) diff --git a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java index b782bdbc..e44577c6 100644 --- a/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java +++ b/src/test/java/org/gridsuite/directory/server/RestResponseEntityExceptionHandlerTest.java @@ -83,11 +83,11 @@ void wrapsRemoteExceptionWhenPayloadInvalid() { ResponseEntity response = handler.invokeHandleRemoteException(exception, request); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_GATEWAY); PowsyblWsProblemDetail body = response.getBody(); assertThat(body).isNotNull(); assertEquals("directory.remoteError", body.getBusinessErrorCode()); - assertThat(body.getDetail()).contains("remote server"); + assertThat(body.getDetail()).contains("502 Bad gateway"); } @Test