From 8d0f6477dad26f074fa16ba7eb7dc9d0177809be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lino?= Date: Mon, 27 Apr 2020 18:37:00 +0100 Subject: [PATCH] Defining a REST API for basic Profile operations. --- .../data/model/AbstractIdentification.java | 1 - .../profile/data/model/Profile.java | 4 +- .../profile/helper/validation/UUIDv4.java | 28 +++ .../validation/annotation/ValidUUIDv4.java | 32 +++ .../profile/web/api/ProfileAPI.java | 186 ++++++++++++++++++ .../profile/web/model/ProfileRead.java | 38 ++++ .../profile/web/model/ProfileWrite.java | 25 +++ .../profile/web/model/error/ErrorDetails.java | 102 ++++++++++ .../model/error/InvalidParameterDetail.java | 20 ++ 9 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/UUIDv4.java create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/annotation/ValidUUIDv4.java create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/web/api/ProfileAPI.java create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileRead.java create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileWrite.java create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/ErrorDetails.java create mode 100644 src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/InvalidParameterDetail.java diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/AbstractIdentification.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/AbstractIdentification.java index a6a78d4..bdeea0a 100644 --- a/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/AbstractIdentification.java +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/AbstractIdentification.java @@ -13,7 +13,6 @@ import java.util.UUID; @Data @SuperBuilder @NoArgsConstructor -// @AllArgsConstructor public class AbstractIdentification extends AbstractAudit { /* A unique identifier of this model entity. */ diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/Profile.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/Profile.java index 7ee536f..d78db9b 100644 --- a/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/Profile.java +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/data/model/Profile.java @@ -11,10 +11,12 @@ import org.springframework.data.mongodb.core.mapping.Document; @Data @SuperBuilder @AllArgsConstructor -// @NoArgsConstructor @Document public class Profile extends AbstractIdentification { /** The full name of the person. */ private final String fullName = null; + + /** Activation status of this profile. */ + private final boolean isActive = true; } diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/UUIDv4.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/UUIDv4.java new file mode 100644 index 0000000..8e41410 --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/UUIDv4.java @@ -0,0 +1,28 @@ +package com.joaolino.demo.spring.microservice.profile.helper.validation; + +import com.joaolino.demo.spring.microservice.profile.helper.validation.annotation.ValidUUIDv4; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** A UUID v4 constraint validator. */ +public class UUIDv4 implements ConstraintValidator { + + /** + * Validates if a given UUID is of the version 4 and has the correct format of a UUID + * + * @param obj Represents the UUID of a given + * @param context {@link ConstraintValidator} + * @return Returns a boolean value that represents if the UUID is version is valid + */ + @Override + public boolean isValid(final UUID obj, final ConstraintValidatorContext context) { + final Pattern pattern = + Pattern.compile("^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$"); + final Matcher matcher = pattern.matcher(obj.toString().toUpperCase()); + return matcher.matches(); + } +} diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/annotation/ValidUUIDv4.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/annotation/ValidUUIDv4.java new file mode 100644 index 0000000..c42ec70 --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/helper/validation/annotation/ValidUUIDv4.java @@ -0,0 +1,32 @@ +package com.joaolino.demo.spring.microservice.profile.helper.validation.annotation; + +import com.joaolino.demo.spring.microservice.profile.helper.validation.UUIDv4; + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Interface that will represent the validation of a version 4 UUID. */ +@Constraint(validatedBy = UUIDv4.class) +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ValidUUIDv4 { + + /** + * Should default to an error message key, made of the fully-qualified class name of the + * constraint, followed by .message. For example "{com.acme.constraints.NotSafe.message}". + */ + String message() default "Invalid UUIDv4"; + + /** for user to customize the targeted groups */ + Class[] groups() default {}; + + /** + * For extensibility purposes. Payload type that can be attached to a given constraint + * declaration. Can be used to carry on metadata information consumed by a validation client. + */ + Class[] payload() default {}; +} diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/web/api/ProfileAPI.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/api/ProfileAPI.java new file mode 100644 index 0000000..c36741b --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/api/ProfileAPI.java @@ -0,0 +1,186 @@ +package com.joaolino.demo.spring.microservice.profile.web.api; + +import com.joaolino.demo.spring.microservice.profile.web.model.ProfileRead; +import com.joaolino.demo.spring.microservice.profile.web.model.ProfileWrite; +import com.joaolino.demo.spring.microservice.profile.web.model.error.ErrorDetails; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +/** Profile Microservice's REST API. */ +@Tag(name = "profile", description = "Endpoints exposed by the Profile microservice REST API.") +public interface ProfileAPI { + + /** + * Add a new profile to the store. + * + * @param profileWrite String with profile payload. + * @return A stream containing the profile added to the store. + */ + @RequestMapping( + method = RequestMethod.POST, + path = "/profiles", + consumes = {"application/json", "application/xml"}, + produces = {"application/json", "application/xml"}) + @Operation(summary = "Add a new profile to the store.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "The profile was added.", + content = @Content(schema = @Schema(implementation = ProfileRead.class))), + @ApiResponse( + responseCode = "400", + description = "The profile was not added.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "403", + description = "You are not allowed to preform this action, the profile was not added.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "422", + description = "Received invalid data, the profile was not added.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "500", + description = "Processing failure, the profile was not added.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))) + }) + ResponseEntity> addProfile( + @Parameter( + description = + "The object that contains the necessary information to add a new profile.", + name = "profileWrite", + required = true) + @RequestBody + ProfileWrite profileWrite); + + /** + * Find profiles identified by the received profile ID. + * + * @param profileId The ID of the profile to be retrieved. + * @return The profile with the ID provided. + */ + @RequestMapping( + method = RequestMethod.GET, + path = "/profiles/{profileId}", + produces = {"application/json", "application/xml"}) + @Operation(summary = "Find a profile identified by received provided ID.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Successful operation.", + content = @Content(schema = @Schema(implementation = ProfileRead.class))), + @ApiResponse( + responseCode = "400", + description = "Invalid ID supplied.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "404", + description = "Profile not found.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "500", + description = "Processing failure.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))) + }) + ResponseEntity> getProfile( + @Parameter( + description = "The profile's unique identifier.", + name = "profileId", + example = "d4f1c83f-3fa7-413b-8d37-8fd5404b382f", + required = true) + @PathVariable(value = "profileId", required = false) + UUID profileId); + + /** + * Find profiles based on the received request parameters. + * + * @param fullName the Full Name in the profile. + * @param isActive the activation status of the profile. + * @return A stream of matching profiles. + */ + @RequestMapping( + method = RequestMethod.GET, + path = "/profiles", + produces = {"application/json", "application/xml"}) + @Operation( + summary = + "Find profiles matching the provided Profile properties, e.g. Full Name and activation status.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Successful operation.", + content = @Content(schema = @Schema(implementation = ProfileRead.class))), + @ApiResponse( + responseCode = "400", + description = "Invalid ID supplied.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "404", + description = "Profile not found.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "500", + description = "Processing failure.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))) + }) + ResponseEntity> getProfiles( + @Parameter(description = "A full name of the person's profile.", name = "fullName") + @RequestParam(value = "fullName", required = false) + String fullName, + @Parameter( + description = "The profile activation status.", + name = "isActive", + example = "false") + @RequestParam(value = "isActive", required = false) + Boolean isActive); + + /** + * Deactivate a profile. + * + * @param profileId ID of the profile to be deactivated. + * @return The deactivated profile. + */ + @RequestMapping(method = RequestMethod.PUT, path = "/profiles/{profileId}/deactivate") + @Operation(summary = "Deactivate a profile.") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "Successful operation.", + content = @Content(schema = @Schema(implementation = ProfileRead.class))), + @ApiResponse( + responseCode = "400", + description = "Invalid ID supplied.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "404", + description = "Profile not found.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))), + @ApiResponse( + responseCode = "500", + description = "Processing failure.", + content = @Content(schema = @Schema(implementation = ErrorDetails.class))) + }) + ResponseEntity> deactivateProfile( + @Parameter( + description = "The profile's unique identifier.", + name = "profileId", + example = "d4f1c83f-3fa7-413b-8d37-8fd5404b382f", + required = true) + @PathVariable("profileId") + UUID profileId); +} diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileRead.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileRead.java new file mode 100644 index 0000000..1e8eb19 --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileRead.java @@ -0,0 +1,38 @@ +package com.joaolino.demo.spring.microservice.profile.web.model; + +import com.joaolino.demo.spring.microservice.profile.helper.validation.annotation.ValidUUIDv4; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import javax.validation.constraints.NotNull; +import java.util.UUID; + +/** + * Represents a Profile Read object. This is only returned by the REST interface, in the context of + * communication with other services/platforms, and is not intended to be used in any internal + * operations. It contains the profile ID, a full name and the profile's activation state. + */ +@Data +@AllArgsConstructor +@Schema(description = "The Profile Read object, used when returning a Profiles in REST endpoints.") +@Builder +public class ProfileRead { + + /** The identifier used to uniquely identify a profile. */ + @Schema(required = true, description = "The identifier used to uniquely identify a profile.") + @NotNull + @ValidUUIDv4 + private UUID id; + + /** The full name of the person this profile belongs to. */ + @Schema(required = true, description = "The full name of the person this profile belongs to.") + @NotNull + private String fullName; + + /** Flag indicating the activation status of the profile. */ + @Schema(required = true, description = "Flag indicating the activation status of the profile.") + @NotNull + private Boolean isActive; +} diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileWrite.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileWrite.java new file mode 100644 index 0000000..e857011 --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/ProfileWrite.java @@ -0,0 +1,25 @@ +package com.joaolino.demo.spring.microservice.profile.web.model; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +import javax.validation.constraints.NotNull; + +/** + * Represents a Profile Write object. This is only accepted by the REST interface, in the context of + * communication with other services/platforms, and is not intended to be used in any internal + * operations. It contains the full name of the person the profile belongs to. + */ +@Data +@AllArgsConstructor +@Schema(description = "The Profile Write object, used when receiving a Profiles in REST endpoints.") +@Builder +public class ProfileWrite { + + /** The full name of the person this profile belongs to. */ + @Schema(required = true, description = "The full name of the person this profile belongs to.") + @NotNull + private String fullName; +} diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/ErrorDetails.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/ErrorDetails.java new file mode 100644 index 0000000..3f04459 --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/ErrorDetails.java @@ -0,0 +1,102 @@ +package com.joaolino.demo.spring.microservice.profile.web.model.error; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; +import org.springframework.http.HttpStatus; + +import java.net.URI; +import java.util.List; +import java.util.UUID; + +/** + * The common error object, used to standardize error communication between systems. Adheres to the + * RFC7807 specification. + */ +@Data +@Builder +@Schema( + description = + "The common error object, used to standardize error communication between systems. Adheres to the RFC7807 specification.") +public class ErrorDetails { + + // Standard Members + /** + * The primary identifier for the problem. A URI reference that illustrates the general type of + * problem. It's encouraged that it provides human readable documentation for the problem. If + * absent, it's assumed to be represent a blank value. + */ + @Schema( + description = + "The primary identifier for the problem. A URI reference that illustrates the general type of problem. It's encouraged that it provides human readable documentation for the problem. If absent, it's assumed to be represent a blank value.") + private URI type; + + /** A brief description of the error. */ + @Schema(description = "A brief description of the error.") + private String title; + + /** + * This member represents the HTTP status code returned by the application. Must always reflect + * what is being sent in the HTTP headers. It exists only in an advisory capacity and should not + * impact the error interpretation, it should only inform the user if presented. + */ + @Schema( + description = + "This member represents the HTTP status code returned by the application. Must always reflect what is being sent in the HTTP headers. It exists only in an advisory capacity and should not impact the error interpretation, it should only inform the user if presented.") + private HttpStatus status; + + /** + * A detailed explanation of the error. Provided to help the error consumer correct the problem. + */ + @Schema( + description = + "A detailed explanation of the error. Provided to help the error consumer correct the problem.") + private String detail; + + /** + * A URI that references the specific occurrence of the problem. Tends to represent the URI of the + * API call. + */ + @Schema( + description = + "A URI that references the specific occurrence of the problem. Tends to represent the URI of the API call.") + private URI instance; + + // Extensions + /** + * For node tracing purposes. Should identify the information processing flow that was executing + * at the time this error occurred. + */ + @Schema( + description = + "For node tracing purposes. Should identify the information processing flow that was executing at the time this error occurred.", + required = true) + private String processingFlowId; + + /** + * For user tracing purposes. Should identify the consumer session triggering the action that + * eventually resulted in this error. + */ + @Schema( + description = + "For user tracing purposes. Should identify the consumer session triggering the action that eventually resulted in this error.", + required = true) + private String consumerSessionId; + + /** + * Used when handling validation errors. Can contain multiple invalid param reports for a single + * call. + */ + @Schema( + description = + "Used when handling validation errors. Can contain multiple invalid param reports for a single call.") + private List invalidParams; + + /** The ID of the entity that was accessed / requested. */ + @Schema(description = "The ID of the entity that was accessed / requested.") + private UUID entityId; + + /** The name of the entity that was accessed / requested. */ + @Schema(description = "The name of the entity that was accessed / requested.") + private String entityName; +} diff --git a/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/InvalidParameterDetail.java b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/InvalidParameterDetail.java new file mode 100644 index 0000000..fee26d8 --- /dev/null +++ b/src/main/java/com/joaolino/demo/spring/microservice/profile/web/model/error/InvalidParameterDetail.java @@ -0,0 +1,20 @@ +package com.joaolino.demo.spring.microservice.profile.web.model.error; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** Used when handling validation errors. Details what parameter is invalid and the reason why. */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class InvalidParameterDetail { + + /** The name of the parameter that is invalid. */ + private String name; + + /** The reason why the parameter was asserted as invalid. */ + private String reason; +} -- 2.24.1