Skip to content

Commit 9ef2dfc

Browse files
authored
Merge pull request #6 from marmoure/feature/vlidation
Add schema validation, exception handling, and controller tests
2 parents 663078e + 0275584 commit 9ef2dfc

25 files changed

Lines changed: 1068 additions & 208 deletions

pom.xml

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<micronaut.validation.version>4.12.0</micronaut.validation.version>
2222
<micronaut.test.resources.version>2.8.2</micronaut.test.resources.version>
2323
<micronaut.runtime>netty</micronaut.runtime>
24+
<junit.version>6.0.1</junit.version>
2425
<exec.mainClass>com.evolvedbinary.bblValidator.Application</exec.mainClass>
2526
</properties>
2627

@@ -29,7 +30,8 @@
2930
<dependency>
3031
<groupId>io.micronaut</groupId>
3132
<artifactId>micronaut-http-server-netty</artifactId>
32-
<version>${micronaut.version}</version>
33+
<!-- TODO(YB) check for updates https://www.mend.io/vulnerability-database/CVE-2025-67735 -->
34+
<version>4.10.12</version>
3335
</dependency>
3436

3537
<!-- Micronaut Inject -->
@@ -105,12 +107,53 @@
105107
<version>1.4.1</version>
106108
</dependency>
107109

110+
<dependency>
111+
<groupId>org.apache.httpcomponents</groupId>
112+
<artifactId>httpclient</artifactId>
113+
<version>4.5.14</version>
114+
</dependency>
115+
108116
<!-- https://mvnrepository.com/artifact/com.fasterxml.uuid/java-uuid-generator -->
109117
<dependency>
110118
<groupId>com.fasterxml.uuid</groupId>
111119
<artifactId>java-uuid-generator</artifactId>
112120
<version>5.2.0</version>
113121
</dependency>
122+
123+
<dependency>
124+
<groupId>net.jcip</groupId>
125+
<artifactId>jcip-annotations</artifactId>
126+
<version>1.0</version>
127+
</dependency>
128+
129+
<!-- Test Dependencies -->
130+
<dependency>
131+
<groupId>io.micronaut.test</groupId>
132+
<artifactId>micronaut-test-junit5</artifactId>
133+
<version>4.6.0</version>
134+
<scope>test</scope>
135+
</dependency>
136+
137+
<dependency>
138+
<groupId>org.junit.jupiter</groupId>
139+
<artifactId>junit-jupiter-api</artifactId>
140+
<version>${junit.version}</version>
141+
<scope>test</scope>
142+
</dependency>
143+
144+
<dependency>
145+
<groupId>org.junit.jupiter</groupId>
146+
<artifactId>junit-jupiter-engine</artifactId>
147+
<version>${junit.version}</version>
148+
<scope>test</scope>
149+
</dependency>
150+
151+
<dependency>
152+
<groupId>io.micronaut</groupId>
153+
<artifactId>micronaut-http-client</artifactId>
154+
<version>${micronaut.version}</version>
155+
<scope>test</scope>
156+
</dependency>
114157
</dependencies>
115158

116159
<build>
@@ -120,6 +163,12 @@
120163
<filtering>true</filtering>
121164
</resource>
122165
</resources>
166+
<testResources>
167+
<testResource>
168+
<directory>src/test/resources</directory>
169+
<filtering>true</filtering>
170+
</testResource>
171+
</testResources>
123172

124173
<plugins>
125174
<!-- Maven Compiler Plugin -->

src/main/java/com/evolvedbinary/bblValidator/Application.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
public class Application {
66

7-
public static void main(String[] args) {
7+
public static void main(final String[] args) {
88
Micronaut.run(Application.class, args);
99
}
1010
}
Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package com.evolvedbinary.bblValidator.controller;
22

3+
import com.evolvedbinary.bblValidator.dto.ErrorResponse;
34
import com.evolvedbinary.bblValidator.dto.SchemaInfo;
45
import com.evolvedbinary.bblValidator.service.SchemaService;
6+
import io.micronaut.http.HttpRequest;
7+
import io.micronaut.http.HttpResponse;
58
import io.micronaut.http.MediaType;
69
import io.micronaut.http.annotation.Controller;
710
import io.micronaut.http.annotation.Get;
@@ -10,22 +13,41 @@
1013
import jakarta.inject.Inject;
1114

1215
import java.util.List;
16+
import java.util.stream.Collectors;
1317

1418
@Controller("/schema")
1519
public class SchemaController {
20+
public static final MediaType CSV_SCHEMA_MEDIA_TYPE = MediaType.of("text/csv-schema");
1621

1722
@Inject
1823
SchemaService schemaService;
1924

2025
@Get
2126
@Produces(MediaType.APPLICATION_JSON)
22-
public List<SchemaInfo> listSchemas() {
23-
return schemaService.listSchemas();
27+
public List<SchemaInfo> listSchemas(final HttpRequest<?> request) {
28+
final String host = request.getHeaders().get("Host");
29+
final String path = request.getPath().replace("/schema", "/schema/");
30+
final String protocol = request.isSecure() ? "https://" : "http://";
31+
final String url = protocol + host + path;
32+
33+
return schemaService.listSchemas().stream()
34+
.map(schema -> new SchemaInfo(schema.getId(), schema.getName(), schema.getVersion(), schema.getDate(), url + schema.getId(), schema.getDescription()))
35+
.collect(Collectors.toList());
2436
}
2537

2638
@Get("/{schema-id}")
2739
@Produces("text/csv-schema")
28-
public String getSchema(@PathVariable("schema-id") String schemaId) throws Exception {
29-
return schemaService.getSchema(schemaId);
40+
public HttpResponse<Object> getSchema(@PathVariable("schema-id") final String schemaId) {
41+
final String schema = schemaService.getSchema(schemaId);
42+
if (schema == null) {
43+
return HttpResponse
44+
.badRequest()
45+
.contentType(MediaType.APPLICATION_JSON_TYPE)
46+
.body(new ErrorResponse(ErrorResponse.Code.SCHEMA_NOT_FOUND,"Schema not found with ID: " + schemaId));
47+
}
48+
return HttpResponse
49+
.ok()
50+
.contentType(CSV_SCHEMA_MEDIA_TYPE)
51+
.body(schema);
3052
}
3153
}
Lines changed: 57 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package com.evolvedbinary.bblValidator.controller;
22

3-
import com.evolvedbinary.bblValidator.dto.ValidationError;
3+
import com.evolvedbinary.bblValidator.dto.ErrorResponse;
4+
import com.evolvedbinary.bblValidator.dto.ResponseObject;
45
import com.evolvedbinary.bblValidator.dto.ValidationForm;
56
import com.evolvedbinary.bblValidator.dto.ValidationResponse;
67
import com.evolvedbinary.bblValidator.service.CsvValidationService;
78
import com.evolvedbinary.bblValidator.service.FileDownloadService;
9+
import com.evolvedbinary.bblValidator.service.SchemaService;
10+
import io.micronaut.core.annotation.Nullable;
11+
import io.micronaut.http.HttpResponse;
812
import io.micronaut.http.MediaType;
913
import io.micronaut.http.annotation.Body;
1014
import io.micronaut.http.annotation.Consumes;
@@ -16,8 +20,8 @@
1620
import org.slf4j.LoggerFactory;
1721

1822
import java.io.IOException;
23+
import java.nio.file.Files;
1924
import java.nio.file.Path;
20-
import java.util.List;
2125

2226
@Controller("/validate")
2327
public class ValidateController {
@@ -27,6 +31,8 @@ public class ValidateController {
2731
FileDownloadService fileDownloadService;
2832
@Inject
2933
CsvValidationService csvValidationService;
34+
@Inject
35+
SchemaService schemaService;
3036

3137
/**
3238
* Handles form URL encoded validation requests.
@@ -36,14 +42,21 @@ public class ValidateController {
3642
*/
3743
@Post
3844
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
39-
public ValidationResponse validateForm(@Body ValidationForm form) {
45+
public HttpResponse<ResponseObject> validateForm(@Body final ValidationForm form) {
46+
if (null == schemaService.getSchema(form.schemaId())) {
47+
return HttpResponse.badRequest().body(new ErrorResponse(ErrorResponse.Code.SCHEMA_NOT_FOUND,"Schema not found with ID: " + form.schemaId()));
48+
}
4049
try {
41-
Path downloadedFile = fileDownloadService.downloadToTemp(form.url());
42-
LOG.info("File downloaded to: {}", downloadedFile);
43-
return performValidation(downloadedFile, form.schemaId());
44-
} catch (IOException e) {
45-
LOG.error("Failed to download file from URL: {}", form.url(), e);
46-
return createErrorResponse("Download failed: " + e.getMessage(), 0);
50+
final Path downloadedFile = fileDownloadService.downloadToTemp(form.url());
51+
LOG.trace("File downloaded to: {}", downloadedFile);
52+
try {
53+
return HttpResponse.ok(performValidation(downloadedFile, form.schemaId()));
54+
} finally {
55+
Files.delete(downloadedFile);
56+
}
57+
} catch (final IOException e) {
58+
LOG.trace("Failed to download file from URL: {}", form.url());
59+
return HttpResponse.badRequest().body(new ErrorResponse(ErrorResponse.Code.NON_RESOLVABLE_URL,"Unable to resolve url : " + form.url()));
4760
}
4861
}
4962

@@ -56,15 +69,25 @@ public ValidationResponse validateForm(@Body ValidationForm form) {
5669
*/
5770
@Post
5871
@Consumes(MediaType.TEXT_CSV)
59-
public ValidationResponse validateCsv(@QueryValue("schema-id") String schemaId,
60-
@Body String csvContent) {
72+
public HttpResponse<ResponseObject> validateCsv(@QueryValue("schema-id") final String schemaId,
73+
@Nullable @Body final String csvContent) {
74+
if (schemaService.getSchema(schemaId) == null) {
75+
return HttpResponse.badRequest().body(new ErrorResponse(ErrorResponse.Code.SCHEMA_NOT_FOUND,"Schema not found with ID: " + schemaId));
76+
}
77+
if (csvContent == null || csvContent.isEmpty()) {
78+
return HttpResponse.badRequest().body(new ErrorResponse(ErrorResponse.Code.NO_CSV,"Empty CSV content"));
79+
}
6180
try {
62-
Path tempFile = fileDownloadService.saveContentToTemp(csvContent);
63-
LOG.info("CSV content saved to: {}", tempFile);
64-
return performValidation(tempFile, schemaId);
65-
} catch (IOException e) {
81+
final Path tempFile = fileDownloadService.saveContentToTemp(csvContent);
82+
try {
83+
LOG.trace("CSV content saved to: {}", tempFile);
84+
return HttpResponse.ok(performValidation(tempFile, schemaId));
85+
} finally {
86+
Files.delete(tempFile);
87+
}
88+
} catch (final IOException e) {
6689
LOG.error("Failed to save CSV content to temp file", e);
67-
return createErrorResponse("Failed to save content: " + e.getMessage(), 0);
90+
return HttpResponse.serverError().body(new ErrorResponse(ErrorResponse.Code.UNEXPECTED_ERROR,"Unable to store CSV: " + e.getMessage()));
6891
}
6992
}
7093

@@ -77,31 +100,32 @@ public ValidationResponse validateCsv(@QueryValue("schema-id") String schemaId,
77100
*/
78101
@Post
79102
@Consumes(MediaType.ALL)
80-
public ValidationResponse validateParams(@QueryValue("schema-id") String schemaId,
81-
@QueryValue String url) {
103+
public HttpResponse<ResponseObject> validateParams(@QueryValue("schema-id") final String schemaId,
104+
@QueryValue final String url) {
105+
if (schemaService.getSchema(schemaId) == null) {
106+
return HttpResponse.badRequest().body(new ErrorResponse(ErrorResponse.Code.SCHEMA_NOT_FOUND,"Schema not found with ID: " + schemaId));
107+
}
82108
try {
83-
Path downloadedFile = fileDownloadService.downloadToTemp(url);
84-
LOG.info("File downloaded to: {}", downloadedFile);
85-
return performValidation(downloadedFile, schemaId);
86-
} catch (IOException e) {
87-
LOG.error("Failed to download file from URL: {}", url, e);
88-
return createErrorResponse("Download failed: " + e.getMessage(), 0);
109+
final Path downloadedFile = fileDownloadService.downloadToTemp(url);
110+
LOG.trace("File downloaded to: {}", downloadedFile);
111+
try {
112+
return HttpResponse.ok(performValidation(downloadedFile, schemaId));
113+
} finally {
114+
Files.delete(downloadedFile);
115+
}
116+
} catch (final IOException e) {
117+
LOG.trace("Failed to download file from URL: {}", url);
118+
return HttpResponse.badRequest().body(new ErrorResponse(ErrorResponse.Code.NON_RESOLVABLE_URL,"Unable to resolve url : " + url));
89119
}
90120
}
91121

92-
private ValidationResponse performValidation(Path csvFile, String schemaId) {
93-
CsvValidationService.ValidationResult result = csvValidationService.validateCsvFile(csvFile, schemaId);
122+
private ResponseObject performValidation(final Path csvFile, final String schemaId) {
123+
final CsvValidationService.ValidationResult result = csvValidationService.validateCsvFile(csvFile, schemaId);
94124

95125
if (result.hasErrorMessage()) {
96-
return createErrorResponse(result.getErrorMessage(), result.getExecutionTimeMs());
126+
return new ErrorResponse(ErrorResponse.Code.VALIDATION_ERROR,"An error occurred: " + result.getErrorMessage());
97127
}
98128

99129
return new ValidationResponse(result.isValid(), result.getErrors(), result.getExecutionTimeMs());
100130
}
101-
102-
private ValidationResponse createErrorResponse(String errorMessage, long executionTimeMs) {
103-
return new ValidationResponse(false,
104-
List.of(new ValidationError(errorMessage, 0, 0)),
105-
executionTimeMs);
106-
}
107131
}

src/main/java/com/evolvedbinary/bblValidator/dto/ApiVersion.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,23 @@
22

33
import io.micronaut.serde.annotation.Serdeable;
44

5+
/**
6+
* Data transfer object representing the API version information.
7+
*/
58
@Serdeable
69
public class ApiVersion {
710

811
private String version;
912

10-
public ApiVersion() {
11-
}
12-
13-
public ApiVersion(String version) {
13+
public ApiVersion(final String version) {
1414
this.version = version;
1515
}
1616

1717
public String getVersion() {
1818
return version;
1919
}
2020

21-
public void setVersion(String version) {
21+
public void setVersion(final String version) {
2222
this.version = version;
2323
}
2424
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.evolvedbinary.bblValidator.dto;
2+
3+
import io.micronaut.serde.annotation.Serdeable;
4+
5+
/**
6+
* Response object representing an error that occurred during validation.
7+
*/
8+
@Serdeable
9+
public final class ErrorResponse implements ResponseObject {
10+
public enum Code {
11+
/**
12+
* provided schema id does not exist
13+
*/
14+
SCHEMA_NOT_FOUND,
15+
16+
/**
17+
* A fatal error has occurred
18+
*/
19+
UNEXPECTED_ERROR,
20+
21+
/**
22+
* No csv content to validate
23+
*/
24+
NO_CSV,
25+
26+
/**
27+
* The url is malformed or do not resolve
28+
*/
29+
NON_RESOLVABLE_URL,
30+
31+
/**
32+
* The validation failed
33+
*/
34+
VALIDATION_ERROR
35+
}
36+
37+
private final Code code;
38+
private final String description;
39+
40+
public ErrorResponse(final Code code, final String description) {
41+
this.code = code;
42+
this.description = description;
43+
}
44+
45+
public Code getCode() {
46+
return code;
47+
}
48+
49+
public String getDescription() {
50+
return description;
51+
}
52+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.evolvedbinary.bblValidator.dto;
2+
3+
/**
4+
* This interface serves as a marker for all response types that can be returned
5+
* by a controller.
6+
*
7+
* @see ErrorResponse
8+
* @see ValidationResponse
9+
*/
10+
public sealed interface ResponseObject permits ErrorResponse, ValidationResponse {
11+
12+
}

0 commit comments

Comments
 (0)