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>
|
||||
<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>
|
||||
|
|
|
@ -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);;
|
||||
}
|
||||
|
|
|
@ -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,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue