Skip to content

Commit 60c5067

Browse files
Refactor component creation and retrieval logic; enhance error handli… (#17)
…ng and response mapping --------- Co-authored-by: Jorge Romero <jorge.romero.ext@boehringer-ingelheim.com>
1 parent aee0c8f commit 60c5067

27 files changed

Lines changed: 830 additions & 214 deletions

File tree

api-project-component-v0/openapi/api-project-component-v0.yaml

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ paths:
2727
- name: projectId
2828
in: path
2929
required: true
30-
description: Project id
30+
description: Project key
3131
schema:
3232
type: string
3333
requestBody:
@@ -37,8 +37,8 @@ paths:
3737
schema:
3838
$ref: '#/components/schemas/CreateComponentRequest'
3939
responses:
40-
'201':
41-
description: Created
40+
'200':
41+
description: OK
4242
headers:
4343
Location:
4444
schema:
@@ -49,14 +49,26 @@ paths:
4949
$ref: '#/components/schemas/CreateComponentResponse'
5050
'400':
5151
description: Bad Request
52+
content:
53+
application/json:
54+
schema:
55+
$ref: '#/components/schemas/CreateComponentResponse'
5256
'401':
5357
$ref: '#/components/responses/UnauthorizedError'
5458
'403':
5559
description: Forbidden
5660
'404':
5761
description: Endpoint not found / Project not found / Product not found
62+
content:
63+
application/json:
64+
schema:
65+
$ref: '#/components/schemas/CreateComponentResponse'
5866
'500':
5967
description: Internal Server Error
68+
content:
69+
application/json:
70+
schema:
71+
$ref: '#/components/schemas/CreateComponentResponse'
6072

6173
/projects/{projectId}/components/{componentId}:
6274
get:
@@ -69,7 +81,7 @@ paths:
6981
- name: projectId
7082
in: path
7183
required: true
72-
description: Project id
84+
description: Project key
7385
schema:
7486
type: string
7587
- name: componentId
@@ -78,6 +90,7 @@ paths:
7890
description: Component id
7991
schema:
8092
type: string
93+
format: uuid
8194
responses:
8295
'200':
8396
description: Component information
@@ -105,6 +118,10 @@ components:
105118
schemas:
106119
Component:
107120
type: object
121+
x-class-extra-annotation: |
122+
@com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)
123+
required:
124+
- params
108125
properties:
109126
id:
110127
type: string
@@ -117,50 +134,94 @@ components:
117134
description: Description of the product
118135
productName:
119136
type: string
120-
description: Name of the product (e.g. Docker plain)
137+
description: Name of the product
121138
productId:
122139
type: string
123140
description: Product ID
124141
environment:
125-
type: string
126-
description: Environment (e.g. DEV)
142+
$ref: '#/components/schemas/EnvironmentsDTO'
143+
description: Environment
127144
status:
128-
type: string
129-
description: Status of the component (e.g. READY, NOT_READY)
145+
$ref: '#/components/schemas/ComponentsStatusDTO'
146+
description: Status of the component
130147
resultTraceback:
131-
type: string
148+
type: object
149+
nullable: true
132150
description: Traceback information in case of error
151+
x-field-extra-annotation: |
152+
@com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY)
133153
repositoryURL:
134154
type: string
135155
description: URL of the repository (for ODS products)
136156
params:
137157
type: object
138158
description: Additional parameters (key-value pairs)
159+
additionalProperties: true
139160
component-type:
140161
type: string
141162
description: Type of component (ods|awx)
142163
CreateComponentResponse:
143164
type: object
165+
required:
166+
- timestamp
167+
- httpStatus
168+
- errorKey
169+
- message
170+
- path
144171
properties:
145-
errorCode:
172+
timestamp:
146173
type: integer
147-
field:
174+
format: int64
175+
httpStatus:
176+
type: string
177+
errorKey:
178+
type: string
179+
error:
148180
type: string
149181
message:
150182
type: string
151-
location:
183+
path:
152184
type: string
153-
format: url
154185
CreateComponentRequest:
155186
type: object
187+
required:
188+
- name
189+
- productId
190+
- params
156191
properties:
157192
name:
158193
type: string
159-
description: component name
194+
description: Component name
195+
minLength: 2
196+
maxLength: 52
197+
pattern: "^[a-z]+(-?[a-z0-9]+)*$"
198+
x-field-extra-annotation: |
199+
@Size(min=2, message = "The component name must contain more than 1 character.")
200+
@Size(max=52, message = "The component name must contain less than 53 characters.")
201+
@Pattern(regexp = "^[a-z]+(-?[a-z0-9]+)*$", message = "Only lowercase letters and numbers are allowed (example: componentname8), with optional dashes in between (example: my-component). The first character must be a letter.")
160202
productId:
161203
type: string
162-
description: product id
204+
description: Product id
205+
registerOnly:
206+
type: boolean
207+
description: Register only flag (no provisioning)
208+
default: false
163209
params:
164210
type: object
165-
additionalProperties:
166-
type: string
211+
description: Additional parameters (key-value pairs)
212+
additionalProperties: true
213+
EnvironmentsDTO:
214+
type: string
215+
enum:
216+
- DEV
217+
- QA
218+
- PROD
219+
ComponentsStatusDTO:
220+
type: string
221+
enum:
222+
- READY
223+
- RUNNING
224+
- FAILED
225+
- DELETING
226+
- DELETED
227+
- UNKNOWN
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package org.opendevstack.apiservice.project.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.openapitools.jackson.nullable.JsonNullable;
6+
import org.openapitools.jackson.nullable.JsonNullableModule;
7+
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
8+
import org.springframework.context.annotation.Bean;
9+
import org.springframework.context.annotation.Configuration;
10+
11+
@Configuration
12+
public class JsonNullableConfig {
13+
14+
@Bean
15+
public Jackson2ObjectMapperBuilderCustomizer jsonNullableCustomizer() {
16+
return builder -> {
17+
builder.modulesToInstall(JsonNullableModule.class);
18+
builder.postConfigurer(this::configureJsonNullableInclusion);
19+
};
20+
}
21+
22+
private void configureJsonNullableInclusion(ObjectMapper objectMapper) {
23+
objectMapper.configOverride(JsonNullable.class)
24+
.setInclude(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.NON_ABSENT));
25+
}
26+
}
Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,54 @@
11
package org.opendevstack.apiservice.project.controller;
22

3+
import org.opendevstack.apiservice.project.exception.ComponentErrorKey;
34
import org.opendevstack.apiservice.project.model.CreateComponentResponse;
45
import org.springframework.http.HttpStatus;
56

6-
public class ComponentsResponseFactory {
7+
public final class ComponentsResponseFactory {
78

89
private ComponentsResponseFactory() {
910
}
1011

11-
public static CreateComponentResponse error(String projectId) {
12+
public static CreateComponentResponse entityCreated(String projectId, String componentId) {
13+
String path = String.format("/api/pub/v0/projects/%s/components/%s", projectId, componentId);
14+
return ok(path, "Component created");
15+
}
16+
17+
public static CreateComponentResponse badRequest(String path, String message, ComponentErrorKey errorKey) {
18+
return buildResponse(HttpStatus.BAD_REQUEST, errorKey, path, message);
19+
}
20+
21+
public static CreateComponentResponse forbidden(String path, String message, ComponentErrorKey errorKey) {
22+
return buildResponse(HttpStatus.FORBIDDEN, errorKey, path, message);
23+
}
24+
25+
public static CreateComponentResponse notFound(String path, String message, ComponentErrorKey errorKey) {
26+
return buildResponse(HttpStatus.NOT_FOUND, errorKey, path, message);
27+
}
28+
29+
public static CreateComponentResponse internalError(String path, String message, ComponentErrorKey errorKey) {
30+
return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorKey, path, message);
31+
}
32+
33+
private static CreateComponentResponse buildResponse(HttpStatus httpStatus, ComponentErrorKey errorKey, String path,
34+
String message) {
1235
CreateComponentResponse response = new CreateComponentResponse();
13-
response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
14-
response.setMessage("Failed to create component for project '" + projectId + "'");
36+
response.setTimestamp(System.currentTimeMillis());
37+
response.setHttpStatus(httpStatus.name());
38+
response.setErrorKey(errorKey.getKey());
39+
response.setError(errorKey.getMessage());
40+
response.setMessage(message);
41+
response.setPath(path);
1542
return response;
1643
}
1744

18-
public static CreateComponentResponse entityCreated(String projectId, String componentId) {
45+
public static CreateComponentResponse ok(String path, String message) {
1946
CreateComponentResponse response = new CreateComponentResponse();
20-
response.setErrorCode(HttpStatus.CREATED.value());
21-
response.setMessage(componentId + " component created successfully in project " + projectId);
47+
response.setTimestamp(System.currentTimeMillis());
48+
response.setHttpStatus(HttpStatus.OK.name());
49+
response.setErrorKey(ComponentErrorKey.OK.getKey());
50+
response.setMessage(message);
51+
response.setPath(path);
2252
return response;
2353
}
2454
}

api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import lombok.AllArgsConstructor;
44
import lombok.extern.slf4j.Slf4j;
55
import org.opendevstack.apiservice.project.api.ProjectComponentsApi;
6+
import org.opendevstack.apiservice.project.exception.ComponentCreationException;
7+
import org.opendevstack.apiservice.project.exception.ComponentNotFoundException;
68
import org.opendevstack.apiservice.project.facade.ComponentsFacade;
79
import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper;
810
import org.opendevstack.apiservice.project.model.Component;
@@ -13,44 +15,43 @@
1315
import org.springframework.web.bind.annotation.RequestMapping;
1416
import org.springframework.web.bind.annotation.RestController;
1517

18+
import java.util.UUID;
19+
1620
@RestController
1721
@AllArgsConstructor
1822
@Slf4j
19-
@RequestMapping("/api/pub/v0")
23+
@RequestMapping(ProjectComponentsController.API_BASE_PATH)
2024
public class ProjectComponentsController implements ProjectComponentsApi {
2125

26+
public static final String API_BASE_PATH = "/api/pub/v0";
27+
2228
private final ComponentsFacade componentsFacade;
2329

2430
private final ComponentResponseMapper componentResponseMapper;
2531

2632
@Override
2733
public ResponseEntity<CreateComponentResponse> createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
28-
try {
29-
Component component = componentsFacade.createProjectComponent(projectId, createComponentRequest);
30-
log.info("Created component {} for project id {} and request {}", component, projectId, createComponentRequest);
31-
if (component == null) {
32-
log.error("Failed to create component for project '{}'", projectId);
33-
return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId));
34-
}
35-
return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId, component.getId()));
36-
} catch (Exception e) {
37-
log.error("Error while trying to create component for project '" + projectId + "': " + e.getMessage(), e);
38-
return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId));
34+
Component component = componentsFacade.createProjectComponent(projectId, createComponentRequest);
35+
if (component == null) {
36+
throw new ComponentCreationException(String.format("Failed to create component for project '%s'", projectId));
3937
}
38+
39+
log.info("Created component {} for project id {} and request {}", component, projectId, createComponentRequest);
40+
return componentResponseMapper.toResponseEntity(
41+
ComponentsResponseFactory.entityCreated(projectId, component.getId())
42+
);
4043
}
4144

4245
@Override
43-
public ResponseEntity<Component> getProjectComponent(String projectId, String componentId) {
44-
try {
45-
Component component = componentsFacade.getProjectComponent(projectId, componentId);
46-
log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component);
47-
if (component == null) {
48-
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
49-
}
50-
return ResponseEntity.status(HttpStatus.OK).body(component);
51-
} catch (Exception e) {
52-
log.error("Error retrieving component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e);
53-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
46+
public ResponseEntity<Component> getProjectComponent(String projectId, UUID componentId) {
47+
Component component = componentsFacade.getProjectComponent(projectId, componentId.toString());
48+
if (component == null) {
49+
throw new ComponentNotFoundException(
50+
String.format("Component '%s' not found for project '%s'", componentId, projectId)
51+
);
5452
}
53+
54+
log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component);
55+
return ResponseEntity.status(HttpStatus.OK).body(component);
5556
}
5657
}

0 commit comments

Comments
 (0)