Merge pull request #5206 from JonCook/master

BAEL-2060 - Bean Validation in Jersey
This commit is contained in:
José Carlos Valero Sánchez 2018-09-14 11:15:51 +02:00 committed by GitHub
commit d2d6f2f501
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 269 additions and 11 deletions

View File

@ -39,6 +39,11 @@
<artifactId>jersey-mvc-freemarker</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-bean-validation</artifactId>
<version>${jersey.version}</version>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.test-framework</groupId>
<artifactId>jersey-test-framework-core</artifactId>

View File

@ -1,12 +1,14 @@
package com.baeldung.jersey.server.config;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.ServerProperties;
import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature;
public class ViewApplicationConfig extends ResourceConfig {
public ViewApplicationConfig() {
packages("com.baeldung.jersey.server");
property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
property(FreemarkerMvcFeature.TEMPLATE_BASE_PATH, "templates/freemarker");
register(FreemarkerMvcFeature.class);;
}

View File

@ -0,0 +1,35 @@
package com.baeldung.jersey.server.constraints;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.regex.Pattern;
import javax.validation.Constraint;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.Payload;
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { SerialNumber.Validator.class })
public @interface SerialNumber {
String message()
default "Fruit serial number is not valid";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
public class Validator implements ConstraintValidator<SerialNumber, String> {
@Override
public void initialize(final SerialNumber serial) {
}
@Override
public boolean isValid(final String serial, final ConstraintValidatorContext constraintValidatorContext) {
final String serialNumRegex = "^\\d{3}-\\d{3}-\\d{4}$";
return Pattern.matches(serialNumRegex, serial);
}
}
}

View File

@ -1,9 +1,22 @@
package com.baeldung.jersey.server.model;
import javax.validation.constraints.Min;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;
@XmlRootElement
public class Fruit {
private final String name;
private final String colour;
@Min(value = 10, message = "Fruit weight must be 10 or greater")
private Integer weight;
@Size(min = 5, max = 200)
private String name;
@Size(min = 5, max = 200)
private String colour;
private String serial;
public Fruit() {
}
public Fruit(String name, String colour) {
this.name = name;
@ -14,7 +27,31 @@ public class Fruit {
return name;
}
public void setName(String name) {
this.name = name;
}
public void setColour(String colour) {
this.colour = colour;
}
public String getColour() {
return colour;
}
public Integer getWeight() {
return weight;
}
public void setWeight(Integer weight) {
this.weight = weight;
}
public String getSerial() {
return serial;
}
public void setSerial(String serial) {
this.serial = serial;
}
}

View File

@ -0,0 +1,26 @@
package com.baeldung.jersey.server.providers;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
public class FruitExceptionMapper implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(final ConstraintViolationException exception) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(prepareMessage(exception))
.type("text/plain")
.build();
}
private String prepareMessage(ConstraintViolationException exception) {
final StringBuilder message = new StringBuilder();
for (ConstraintViolation<?> cv : exception.getConstraintViolations()) {
message.append(cv.getPropertyPath() + " " + cv.getMessage() + "\n");
}
return message.toString();
}
}

View File

@ -5,7 +5,13 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
@ -15,7 +21,9 @@ import org.glassfish.jersey.server.mvc.ErrorTemplate;
import org.glassfish.jersey.server.mvc.Template;
import org.glassfish.jersey.server.mvc.Viewable;
import com.baeldung.jersey.server.constraints.SerialNumber;
import com.baeldung.jersey.server.model.Fruit;
import com.baeldung.jersey.service.SimpleStorageService;
@Path("/fruit")
public class FruitResource {
@ -52,4 +60,49 @@ public class FruitResource {
return name;
}
@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void createFruit(
@NotNull(message = "Fruit name must not be null") @FormParam("name") String name,
@NotNull(message = "Fruit colour must not be null") @FormParam("colour") String colour) {
Fruit fruit = new Fruit(name, colour);
SimpleStorageService.storeFruit(fruit);
}
@PUT
@Path("/update")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void updateFruit(@SerialNumber @FormParam("serial") String serial) {
Fruit fruit = new Fruit();
fruit.setSerial(serial);
SimpleStorageService.storeFruit(fruit);
}
@POST
@Path("/create")
@Consumes(MediaType.APPLICATION_JSON)
public void createFruit(@Valid Fruit fruit) {
SimpleStorageService.storeFruit(fruit);
}
@GET
@Valid
@Produces(MediaType.APPLICATION_JSON)
@Path("/search/{name}")
public Fruit findFruitByName(@PathParam("name") String name) {
return SimpleStorageService.findByName(name);
}
@GET
@Produces(MediaType.TEXT_HTML)
@Path("/exception")
@Valid
public Fruit exception() {
Fruit fruit = new Fruit();
fruit.setName("a");
fruit.setColour("b");
return fruit;
}
}

View File

@ -0,0 +1,25 @@
package com.baeldung.jersey.service;
import java.util.HashMap;
import java.util.Map;
import com.baeldung.jersey.server.model.Fruit;
public class SimpleStorageService {
private static final Map<String, Fruit> fruits = new HashMap<String, Fruit>();
public static void storeFruit(final Fruit fruit) {
fruits.put(fruit.getName(), fruit);
}
public static Fruit findByName(final String name) {
return fruits.entrySet()
.stream()
.filter(map -> name.equals(map.getKey()))
.map(map -> map.getValue())
.findFirst()
.get();
}
}

View File

@ -2,41 +2,116 @@ package com.baeldung.jersey.server.rest;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Assert;
import org.glassfish.jersey.test.TestProperties;
import org.junit.Test;
import com.baeldung.jersey.server.config.ViewApplicationConfig;
import com.baeldung.jersey.server.model.Fruit;
import com.baeldung.jersey.server.providers.FruitExceptionMapper;
public class FruitResourceIntegrationTest extends JerseyTest {
@Override
protected Application configure() {
return new ViewApplicationConfig();
enable(TestProperties.LOG_TRAFFIC);
enable(TestProperties.DUMP_ENTITY);
ViewApplicationConfig config = new ViewApplicationConfig();
config.register(FruitExceptionMapper.class);
return config;
}
@Test
public void testAllFruit() {
public void givenGetAllFruit_whenCorrectRequest_thenAllTemplateInvoked() {
final String response = target("/fruit/all").request()
.get(String.class);
Assert.assertThat(response, allOf(containsString("banana"), containsString("apple"), containsString("kiwi")));
assertThat(response, allOf(containsString("banana"), containsString("apple"), containsString("kiwi")));
}
@Test
public void testIndex() {
public void givenGetFruit_whenCorrectRequest_thenIndexTemplateInvoked() {
final String response = target("/fruit").request()
.get(String.class);
Assert.assertThat(response, containsString("Welcome Fruit Index Page!"));
assertThat(response, containsString("Welcome Fruit Index Page!"));
}
@Test
public void testErrorTemplate() {
public void givenGetFruitByName_whenFruitUnknown_thenErrorTemplateInvoked() {
final String response = target("/fruit/orange").request()
.get(String.class);
Assert.assertThat(response, containsString("Error - Fruit not found: orange!"));
assertThat(response, containsString("Error - Fruit not found: orange!"));
}
@Test
public void givenCreateFruit_whenFormContainsNullParam_thenResponseCodeIsBadRequest() {
Form form = new Form();
form.param("name", "apple");
form.param("colour", null);
Response response = target("fruit/create").request(MediaType.APPLICATION_FORM_URLENCODED)
.post(Entity.form(form));
assertEquals("Http Response should be 400 ", 400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("Fruit colour must not be null"));
}
@Test
public void givenUpdateFruit_whenFormContainsBadSerialParam_thenResponseCodeIsBadRequest() {
Form form = new Form();
form.param("serial", "2345-2345");
Response response = target("fruit/update").request(MediaType.APPLICATION_FORM_URLENCODED)
.put(Entity.form(form));
assertEquals("Http Response should be 400 ", 400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("Fruit serial number is not valid"));
}
@Test
public void givenCreateFruit_whenFruitIsInvalid_thenResponseCodeIsBadRequest() {
Fruit fruit = new Fruit("Blueberry", "purple");
fruit.setWeight(1);
Response response = target("fruit/create").request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(fruit, MediaType.APPLICATION_JSON_TYPE));
assertEquals("Http Response should be 400 ", 400, response.getStatus());
assertThat(response.readEntity(String.class), containsString("Fruit weight must be 10 or greater"));
}
@Test
public void givenFruitExists_whenSearching_thenResponseContainsFruit() {
Fruit fruit = new Fruit();
fruit.setName("strawberry");
fruit.setWeight(20);
Response response = target("fruit/create").request(MediaType.APPLICATION_JSON_TYPE)
.post(Entity.entity(fruit, MediaType.APPLICATION_JSON_TYPE));
assertEquals("Http Response should be 204 ", 204, response.getStatus());
final String json = target("fruit/search/strawberry").request()
.get(String.class);
assertThat(json, containsString("{\"name\":\"strawberry\",\"weight\":20}"));
}
@Test
public void givenFruit_whenFruitIsInvalid_thenReponseContainsCustomExceptions() {
final Response response = target("fruit/exception").request()
.get();
assertEquals("Http Response should be 400 ", 400, response.getStatus());
String responseString = response.readEntity(String.class);
assertThat(responseString, containsString("exception.<return value>.colour size must be between 5 and 200"));
assertThat(responseString, containsString("exception.<return value>.name size must be between 5 and 200"));
}
}