BAEL-5157 - Exception Handling with Jersey (#12045)
First draft: https://drafts.baeldung.com/wp-admin/post.php?post=131880&action=edit
This commit is contained in:
parent
4ad546c702
commit
620653f6a6
|
@ -0,0 +1,19 @@
|
|||
package com.baeldung.jersey.exceptionhandling;
|
||||
|
||||
import javax.ws.rs.ApplicationPath;
|
||||
|
||||
import org.glassfish.jersey.server.ResourceConfig;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.IllegalArgumentExceptionMapper;
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.ServerExceptionMapper;
|
||||
|
||||
@ApplicationPath("/exception-handling/*")
|
||||
public class ExceptionHandlingConfig extends ResourceConfig {
|
||||
|
||||
public ExceptionHandlingConfig() {
|
||||
packages("com.baeldung.jersey.exceptionhandling.rest");
|
||||
register(IllegalArgumentExceptionMapper.class);
|
||||
register(ServerExceptionMapper.class);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.baeldung.jersey.exceptionhandling.data;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.repo.Identifiable;
|
||||
|
||||
public class Stock implements Identifiable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private String id;
|
||||
private Double price;
|
||||
|
||||
public Stock() {
|
||||
}
|
||||
|
||||
public Stock(String id, Double price) {
|
||||
this.id = id;
|
||||
this.price = price;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Double getPrice() {
|
||||
return price;
|
||||
}
|
||||
|
||||
public void setPrice(Double price) {
|
||||
this.price = price;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.baeldung.jersey.exceptionhandling.data;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.repo.Identifiable;
|
||||
|
||||
public class Wallet implements Identifiable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public static final Double MIN_CHARGE = 50.0;
|
||||
public static final String MIN_CHARGE_MSG = "minimum charge is: " + MIN_CHARGE;
|
||||
|
||||
private String id;
|
||||
private Double balance = 0.0;
|
||||
|
||||
public Wallet() {
|
||||
}
|
||||
|
||||
public Wallet(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Double getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public void setBalance(Double balance) {
|
||||
this.balance = balance;
|
||||
}
|
||||
|
||||
public Double addBalance(Double amount) {
|
||||
if (balance == null)
|
||||
balance = 0.0;
|
||||
|
||||
return balance += amount;
|
||||
}
|
||||
|
||||
public boolean hasFunds(Double amount) {
|
||||
if (balance == null || amount == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (balance - amount) >= 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.baeldung.jersey.exceptionhandling.repo;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public class Db<T extends Identifiable> {
|
||||
private Map<String, T> db = new HashMap<>();
|
||||
|
||||
public Optional<T> findById(String id) {
|
||||
return Optional.ofNullable(db.get(id));
|
||||
}
|
||||
|
||||
public String save(T t) {
|
||||
String id = t.getId();
|
||||
if (id == null) {
|
||||
id = UUID.randomUUID()
|
||||
.toString();
|
||||
t.setId(id);
|
||||
}
|
||||
db.put(id, t);
|
||||
return id;
|
||||
}
|
||||
|
||||
public void remove(T t) {
|
||||
db.entrySet()
|
||||
.removeIf(entry -> entry.getValue()
|
||||
.getId()
|
||||
.equals(t.getId()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.baeldung.jersey.exceptionhandling.repo;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface Identifiable extends Serializable {
|
||||
void setId(String id);
|
||||
|
||||
String getId();
|
||||
|
||||
public static void assertValid(Identifiable i) {
|
||||
if (i == null)
|
||||
throw new IllegalArgumentException("object cannot be null");
|
||||
|
||||
if (i.getId() == null)
|
||||
throw new IllegalArgumentException("object id cannot be null");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.data.Stock;
|
||||
import com.baeldung.jersey.exceptionhandling.repo.Db;
|
||||
import com.baeldung.jersey.exceptionhandling.service.Repository;
|
||||
|
||||
@Path("/stocks")
|
||||
public class StocksResource {
|
||||
private static final Db<Stock> stocks = Repository.STOCKS_DB;
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response post(Stock stock) {
|
||||
stocks.save(stock);
|
||||
|
||||
return Response.ok(stock)
|
||||
.build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{ticker}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response get(@PathParam("ticker") String id) {
|
||||
Optional<Stock> stock = stocks.findById(id);
|
||||
stock.orElseThrow(IllegalArgumentException::new);
|
||||
|
||||
return Response.ok(stock.get())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
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;
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.data.Stock;
|
||||
import com.baeldung.jersey.exceptionhandling.data.Wallet;
|
||||
import com.baeldung.jersey.exceptionhandling.repo.Db;
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.InvalidTradeException;
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.RestErrorResponse;
|
||||
import com.baeldung.jersey.exceptionhandling.service.Repository;
|
||||
|
||||
@Path("/wallets")
|
||||
public class WalletsResource {
|
||||
private static final Db<Stock> stocks = Repository.STOCKS_DB;
|
||||
private static final Db<Wallet> wallets = Repository.WALLETS_DB;
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response post(Wallet wallet) {
|
||||
wallets.save(wallet);
|
||||
|
||||
return Response.ok(wallet)
|
||||
.build();
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{id}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response get(@PathParam("id") String id) {
|
||||
Optional<Wallet> wallet = wallets.findById(id);
|
||||
wallet.orElseThrow(IllegalArgumentException::new);
|
||||
|
||||
return Response.ok(wallet.get())
|
||||
.build();
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{id}/{amount}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response putAmount(@PathParam("id") String id, @PathParam("amount") Double amount) {
|
||||
Optional<Wallet> wallet = wallets.findById(id);
|
||||
wallet.orElseThrow(IllegalArgumentException::new);
|
||||
|
||||
if (amount < Wallet.MIN_CHARGE) {
|
||||
throw new InvalidTradeException(Wallet.MIN_CHARGE_MSG);
|
||||
}
|
||||
|
||||
wallet.get()
|
||||
.addBalance(amount);
|
||||
wallets.save(wallet.get());
|
||||
|
||||
return Response.ok(wallet)
|
||||
.build();
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{wallet}/buy/{ticker}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response postBuyStock(@PathParam("wallet") String walletId, @PathParam("ticker") String id) {
|
||||
Optional<Stock> stock = stocks.findById(id);
|
||||
stock.orElseThrow(InvalidTradeException::new);
|
||||
|
||||
Optional<Wallet> w = wallets.findById(walletId);
|
||||
w.orElseThrow(InvalidTradeException::new);
|
||||
|
||||
Wallet wallet = w.get();
|
||||
Double price = stock.get()
|
||||
.getPrice();
|
||||
|
||||
if (!wallet.hasFunds(price)) {
|
||||
RestErrorResponse response = new RestErrorResponse();
|
||||
response.setSubject(wallet);
|
||||
response.setMessage("insufficient balance");
|
||||
throw new WebApplicationException(Response.status(Status.NOT_ACCEPTABLE)
|
||||
.entity(response)
|
||||
.build());
|
||||
}
|
||||
|
||||
wallet.addBalance(-price);
|
||||
wallets.save(wallet);
|
||||
|
||||
return Response.ok(wallet)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest.exceptions;
|
||||
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
|
||||
public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {
|
||||
public static final String DEFAULT_MESSAGE = "an illegal argument was provided";
|
||||
|
||||
@Override
|
||||
public Response toResponse(final IllegalArgumentException exception) {
|
||||
return Response.status(Response.Status.EXPECTATION_FAILED)
|
||||
.entity(build(exception.getMessage()))
|
||||
.type(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
}
|
||||
|
||||
private RestErrorResponse build(String message) {
|
||||
RestErrorResponse response = new RestErrorResponse();
|
||||
response.setMessage(DEFAULT_MESSAGE + ": " + message);
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest.exceptions;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
public class InvalidTradeException extends WebApplicationException {
|
||||
private static final long serialVersionUID = 1L;
|
||||
private static final String MESSAGE = "invalid trade operation";
|
||||
|
||||
public InvalidTradeException() {
|
||||
super(MESSAGE, Response.Status.NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
public InvalidTradeException(String detail) {
|
||||
super(MESSAGE + ": " + detail, Response.Status.NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest.exceptions;
|
||||
|
||||
public class RestErrorResponse {
|
||||
private Object subject;
|
||||
private String message;
|
||||
|
||||
public RestErrorResponse() {
|
||||
}
|
||||
|
||||
public RestErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public RestErrorResponse(Object subject, String message) {
|
||||
this.subject = subject;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Object getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(Object subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest.exceptions;
|
||||
|
||||
import javax.ws.rs.WebApplicationException;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
import javax.ws.rs.ext.ExceptionMapper;
|
||||
|
||||
public class ServerExceptionMapper implements ExceptionMapper<WebApplicationException> {
|
||||
public static final String HTTP_405_MESSAGE = "METHOD_NOT_ALLOWED";
|
||||
|
||||
@Override
|
||||
public Response toResponse(final WebApplicationException exception) {
|
||||
String message = exception.getMessage();
|
||||
Response response = exception.getResponse();
|
||||
Status status = response.getStatusInfo()
|
||||
.toEnum();
|
||||
|
||||
switch (status) {
|
||||
case METHOD_NOT_ALLOWED:
|
||||
message = HTTP_405_MESSAGE;
|
||||
break;
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
message = "internal validation - " + exception;
|
||||
break;
|
||||
default:
|
||||
message = "[unhandled response code] " + exception;
|
||||
}
|
||||
|
||||
return Response.status(status)
|
||||
.entity(status + ": " + message)
|
||||
.type(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package com.baeldung.jersey.exceptionhandling.service;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.data.Stock;
|
||||
import com.baeldung.jersey.exceptionhandling.data.Wallet;
|
||||
import com.baeldung.jersey.exceptionhandling.repo.Db;
|
||||
|
||||
public class Repository {
|
||||
public static Db<Stock> STOCKS_DB = new Db<>();
|
||||
public static Db<Wallet> WALLETS_DB = new Db<>();
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package com.baeldung.jersey.exceptionhandling.rest;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import javax.ws.rs.client.Entity;
|
||||
import javax.ws.rs.client.Invocation;
|
||||
import javax.ws.rs.core.Application;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.Response.Status;
|
||||
|
||||
import org.glassfish.jersey.test.JerseyTest;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.baeldung.jersey.exceptionhandling.ExceptionHandlingConfig;
|
||||
import com.baeldung.jersey.exceptionhandling.data.Stock;
|
||||
import com.baeldung.jersey.exceptionhandling.data.Wallet;
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.IllegalArgumentExceptionMapper;
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.RestErrorResponse;
|
||||
import com.baeldung.jersey.exceptionhandling.rest.exceptions.ServerExceptionMapper;
|
||||
|
||||
public class StocksResourceIntegrationTest extends JerseyTest {
|
||||
private static final Entity<String> EMPTY_BODY = Entity.json("");
|
||||
private static final Stock STOCK = new Stock("BAEL", 51.57);
|
||||
private static final String MY_WALLET = "MY-WALLET";
|
||||
private static final Wallet WALLET = new Wallet(MY_WALLET);
|
||||
private static final int INSUFFICIENT_AMOUNT = (int) (Wallet.MIN_CHARGE - 1);
|
||||
|
||||
@Override
|
||||
protected Application configure() {
|
||||
return new ExceptionHandlingConfig();
|
||||
}
|
||||
|
||||
private Invocation.Builder stocks(String path) {
|
||||
return target("/stocks" + path).request();
|
||||
}
|
||||
|
||||
private Invocation.Builder wallets(String path, Object... args) {
|
||||
return target("/wallets" + String.format(path, args)).request();
|
||||
}
|
||||
|
||||
private Entity<?> entity(Object object) {
|
||||
return Entity.entity(object, MediaType.APPLICATION_JSON_TYPE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenMethodNotAllowed_thenCustomMessage() {
|
||||
Response response = stocks("").get();
|
||||
|
||||
assertEquals(Status.METHOD_NOT_ALLOWED.getStatusCode(), response.getStatus());
|
||||
|
||||
String content = response.readEntity(String.class);
|
||||
assertThat(content, containsString(ServerExceptionMapper.HTTP_405_MESSAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void whenTickerNotExists_thenRestErrorResponse() {
|
||||
Response response = stocks("/UNDEFINED").get();
|
||||
|
||||
assertEquals(Status.EXPECTATION_FAILED.getStatusCode(), response.getStatus());
|
||||
|
||||
RestErrorResponse content = response.readEntity(RestErrorResponse.class);
|
||||
assertThat(content.getMessage(), startsWith(IllegalArgumentExceptionMapper.DEFAULT_MESSAGE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenAmountLessThanMinimum_whenAddingToWallet_thenInvalidTradeException() {
|
||||
wallets("").post(entity(WALLET));
|
||||
Response response = wallets("/%s/%d", MY_WALLET, INSUFFICIENT_AMOUNT).put(EMPTY_BODY);
|
||||
|
||||
assertEquals(Status.NOT_ACCEPTABLE.getStatusCode(), response.getStatus());
|
||||
|
||||
String content = response.readEntity(String.class);
|
||||
assertThat(content, containsString(Wallet.MIN_CHARGE_MSG));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenInsifficientFunds_whenBuyingStock_thenWebApplicationException() {
|
||||
stocks("").post(entity(STOCK));
|
||||
wallets("").post(entity(WALLET));
|
||||
|
||||
Response response = wallets("/%s/buy/%s", MY_WALLET, STOCK.getId()).post(EMPTY_BODY);
|
||||
assertEquals(Status.NOT_ACCEPTABLE.getStatusCode(), response.getStatus());
|
||||
|
||||
RestErrorResponse content = response.readEntity(RestErrorResponse.class);
|
||||
assertNotNull(content.getSubject());
|
||||
|
||||
HashMap<?, ?> subject = (HashMap<?, ?>) content.getSubject();
|
||||
assertEquals(subject.get("id"), WALLET.getId());
|
||||
assertTrue(WALLET.getBalance() < Wallet.MIN_CHARGE);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue