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:
Ulisses Lima 2022-04-11 13:43:37 -03:00 committed by GitHub
parent 4ad546c702
commit 620653f6a6
13 changed files with 510 additions and 0 deletions

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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()));
}
}

View File

@ -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");
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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<>();
}

View File

@ -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);
}
}