From 80ced85250de8ac5b956e337ff8c5d18b59e1900 Mon Sep 17 00:00:00 2001 From: Nitin Vavdiya Date: Wed, 19 Jul 2023 16:59:24 +0530 Subject: [PATCH] fix: validation erorr message not shown in reponse, docs: CGD-391: sample repomse added in wallet APIs --- .../config/ApplicationConfig.java | 31 +- .../config/ExceptionHandling.java | 65 ++- .../controller/WalletController.java | 447 +++++++++++++++++- 3 files changed, 515 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java index 3cc57dcda..a8cb28daf 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ApplicationConfig.java @@ -26,24 +26,36 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.smartsensesolutions.java.commons.specification.SpecificationUtil; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.text.StringEscapeUtils; import org.springdoc.core.properties.SwaggerUiConfigProperties; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.MessageSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.nio.charset.StandardCharsets; + /** * The type Application config. */ @Configuration @Slf4j -@RequiredArgsConstructor public class ApplicationConfig implements WebMvcConfigurer { private final SwaggerUiConfigProperties properties; + private final String resourceBundlePath; + + @Autowired + public ApplicationConfig(@Value("${resource.bundle.path:classpath:i18n/language}") String resourceBundlePath, SwaggerUiConfigProperties properties) { + this.resourceBundlePath = resourceBundlePath; + this.properties = properties; + } /** * Object mapper object mapper. @@ -71,4 +83,19 @@ public void addViewControllers(ViewControllerRegistry registry) { log.info("Set landing page to path {}", StringEscapeUtils.escapeJava(redirectUri)); registry.addRedirectViewController("/", redirectUri); } + + @Bean + public MessageSource messageSource() { + ReloadableResourceBundleMessageSource bean = new ReloadableResourceBundleMessageSource(); + bean.setBasename(resourceBundlePath); + bean.setDefaultEncoding(StandardCharsets.UTF_8.name()); + return bean; + } + + @Bean + public LocalValidatorFactoryBean validator() { + LocalValidatorFactoryBean beanValidatorFactory = new LocalValidatorFactoryBean(); + beanValidatorFactory.setValidationMessageSource(messageSource()); + return beanValidatorFactory; + } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java index b94233a3a..813a00fb4 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/config/ExceptionHandling.java @@ -21,13 +21,20 @@ package org.eclipse.tractusx.managedidentitywallets.config; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import lombok.extern.slf4j.Slf4j; import org.eclipse.tractusx.managedidentitywallets.exception.*; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; /** @@ -35,7 +42,7 @@ */ @RestControllerAdvice @Slf4j -public class ExceptionHandling extends ResponseEntityExceptionHandler { +public class ExceptionHandling { /** * The constant TIMESTAMP. @@ -98,6 +105,37 @@ ProblemDetail handleBadDataException(BadDataException e) { return problemDetail; } + + /** + * Handle validation problem detail. + * + * @param e the e + * @return the problem detail + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + ProblemDetail handleValidation(MethodArgumentNotValidException e) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage()); + problemDetail.setTitle("Invalid data provided"); + problemDetail.setProperty(TIMESTAMP, System.currentTimeMillis()); + problemDetail.setProperty("errors", handleValidationError(e.getFieldErrors())); + return problemDetail; + } + + /** + * Handle validation problem detail. + * + * @param exception the exception + * @return the problem detail + */ + @ExceptionHandler(ConstraintViolationException.class) + ProblemDetail handleValidation(ConstraintViolationException exception) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, exception.getMessage()); + problemDetail.setTitle("Invalid data provided"); + problemDetail.setProperty(TIMESTAMP, System.currentTimeMillis()); + problemDetail.setProperty("errors", exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).toList()); + return problemDetail; + } + /** * Handle duplicate credential problem problem detail. * @@ -112,6 +150,12 @@ ProblemDetail handleDuplicateCredentialProblem(RuntimeException e) { return problemDetail; } + /** + * Handle not found credential problem detail. + * + * @param e the e + * @return the problem detail + */ @ExceptionHandler(CredentialNotFoundProblem.class) ProblemDetail handleNotFoundCredentialProblem(CredentialNotFoundProblem e) { ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage()); @@ -120,6 +164,12 @@ ProblemDetail handleNotFoundCredentialProblem(CredentialNotFoundProblem e) { return problemDetail; } + /** + * Handle exception problem detail. + * + * @param e the e + * @return the problem detail + */ @ExceptionHandler(Exception.class) ProblemDetail handleException(Exception e) { log.error("Error ", e); @@ -128,4 +178,15 @@ ProblemDetail handleException(Exception e) { problemDetail.setProperty(TIMESTAMP, System.currentTimeMillis()); return problemDetail; } + + /** + * @param fieldErrors errors + * @return ResponseEntity with error details + */ + private Map handleValidationError(List fieldErrors) { + + Map messages = new HashMap<>(); + fieldErrors.forEach(fieldError -> messages.put(fieldError.getField(), fieldError.getDefaultMessage())); + return messages; + } } diff --git a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java index d72ba2820..92aa0c7e6 100644 --- a/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java +++ b/src/main/java/org/eclipse/tractusx/managedidentitywallets/controller/WalletController.java @@ -25,6 +25,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -68,6 +69,88 @@ public class WalletController extends BaseController { """) }) }) + @ApiResponse(responseCode = "400", description = "The input does not comply to the syntax requirements", content = { + @Content(examples = { + @ExampleObject(name = "Response in case of invalid data provided", value = """ + { + "type": "about:blank", + "title": "Invalid data provided", + "status": 400, + "detail": "details", + "instance": "API endpoint", + "properties": + { + "timestamp": 1689760833962, + "errors": + { + "filed": "filed error message" + } + } + } + """) + }) + }) + @ApiResponse(responseCode = "401", description = "The request could not be completed due to a failed authorization.", content = {@Content(examples = {})}) + @ApiResponse(responseCode = "403", description = "The request could not be completed due to a forbidden access", content = {@Content(examples = {})}) + @ApiResponse(responseCode = "409", description = "The request could not be completed due to a conflict.", content = {@Content(examples = { + @ExampleObject(name = "Wallet already exist", value = """ + { + "type": "about:blank", + "title": "Wallet is already exists for bpn BPNL000000000001", + "status": 409, + "detail": "Wallet is already exists for bpn BPNL000000000001", + "instance": "/api/wallets", + "properties": { + "timestamp": 1689762639948 + } + } + """) + })}) + @ApiResponse(responseCode = "500", description = "Any other internal server error", content = {@Content(examples = { + @ExampleObject(name = "Internal server error", value = """ + { + "type": "about:blank", + "title": "Error Title", + "status": 500, + "detail": "Error Details", + "instance": "API endpoint", + "properties": { + "timestamp": 1689762476720 + } + } + """) + })}) + @ApiResponse(responseCode = "201", content = { + @Content(examples = { + @ExampleObject(name = "Success response", value = """ + { + "name": "companyA", + "did": "did:web:localhost:BPNL000000000501", + "bpn": "BPNL000000000501", + "algorithm": "ED25519", + "didDocument": { + "id": "did:web:localhost:BPNL000000000501", + "verificationMethod": [ + { + "controller": "did:web:localhost:BPNL000000000501", + "id": "did:web:localhost:BPNL000000000501#", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "0Ap6FsX5UuRBIoOzxWtcFA2ymnqXw0U08Ino_mIuYM4" + }, + "type": "JsonWebKey2020" + } + ], + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3c.github.io/vc-jws-2020/contexts/v1" + ] + } + } + """) + }) + }) @Operation(summary = "Create Wallet", description = "Permission: **add_wallets** \n\n Create a wallet and store it") @PostMapping(path = RestURI.WALLETS, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity createWallet(@Valid @RequestBody CreateWalletRequest request) { @@ -87,32 +170,108 @@ public ResponseEntity createWallet(@Valid @RequestBody CreateWalletReque @io.swagger.v3.oas.annotations.parameters.RequestBody(content = { @Content(examples = @ExampleObject(""" { - "id": "http://example.edu/credentials/3732", - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "type": [ - "University-Degree-Credential", "VerifiableCredential" - ], - "issuer": "did:example:76e12ec712ebc6f1c221ebfeb1f", - "issuanceDate": "2019-06-16T18:56:59Z", - "expirationDate": "2019-06-17T18:56:59Z", - "credentialSubject": [{ - "college": "Test-University" - }], - "proof": { - "type": "Ed25519Signature2018", - "created": "2021-11-17T22:20:27Z", - "proofPurpose": "assertionMethod", - "verificationMethod": "did:example:76e12ec712ebc6f1c221ebfeb1f#key-1", - "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFZERTQSJ9..JNerzfrK46Mq4XxYZEnY9xOK80xsEaWCLAHuZsFie1-NTJD17wWWENn_DAlA_OwxGF5dhxUJ05P6Dm8lcmF5Cg" - } + "@context": + [ + "https://www.w3.org/2018/credentials/v1", + "https://registry.lab.gaia-x.eu/development/api/trusted-shape-registry/v1/shapes/jsonld/trustframework#" + ], + "type": + [ + "LegalParticipant", "VerifiableCredential" + ], + "id": "did:web:localhost", + "issuer": "did:web:localhost", + "issuanceDate": "2023-05-04T07:36:03.633Z", + "credentialSubject": + { + "id": "https://localhost/.well-known/participant.json", + "type": "gx:LegalParticipant", + "gx:legalName": "Demo", + "gx:legalRegistrationNumber": + { + "gx:taxID": "113123123" + }, + "gx:headquarterAddress": + { + "gx:countrySubdivisionCode": "BE-BRU" + }, + "gx:legalAddress": + { + "gx:countrySubdivisionCode": "BE-BRU" + }, + "gx-terms-and-conditions:gaiaxTermsAndConditions": "70c1d713215f95191a11d38fe2341faed27d19e083917bc8732ca4fea4976700" + }, + "proof": + { + "type": "JsonWebSignature2020", + "created": "2023-05-04T07:36:04.079Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:web:localhost", + "jws": "eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..iHki8WC3nPfcSRkC_AV4tXh0ikfT7BLPTGc_0ecI8zontTmJLqwcpPfAt0PFsoo3SkZgc6j636z55jj5tagBc-OKoiDu7diWryNAnL9ASsmWJyrPhOKVARs6x6PxVaTFBuyCfAHZeipxmkcYfNB_jooIXO2HuRcL2odhsQHELkGc5IDD-aBMWyNpfVAaYQ-cCzvDflZQlsowziUKfMkBfwpwgMdXFIgKWYdDIRvzA-U-XiC11-6QV7tPeKsMguEU0F5bh8cCEm2rooqXtENcsM_7cqFdQoOyblJyM-agoz2LUTj9QIdn9_gnNkGN-2U7_qBJWmHkK1Hm_mHqcNeeQw" + } } """)) }) + @ApiResponse(responseCode = "401", description = "The request could not be completed due to a failed authorization.", content = {@Content(examples = {})}) + @ApiResponse(responseCode = "403", description = "The request could not be completed due to a forbidden access", content = {@Content(examples = {})}) + @ApiResponse(responseCode = "500", description = "Any other internal server error", content = {@Content(examples = { + @ExampleObject(name = "Internal server error", value = """ + { + "type": "about:blank", + "title": "Error Title", + "status": 500, + "detail": "Error Details", + "instance": "API endpoint", + "properties": { + "timestamp": 1689762476720 + } + } + """) + })}) + @ApiResponse(responseCode = "400", description = "The input does not comply to the syntax requirements", content = { + @Content(examples = { + @ExampleObject(name = "Response in case of invalid data provided", value = """ + { + "type": "about:blank", + "title": "title", + "status": 400, + "detail": "details", + "instance": "API endpoint", + "properties": + { + "timestamp": 1689760833962, + "errors": + { + } + } + } + """) + }) + }) + @ApiResponse(responseCode = "404", description = "Wallet not found with provided identifier", content = {@Content(examples = { + @ExampleObject(name = "Wallet not found with provided identifier", value = """ + { + "type": "about:blank", + "title": "Wallet not found for identifier did:web:localhost:BPNL0000000", + "status": 404, + "detail": "Wallet not found for identifier did:web:localhost:BPNL0000000", + "instance": "/api/wallets/did%3Aweb%3Alocalhost%3ABPNL0000000/credentials", + "properties": { + "timestamp": 1689765541959 + } + } + """) + })}) + @ApiResponse(responseCode = "201", description = "Success Response", content = {@Content(examples = { + @ExampleObject(name = "Success Response", value = """ + { + "message": "Credential with id did:web:localhost has been successfully stored" + } + """) + })}) public ResponseEntity> storeCredential(@RequestBody Map data, - @Parameter(description = "Did or BPN") @PathVariable(name = "identifier") String identifier, Principal principal) { + @Parameter(description = "Did or BPN", examples = {@ExampleObject(name = "bpn", value = "BPNL000000000000", description = "bpn"), @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000000")}) @PathVariable(name = "identifier") String identifier, Principal principal) { + return ResponseEntity.status(HttpStatus.CREATED).body(service.storeCredential(data, identifier, getBPNFromToken(principal))); } @@ -123,12 +282,156 @@ public ResponseEntity> storeCredential(@RequestBody Map getWalletByIdentifier(@Parameter(description = "Did or BPN") @PathVariable(name = "identifier") String identifier, + public ResponseEntity getWalletByIdentifier(@Parameter(description = "Did or BPN", examples = {@ExampleObject(name = "bpn", value = "BPNL000000000501", description = "bpn"), @ExampleObject(description = "did", name = "did", value = "did:web:localhost:BPNL000000000501")}) @PathVariable(name = "identifier") String identifier, @RequestParam(name = "withCredentials", defaultValue = "false") boolean withCredentials, Principal principal) { - return ResponseEntity.status(HttpStatus.OK).body(service.getWalletByIdentifier(identifier, withCredentials, getBPNFromToken(principal))); } @@ -137,6 +440,102 @@ public ResponseEntity getWalletByIdentifier(@Parameter(description = "Di * * @return the wallets */ + @ApiResponse(responseCode = "401", description = "The request could not be completed due to a failed authorization.", content = {@Content(examples = {})}) + @ApiResponse(responseCode = "403", description = "The request could not be completed due to a forbidden access", content = {@Content(examples = {})}) + @ApiResponse(responseCode = "500", description = "Any other internal server error", content = {@Content(examples = { + @ExampleObject(name = "Internal server error", value = """ + { + "type": "about:blank", + "title": "Error Title", + "status": 500, + "detail": "Error Details", + "instance": "API endpoint", + "properties": { + "timestamp": 1689762476720 + } + } + """) + })}) + @ApiResponse(responseCode = "400", description = "The input does not comply to the syntax requirements", content = { + @Content(examples = { + @ExampleObject(name = "Response in case of invalid data provided", value = """ + { + "type": "about:blank", + "title": "title", + "status": 400, + "detail": "details", + "instance": "API endpoint", + "properties": + { + "timestamp": 1689760833962, + "errors": + { + } + } + } + """) + }) + }) + @ApiResponse(responseCode = "200", description = "Wallet list", content = { + @Content(examples = { + @ExampleObject(name = "Wallet list", value = """ + { + "content": [ + { + "name": "companyA", + "did": "did:web:localhost:BPNL000000000001", + "bpn": "BPNL000000000001", + "algorithm": "ED25519", + "didDocument": { + "id": "did:web:localhost:BPNL000000000001", + "verificationMethod": [ + { + "controller": "did:web:localhost:BPNL000000000001", + "id": "did:web:localhost:BPNL000000000001#", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "mhph0ZSVk7cDVmazbaaC3jBDpphW4eNygAK9gHPlMow" + }, + "type": "JsonWebKey2020" + } + ], + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3c.github.io/vc-jws-2020/contexts/v1" + ] + } + } + ], + "pageable": { + "sort": { + "empty": false, + "sorted": true, + "unsorted": false + }, + "offset": 0, + "pageNumber": 0, + "pageSize": 1, + "paged": true, + "unpaged": false + }, + "totalElements": 3, + "totalPages": 3, + "last": false, + "size": 1, + "number": 0, + "sort": { + "empty": false, + "sorted": true, + "unsorted": false + }, + "first": true, + "numberOfElements": 1, + "empty": false + } + """) + }) + }) @Operation(summary = "List of wallets", description = "Permission: **view_wallets** \n\n Retrieve list of registered wallets") @GetMapping(path = RestURI.WALLETS, produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity> getWallets(@RequestParam(required = false, defaultValue = "0") int pageNumber,