Skip to content

Commit d2bd7be

Browse files
authored
Feature/add project request validation and exception handling (#11)
1 parent 1726695 commit d2bd7be

11 files changed

Lines changed: 530 additions & 122 deletions

File tree

api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandler.java

Lines changed: 84 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.opendevstack.apiservice.projectusers.exception;
22

3+
import org.opendevstack.apiservice.projectusers.controller.ProjectUserController;
34
import org.opendevstack.apiservice.projectusers.model.ValidationErrorResponse;
45
import org.opendevstack.apiservice.projectusers.model.BaseApiResponse;
56
import org.opendevstack.apiservice.projectusers.model.FieldError;
@@ -32,7 +33,7 @@
3233
* messages.
3334
*/
3435
@Slf4j
35-
@ControllerAdvice
36+
@ControllerAdvice(assignableTypes = ProjectUserController.class)
3637
public class GlobalExceptionHandler {
3738

3839
/**
@@ -69,17 +70,17 @@ public ResponseEntity<ValidationErrorResponse> handleMethodArgumentNotValidExcep
6970
fieldErrors.add(fieldError);
7071
});
7172

72-
String errorMessage = String.format(
73-
ErrorMessages.REQUEST_VALIDATION_FAILED,
74-
fieldErrors.size());
75-
76-
ValidationErrorResponse errorResponse = new ValidationErrorResponse();
77-
errorResponse.setSuccess(false);
78-
errorResponse.setMessage(errorMessage);
79-
errorResponse.setErrorCode(ErrorCodes.PROJECT_USER_ERROR);
80-
errorResponse.setFieldErrors(fieldErrors);
81-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
82-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
73+
String errorMessage = String.format(
74+
ErrorMessages.REQUEST_VALIDATION_FAILED,
75+
fieldErrors.size());
76+
77+
ValidationErrorResponse errorResponse = new ValidationErrorResponse();
78+
errorResponse.setSuccess(false);
79+
errorResponse.setMessage(errorMessage);
80+
errorResponse.setErrorCode(ErrorCodes.PROJECT_USER_ERROR);
81+
errorResponse.setFieldErrors(fieldErrors);
82+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
83+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
8384
}
8485

8586
/**
@@ -96,13 +97,13 @@ public ResponseEntity<BaseApiResponse> handleConstraintViolationException(
9697
.map(this::formatConstraintViolation)
9798
.toList();
9899

99-
String errorMessage = String.format(ErrorMessages.PARAMETER_VALIDATION_FAILED, String.join("; ", errors));
100-
BaseApiResponse errorResponse = new BaseApiResponse();
101-
errorResponse.setSuccess(false);
102-
errorResponse.setMessage(errorMessage);
103-
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
104-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
105-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
100+
String errorMessage = String.format(ErrorMessages.PARAMETER_VALIDATION_FAILED, String.join("; ", errors));
101+
BaseApiResponse errorResponse = new BaseApiResponse();
102+
errorResponse.setSuccess(false);
103+
errorResponse.setMessage(errorMessage);
104+
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
105+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
106+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
106107
}
107108

108109
/**
@@ -115,8 +116,8 @@ public ResponseEntity<BaseApiResponse> handleHttpMessageNotReadableException(
115116

116117
log.warn("Invalid request body: {}", ex.getMessage());
117118

118-
String errorMessage = ErrorMessages.INVALID_REQUEST_BODY;
119-
String errorCode = ErrorCodes.PROJECT_USER_ERROR;
119+
String errorMessage = ErrorMessages.INVALID_REQUEST_BODY;
120+
String errorCode = ErrorCodes.PROJECT_USER_ERROR;
120121

121122
Throwable cause = ex.getCause();
122123

@@ -172,15 +173,15 @@ public ResponseEntity<BaseApiResponse> handleMissingPathVariableException(
172173

173174
log.warn("Missing path variable: {}", ex.getMessage());
174175

175-
String errorMessage = String.format(
176-
ErrorMessages.REQUIRED_PATH_PARAMETER_MISSING,
177-
ex.getVariableName());
178-
BaseApiResponse errorResponse = new BaseApiResponse();
179-
errorResponse.setSuccess(false);
180-
errorResponse.setMessage(errorMessage);
181-
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
182-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
183-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
176+
String errorMessage = String.format(
177+
ErrorMessages.REQUIRED_PATH_PARAMETER_MISSING,
178+
ex.getVariableName());
179+
BaseApiResponse errorResponse = new BaseApiResponse();
180+
errorResponse.setSuccess(false);
181+
errorResponse.setMessage(errorMessage);
182+
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
183+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
184+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
184185
}
185186

186187
/**
@@ -192,15 +193,15 @@ public ResponseEntity<BaseApiResponse> handleMissingServletRequestParameterExcep
192193

193194
log.warn("Missing request parameter: {}", ex.getMessage());
194195

195-
String errorMessage = String.format(
196-
ErrorMessages.REQUIRED_REQUEST_PARAMETER_MISSING,
197-
ex.getParameterName(), ex.getParameterType());
198-
BaseApiResponse errorResponse = new BaseApiResponse();
199-
errorResponse.setSuccess(false);
200-
errorResponse.setMessage(errorMessage);
201-
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
202-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
203-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
196+
String errorMessage = String.format(
197+
ErrorMessages.REQUIRED_REQUEST_PARAMETER_MISSING,
198+
ex.getParameterName(), ex.getParameterType());
199+
BaseApiResponse errorResponse = new BaseApiResponse();
200+
errorResponse.setSuccess(false);
201+
errorResponse.setMessage(errorMessage);
202+
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
203+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
204+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
204205
}
205206

206207
/**
@@ -212,17 +213,17 @@ public ResponseEntity<BaseApiResponse> handleMethodArgumentTypeMismatchException
212213

213214
log.warn("Method argument type mismatch: {}", ex.getMessage());
214215

215-
String errorMessage = String.format(
216-
ErrorMessages.PARAMETER_TYPE_CONVERSION_FAILED,
217-
ex.getName(),
218-
ex.getValue(),
219-
ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown");
220-
BaseApiResponse errorResponse = new BaseApiResponse();
221-
errorResponse.setSuccess(false);
222-
errorResponse.setMessage(errorMessage);
223-
errorResponse.setError(ErrorCodes.INVALID_ROLE);
224-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
225-
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
216+
String errorMessage = String.format(
217+
ErrorMessages.PARAMETER_TYPE_CONVERSION_FAILED,
218+
ex.getName(),
219+
ex.getValue(),
220+
ex.getRequiredType() != null ? ex.getRequiredType().getSimpleName() : "unknown");
221+
BaseApiResponse errorResponse = new BaseApiResponse();
222+
errorResponse.setSuccess(false);
223+
errorResponse.setMessage(errorMessage);
224+
errorResponse.setError(ErrorCodes.INVALID_ROLE);
225+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
226+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
226227
}
227228

228229
/**
@@ -233,11 +234,11 @@ public ResponseEntity<BaseApiResponse> handleProjectNotFoundException(
233234
ProjectNotFoundException ex) {
234235

235236
log.warn("Project not found: {}", ex.getMessage());
236-
BaseApiResponse errorResponse = new BaseApiResponse();
237-
errorResponse.setSuccess(false);
238-
errorResponse.setMessage(ex.getMessage());
239-
errorResponse.setError(ex.getErrorCode());
240-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
237+
BaseApiResponse errorResponse = new BaseApiResponse();
238+
errorResponse.setSuccess(false);
239+
errorResponse.setMessage(ex.getMessage());
240+
errorResponse.setError(ex.getErrorCode());
241+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
241242
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
242243
}
243244

@@ -249,11 +250,11 @@ public ResponseEntity<BaseApiResponse> handleUserNotFoundException(
249250
UserNotFoundException ex) {
250251

251252
log.warn("User not found: {}", ex.getMessage());
252-
BaseApiResponse errorResponse = new BaseApiResponse();
253-
errorResponse.setSuccess(false);
254-
errorResponse.setMessage(ex.getMessage());
255-
errorResponse.setError(ex.getErrorCode());
256-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
253+
BaseApiResponse errorResponse = new BaseApiResponse();
254+
errorResponse.setSuccess(false);
255+
errorResponse.setMessage(ex.getMessage());
256+
errorResponse.setError(ex.getErrorCode());
257+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
257258
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse);
258259
}
259260

@@ -297,11 +298,11 @@ public ResponseEntity<BaseApiResponse> handleInvalidRoleException(
297298
InvalidRoleException ex) {
298299

299300
log.warn("Invalid role: {}", ex.getMessage());
300-
BaseApiResponse errorResponse = new BaseApiResponse();
301-
errorResponse.setSuccess(false);
302-
errorResponse.setMessage(ex.getMessage());
303-
errorResponse.setError(ex.getErrorCode());
304-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
301+
BaseApiResponse errorResponse = new BaseApiResponse();
302+
errorResponse.setSuccess(false);
303+
errorResponse.setMessage(ex.getMessage());
304+
errorResponse.setError(ex.getErrorCode());
305+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
305306
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
306307
}
307308

@@ -313,12 +314,12 @@ public ResponseEntity<BaseApiResponse> handleAutomationPlatformException(
313314
AutomationPlatformException ex) {
314315

315316
log.error("Automation platform error: {}", ex.getMessage(), ex);
316-
BaseApiResponse errorResponse = new BaseApiResponse();
317-
errorResponse.setSuccess(false);
318-
errorResponse.setMessage(String.format(ErrorMessages.EXTERNAL_SERVICE_ERROR, ex.getMessage()));
319-
errorResponse.setError(ex.getErrorCode());
320-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
321-
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(errorResponse);
317+
BaseApiResponse errorResponse = new BaseApiResponse();
318+
errorResponse.setSuccess(false);
319+
errorResponse.setMessage(String.format(ErrorMessages.EXTERNAL_SERVICE_ERROR, ex.getMessage()));
320+
errorResponse.setError(ex.getErrorCode());
321+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
322+
return ResponseEntity.status(HttpStatus.BAD_GATEWAY).body(errorResponse);
322323
}
323324

324325
/**
@@ -327,12 +328,12 @@ public ResponseEntity<BaseApiResponse> handleAutomationPlatformException(
327328
@ExceptionHandler(ProjectUserException.class)
328329
public ResponseEntity<BaseApiResponse> handleProjectUserException(ProjectUserException ex) {
329330
log.error("Project user operation failed: {}", ex.getMessage(), ex);
330-
BaseApiResponse errorResponse = new BaseApiResponse();
331-
errorResponse.setSuccess(false);
332-
errorResponse.setMessage(String.format(ErrorMessages.OPERATION_FAILED, ex.getMessage()));
333-
errorResponse.setError(ex.getErrorCode());
334-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
335-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
331+
BaseApiResponse errorResponse = new BaseApiResponse();
332+
errorResponse.setSuccess(false);
333+
errorResponse.setMessage(String.format(ErrorMessages.OPERATION_FAILED, ex.getMessage()));
334+
errorResponse.setError(ex.getErrorCode());
335+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
336+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
336337
}
337338

338339
/**
@@ -341,12 +342,12 @@ public ResponseEntity<BaseApiResponse> handleProjectUserException(ProjectUserExc
341342
@ExceptionHandler(Exception.class)
342343
public ResponseEntity<BaseApiResponse> handleGenericException(Exception ex) {
343344
log.error("Unexpected error occurred: {}", ex.getMessage(), ex);
344-
BaseApiResponse errorResponse = new BaseApiResponse();
345-
errorResponse.setSuccess(false);
346-
errorResponse.setMessage(ErrorMessages.UNEXPECTED_ERROR);
347-
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
348-
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
349-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
345+
BaseApiResponse errorResponse = new BaseApiResponse();
346+
errorResponse.setSuccess(false);
347+
errorResponse.setMessage(ErrorMessages.UNEXPECTED_ERROR);
348+
errorResponse.setError(ErrorCodes.PROJECT_USER_ERROR);
349+
errorResponse.setTimestamp(java.time.OffsetDateTime.now());
350+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse);
350351
}
351352

352353
/**

api-project/openapi/api-project.yaml

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
openapi: 3.0.3
1+
openapi: 3.0.4
22
info:
33
title: ODS API Server
44
description: API documentation for ODS (Open DevStack) API Service
@@ -124,22 +124,44 @@ components:
124124
projectKey:
125125
type: string
126126
description: Optional project key. If not provided, a unique key will be generated.
127-
projectKeyPattern:
128-
type: string
129-
description: Optional pattern for generating the project key (e.g. 'SS%06d').
127+
pattern: "^[A-Z]{2}[A-Z0-9]{1,8}$"
128+
minLength: 3
129+
maxLength: 10
130130
projectName:
131131
type: string
132132
description: Name of the project.
133+
pattern: "^[A-Za-z0-9 ]{0,80}$"
134+
maxLength: 80
133135
projectDescription:
134136
type: string
135137
description: Description of the project.
136-
required:
137-
- projectName
138+
maxLength: 255
139+
projectFlavor:
140+
type: string
141+
description: Flavor of the project. Either projectFlavor or configurationItem must be provided.
142+
configurationItem:
143+
type: string
144+
description: Configuration item for the project. Either projectFlavor or configurationItem must be provided.
145+
location:
146+
type: string
147+
description: Location of the project.
148+
x2OdsAccount:
149+
type: string
150+
description: Technical account of the project.
151+
owner:
152+
type: string
153+
description: Owner of the project.
154+
oneOf:
155+
- required: [projectFlavor]
156+
- required: [configurationItem]
157+
additionalProperties: false
138158
CreateProjectResponse:
139159
type: object
140160
properties:
141161
projectKey:
142162
type: string
163+
httStatus:
164+
type: string
143165
status:
144166
type: string
145167
projectFlavor:
@@ -153,4 +175,4 @@ components:
153175
errorDescription:
154176
type: string
155177
location:
156-
type: string
178+
type: string

api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
1010
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
1111
import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
12+
import org.opendevstack.apiservice.project.validation.ProjectRequestValidator;
1213
import org.springframework.http.HttpStatus;
1314
import org.springframework.http.ResponseEntity;
1415
import org.springframework.web.bind.annotation.GetMapping;
@@ -30,9 +31,12 @@ public class ProjectController implements ProjectsApi {
3031

3132
private final ProjectsFacade projectsFacade;
3233

34+
private final ProjectRequestValidator projectRequestValidator;
35+
3336
@PostMapping
3437
@Override
3538
public ResponseEntity<CreateProjectResponse> createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) {
39+
projectRequestValidator.validate(createProjectRequest);
3640
try {
3741
return ResponseEntity
3842
.status(HttpStatus.OK)

0 commit comments

Comments
 (0)