com.moodysalem.jaxrs.lib.resources.EntityResource Maven / Gradle / Ivy
package com.moodysalem.jaxrs.lib.resources;
import com.moodysalem.hibernate.model.BaseEntity;
import com.moodysalem.jaxrs.lib.exceptions.RequestProcessingException;
import org.hibernate.exception.ConstraintViolationException;
import javax.persistence.EntityManager;
import javax.persistence.EntityTransaction;
import javax.persistence.PersistenceException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import javax.validation.ConstraintViolation;
import javax.ws.rs.*;
import javax.ws.rs.Path;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.sql.SQLException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
/**
* This resource implements a common REST interface for CRUD against a particular
* entity type
*
* @param entity type to allow CRUD
*/
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public abstract class EntityResource {
private static final Logger LOG = Logger.getLogger(EntityResource.class.getName());
private static final String S_WITH_ID_S_NOT_FOUND = "%s with ID %s not found";
/**
* Return the Class that this resource manages
*
* @return class that the resource manages
*/
public abstract Class getEntityClass();
protected abstract ContainerRequestContext getContainerRequestContext();
protected abstract EntityManager getEntityManager();
//////////////////////////////CONSTANTS//////////////////////////////////////////
// sorting behavior
public abstract String getSortQueryParameterName();
public abstract String getSortInfoSeparator();
public abstract String getSortPathSeparator();
public abstract int getMaxNumberOfSorts();
// paging behavior
public abstract String getStartQueryParameterName();
public abstract String getCountQueryParameterName();
public abstract Integer getMaxPerPage();
// name of the header that should indicate the first record returned
public abstract String getStartHeader();
// name of the header that should indicate the count of records returned
public abstract String getCountHeader();
// name of the header that should include the total count of records that fit the criteria
public abstract String getTotalCountHeader();
// whether the user is authenticated
public abstract boolean isLoggedIn();
// whether the resource rqeuires login
public abstract boolean requiresLogin();
// whether the entity can be created
public abstract boolean canCreate(T entity);
// whether the entity can be edited
public abstract boolean canEdit(T entity);
// whether the entity can be deleted
public abstract boolean canDelete(T entity);
// fill errors array with any validation error strings
protected abstract void validateEntity(List errors, T entity);
// perform these actions before persisting the entity
public abstract void beforeCreate(T entity);
// perform these actions before merging the entity changes
public abstract void beforeEdit(T oldEntity, T entity);
// get a list of query predicates for lists
protected abstract void getPredicatesFromRequest(List predicates, Root root);
// perform these actions after creating an entity
public abstract void afterCreate(T entity);
// used to perform transformations before sending back an entity in a response
public abstract void beforeSend(T entity);
// error message templates
private static final String NOT_FOUND = "%1$s with ID %2$s not found.";
private static final String NOT_AUTHORIZED_TO_CREATE = "Not authorized to create %1$s.";
private static final String NOT_AUTHORIZED_TO_EDIT = "Not authorized to edit %1$s with ID %2$s";
private static final String NOT_AUTHORIZED_TO_DELETE = "Not authorized to delete %1$s with ID %2$s.";
private static final String VERSION_CONFLICT_ERROR = "%1$s with ID %2$s has since been edited.";
////////////////////////////////////GET/////////////////////////////////////////
private void checkLoggedIn() {
if (requiresLogin() && !isLoggedIn()) {
throw new RequestProcessingException(Response.Status.UNAUTHORIZED, "You must be logged in to access this resource.");
}
}
/**
* Get a single entity with an ID
*
* @param id of the entity
* @return the entity corresponding to the ID
*/
@GET
@Path("{id}")
public Response get(@PathParam("id") UUID id) {
checkLoggedIn();
T entity = getEntityWithId(id);
if (entity == null) {
throw new RequestProcessingException(Response.Status.NOT_FOUND,
String.format(NOT_FOUND, getEntityName(), id));
}
beforeSend(entity);
return Response.ok(entity).build();
}
/**
* Helper method to get a single query parameter
*
* @param param name of the query parameter
* @return the value assigned to the query parameter
*/
private String getQueryParameter(String param) {
List params = getQueryParameters(param);
return params != null && params.size() > 0 ? params.get(0) : null;
}
/**
* Helper method to get a list of values associated with a query parameter
*
* @param param name of the query parameter
* @return list of values assigned to query parameter
*/
private List getQueryParameters(String param) {
return getContainerRequestContext().getUriInfo().getQueryParameters().get(param);
}
/**
* Get the first record that should be returned
*
* @return an int corresponding to the first record to return
*/
private int getStart() {
String start = getQueryParameter(getStartQueryParameterName());
if (start != null) {
try {
return Math.max(Integer.parseInt(start), 0);
} catch (Exception e) {
LOG.log(Level.WARNING, "Invalid start received", e);
}
}
return 0;
}
/**
* Get the # of records that should be returned
*
* @return the # of records, or null if all should be returned
*/
private Integer getCount() {
String countString = getQueryParameter(getCountQueryParameterName());
Integer maxCount = getMaxPerPage();
Integer count = null;
if (countString != null) {
try {
count = Integer.parseInt(countString);
} catch (NumberFormatException e) {
LOG.fine(String.format("Invalid count passed to GET: %s", countString));
}
}
if (count != null) {
if (maxCount != null) {
return Math.max(Math.min(count, maxCount), 0);
} else {
return Math.max(count, 0);
}
}
return maxCount;
}
/**
* Return a list of type T to the client, including headers about pagination
*
* @return response with entity list and headers corresponding to pagination details
*/
@GET
public Response getList() {
checkLoggedIn();
Integer count = getCount();
int start = getStart();
// get the entities
List entities = getListOfEntities(count, start);
entities.forEach(this::beforeSend);
// count the total number of the results that would've been returned
long totalCount = getTotalCountOfEntities();
// return the filtered and mapped list of entities
return Response.ok(entities)
.header(getStartHeader(), start)
.header(getCountHeader(), count)
.header(getTotalCountHeader(), totalCount)
.build();
}
/**
* Return the entity with the ID, filtered by the predicates associated with the request
*
* @param id of the entity to get
* @return entity of type T with ID id
*/
private T getEntityWithId(UUID id) {
EntityManager em = getEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(this.getEntityClass());
Root from = cq.from(this.getEntityClass());
List predicates = getPredicatesFromRequest(from);
predicates.add(cb.equal(from.get("id"), id));
Predicate[] pArray = new Predicate[predicates.size()];
predicates.toArray(pArray);
List entity = em.createQuery(cq.select(from).where(pArray)).getResultList();
return (entity.size() == 1 ? entity.get(0) : null);
}
/**
* Get the total count of entities in the database that match the predicates
*
* @return the total count of entities that match the predicates
*/
private long getTotalCountOfEntities() {
EntityManager em = getEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(Long.class);
Root root = cq.from(this.getEntityClass());
List predicates = getPredicatesFromRequest(root);
Predicate[] pArray = new Predicate[predicates.size()];
predicates.toArray(pArray);
CriteriaQuery countQuery = cq.select(cb.count(root)).where(pArray);
return em.createQuery(countQuery).getSingleResult();
}
/**
* Get a list of entities with a maximum size of count, starting at start
*
* @param count max # of entities to get
* @param start which entity to start at
* @return a list of type T from the database
*/
private List getListOfEntities(Integer count, int start) {
if (count != null && count <= 0) {
return Collections.emptyList();
}
EntityManager em = getEntityManager();
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery(this.getEntityClass());
Root from = cq.from(this.getEntityClass());
cq.select(from);
List predicates = getPredicatesFromRequest(from);
if (predicates.size() > 0) {
Predicate[] pArray = new Predicate[predicates.size()];
predicates.toArray(pArray);
cq.where(pArray);
}
// parse the request to return the orders that should be applied
List orderBys = getOrderFromRequest(from);
if (orderBys.size() > 0) {
Order[] oArray = new Order[orderBys.size()];
orderBys.toArray(oArray);
cq.orderBy(orderBys);
}
TypedQuery query = em.createQuery(cq)
.setFirstResult(start);
if (count != null) {
query.setMaxResults(count);
}
return query.getResultList();
}
/**
* Get a list of Orders from the request
*
* @param from the root of the query
* @return a list of orders
*/
private List getOrderFromRequest(Root from) {
List orders = new ArrayList<>();
getOrderFromRequest(from, orders);
return orders;
}
// throw an error if the object is null
private void notNull(Object shouldNotBeNull, String nameOfParameter) {
if (shouldNotBeNull == null) {
throw new RequestProcessingException(
Response.Status.BAD_REQUEST,
String.format("%1$s should not be null.", nameOfParameter)
);
}
}
/**
* Resolves sorting orders from the request by matching them to attribute on the model
*
* @param from root of the query
*/
private void getOrderFromRequest(Root from, List orders) {
List sorts = getQueryParameters(Pattern.quote(getSortQueryParameterName()));
if (sorts == null || sorts.size() == 0) {
return;
}
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
for (String sortOrder : sorts) {
String[] pieces = sortOrder.split(Pattern.quote(getSortInfoSeparator()));
if (pieces.length <= 1) {
continue;
}
boolean asc = (pieces[0].equals("A"));
javax.persistence.criteria.Path sortBy;
try {
LinkedList sortAttributePieces = new LinkedList<>(Arrays.asList(pieces[1].split(getSortPathSeparator())));
if (sortAttributePieces.size() > 1) {
Join j = from.join(sortAttributePieces.pop(), JoinType.LEFT);
while (sortAttributePieces.size() > 1) {
String attribute = sortAttributePieces.pop();
j = j.join(attribute, JoinType.LEFT);
}
sortBy = j.get(sortAttributePieces.get(0));
} else {
sortBy = from.get(sortAttributePieces.get(0));
}
} catch (IllegalArgumentException e) {
LOG.log(Level.WARNING, "Failed to parse sort: " + sortOrder, e);
continue;
}
if (asc) {
orders.add(cb.asc(sortBy));
} else {
orders.add(cb.desc(sortBy));
}
if (orders.size() > getMaxNumberOfSorts()) {
break;
}
}
}
/**
* Parse the request to get the query parameters to append to the GET requests
*
* @param root the root of the query
* @return a list of predicates to apply to the query
*/
private List getPredicatesFromRequest(Root root) {
List predicates = new ArrayList<>();
getPredicatesFromRequest(predicates, root);
return predicates;
}
////////////////////////////////////GET/////////////////////////////////////////
///////////////////////////////////POST/////////////////////////////////////////
/**
* Create an entity
*
* @param entity data to persist
* @return a response containing the saved entity
*/
@POST
public Response post(T entity) {
checkLoggedIn();
notNull(entity, getEntityName());
if (!canCreate(entity)) {
throw new RequestProcessingException(Response.Status.FORBIDDEN,
String.format(NOT_AUTHORIZED_TO_CREATE, getEntityName()));
}
beforeCreate(entity);
List errors = validateEntity(entity);
if (errors.size() > 0) {
String[] errs = new String[errors.size()];
errors.toArray(errs);
throw new RequestProcessingException(Response.Status.BAD_REQUEST, errs);
}
boolean needsTx = noTx();
try {
if (needsTx) {
openTransaction();
}
getEntityManager().persist(entity);
if (needsTx) {
commit();
}
afterCreate(entity);
} catch (Exception e) {
if (needsTx) {
rollback();
}
LOG.log(Level.SEVERE, "Failed to create entity", e);
throw new RequestProcessingException(
Response.Status.CONFLICT,
translateExceptionToMessage(e)
);
}
return get(entity.getId());
}
/**
* Translates an exception to a human readable message
*
* @param e exception to translate
* @return a human readable message from the exception, if recognized
*/
private String translateExceptionToMessage(Exception e) {
if (e == null) {
return null;
}
String msg = e.getMessage();
if (e instanceof PersistenceException) {
PersistenceException pe = (PersistenceException) e;
if (e.getCause() instanceof ConstraintViolationException) {
e = (Exception) pe.getCause();
}
}
if (e instanceof ConstraintViolationException) {
ConstraintViolationException cve = (ConstraintViolationException) e;
SQLException se = cve.getSQLException();
StringBuilder sb = new StringBuilder();
while (se != null) {
if (sb.length() > 0) {
sb.append(", ");
}
sb.append(se.getMessage());
se = se.getNextException();
}
msg = sb.toString();
}
if (e instanceof javax.validation.ConstraintViolationException) {
StringBuilder sb = new StringBuilder();
javax.validation.ConstraintViolationException cve = (javax.validation.ConstraintViolationException) e;
if (cve.getConstraintViolations() != null) {
for (ConstraintViolation cv : cve.getConstraintViolations()) {
if (sb.length() > 0) {
sb.append(", ");
}
String prop = cv.getPropertyPath() != null ? cv.getPropertyPath().toString() : getEntityName();
String error = cv.getMessage() != null ? cv.getMessage() : "unknown error";
sb.append(String.format("Invalid %s: %s", prop, error));
}
}
msg = sb.toString();
}
return msg;
}
/**
* Validate an entity and return a list of validation errors
*
* @param entity to validate
* @return a list of errors
*/
private List validateEntity(T entity) {
List errors = new ArrayList<>();
validateEntity(errors, entity);
return errors;
}
///////////////////////////////////POST/////////////////////////////////////////
///////////////////////////////////PUT/////////////////////////////////////////
/**
* Save an entity that already exists
*
* @param id of the entity to save
* @param entity data to merge
* @return saved entity
*/
@PUT
@Path("{id}")
public Response put(@PathParam("id") UUID id, T entity) {
checkLoggedIn();
notNull(entity, getEntityName());
T entityToEdit = getEntityWithId(id);
// check the entity with this ID truly exists and is visible to the user
if (entityToEdit == null) {
throw new RequestProcessingException(Response.Status.NOT_FOUND,
String.format(NOT_FOUND, getEntityName(), id));
}
// check for permission to edit
if (!canEdit(entityToEdit)) {
throw new RequestProcessingException(Response.Status.FORBIDDEN,
String.format(NOT_AUTHORIZED_TO_EDIT, getEntityName(), id));
}
// check for version conflicts so we can give a more useful message
if (entityToEdit.getVersion() != entity.getVersion()) {
throw new RequestProcessingException(Response.Status.CONFLICT,
String.format(VERSION_CONFLICT_ERROR, getEntityName(), id));
}
// make sure the path param matches the id of the entity they are putting
entity.setId(id);
beforeEdit(entityToEdit, entity);
List errors = validateEntity(entity);
if (errors.size() > 0) {
String[] errs = new String[errors.size()];
errors.toArray(errs);
throw new RequestProcessingException(Response.Status.BAD_REQUEST, errs);
}
boolean needsTx = noTx();
try {
if (needsTx) {
openTransaction();
}
getEntityManager().merge(entity);
if (needsTx) {
commit();
}
} catch (Exception e) {
if (needsTx) {
rollback();
}
LOG.log(Level.SEVERE, "Failed to save changes to single entity", e);
rollback();
throw new RequestProcessingException(
Response.Status.INTERNAL_SERVER_ERROR,
translateExceptionToMessage(e)
);
}
return get(id);
}
///////////////////////////////////PUT/////////////////////////////////////////
///////////////////////////////////DELETE/////////////////////////////////////////
/**
* Delete a single entity
*
* @param id of the entity to delete
* @return 204 if successful, otherwise error message
*/
@DELETE
@Path("{id}")
public Response delete(@PathParam("id") UUID id) {
checkLoggedIn();
T entity = getEntityWithId(id);
if (entity == null) {
throw new RequestProcessingException(Response.Status.NOT_FOUND,
String.format(S_WITH_ID_S_NOT_FOUND, getEntityName(), id));
}
if (!canDelete(entity)) {
throw new RequestProcessingException(Response.Status.FORBIDDEN,
String.format(NOT_AUTHORIZED_TO_DELETE, getEntityName(), entity.getId()));
}
try {
openTransaction();
try {
deleteEntity(entity);
} catch (Exception e) {
LOG.log(Level.SEVERE, "failed to delete single entity", e);
throw new RequestProcessingException(Response.Status.INTERNAL_SERVER_ERROR,
"Failed to delete resource", translateExceptionToMessage(e));
}
commit();
} catch (Exception e) {
rollback();
LOG.log(Level.SEVERE, "Failed to delete resource", e);
throw new RequestProcessingException(
Response.Status.CONFLICT,
translateExceptionToMessage(e)
);
}
return Response.status(Response.Status.NO_CONTENT).build();
}
@DELETE
public Response deleteAll() {
checkLoggedIn();
List toDelete = getListOfEntities(null, 0);
if (!toDelete.stream().allMatch(this::canDelete)) {
throw new RequestProcessingException(Response.Status.FORBIDDEN, "You are not permitted to delete all the entities matching your request.");
}
try {
openTransaction();
toDelete.stream().forEach(this::deleteEntity);
commit();
} catch (Exception e) {
rollback();
LOG.log(Level.SEVERE, "Failed to delete resources", e);
throw new RequestProcessingException(Response.Status.CONFLICT, "Failed to delete resource",
translateExceptionToMessage(e));
}
return Response.noContent().build();
}
// override this method delete by e.g. setting a field
private void deleteEntity(T entityToDelete) {
getEntityManager().remove(entityToDelete);
}
///////////////////////////////////DELETE END/////////////////////////////////////////
////////////////////////////TRANSACTION HELPERS////////////////////////////////////////////////////
private EntityTransaction etx;
public void openTransaction() {
if (etx != null) {
throw new RequestProcessingException(Response.Status.INTERNAL_SERVER_ERROR, "Transaction was opened twice.");
}
etx = getEntityManager().getTransaction();
etx.begin();
}
public boolean noTx() {
return (etx == null || !etx.isActive());
}
public void commit() {
if (etx == null) {
throw new RequestProcessingException(Response.Status.INTERNAL_SERVER_ERROR, "Transaction was closed while not open.");
}
getEntityManager().flush();
etx.commit();
etx = null;
}
public void rollback() {
if (etx == null) {
return;
}
etx.rollback();
etx = null;
}
/**
* Return the name of the entity class
*
* @return name of the entity class, used in error messages
*/
private String getEntityName() {
return getEntityClass().getSimpleName();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy