io.katharsis.validation.internal.ConstraintViolationExceptionMapper Maven / Gradle / Ivy
The newest version!
package io.katharsis.validation.internal;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ElementKind;
import javax.validation.Path.Node;
import io.katharsis.errorhandling.ExceptionMapperHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.katharsis.core.internal.utils.PropertyUtils;
import io.katharsis.errorhandling.ErrorData;
import io.katharsis.errorhandling.ErrorDataBuilder;
import io.katharsis.errorhandling.ErrorResponse;
import io.katharsis.errorhandling.mapper.ExceptionMapper;
import io.katharsis.module.Module.ModuleContext;
import io.katharsis.resource.information.ResourceField;
import io.katharsis.resource.information.ResourceInformation;
import io.katharsis.resource.registry.RegistryEntry;
import io.katharsis.resource.registry.ResourceRegistry;
public class ConstraintViolationExceptionMapper implements ExceptionMapper {
private static final Logger LOGGER = LoggerFactory.getLogger(ConstraintViolationExceptionMapper.class);
private static final String HIBERNATE_PROPERTY_NODE_IMPL = "org.hibernate.validator.path.PropertyNode";
private static final Object HIBERNATE_PROPERTY_NODE_ENGINE_IMPL = "org.hibernate.validator.internal.engine.path.NodeImpl";
protected static final String META_RESOURCE_ID = "resourceId";
protected static final String META_RESOURCE_TYPE = "resourceType";
protected static final String META_TYPE_KEY = "type";
protected static final String META_TYPE_VALUE = "ConstraintViolation";
protected static final String META_MESSAGE_TEMPLATE = "messageTemplate";
private ModuleContext context;
static final int UNPROCESSABLE_ENTITY_422 = 422;
private static final String DEFAULT_PRIMARY_KEY_NAME = "id";
public ConstraintViolationExceptionMapper(ModuleContext context) {
this.context = context;
}
@Override
public ErrorResponse toErrorResponse(ConstraintViolationException cve) {
LOGGER.warn("a ConstraintViolationException occured", cve);
List errors = new ArrayList<>();
for (ConstraintViolation> violation : cve.getConstraintViolations()) {
ErrorDataBuilder builder = ErrorData.builder();
builder = builder.addMetaField(META_TYPE_KEY, META_TYPE_VALUE);
builder = builder.setStatus(String.valueOf(UNPROCESSABLE_ENTITY_422));
builder = builder.setTitle(violation.getMessage());
builder = builder.setCode(toCode(violation));
if (violation.getMessageTemplate() != null) {
builder = builder.addMetaField(META_MESSAGE_TEMPLATE, violation.getMessageTemplate());
}
// for now we just provide root resource validation information
// depending on bulk update spec, we might also provide the leaf information in the future
if (violation.getRootBean() != null) {
ResourceRef resourceRef = resolvePath(violation);
builder = builder.addMetaField(META_RESOURCE_ID, resourceRef.getRootResourceId());
builder = builder.addMetaField(META_RESOURCE_TYPE, resourceRef.getRootResourceType());
builder = builder.setSourcePointer(resourceRef.getRootSourcePointer());
}
ErrorData error = builder.build();
errors.add(error);
}
return ErrorResponse.builder().setStatus(UNPROCESSABLE_ENTITY_422).setErrorData(errors).build();
}
private String toCode(ConstraintViolation> violation) {
if (violation.getConstraintDescriptor() != null) {
Annotation annotation = violation.getConstraintDescriptor().getAnnotation();
if (annotation != null) {
Class> clazz = annotation.getClass();
Class> superclass = annotation.getClass().getSuperclass();
Class>[] interfaces = annotation.getClass().getInterfaces();
if (superclass == Proxy.class && interfaces.length == 1) {
clazz = interfaces[0];
}
return clazz.getName();
}
}
if (violation.getMessageTemplate() != null) {
return violation.getMessageTemplate().replace("{", "").replaceAll("}", "");
}
return null;
}
@Override
@SuppressWarnings({ "rawtypes", "unchecked" })
public ConstraintViolationException fromErrorResponse(ErrorResponse errorResponse) {
Set violations = new HashSet();
Iterable errors = errorResponse.getErrors();
for (ErrorData error : errors) {
ConstraintViolationImpl violation = ConstraintViolationImpl.fromError(context.getResourceRegistry(), error);
violations.add(violation);
}
return new ConstraintViolationException(null, violations);
}
@Override
public boolean accepts(ErrorResponse errorResponse) {
return ExceptionMapperHelper.accepts(errorResponse, UNPROCESSABLE_ENTITY_422, META_TYPE_VALUE);
}
/**
* Translate validated bean and root path into validated resource and
* resource path. For example, embeddables belonging to an entity resource
* are mapped back to an entity violation and a proper path to the
* embeddable attribute.
*
* @param violation to compute the reference
* @return computaed reference
*/
private ResourceRef resolvePath(ConstraintViolation> violation) {
Object resource = violation.getRootBean();
Object nodeObject = resource;
ResourceRef ref = new ResourceRef(resource);
Iterator iterator = violation.getPropertyPath().iterator();
while (iterator.hasNext()) {
Node node = iterator.next();
// ignore methods/parameters
if (node.getKind() == ElementKind.METHOD) {
continue;
}
if (node.getKind() == ElementKind.PARAMETER) {
resource = getParameterValue(node);
nodeObject = resource;
ref = new ResourceRef(resource);
assertResource(resource);
continue;
}
// visit list, set, map references
nodeObject = ref.getNodeReference(nodeObject, node);
ref.visitNode(nodeObject);
// visit property
nodeObject = ref.visitProperty(nodeObject, node);
}
return ref;
}
private void assertResource(Object resource) {
if (!isResource(resource.getClass())) {
throw new IllegalStateException("a resource must be used as root, got " + resource + " instead");
}
}
private static Object getValue(Node propertyNode) {
// bean validation not sufficient for sets
// not possible to access elements, reverting to
// Hibernate implementation
// TODO investigate other implementation next to
// hibernate, JSR 303 v1.1 not sufficient
if (propertyNode.getClass().getName().equals(HIBERNATE_PROPERTY_NODE_IMPL)
|| propertyNode.getClass().getName().equals(HIBERNATE_PROPERTY_NODE_ENGINE_IMPL)) { // NOSONAR
try {
Method parentMethod = propertyNode.getClass().getMethod("getParent");
Method valueMethod = propertyNode.getClass().getMethod("getValue");
Object parentNode = parentMethod.invoke(propertyNode);
if (parentNode != null) {
return valueMethod.invoke(parentNode);
}
else {
return valueMethod.invoke(propertyNode);
}
}
catch (Exception e) {
throw new UnsupportedOperationException(e);
}
}
else {
throw new UnsupportedOperationException(
"cannot convert violations for java.util.Set elements, consider using Hibernate validator");
}
}
private static Object getParameterValue(Node propertyNode) {
// bean validation not sufficient for sets
// not possible to access elements, reverting to
// Hibernate implementation
// TODO investigate other implementation next to
// hibernate, JSR 303 v1.1 not sufficient
if (propertyNode.getClass().getName().equals(HIBERNATE_PROPERTY_NODE_IMPL)
|| propertyNode.getClass().getName().equals(HIBERNATE_PROPERTY_NODE_ENGINE_IMPL)) { // NOSONAR
try {
Method valueMethod = propertyNode.getClass().getMethod("getValue");
return valueMethod.invoke(propertyNode);
}
catch (Exception e) {
throw new UnsupportedOperationException(e);
}
}
else {
throw new UnsupportedOperationException(
"cannot convert violations for java.util.Set elements, consider using Hibernate validator");
}
}
class ResourceRef {
private Object rootResource;
private Object leafResource;
private StringBuilder rootSourcePointer = new StringBuilder();
private StringBuilder leafSourcePointer = new StringBuilder();
public ResourceRef(Object resource) {
this.leafResource = resource;
this.rootResource = resource;
}
public String getRootSourcePointer() {
return rootSourcePointer.toString();
}
public Object visitProperty(Object nodeObject, Node node) {
Object next;
if (node.getKind() == ElementKind.PROPERTY) {
next = PropertyUtils.getProperty(nodeObject, node.getName());
}
else if (node.getKind() == ElementKind.BEAN) {
next = nodeObject;
}
else {
throw new UnsupportedOperationException("unknown node: " + node);
}
if (node.getName() != null) {
appendSeparator();
if (!isResource(nodeObject.getClass()) || isPrimaryKey(nodeObject.getClass(), node.getName())) {
// continue along attributes path or primary key on root
appendSourcePointer(node.getName());
}
else if (isAssociation(nodeObject.getClass(), node.getName())) {
appendSourcePointer("data/relationships/");
appendSourcePointer(node.getName());
}
else {
appendSourcePointer("data/attributes/");
appendSourcePointer(node.getName());
}
}
return next;
}
private Object getNodeReference(Object element, Node node) {
Integer index = node.getIndex();
Object key = node.getKey();
if (index != null) {
appendSeparator();
appendSourcePointer(index);
return ((List>) element).get(index);
}
else if (key != null) {
appendSeparator();
appendSourcePointer(key);
return ((Map, ?>) element).get(key);
}
else if (element instanceof Set && getValue(node) != null) {
Object elementEntry = getValue(node);
// since sets get translated to arrays, we do the same here
// katharsis-client allocates sets that preserver the order
// of arrays
List
© 2015 - 2024 Weber Informatics LLC | Privacy Policy