Merge pull request #5206 from JonCook/master
BAEL-2060 - Bean Validation in Jersey
This commit is contained in:
commit
d2d6f2f501
|
@ -39,6 +39,11 @@
|
||||||
<artifactId>jersey-mvc-freemarker</artifactId>
|
<artifactId>jersey-mvc-freemarker</artifactId>
|
||||||
<version>${jersey.version}</version>
|
<version>${jersey.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.glassfish.jersey.ext</groupId>
|
||||||
|
<artifactId>jersey-bean-validation</artifactId>
|
||||||
|
<version>${jersey.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.glassfish.jersey.test-framework</groupId>
|
<groupId>org.glassfish.jersey.test-framework</groupId>
|
||||||
<artifactId>jersey-test-framework-core</artifactId>
|
<artifactId>jersey-test-framework-core</artifactId>
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package com.baeldung.jersey.server.config;
|
package com.baeldung.jersey.server.config;
|
||||||
|
|
||||||
import org.glassfish.jersey.server.ResourceConfig;
|
import org.glassfish.jersey.server.ResourceConfig;
|
||||||
|
import org.glassfish.jersey.server.ServerProperties;
|
||||||
import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature;
|
import org.glassfish.jersey.server.mvc.freemarker.FreemarkerMvcFeature;
|
||||||
|
|
||||||
public class ViewApplicationConfig extends ResourceConfig {
|
public class ViewApplicationConfig extends ResourceConfig {
|
||||||
|
|
||||||
public ViewApplicationConfig() {
|
public ViewApplicationConfig() {
|
||||||
packages("com.baeldung.jersey.server");
|
packages("com.baeldung.jersey.server");
|
||||||
|
property(ServerProperties.BV_SEND_ERROR_IN_RESPONSE, true);
|
||||||
property(FreemarkerMvcFeature.TEMPLATE_BASE_PATH, "templates/freemarker");
|
property(FreemarkerMvcFeature.TEMPLATE_BASE_PATH, "templates/freemarker");
|
||||||
register(FreemarkerMvcFeature.class);;
|
register(FreemarkerMvcFeature.class);;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,20 +1,57 @@
|
||||||
package com.baeldung.jersey.server.model;
|
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 {
|
public class Fruit {
|
||||||
|
|
||||||
private final String name;
|
@Min(value = 10, message = "Fruit weight must be 10 or greater")
|
||||||
private final String colour;
|
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) {
|
public Fruit(String name, String colour) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.colour = colour;
|
this.colour = colour;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColour(String colour) {
|
||||||
|
this.colour = colour;
|
||||||
|
}
|
||||||
|
|
||||||
public String getColour() {
|
public String getColour() {
|
||||||
return colour;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -5,7 +5,13 @@ import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.GET;
|
||||||
|
import javax.ws.rs.POST;
|
||||||
|
import javax.ws.rs.PUT;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.PathParam;
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.Produces;
|
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.Template;
|
||||||
import org.glassfish.jersey.server.mvc.Viewable;
|
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.server.model.Fruit;
|
||||||
|
import com.baeldung.jersey.service.SimpleStorageService;
|
||||||
|
|
||||||
@Path("/fruit")
|
@Path("/fruit")
|
||||||
public class FruitResource {
|
public class FruitResource {
|
||||||
|
@ -52,4 +60,49 @@ public class FruitResource {
|
||||||
return name;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -2,41 +2,116 @@ package com.baeldung.jersey.server.rest;
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.allOf;
|
import static org.hamcrest.CoreMatchers.allOf;
|
||||||
import static org.hamcrest.CoreMatchers.containsString;
|
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.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.glassfish.jersey.test.JerseyTest;
|
||||||
import org.junit.Assert;
|
import org.glassfish.jersey.test.TestProperties;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import com.baeldung.jersey.server.config.ViewApplicationConfig;
|
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 {
|
public class FruitResourceIntegrationTest extends JerseyTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Application configure() {
|
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
|
@Test
|
||||||
public void testAllFruit() {
|
public void givenGetAllFruit_whenCorrectRequest_thenAllTemplateInvoked() {
|
||||||
final String response = target("/fruit/all").request()
|
final String response = target("/fruit/all").request()
|
||||||
.get(String.class);
|
.get(String.class);
|
||||||
Assert.assertThat(response, allOf(containsString("banana"), containsString("apple"), containsString("kiwi")));
|
assertThat(response, allOf(containsString("banana"), containsString("apple"), containsString("kiwi")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIndex() {
|
public void givenGetFruit_whenCorrectRequest_thenIndexTemplateInvoked() {
|
||||||
final String response = target("/fruit").request()
|
final String response = target("/fruit").request()
|
||||||
.get(String.class);
|
.get(String.class);
|
||||||
Assert.assertThat(response, containsString("Welcome Fruit Index Page!"));
|
assertThat(response, containsString("Welcome Fruit Index Page!"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testErrorTemplate() {
|
public void givenGetFruitByName_whenFruitUnknown_thenErrorTemplateInvoked() {
|
||||||
final String response = target("/fruit/orange").request()
|
final String response = target("/fruit/orange").request()
|
||||||
.get(String.class);
|
.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"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue