diff --git a/ms-common-impl/pom.xml b/ms-common-impl/pom.xml index e2979abdd..714c221fb 100644 --- a/ms-common-impl/pom.xml +++ b/ms-common-impl/pom.xml @@ -9,7 +9,7 @@ ms-common-impl Common Microservice Implementation - 3.1.4.Final + 4.0.0.Beta2 false 1.7.16 @@ -37,10 +37,6 @@ net.jcip jcip-annotations - - org.apache.commons - commons-lang3 - @@ -74,16 +70,27 @@ - org.apache.bval - bval-jsr + org.eclipse + yasson + 1.0.1 + runtime + + + org.glassfish + javax.el + runtime + + + org.glassfish + javax.json 1.1.2 runtime - - - commons-logging - commons-logging - - + + + org.hibernate + hibernate-validator + 6.0.8.Final + runtime net.trajano.commons diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/VertxMicroserviceEngine.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/VertxMicroserviceEngine.java index 7d44fae9d..20c0baec5 100644 --- a/ms-common-impl/src/main/java/net/trajano/ms/vertx/VertxMicroserviceEngine.java +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/VertxMicroserviceEngine.java @@ -67,7 +67,7 @@ public class VertxMicroserviceEngine implements * Sets the system properties and sets up the logger. {@inheritDoc} */ @Override - public Object[] bootstrap() { + public Class[] bootstrap() { System.setProperty("vertx.logger-delegate-factory-class-name", "io.vertx.core.logging.SLF4JLogDelegateFactory"); @@ -76,7 +76,7 @@ public Object[] bootstrap() { System.setProperty("logging.config", logbackFile.getAbsolutePath()); } - return new Object[] { + return new Class[] { VertxConfig.class, VertxMicroserviceEngine.class }; diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/beans/BeanValidator.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/beans/BeanValidator.java new file mode 100644 index 000000000..9da73ee36 --- /dev/null +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/beans/BeanValidator.java @@ -0,0 +1,181 @@ +package net.trajano.ms.vertx.beans; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.validation.Valid; +import javax.validation.Validator; +import javax.validation.constraints.NotNull; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.InternalServerErrorException; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; + +import org.jboss.resteasy.spi.HttpRequest; +import org.jboss.resteasy.spi.validation.GeneralValidator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import net.trajano.ms.core.ConstraintViolationResponse; +import net.trajano.ms.core.ErrorResponses; + +/** + * This performs validation checks on the request message. If the request + * message does not pass validation it will throw a bad request exception. + * + * @author Archimedes Trajano + */ +@Component +public class BeanValidator implements + GeneralValidator { + + /** + * Logger. + */ + private static final Logger LOG = LoggerFactory.getLogger(BeanValidator.class); + + @Autowired + private Validator validator; + + @Override + public void checkViolations(final HttpRequest request) { + + // does nothing + + } + + /** + * Obtain the name from JAX-RS annotations. If the parameter is not annotated is + * uses "body" as the name. + * + * @param parameter + * parameter + * @return parameter name + */ + private String getParameterName(final Parameter parameter) { + + final String name; + if (parameter.getAnnotation(FormParam.class) != null) { + name = "form." + parameter.getAnnotation(FormParam.class).value(); + } else if (parameter.getAnnotation(QueryParam.class) != null) { + name = "query." + parameter.getAnnotation(QueryParam.class).value(); + } else if (parameter.getAnnotation(PathParam.class) != null) { + name = "path." + parameter.getAnnotation(PathParam.class).value(); + } else if (parameter.getAnnotation(HeaderParam.class) != null) { + name = "header." + parameter.getAnnotation(HeaderParam.class).value(); + } else if (parameter.isAnnotationPresent(Context.class)) { + name = parameter.getType().getName(); + } else { + name = "body"; + } + return name; + } + + @Override + public boolean isMethodValidatable(final Method method) { + + return true; + } + + @Override + public boolean isValidatable(final Class clazz) { + + return true; + } + + @Override + public void validate(final HttpRequest request, + final Object object, + final Class... groups) { + + LOG.debug("validate {} {}", request, object); + + } + + /** + * {@inheritDoc}. {@link Context} annotated parameters are skipped unless the + * {@link Valid} annotation is present. + */ + @Override + public void validateAllParameters(final HttpRequest request, + final Object object, + final Method method, + final Object[] parameterValues, + final Class... groups) { + + LOG.debug("validateAllParameters {} {} {}", request, object, parameterValues); + + final Map>> violationMap = new LinkedHashMap<>(); + boolean hasViolation = false; + for (int i = 0; i < method.getParameterCount(); ++i) { + + final Parameter parameter = method.getParameters()[i]; + if (parameter.isAnnotationPresent(Context.class) && !parameter.isAnnotationPresent(Valid.class)) { + continue; + } + + final Object value = parameterValues[i]; + + final String name = getParameterName(parameter); + + final Set> violations; + if (value == null) { + if (parameter.isAnnotationPresent(NotNull.class)) { + throw ErrorResponses.invalidRequest("missing value for " + name); + } else { + continue; + } + } else { + violations = validator.validate(value); + } + if (violations.isEmpty()) { + continue; + } + + violationMap.put(name, violations); + hasViolation = true; + } + if (hasViolation) { + throw new BadRequestException(Response.status(Status.BAD_REQUEST).entity(new ConstraintViolationResponse(violationMap)).build()); + } + + } + + @Override + public void validateReturnValue(final HttpRequest request, + final Object object, + final Method method, + final Object returnValue, + final Class... groups) { + + LOG.debug("validateReturnValue {} {} {}", request, object, returnValue); + + if (returnValue == null) { + if (method.isAnnotationPresent(NotNull.class)) { + throw ErrorResponses.internalServerError("unexpected null result"); + } else { + return; + } + } + + final Set> errors = validator.validate(returnValue); + + if (!errors.isEmpty()) { + throw new InternalServerErrorException("result object validation failed", new ConstraintViolationException(errors)); + } + + } + +} diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/beans/ValidationProvider.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/beans/ValidationProvider.java new file mode 100644 index 000000000..359842b62 --- /dev/null +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/beans/ValidationProvider.java @@ -0,0 +1,17 @@ +package net.trajano.ms.vertx.beans; + +import javax.validation.Validation; +import javax.validation.Validator; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ValidationProvider { + + @Bean + public Validator beanValidationValidator() { + + return Validation.buildDefaultValidatorFactory().getValidator(); + } +} diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/AbstractJsonExceptionMapper.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/AbstractJsonExceptionMapper.java new file mode 100644 index 000000000..14c8afc5e --- /dev/null +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/AbstractJsonExceptionMapper.java @@ -0,0 +1,91 @@ +package net.trajano.ms.vertx.jaxrs; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.annotation.PostConstruct; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; +import javax.ws.rs.ext.ExceptionMapper; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; + +import net.trajano.ms.Microservice; + +public abstract class AbstractJsonExceptionMapper implements + ExceptionMapper { + + private static final Logger LOG = LoggerFactory.getLogger(Microservice.class); + + /** + * Supported media types. + */ + private static final Set SUPPORTED_MEDIA_TYPES = new HashSet<>(Arrays.asList( + MediaType.APPLICATION_JSON_TYPE, + MediaType.APPLICATION_XML_TYPE, + MediaType.TEXT_XML_TYPE, + MediaType.TEXT_PLAIN_TYPE, + MediaType.TEXT_HTML_TYPE)); + + @Context + private HttpHeaders headers; + + @Value("${microservice.show_stack_trace:#{null}}") + protected Boolean showStackTrace; + + @Context + protected UriInfo uriInfo; + + /** + * Determines the appropriate media type based on what is requested. If wildcard + * use JSON. + * + * @return media type appropriate for request + */ + protected MediaType getAppropriateMediaType() { + + final List acceptableMediaTypes = headers.getAcceptableMediaTypes(); + for (final MediaType mediaType : acceptableMediaTypes) { + if (mediaType.equals(MediaType.WILDCARD_TYPE)) { + return MediaType.APPLICATION_JSON_TYPE; + } else if (SUPPORTED_MEDIA_TYPES.contains(mediaType)) { + return mediaType; + } + } + return MediaType.APPLICATION_JSON_TYPE; + + } + + /** + * This sets the context data so the mapper can be unit tested. + */ + public void setContextData(final HttpHeaders headers, + final UriInfo uriInfo, + final boolean showStackTrace) { + + this.headers = headers; + this.uriInfo = uriInfo; + this.showStackTrace = showStackTrace; + + } + + /** + * If the show request URI or show stack trace are not defined, it will default + * to whether the current logger is on debug mode or not. + */ + @PostConstruct + public void setDebugFlags() { + + if (showStackTrace == null) { + showStackTrace = LOG.isDebugEnabled(); + LOG.debug("stack trace enabled if this is shown"); + } + } + +} diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/ConstraintViolationExceptionMapper.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/ConstraintViolationExceptionMapper.java new file mode 100644 index 000000000..e99ffc518 --- /dev/null +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/ConstraintViolationExceptionMapper.java @@ -0,0 +1,58 @@ +package net.trajano.ms.vertx.jaxrs; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.Provider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import net.trajano.ms.Microservice; +import net.trajano.ms.core.ConstraintViolationResponse; + +@Configuration +@Component +@Provider +@Produces({ + MediaType.APPLICATION_JSON, + MediaType.APPLICATION_XML, + MediaType.TEXT_XML, + MediaType.TEXT_PLAIN +}) +public class ConstraintViolationExceptionMapper extends AbstractJsonExceptionMapper { + + private static final Logger LOG = LoggerFactory.getLogger(Microservice.class); + + @Override + public Response toResponse(final ConstraintViolationException exception) { + + if (LOG.isDebugEnabled()) { + LOG.warn("uri={} message={}", uriInfo.getRequestUri(), exception.getMessage(), exception); + for (final ConstraintViolation violation : exception.getConstraintViolations()) { + LOG.debug("message={} obj={} path={}", violation.getMessage(), violation.getRootBean(), violation.getPropertyPath()); + } + } else { + LOG.warn("uri={} message={}", uriInfo.getRequestUri(), exception.getMessage()); + } + final MediaType mediaType = getAppropriateMediaType(); + + if (mediaType.isCompatible(MediaType.TEXT_PLAIN_TYPE) || mediaType.isCompatible(MediaType.TEXT_HTML_TYPE)) { + return Response.status(Status.BAD_REQUEST) + .entity(exception.getMessage()) + .type(mediaType) + .build(); + } else { + return Response.status(Status.BAD_REQUEST) + .entity(new ConstraintViolationResponse(exception, uriInfo, showStackTrace)) + .type(mediaType) + .build(); + } + } + +} diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/GeneralValidatorContextResolver.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/GeneralValidatorContextResolver.java new file mode 100644 index 000000000..f1148aa1a --- /dev/null +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/GeneralValidatorContextResolver.java @@ -0,0 +1,28 @@ +package net.trajano.ms.vertx.jaxrs; + +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import org.jboss.resteasy.spi.validation.GeneralValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Provider +@Produces(MediaType.WILDCARD) +public class GeneralValidatorContextResolver implements + ContextResolver { + + @Autowired + private GeneralValidator generalValidator; + + @Override + public GeneralValidator getContext(final Class type) { + + return generalValidator; + + } + +} diff --git a/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/JsonExceptionMapper.java b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/JsonExceptionMapper.java index 604246f48..bca699ba6 100644 --- a/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/JsonExceptionMapper.java +++ b/ms-common-impl/src/main/java/net/trajano/ms/vertx/jaxrs/JsonExceptionMapper.java @@ -1,30 +1,20 @@ package net.trajano.ms.vertx.jaxrs; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.annotation.PostConstruct; import javax.ws.rs.ClientErrorException; import javax.ws.rs.NotFoundException; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriInfo; -import javax.ws.rs.ext.ExceptionMapper; import javax.ws.rs.ext.Provider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.stereotype.Component; +import net.trajano.ms.Microservice; import net.trajano.ms.core.ErrorResponse; @Configuration @@ -36,49 +26,9 @@ MediaType.TEXT_XML, MediaType.TEXT_PLAIN }) -public class JsonExceptionMapper implements - ExceptionMapper { - - private static final Logger LOG = LoggerFactory.getLogger(JsonExceptionMapper.class); - - /** - * Supported media types. - */ - private static final Set SUPPORTED_MEDIA_TYPES = new HashSet<>(Arrays.asList( - MediaType.APPLICATION_JSON_TYPE, - MediaType.APPLICATION_XML_TYPE, - MediaType.TEXT_XML_TYPE, - MediaType.TEXT_PLAIN_TYPE, - MediaType.TEXT_HTML_TYPE)); - - @Context - private HttpHeaders headers; - - @Value("${microservice.show_stack_trace:#{null}}") - private Boolean showStackTrace; - - @Context - private UriInfo uriInfo; - - /** - * Determines the appropriate media type based on what is requested. If wildcard - * use JSON. - * - * @return media type appropriate for request - */ - private MediaType getAppropriateMediaType() { - - final List acceptableMediaTypes = headers.getAcceptableMediaTypes(); - for (final MediaType mediaType : acceptableMediaTypes) { - if (mediaType.equals(MediaType.WILDCARD_TYPE)) { - return MediaType.APPLICATION_JSON_TYPE; - } else if (SUPPORTED_MEDIA_TYPES.contains(mediaType)) { - return mediaType; - } - } - return MediaType.APPLICATION_JSON_TYPE; +public class JsonExceptionMapper extends AbstractJsonExceptionMapper { - } + private static final Logger LOG = LoggerFactory.getLogger(Microservice.class); /** * Log the exception if it is not NotFoundException and only use warn if it is a @@ -97,32 +47,6 @@ private void log(final Throwable exception) { } } - /** - * This sets the context data so the mapper can be unit tested. - */ - public void setContextData(final HttpHeaders headers, - final UriInfo uriInfo, - final boolean showStackTrace) { - - this.headers = headers; - this.uriInfo = uriInfo; - this.showStackTrace = showStackTrace; - - } - - /** - * If the show request URI or show stack trace are not defined, it will default - * to whether the current logger is on debug mode or not. - */ - @PostConstruct - public void setDebugFlags() { - - if (showStackTrace == null) { - showStackTrace = LOG.isDebugEnabled(); - LOG.debug("stack trace enabled if this is shown"); - } - } - @Override public Response toResponse(final Throwable exception) { diff --git a/ms-common-impl/src/test/java/net/trajano/ms/vertx/test/SpringJaxrsHandlerTest.java b/ms-common-impl/src/test/java/net/trajano/ms/vertx/test/SpringJaxrsHandlerTest.java index 0bcbe4cfc..f70e59d69 100644 --- a/ms-common-impl/src/test/java/net/trajano/ms/vertx/test/SpringJaxrsHandlerTest.java +++ b/ms-common-impl/src/test/java/net/trajano/ms/vertx/test/SpringJaxrsHandlerTest.java @@ -65,7 +65,7 @@ public void testAsync() { assertNotNull(engine); final Response response = ClientBuilder.newClient().target(baseUri).path("/api/hello/async").request().get(); assertEquals(200, response.getStatus()); - assertTrue(response.readEntity(String.class).startsWith("{")); + assertTrue(response.readEntity(String.class).contains("accounts.google.com")); } diff --git a/ms-common/pom.xml b/ms-common/pom.xml index 4171ac83b..f9bdc09c9 100644 --- a/ms-common/pom.xml +++ b/ms-common/pom.xml @@ -9,7 +9,7 @@ ms-common Microservice Common - 3.5.1.Final + 4.0.0.Beta3 @@ -39,17 +39,20 @@ javax.annotation javax.annotation-api - 1.2 javax.enterprise cdi-api 1.2 + + javax.validation + validation-api + javax.ws.rs javax.ws.rs-api - 2.0.1 + 2.1 org.bitbucket.b_c @@ -121,11 +124,6 @@ jcl-over-slf4j test - - org.slf4j - slf4j-jdk14 - test - org.springframework.boot spring-boot-starter-test @@ -152,7 +150,7 @@ false - + diff --git a/ms-common/src/main/java/net/trajano/ms/Microservice.java b/ms-common/src/main/java/net/trajano/ms/Microservice.java index 9b21e1421..2bf3363dc 100644 --- a/ms-common/src/main/java/net/trajano/ms/Microservice.java +++ b/ms-common/src/main/java/net/trajano/ms/Microservice.java @@ -9,6 +9,7 @@ import org.springframework.boot.Banner.Mode; import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; import net.trajano.ms.spi.MicroserviceEngine; @@ -79,14 +80,14 @@ public static void run(final Class applicationClass, } Microservice.applicationClass = applicationClass; - final Object[] bootstrapObjects = microserviceEngine.bootstrap(); - final Object[] sources = new Object[extraSources.length + bootstrapObjects.length]; + final Class[] bootstrapObjects = microserviceEngine.bootstrap(); + final Class[] sources = new Class[extraSources.length + bootstrapObjects.length]; System.arraycopy(extraSources, 0, sources, 0, extraSources.length); System.arraycopy(bootstrapObjects, 0, sources, extraSources.length, bootstrapObjects.length); final SpringApplication springApplication = new SpringApplication(sources); - springApplication.setWebEnvironment(false); + springApplication.setWebApplicationType(WebApplicationType.NONE); springApplication .setBannerMode(Mode.OFF); springApplication.run(args); diff --git a/ms-common/src/main/java/net/trajano/ms/core/ConstraintViolationResponse.java b/ms-common/src/main/java/net/trajano/ms/core/ConstraintViolationResponse.java new file mode 100644 index 000000000..b38ee9bee --- /dev/null +++ b/ms-common/src/main/java/net/trajano/ms/core/ConstraintViolationResponse.java @@ -0,0 +1,130 @@ +package net.trajano.ms.core; + +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import javax.ws.rs.core.UriInfo; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +import io.swagger.annotations.ApiModelProperty; + +/** + * Contraint violation response. This is the error response that is sent when + * there is a Bean Validation error on the input data. + * + * @author Archimedes Trajano + */ +@XmlRootElement +@XmlType(propOrder = { + "error", + "errorDescription", + "errorClass", + "violations", + "requestId", + "requestUri", + "threadId", + "host", + "jwtId", + "stackTrace", + "cause" +}) +@JsonInclude(Include.NON_NULL) +public class ConstraintViolationResponse extends ErrorResponse { + + @XmlAccessorType(XmlAccessType.PROPERTY) + @XmlType(propOrder = { + "message", + "messageTemplate", + "path" + }) + @SuppressWarnings("unused") + private static class ConstraintViolationElement { + + /** + * Prefix added to the path. + */ + private final String pathPrefix; + + /** + * The wrapped violation. + */ + private final ConstraintViolation violation; + + ConstraintViolationElement(final ConstraintViolation violation) { + + this.violation = violation; + pathPrefix = ""; + } + + ConstraintViolationElement(final ConstraintViolation violation, + final String pathPrefix) { + + this.violation = violation; + this.pathPrefix = pathPrefix + "."; + } + + public String getMessage() { + + return violation.getMessage(); + } + + public String getPath() { + + return pathPrefix + violation.getPropertyPath().toString(); + } + + public String getTemplate() { + + return violation.getMessageTemplate(); + } + } + + /** + * The request ID. This is obtained from the header. + */ + @ApiModelProperty(name = "violations", + value = "Constraint violations.") + @XmlElement(name = "violations") + private final List violations; + + /** + * Wraps a {@link Throwable} in an {@link ErrorResponse} with full stack trace + * and cause if requested. + * + * @param e + * constraint violation exception to wrap + * @param uriInfo + * URI info + * @param showStackTrace + * flag to determine whether the stack trace is to be shown. + */ + public ConstraintViolationResponse(final ConstraintViolationException e, + final UriInfo uriInfo, + final boolean showStackTrace) { + + super(e, uriInfo, showStackTrace); + violations = StreamSupport.stream(e.getConstraintViolations().spliterator(), false).map(v -> new ConstraintViolationElement(v)).collect(Collectors.toList()); + + } + + public ConstraintViolationResponse(final Map>> violationMap) { + + super(ErrorCodes.INVALID_REQUEST, "constraints violated"); + violations = new LinkedList<>(); + violationMap.forEach((key, + violationSet) -> violationSet.forEach(v -> violations.add(new ConstraintViolationElement(v, key)))); + } +} diff --git a/ms-common/src/main/java/net/trajano/ms/spi/MicroserviceEngine.java b/ms-common/src/main/java/net/trajano/ms/spi/MicroserviceEngine.java index e9680133e..d099d06f0 100644 --- a/ms-common/src/main/java/net/trajano/ms/spi/MicroserviceEngine.java +++ b/ms-common/src/main/java/net/trajano/ms/spi/MicroserviceEngine.java @@ -4,11 +4,11 @@ public interface MicroserviceEngine { /** * Performs the initialization of the microservice engine and returns an array - * of objects that would be used to bootstrap Spring. + * of classes that would be used to bootstrap Spring. * - * @return starting context objects. + * @return starting context classes. */ - Object[] bootstrap(); + Class[] bootstrap(); /** * Gets the host name of where the engine is running. This may be diff --git a/ms-common/src/main/javadoc/overview.html b/ms-common/src/main/javadoc/overview.html index 8706254d6..aa5f32960 100644 --- a/ms-common/src/main/javadoc/overview.html +++ b/ms-common/src/main/javadoc/overview.html @@ -15,6 +15,7 @@
  • JAX-RS
  • CDI 1.2
  • +
  • Bean Validation 2.0
  • Annotations 1.2 (for @PermitAll and @RolesRequired annotations)
  • diff --git a/ms-common/src/test/java/net/trajano/ms/common/test/BootstrapTest.java b/ms-common/src/test/java/net/trajano/ms/common/test/BootstrapTest.java index 72a83a8e4..97bc8a23a 100644 --- a/ms-common/src/test/java/net/trajano/ms/common/test/BootstrapTest.java +++ b/ms-common/src/test/java/net/trajano/ms/common/test/BootstrapTest.java @@ -37,7 +37,7 @@ public void setEngines(final MicroserviceEngine... engines) { public void bootstrapTest() throws Exception { final MicroserviceEngine microserviceEngine = Mockito.mock(MicroserviceEngine.class); - Mockito.when(microserviceEngine.bootstrap()).thenReturn(new Object[] { + Mockito.when(microserviceEngine.bootstrap()).thenReturn(new Class[] { MyApp.class }); new TestMicroservice().setEngines(microserviceEngine); @@ -49,7 +49,7 @@ public void bootstrapTest() throws Exception { public void doubleRunTest() throws Exception { final MicroserviceEngine microserviceEngine = Mockito.mock(MicroserviceEngine.class); - Mockito.when(microserviceEngine.bootstrap()).thenReturn(new Object[] { + Mockito.when(microserviceEngine.bootstrap()).thenReturn(new Class[] { MyApp.class }); new TestMicroservice().setApplicationAndEngines(MyApp.class, microserviceEngine); diff --git a/ms-engine-spring-jaxrs/pom.xml b/ms-engine-spring-jaxrs/pom.xml index 46b530027..a78ad0bed 100644 --- a/ms-engine-spring-jaxrs/pom.xml +++ b/ms-engine-spring-jaxrs/pom.xml @@ -37,7 +37,7 @@ javax.ws.rs javax.ws.rs-api - 2.0.1 + 2.1 org.slf4j @@ -56,7 +56,7 @@ org.jboss.resteasy resteasy-jaxrs - 3.1.4.Final + 4.0.0.Beta2 test diff --git a/ms-engine/pom.xml b/ms-engine/pom.xml index d339f6b96..788d54e69 100644 --- a/ms-engine/pom.xml +++ b/ms-engine/pom.xml @@ -11,7 +11,7 @@ This is an implementation of a RESTful microservice engine 2.8.10 - 3.1.4.Final + 4.0.0.Beta2 1.7.16 @@ -79,7 +79,6 @@ javax.annotation javax.annotation-api - 1.2 javax.enterprise @@ -89,7 +88,7 @@ javax.ws.rs javax.ws.rs-api - 2.0.1 + 2.1 org.jboss.resteasy @@ -134,6 +133,10 @@ org.jboss.spec.javax.ws.rs jboss-jaxrs-api_2.0_spec + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + @@ -153,6 +156,10 @@ org.jboss.spec.javax.ws.rs jboss-jaxrs-api_2.0_spec + + org.jboss.spec.javax.ws.rs + jboss-jaxrs-api_2.1_spec + @@ -185,6 +192,12 @@ + + io.vertx + vertx-codegen + ${vertx.version} + test + io.vertx vertx-unit diff --git a/ms-engine/src/main/java/net/trajano/ms/engine/SpringJaxRsHandler.java b/ms-engine/src/main/java/net/trajano/ms/engine/SpringJaxRsHandler.java index b3480b37d..588d4234f 100644 --- a/ms-engine/src/main/java/net/trajano/ms/engine/SpringJaxRsHandler.java +++ b/ms-engine/src/main/java/net/trajano/ms/engine/SpringJaxRsHandler.java @@ -149,7 +149,6 @@ public SpringJaxRsHandler(final ConfigurableApplicationContext baseApplicationCo applicationContext.addBeanFactoryPostProcessor(springBeanProcessor); applicationContext.addApplicationListener(springBeanProcessor); - applicationContext.refresh(); if (baseApplicationContext != null) { baseApplicationContext.getBeansWithAnnotation(Provider.class).forEach( (name, @@ -160,6 +159,7 @@ public SpringJaxRsHandler(final ConfigurableApplicationContext baseApplicationCo } }); } + applicationContext.refresh(); applicationContext.getBeansWithAnnotation(Path.class).forEach((name, obj) -> pathAnnotatedClasses.add(ClassUtils.getUserClass(obj))); @@ -197,7 +197,6 @@ public void handle(final RoutingContext context) { final ResteasyUriInfo uriInfo = new ResteasyUriInfo(serverRequest.absoluteURI(), serverRequest.query(), baseUri.toASCIIString()); final VertxHttpRequest request = new VertxHttpRequest(context, uriInfo, providerFactory); - context.request().setExpectMultipart(isMultipartExpected(request)); try (final VertxHttpResponse response = new VertxHttpResponse(context)) { context.vertx().executeBlocking( diff --git a/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxAsynchronousResponse.java b/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxAsynchronousResponse.java index 4d7c792cc..84e0fb1b6 100644 --- a/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxAsynchronousResponse.java +++ b/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxAsynchronousResponse.java @@ -3,8 +3,8 @@ import java.io.IOException; import java.lang.annotation.Annotation; import java.util.Collection; -import java.util.Collections; import java.util.Date; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -132,6 +132,25 @@ private void cancelTimer() { } } + @Override + public void complete() { + + if (done.get()) { + return; + } + if (cancelled.get()) { + return; + } + done.compareAndSet(false, true); + + } + + @Override + public void completionCallbacks(final Throwable throwable) { + + completionCallbacks.forEach(callback -> callback.onComplete(throwable)); + } + @Override public Annotation[] getAnnotations() { @@ -190,6 +209,9 @@ public boolean isDone() { return done.get(); } + /** + * {@inheritDoc} + */ @Override public boolean isSuspended() { @@ -197,49 +219,58 @@ public boolean isSuspended() { } /** - * Not used. {@inheritDoc} - * - * @return empty set. + * {@inheritDoc} */ @Override - public Collection> register(final Class callback) { + public Collection> register(final Class callbackClass) { - return Collections.emptySet(); + final Object cb = providerFactory.createProviderInstance(callbackClass); + return register(cb); } /** - * Not used. {@inheritDoc} - * - * @return empty map. + * {@inheritDoc} */ @Override public Map, Collection>> register(final Class callback, final Class... callbacks) { - return Collections.emptyMap(); + final Map, Collection>> map = new HashMap<>(); + map.put(callback, register(callback)); + for (final Class other : callbacks) { + map.put(other, register(other)); + } + return map; } /** - * Not used. {@inheritDoc} - * - * @return empty set. + * {@inheritDoc} */ @Override public Collection> register(final Object callback) { - return Collections.emptySet(); + final List> registered = new LinkedList<>(); + if (callback instanceof CompletionCallback) { + completionCallbacks.add((CompletionCallback) callback); + registered.add(CompletionCallback.class); + } + return registered; } /** - * Not used. {@inheritDoc} - * - * @return empty map. + * {@inheritDoc} */ @Override public Map, Collection>> register(final Object callback, final Object... callbacks) { - return Collections.emptyMap(); + final Map, Collection>> map = new HashMap<>(); + map.put(callback.getClass(), register(callback)); + for (final Object other : callbacks) { + map.put(other.getClass(), register(other)); + } + return map; + } /** @@ -287,7 +318,6 @@ private boolean sendData(final Object entity) { done.set(true); cancelTimer(); - completionCallbacks.forEach(callback -> callback.onComplete(null)); return true; } catch (final IOException e) { throw new InternalServerErrorException(e); @@ -373,7 +403,7 @@ public void setWriterInterceptors(final WriterInterceptor[] writerInterceptors) */ private void writeResponse(final Response response) throws IOException { - ServerResponseWriter.writeNomapResponse((BuiltResponse) response, request, new VertxHttpResponse(routingContext), providerFactory); + ServerResponseWriter.writeNomapResponse((BuiltResponse) response, request, new VertxHttpResponse(routingContext), providerFactory, throwable -> completionCallbacks.forEach(c -> c.onComplete(throwable)), true); } } diff --git a/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxHttpResponse.java b/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxHttpResponse.java index ee22ff60f..70b12b204 100644 --- a/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxHttpResponse.java +++ b/ms-engine/src/main/java/net/trajano/ms/engine/internal/resteasy/VertxHttpResponse.java @@ -40,6 +40,13 @@ public void addNewCookie(final NewCookie cookie) { } + @Override + public void flushBuffer() throws IOException { + + os.flush(); + + } + /** * {@inheritDoc} */ diff --git a/ms-engine/src/test/java/net/trajano/ms/engine/test/SpringJaxRsHandlerTest.java b/ms-engine/src/test/java/net/trajano/ms/engine/test/SpringJaxRsHandlerTest.java index d9d867faa..8693f3801 100644 --- a/ms-engine/src/test/java/net/trajano/ms/engine/test/SpringJaxRsHandlerTest.java +++ b/ms-engine/src/test/java/net/trajano/ms/engine/test/SpringJaxRsHandlerTest.java @@ -1,8 +1,8 @@ package net.trajano.ms.engine.test; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -18,7 +18,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.Matchers; +import org.mockito.ArgumentMatchers; import org.mockito.Mockito; import io.netty.util.AsciiString; @@ -80,7 +80,7 @@ public void test400(final TestContext testContext) throws Exception { when(response.headers()).thenReturn(new VertxHttpHeaders()); final Async async = testContext.async(); - when(response.setStatusCode(Matchers.any(Integer.class))).then(invocation -> { + when(response.setStatusCode(ArgumentMatchers.any(Integer.class))).then(invocation -> { try { return response; @@ -152,7 +152,7 @@ public void test404Internal(final TestContext testContext) throws Exception { when(response.headers()).thenReturn(new VertxHttpHeaders()); final Async async = testContext.async(); - when(response.setStatusCode(Matchers.any(Integer.class))).then(invocation -> { + when(response.setStatusCode(ArgumentMatchers.any(Integer.class))).then(invocation -> { try { return response; @@ -189,7 +189,7 @@ public void test500(final TestContext testContext) throws Exception { when(response.headers()).thenReturn(new VertxHttpHeaders()); final Async async = testContext.async(); - when(response.write(Matchers.any(Buffer.class))).thenReturn(response); + when(response.write(ArgumentMatchers.any(Buffer.class))).thenReturn(response); doAnswer(i -> { async.complete(); @@ -224,7 +224,7 @@ public void testFailure(final TestContext testContext) throws Exception { when(response.headers()).thenReturn(new VertxHttpHeaders()); final Async async = testContext.async(); - when(response.setStatusCode(Matchers.any(Integer.class))).then(invocation -> { + when(response.setStatusCode(ArgumentMatchers.any(Integer.class))).then(invocation -> { try { return response; @@ -261,7 +261,7 @@ public void testHandler(final TestContext testContext) throws Exception { when(response.headers()).thenReturn(new VertxHttpHeaders()); final Async async = testContext.async(); - when(response.write(Matchers.any(Buffer.class))).then(invocation -> { + when(response.write(ArgumentMatchers.any(Buffer.class))).then(invocation -> { try { return response; diff --git a/ms-gateway/pom.xml b/ms-gateway/pom.xml index 222ea0c8d..c1720c864 100644 --- a/ms-gateway/pom.xml +++ b/ms-gateway/pom.xml @@ -76,6 +76,12 @@ jcl-over-slf4j runtime + + io.vertx + vertx-codegen + ${vertx.version} + test + net.trajano.commons commons-testing diff --git a/ms-gateway/src/main/java/net/trajano/ms/gateway/GatewayMS.java b/ms-gateway/src/main/java/net/trajano/ms/gateway/GatewayMS.java index b28f3526e..e50b8c254 100644 --- a/ms-gateway/src/main/java/net/trajano/ms/gateway/GatewayMS.java +++ b/ms-gateway/src/main/java/net/trajano/ms/gateway/GatewayMS.java @@ -7,6 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.Banner.Mode; import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -28,7 +29,7 @@ public static void main(final String[] args) { final SpringApplication application = new SpringApplication(GatewayMS.class); application.setBannerMode(Mode.OFF); - application.setWebEnvironment(false); + application.setWebApplicationType(WebApplicationType.NONE); application.run(args); } diff --git a/ms-gateway/src/main/java/net/trajano/ms/gateway/handlers/GlobalFailureHandler.java b/ms-gateway/src/main/java/net/trajano/ms/gateway/handlers/GlobalFailureHandler.java index b0157f640..615d6eb0a 100644 --- a/ms-gateway/src/main/java/net/trajano/ms/gateway/handlers/GlobalFailureHandler.java +++ b/ms-gateway/src/main/java/net/trajano/ms/gateway/handlers/GlobalFailureHandler.java @@ -4,9 +4,14 @@ import java.net.ConnectException; import java.net.UnknownHostException; +import java.util.Locale; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.MessageSourceResolvable; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @@ -26,14 +31,17 @@ @Order(SelfRegisteringRoutingContextHandler.CORE_GLOBAL) public class GlobalFailureHandler extends SelfRegisteringRoutingContextHandler { - private static final String GATEWAY_ERROR = "Gateway Error"; + private static final MessageSourceResolvable GATEWAY_ERROR = new DefaultMessageSourceResolvable("err.gateway_error"); - private static final String GATEWAY_TIMEOUT = "Gateway Timeout"; + private static final MessageSourceResolvable GATEWAY_TIMEOUT = new DefaultMessageSourceResolvable("err.gateway_timeout"); - private static final String INTERNAL_SERVER_ERROR = "Internal Server Error"; + private static final MessageSourceResolvable INTERNAL_SERVER_ERROR = new DefaultMessageSourceResolvable("err.internal_server_error"); private static final Logger LOG = LoggerFactory.getLogger(GlobalFailureHandler.class); + @Autowired + private MessageSource r; + @Override public void handle(final RoutingContext context) { @@ -43,19 +51,19 @@ public void handle(final RoutingContext context) { if (!context.response().ended()) { if (context.failure() instanceof ConnectException) { context.response().setStatusCode(504) - .setStatusMessage(GATEWAY_TIMEOUT) + .setStatusMessage(r.getMessage(GATEWAY_TIMEOUT, Locale.getDefault())) .putHeader(CONTENT_TYPE, MediaTypes.APPLICATION_JSON) - .end(Errors.serverError(GATEWAY_TIMEOUT).toBuffer()); + .end(Errors.serverError(r.getMessage(GATEWAY_TIMEOUT, Locale.getDefault())).toBuffer()); } else if (context.failure() instanceof UnknownHostException) { context.response().setStatusCode(503) - .setStatusMessage(GATEWAY_ERROR) + .setStatusMessage(r.getMessage(GATEWAY_ERROR, Locale.getDefault())) .putHeader(CONTENT_TYPE, MediaTypes.APPLICATION_JSON) - .end(Errors.serverError(GATEWAY_ERROR).toBuffer()); + .end(Errors.serverError(r.getMessage(GATEWAY_ERROR, Locale.getDefault())).toBuffer()); } else { context.response().setStatusCode(context.statusCode() == -1 ? 500 : context.statusCode()) - .setStatusMessage(INTERNAL_SERVER_ERROR) + .setStatusMessage(r.getMessage(INTERNAL_SERVER_ERROR, Locale.getDefault())) .putHeader(HttpHeaders.CONTENT_TYPE, MediaTypes.APPLICATION_JSON) - .end(Errors.serverError(INTERNAL_SERVER_ERROR).toBuffer()); + .end(Errors.serverError(r.getMessage(INTERNAL_SERVER_ERROR, Locale.getDefault())).toBuffer()); } } diff --git a/ms-gateway/src/main/resources/messages.properties b/ms-gateway/src/main/resources/messages.properties new file mode 100644 index 000000000..8c7bb400f --- /dev/null +++ b/ms-gateway/src/main/resources/messages.properties @@ -0,0 +1,3 @@ +err.gateway_error=Gateway Error +err.gateway_timeout=Gateway Timeout +err.internal_server_error=Internal Server Error diff --git a/ms-swagger/src/main/java/net/trajano/ms/swagger/internal/SwaggerCollator.java b/ms-swagger/src/main/java/net/trajano/ms/swagger/internal/SwaggerCollator.java index f8c5d0104..38edb7e1b 100644 --- a/ms-swagger/src/main/java/net/trajano/ms/swagger/internal/SwaggerCollator.java +++ b/ms-swagger/src/main/java/net/trajano/ms/swagger/internal/SwaggerCollator.java @@ -142,15 +142,15 @@ private void processUris(final Map swagger, int j = 0; while (env.containsProperty(String.format("swagger[%d].uris[%d].swagger", i, j))) { + final URL swaggerUrl = env.getProperty(String.format("swagger[%d].uris[%d].swagger", i, j), URL.class); try { - final URL swaggerUrl = env.getProperty(String.format("swagger[%d].uris[%d].swagger", i, j), URL.class); final Swagger remoteSwagger = io.swagger.util.Json.mapper().readerFor(Swagger.class).readValue(swaggerUrl.openConnection().getInputStream()); processPaths(swagger, definitionsMap, securityDefinitionsMap, remoteSwagger, i, j); ++j; } catch (final IOException e) { - throw new UncheckedIOException(e); + throw new UncheckedIOException("IOException processing " + swaggerUrl, e); } } @@ -159,7 +159,7 @@ private void processUris(final Map swagger, /** * Update the current definition maps. - * + * * @param currentDefinitionsMap * current definitions map * @param currentSecurityDefinitionsMap diff --git a/pom.xml b/pom.xml index e95b717b3..9b9672731 100644 --- a/pom.xml +++ b/pom.xml @@ -61,7 +61,7 @@ true app-ms 1.7.16 - 1.5.10.RELEASE + 2.0.2.RELEASE 3.5.1 @@ -134,7 +134,7 @@ org.bitbucket.b_c jose4j - 0.6.1 + 0.6.3 org.springframework.boot diff --git a/sample-authz/clients-docker.json b/sample-authz/clients-docker.json index 1a540a593..dc9a66c71 100644 --- a/sample-authz/clients-docker.json +++ b/sample-authz/clients-docker.json @@ -15,7 +15,7 @@ { "client_id": "oidc_id", "client_secret": "app_secret", - "grant_types": ["refresh_token", "revocation", "urn:ietf:params:oauth:grant-type:jwt-bearer"], + "grant_types": ["openid", "refresh_token", "revocation", "urn:ietf:params:oauth:grant-type:jwt-bearer"], "jwks_uri": "http://oidc-ms/.well-known/jwks", "origin": "http://localhost:3000", "redirect_uri": "http://localhost:3000/cb" diff --git a/sample-ms/src/main/java/net/trajano/ms/example/HelloResource.java b/sample-ms/src/main/java/net/trajano/ms/example/HelloResource.java index 3218e12ef..f7358d7f7 100644 --- a/sample-ms/src/main/java/net/trajano/ms/example/HelloResource.java +++ b/sample-ms/src/main/java/net/trajano/ms/example/HelloResource.java @@ -33,6 +33,7 @@ import org.jboss.resteasy.plugins.providers.multipart.InputPart; import org.jboss.resteasy.plugins.providers.multipart.MultipartFormDataInput; import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; import com.google.gson.JsonElement; import com.google.gson.JsonObject; @@ -43,6 +44,7 @@ import io.swagger.annotations.Info; import io.swagger.annotations.SwaggerDefinition; import net.trajano.ms.core.JsonOps; +import net.trajano.ms.example.beans.ContactCard; import net.trajano.ms.example.domain.MyType; @SwaggerDefinition( @@ -72,6 +74,7 @@ public class HelloResource { public void async(@Suspended final AsyncResponse asyncResponse) throws InterruptedException, ExecutionException { + asyncResponse.register(new TestCallback()); final Future futureResponseFromClient = jaxrsClient.target("https://accounts.google.com/.well-known/openid-configuration").request().header(javax.ws.rs.core.HttpHeaders.USER_AGENT, "curl/7.55.1").async().get(); final Response responseFromClient = futureResponseFromClient.get(); @@ -92,6 +95,16 @@ public Response badClient() { throw new BadRequestException("who's bad", Response.status(Status.BAD_REQUEST).entity(entity).build()); } + @ApiOperation(value = "Echos a validated JSON") + @POST + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Validated + public ContactCard beanValidation(final ContactCard card) { + + return card; + } + @ApiOperation(value = "throws a Runtime Exception") @GET @Path("/cough") diff --git a/sample-ms/src/main/java/net/trajano/ms/example/TestCallback.java b/sample-ms/src/main/java/net/trajano/ms/example/TestCallback.java new file mode 100644 index 000000000..db1774466 --- /dev/null +++ b/sample-ms/src/main/java/net/trajano/ms/example/TestCallback.java @@ -0,0 +1,22 @@ +package net.trajano.ms.example; + +import javax.ws.rs.container.CompletionCallback; +import javax.ws.rs.ext.Provider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Provider +public class TestCallback implements + CompletionCallback { + + private static final Logger LOG = LoggerFactory.getLogger(TestCallback.class); + + @Override + public void onComplete(final Throwable throwable) { + + LOG.debug("Complete throwable={}", throwable, throwable); + + } + +} diff --git a/sample-ms/src/main/java/net/trajano/ms/example/beans/ContactCard.java b/sample-ms/src/main/java/net/trajano/ms/example/beans/ContactCard.java new file mode 100644 index 000000000..7ea56b723 --- /dev/null +++ b/sample-ms/src/main/java/net/trajano/ms/example/beans/ContactCard.java @@ -0,0 +1,63 @@ +package net.trajano.ms.example.beans; + +import java.util.ArrayList; +import java.util.List; + +import javax.validation.Valid; +import javax.validation.constraints.DecimalMin; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +/** + * Example with BeanValidation. + */ +@XmlRootElement +public class ContactCard { + + @XmlElement(name = "children") + @Valid + private final List children = new ArrayList<>(); + + private String fullName; + + @DecimalMin(value = "1") + @NotNull + private Long id; + + private String phone; + + @NotNull(message = "{contact.wrong.name}") + public String getFullName() { + + return fullName; + } + + public Long getId() { + + return id; + } + + @Pattern(message = "{contact.wrong.phone}", + regexp = "[0-9]{3,9}") + public String getPhone() { + + return phone; + } + + public void setFullName(final String fullName) { + + this.fullName = fullName; + } + + public void setId(final Long id) { + + this.id = id; + } + + public void setPhone(final String phone) { + + this.phone = phone; + } +} diff --git a/sample-oidc/application.yml b/sample-oidc/application.yml index 0833b85ac..1d50fbbca 100644 --- a/sample-oidc/application.yml +++ b/sample-oidc/application.yml @@ -28,7 +28,7 @@ http: issuer: http://oidc-ms oidc: - redirect_uri: https://localhost:3001/oidc/cb + redirect_uri: http://localhost:3001/oidc/cb config_file: openidconnect-config.json authorizationEndpoint: http://authorization-ms diff --git a/src/test/artillery/load-test.yml b/src/test/artillery/load-test.yml index 923af9f2f..b8a9f1926 100644 --- a/src/test/artillery/load-test.yml +++ b/src/test/artillery/load-test.yml @@ -10,8 +10,8 @@ config: docker: phases: - duration: 60 - arrivalRate: 10 - target: "http://localhost:3001" + arrivalRate: 20 + target: "http://192.168.1.113:3001" variables: origin: "http://localhost:3000" prod: diff --git a/src/test/postman/Docker.postman_environment.json b/src/test/postman/Docker.postman_environment.json index f5e897813..1b4fc261d 100644 --- a/src/test/postman/Docker.postman_environment.json +++ b/src/test/postman/Docker.postman_environment.json @@ -1,34 +1,40 @@ { - "id": "08bd1cb0-4131-ec31-0d5e-dc1d44ebb0f3", + "id": "a6b8dc56-0cec-4229-d03a-c2b60014590f", "name": "Docker", "values": [ { "enabled": true, "key": "access_token", - "value": "R5Enmtic7QYQ6sRueTNekf4FAdJYpdzVgHfPfNi23Uah4xCyD8msw9VrYE5C7Q6v", - "type": "text" + "type": "text", + "value": "nJNrdlaaxwPlRGuMbmr6uJh61SJUttbxiralvn2rQxQ0Yj1HXjALf8yaVXFOSGbt" }, { "enabled": true, "key": "refresh_token", - "value": "ZgftheeUK6LlQG6zanSHS8QvDQ2GsvSYo5K3EKw1Up0Kq1J7n36ZRqUGmrnoA5fL", - "type": "text" + "type": "text", + "value": "rEbsQzhnJbHYYgyB9LNzlwTJ1eRJIZlS0JrCvJexONTgMUqSCN9LtkecKnjqYrBl" }, { "enabled": true, "key": "client_credentials", - "value": "YXBwX2lkOmFwcF9zZWNyZXQ=", - "type": "text" + "type": "text", + "value": "YXBwX2lkOmFwcF9zZWNyZXQ=" }, { "enabled": true, "key": "base", - "value": "http://localhost:3001", - "type": "text" + "type": "text", + "value": "http://localhost:3001" + }, + { + "enabled": true, + "key": "origin", + "type": "text", + "value": "http://localhost:3000" } ], - "timestamp": 1509336950561, + "timestamp": 1515373593852, "_postman_variable_scope": "environment", - "_postman_exported_at": "2017-10-30T04:16:17.739Z", - "_postman_exported_using": "Postman/5.3.2" + "_postman_exported_at": "2018-01-08T01:12:06.323Z", + "_postman_exported_using": "Postman/5.5.0" } \ No newline at end of file diff --git a/src/test/postman/Sample Test.postman_collection.json b/src/test/postman/Sample Test.postman_collection.json index 6d2e84154..6064a2c52 100644 --- a/src/test/postman/Sample Test.postman_collection.json +++ b/src/test/postman/Sample Test.postman_collection.json @@ -1,682 +1,70 @@ { - "variables": [], "info": { - "name": "Sample Test", - "_postman_id": "65e18988-8cbb-ad32-a61a-79aaf6ac1b29", - "description": "This validates the sample endpoints when they are running in a docker container.", - "schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json" + "name": "SA Mobile Enrolment", + "_postman_id": "f2d17678-17ab-813d-3243-a14e3b728d7f", + "description": "", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ { - "name": "Authenticate", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var jsonData = JSON.parse(responseBody)", - "postman.setEnvironmentVariable(\"access_token\", jsonData.access_token)", - "postman.setEnvironmentVariable(\"refresh_token\", jsonData.refresh_token)", - "", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "", - "tests[\"Response time is less than 200ms\"] = responseTime < 200;" - ] - } - } - ], + "name": "Start Enrollment Process", "request": { - "url": "{{base}}/v1/authn", "method": "POST", "header": [ { "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - }, - { - "key": "Authorization", - "value": "Basic YXBwX2lkOmFwcF9zZWNyZXQ=", - "description": "" - }, - { - "key": "Accept", - "value": "application/json", - "description": "" + "value": "application/json" } ], "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "j_username", - "value": "archie", - "description": "", - "type": "text" - }, - { - "key": "j_password", - "value": "password", - "description": "", - "type": "text" - } - ] + "mode": "raw", + "raw": "{\n \"memberId\":\"merlene\",\n \"dateOfBirth\":\"1990-01-01\",\n \"targetState\": \"http://slashdot.org/\"\n}" }, - "description": "" - }, - "response": [ - { - "id": "84f00471-1d78-45c5-9e6d-f41fbca66cd6", - "name": "Authenticate as Archie", - "originalRequest": { - "url": "{{base}}/v1/authn", - "method": "POST", - "header": [ - { - "key": "Content-Type", - "type": "text", - "name": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - }, - { - "key": "Authorization", - "type": "text", - "name": "Authorization", - "value": "Basic YXBwX2lkOmFwcF9zZWNyZXQ=", - "description": "" - }, - { - "key": "Accept", - "value": "application/json", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "j_username", - "value": "archie", - "description": "", - "type": "text" - }, - { - "key": "j_password", - "value": "password", - "description": "", - "type": "text" - } - ] - } - }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", - "_postman_previewtype": "text", - "header": [ - { - "name": "access-control-allow-origin", - "key": "access-control-allow-origin", - "value": "*", - "description": "Specifies a URI that may access the resource. For requests without credentials, the server may specify '*' as a wildcard, thereby allowing any origin to access the resource." - }, - { - "name": "content-type", - "key": "content-type", - "value": "application/json", - "description": "The mime type of this content" - }, - { - "name": "transfer-encoding", - "key": "transfer-encoding", - "value": "chunked", - "description": "The form of encoding used to safely transfer the entity to the user. Currently defined methods are: chunked, compress, deflate, gzip, identity." - }, - { - "name": "x-request-id", - "key": "x-request-id", - "value": "Kih0UqMw", - "description": "Custom header" - } + "url": { + "raw": "http://localhost:8181/authentication-ms/oidc/enroll", + "protocol": "http", + "host": [ + "localhost" ], - "cookie": [], - "responseTime": 80, - "body": "{\"access_token\":\"lbs6hs8PqpbXDAxuMOyujmY2r8CterRqdxqgWSQm9EwHeb8yx6IuKqXxUbuQ6OHM\",\"expires_in\":300,\"refresh_token\":\"lxEFXHl7wUsdSwqMy4eVrhptv2SqeRmBwkANb03Z6xO2xWgIN966W7lWgWLLp9nQ\",\"token_type\":\"Bearer\"}" - } - ] - }, - { - "name": "displays hello world", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "var jsonData = JSON.parse(responseBody);", - "tests[\"Principal is Archie\"] = jsonData.principal === \"internal-subject-archie\";" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/secure", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Refresh Token", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "var jsonData = JSON.parse(responseBody)", - "postman.setEnvironmentVariable(\"access_token\", jsonData.access_token)", - "postman.setEnvironmentVariable(\"refresh_token\", jsonData.refresh_token)", - "", - "", - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "tests[\"Content-Type is present\"] = postman.getResponseHeader(\"Content-Type\");", - "", - "tests[\"Transfer encoding is not present\"] = !postman.getResponseHeader(\"Transfer-Encoding\");" - ] - } - } - ], - "request": { - "url": "{{base}}/refresh", - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - }, - { - "key": "Authorization", - "value": "Basic {{client_credentials}}", - "description": "" - } - ], - "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "grant_type", - "value": "refresh_token", - "description": "", - "type": "text" - }, - { - "key": "refresh_token", - "value": "{{refresh_token}}", - "description": "", - "type": "text" - } - ] - }, - "description": "" - }, - "response": [] - }, - { - "name": "displays hello world after refresh", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "var jsonData = JSON.parse(responseBody);", - "tests[\"Principal is Archie\"] = jsonData.principal === \"internal-subject-archie\";" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/secure", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "displays openid config of google", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Body matches string\"] = responseBody.has(\"subject_types_supported\");", - "", - "tests[\"Response time is less than 1000ms\"] = responseTime < 1000;" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/hello/openid", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Gets a JSON using GSON", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "var jsonData = JSON.parse(responseBody);", - "tests[\"Hello world\"] = jsonData.Hello === \"world\";" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/hello/j", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Crash", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Body matches string\"] = responseBody.has(\"error\");", - "", - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "tests[\"Status code is 500\"] = responseCode.code === 500;" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/hello/cough", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Client Error", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Body matches string\"] = responseBody.has(\"error\");", - "", - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "tests[\"Status code is 400\"] = responseCode.code === 400;" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/hello/bad", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Upload files", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 500ms\"] = responseTime < 500;", - "", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/hello/upload", - "method": "POST", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": { - "mode": "formdata", - "formdata": [ - { - "key": "uploadedFile", - "description": "", - "type": "file", - "src": "Localhost.postman_environment.json" - }, - { - "key": "uploadedFile", - "description": "", - "type": "file", - "src": "Docker.postman_environment.json" - } + "port": "8181", + "path": [ + "authentication-ms", + "oidc", + "enroll" ] }, - "description": "" - }, - "response": [] - }, - { - "name": "Counts", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Status code is 200\"] = responseCode.code === 200;", - "", - "tests[\"Response time is less than 200ms\"] = responseTime < 200;" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/secure/count", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Get JWKS from Gateway", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ] - } - } - ], - "request": { - "url": "{{base}}/jwks", - "method": "GET", - "header": [], - "body": {}, - "description": "" + "description": "This is where the Member ID and Date of birth are specified." }, "response": [] }, { - "name": "Get Swagger From Gateway", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ] - } - } - ], + "name": "Validate PIN", "request": { - "url": "{{base}}/v1", - "method": "GET", - "header": [], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Logout", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "", - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ] - } - } - ], - "request": { - "url": "{{base}}/logout", - "method": "POST", - "header": [ - { - "key": "Authorization", - "value": "Basic {{client_credentials}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Fail to access because logged out", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Response time is less than 200ms\"] = responseTime < 200;", - "", - "tests[\"Status code is 401\"] = responseCode.code === 401;" - ] - } - } - ], - "request": { - "url": "{{base}}/v1/secure", - "method": "GET", - "header": [ - { - "key": "Accept", - "value": "application/json", - "description": "" - }, - { - "key": "Authorization", - "value": "Bearer {{access_token}}", - "description": "" - } - ], - "body": {}, - "description": "" - }, - "response": [] - }, - { - "name": "Start Google Authentication", - "event": [ - { - "listen": "test", - "script": { - "type": "text/javascript", - "exec": [ - "tests[\"Status code is 200\"] = responseCode.code === 200;" - ] - } - } - ], - "request": { - "url": "{{base}}/oidc/auth/google", "method": "POST", "header": [ { "key": "Content-Type", - "value": "application/x-www-form-urlencoded", - "description": "" - }, - { - "key": "Authorization", - "value": "Basic b2lkY19pZDphcHBfc2VjcmV0", - "description": "" + "value": "application/json" } ], "body": { - "mode": "urlencoded", - "urlencoded": [ - { - "key": "state", - "value": "foo", - "description": "", - "type": "text" - }, - { - "key": "op", - "value": "auth", - "description": "", - "type": "text" - } + "mode": "raw", + "raw": "{\n \"token\":\"e_qNkp3L54tky3U5hdTAS7W8rngu\",\n \"pin\":\"FGZN\"\n}" + }, + "url": { + "raw": "http://localhost:8181/api/oidc/validate_pin", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8181", + "path": [ + "api", + "oidc", + "validate_pin" ] }, - "description": "" + "description": "Validate PIN.\nThe token should be from the start enrollment process\nThe PIN should be visible in the Kube logs.\n\nCalling this will hang Postman because it is redirecting to SecureKey." }, "response": [] }