Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
9e4b4ce
Feature/add project request validation and exception handling
angelmp01 Mar 19, 2026
3058df6
Feature/add global exception handling and validation error responses
angelmp01 Mar 20, 2026
0027df0
Fixed Sonaq warning
angelmp01 Mar 20, 2026
6c541ab
Feature/add client app registration and project creation enhancements
angelmp01 Mar 24, 2026
cd32128
Feature/refactor project creation exceptions and introduce command pa…
angelmp01 Mar 26, 2026
154b8ab
Feature/refactor project creation exceptions and introduce command pa…
angelmp01 Mar 26, 2026
ce5151d
Merge branch 'develop' into feature/create-project-v0
angelmp01 Mar 26, 2026
820057e
Refactor client app clientId to UUID type and update persistence layer
angelmp01 Mar 26, 2026
d27c8a9
Enhance project creation validation: enforce field patterns, allowed …
angelmp01 Mar 27, 2026
8e420f1
Refactor ProjectCreationCommandBuilder: remove unused clientId parame…
angelmp01 Mar 27, 2026
acafb62
Update ProjectCreationCommandBuilderTest: remove clientId parameter f…
angelmp01 Mar 27, 2026
46ccaa3
Refactor project key test UUIDs and externalize LDAP group pattern in…
angelmp01 Mar 30, 2026
e4641bd
Refactor project creation flow: rename createProject to saveProject, …
angelmp01 Mar 31, 2026
50a916a
Refactor project key existence checks: introduce ProjectExistenceServ…
angelmp01 Mar 31, 2026
fb8e595
Refactor ProjectCreationCommandBuilder and ProjectsFacadeImpl: improv…
angelmp01 Mar 31, 2026
629fd5c
Fixed sonarq issues
angelmp01 Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions api-project/openapi/api-project.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,19 +138,21 @@ components:
maxLength: 255
projectFlavor:
type: string
description: Flavor of the project. Either projectFlavor or configurationItem must be provided.
description: Project flavor. Must be provided if configurationItem is not present. If both projectFlavor and configurationItem are missing, error BAD_REQUEST_FLAVOR_CONFIG_ITEM (023) is returned.
configurationItem:
type: string
description: Configuration item for the project. Either projectFlavor or configurationItem must be provided.
description: Configuration item. Must be present if projectFlavor is not provided. If both projectFlavor and configurationItem are missing, error BAD_REQUEST_FLAVOR_CONFIG_ITEM (023) is returned.
location:
type: string
description: Location of the project.
description: Location of the project. Must be one of the allowed locations. If not in the allowed list, error INVALID_LOCATION (011) is returned.
x2OdsAccount:
type: string
description: Technical account of the project.
pattern: "^x2[a-zA-Z0-9]{0,13}$"
owner:
type: string
description: Owner of the project.
description: Owner of the project. If x2OdsAccount is provided but owner is missing, error MANDATORY_OWNER (024) is returned.
pattern: "^[a-z]{1,10}$"
oneOf:
- required: [projectFlavor]
- required: [configurationItem]
Expand Down
13 changes: 13 additions & 0 deletions api-project/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.springdoc</groupId>
Expand All @@ -44,6 +51,12 @@
<artifactId>service-projects</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>org.opendevstack.apiservice</groupId>
<artifactId>persistence</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.jsonwebtoken</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.apiservice.project.api.ProjectsApi;
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
import org.opendevstack.apiservice.project.facade.ProjectsFacade;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.project.validation.ProjectRequestValidator;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand All @@ -19,6 +17,8 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.UUID;

@RestController
@RequestMapping(ProjectController.API_BASE_PATH)
@AllArgsConstructor
Expand All @@ -37,44 +37,27 @@ public class ProjectController implements ProjectsApi {
@Override
public ResponseEntity<CreateProjectResponse> createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) {
projectRequestValidator.validate(createProjectRequest);
try {
return ResponseEntity
.status(HttpStatus.OK)
.header(HTTP_HEADER_LOCATION, API_BASE_PATH)
.body(projectsFacade.createProject(createProjectRequest));
} catch (ProjectCreationException e) {
log.error("Project creation conflict: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
.header(HTTP_HEADER_LOCATION, API_BASE_PATH)
.body(ProjectResponseFactory.conflict(e.getMessage(), API_BASE_PATH));
} catch (ProjectKeyGenerationException e) {
log.error("Failed to generate project key: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.header(HTTP_HEADER_LOCATION, API_BASE_PATH)
.body(ProjectResponseFactory.projectKeyGenerationFailed(API_BASE_PATH));
}
UUID clientId = UUID.fromString("00000000-0000-0000-0000-000000000001");
CreateProjectResponse projectResponse = projectsFacade.createProject(createProjectRequest, clientId);
projectResponse.setLocation(API_BASE_PATH + "/" + projectResponse.getProjectKey());
return ResponseEntity
.status(HttpStatus.OK)
.header(HTTP_HEADER_LOCATION, API_BASE_PATH)
.body(projectResponse);
}

@GetMapping("/{projectKey}")
@Override
public ResponseEntity<CreateProjectResponse> getProject(@PathVariable String projectKey) {
String location = API_BASE_PATH + "/" + projectKey;
try {
CreateProjectResponse response = projectsFacade.getProject(projectKey);
if (response == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.header(HTTP_HEADER_LOCATION, location)
.body(ProjectResponseFactory.notFound(projectKey, location));
}
return ResponseEntity
.status(HttpStatus.OK)
.header(HTTP_HEADER_LOCATION, location)
.body(response);
} catch (Exception e) {
log.error("Unexpected error retrieving project '{}': {}", projectKey, e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
CreateProjectResponse response = projectsFacade.getProject(projectKey);
if (response == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.header(HTTP_HEADER_LOCATION, location)
.body(ProjectResponseFactory.internalError(location));
.body(ProjectResponseFactory.notFound(projectKey, location));
}
return ResponseEntity.status(HttpStatus.OK)
.header(HTTP_HEADER_LOCATION, location)
.body(response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public static CreateProjectResponse internalError(String location) {
location);
}

public static CreateProjectResponse internalError(String location, String message) {
return error(
ErrorKey.INTERNAL_ERROR.getMessage(),
ErrorKey.INTERNAL_ERROR.getKey(),
message,
location);
}

private static CreateProjectResponse error(String error, String errorKey, String message, String location) {
CreateProjectResponse response = new CreateProjectResponse();
response.setError(error);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.opendevstack.apiservice.project.controller.advice;

import lombok.extern.slf4j.Slf4j;
import org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException;
import org.opendevstack.apiservice.project.controller.ProjectController;
import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
import org.opendevstack.apiservice.project.exception.ErrorKey;
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
import org.springframework.http.HttpStatus;
Expand All @@ -23,7 +26,9 @@ public class ProjectExceptionHandler {
"projectName", ErrorKey.PROJECT_NAME_INVALID_FORMAT,
"projectDescription", ErrorKey.PROJECT_DESCRIPTION_INVALID_FORMAT,
"projectFlavor", ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM,
"configurationItem", ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM
"configurationItem", ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM,
"x2OdsAccount", ErrorKey.PROJECT_X2ACCOUNT_INVALID_FORMAT,
"owner", ErrorKey.PROJECT_OWNER_INVALID_FORMAT
);

@ExceptionHandler(MethodArgumentNotValidException.class)
Expand Down Expand Up @@ -55,6 +60,18 @@ public ResponseEntity<CreateProjectResponse> handleMethodArgumentNotValidExcepti
return ResponseEntity.badRequest().body(response);
}

@ExceptionHandler(ClientAppNotRegisteredException.class)
public ResponseEntity<CreateProjectResponse> handleClientAppNotRegisteredException(
ClientAppNotRegisteredException ex) {
log.warn("ClientApp registration error: {}", ex.getMessage());
CreateProjectResponse response = new CreateProjectResponse();
response.setLocation(ProjectController.API_BASE_PATH);
response.setError(HttpStatus.FORBIDDEN.getReasonPhrase());
response.setErrorKey(ErrorKey.CLIENT_APP_NOT_REGISTERED.getKey());
response.setMessage(ErrorKey.CLIENT_APP_NOT_REGISTERED.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}

@ExceptionHandler(ProjectValidationException.class)
public ResponseEntity<CreateProjectResponse> handleValidationException(ProjectValidationException ex) {
log.warn("Validation error: {}", ex.getMessage());
Expand All @@ -66,6 +83,41 @@ public ResponseEntity<CreateProjectResponse> handleValidationException(ProjectVa
response.setMessage(errorKey.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}

@ExceptionHandler(ProjectCreationException.class)
public ResponseEntity<CreateProjectResponse> handleProjectCreationException(
ProjectCreationException ex) {
log.error("Project creation error: {}", ex.getMessage(), ex);
CreateProjectResponse response = new CreateProjectResponse();
response.setLocation(ProjectController.API_BASE_PATH);
response.setError(ErrorKey.INTERNAL_ERROR.getMessage());
response.setErrorKey(ErrorKey.INTERNAL_ERROR.getKey());
response.setMessage(ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

@ExceptionHandler(AutomationPlatformException.class)
public ResponseEntity<CreateProjectResponse> handleAutomationPlatformException(
AutomationPlatformException ex) {
log.error("Failed to execute automated job: {}", ex.getMessage(), ex);
CreateProjectResponse response = new CreateProjectResponse();
response.setLocation(ProjectController.API_BASE_PATH);
response.setError(ErrorKey.INTERNAL_ERROR.getMessage());
response.setErrorKey(ErrorKey.INTERNAL_ERROR.getKey());
response.setMessage(ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<CreateProjectResponse> handleGenericException(Exception ex) {
log.error("Unexpected error: {}", ex.getMessage(), ex);
CreateProjectResponse response = new CreateProjectResponse();
response.setLocation(ProjectController.API_BASE_PATH);
response.setError(ErrorKey.INTERNAL_ERROR.getMessage());
response.setErrorKey(ErrorKey.INTERNAL_ERROR.getKey());
response.setMessage("An error occurred while processing the request.");
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.opendevstack.apiservice.project.exception;

public class ClientAppNotRegisteredException extends RuntimeException {

public ClientAppNotRegisteredException(String clientId) {
super(String.format("ClientApp with clientId '%s' is not registered", clientId));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ public enum ErrorKey {
BAD_REQUEST_FLAVOR_CONFIG_ITEM("023", "Project flavour and config item cannot be both null"),
MANDATORY_OWNER("024", "Owner must be present if the X2 account is present"),
PROJECT_ALREADY_EXISTS("025", "Project already exists"),
PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS("026", "Project with same project name already exists");
PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS("026", "Project with same project name already exists"),
CLIENT_APP_NOT_REGISTERED("027", "ClientApp not registered, manual registration required"),
INVALID_PROJECT_FLAVOR("028", ErrorMessage.BAD_REQUEST),
INVALID_CONFIG_ITEM("029", ErrorMessage.BAD_REQUEST);

private String key;
private String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.opendevstack.apiservice.project.exception;

public class ProjectCreationException extends Exception {
public class ProjectCreationException extends RuntimeException {

public ProjectCreationException(String message) {
super(message);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package org.opendevstack.apiservice.project.facade;

import org.opendevstack.apiservice.project.exception.ProjectCreationException;
import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;

import java.util.UUID;

public interface ProjectsFacade {

CreateProjectResponse createProject(CreateProjectRequest request)
throws ProjectCreationException, ProjectKeyGenerationException;
CreateProjectResponse createProject(CreateProjectRequest request, UUID clientId);

CreateProjectResponse getProject(String projectKey);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.opendevstack.apiservice.project.facade.impl;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.UUID;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ProjectCreationCommand {

private String projectKey;

private String projectName;

private String projectDescription;

private String projectFlavor;

private String configurationItem;

private String location;

private String x2OdsAccount;

private String owner;

private UUID clientId;
}
Loading
Loading