package org.jboss.resteasy.plugins.validation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Set; import javax.enterprise.context.ApplicationScoped; import javax.validation.ConstraintViolation; import javax.validation.ConstraintViolationException; import javax.validation.MessageInterpolator; import javax.validation.ValidationException; import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.validation.executable.ExecutableType; import javax.validation.executable.ValidateOnExecution; import org.jboss.resteasy.api.validation.ConstraintType.Type; import org.jboss.resteasy.api.validation.ResteasyConstraintViolation; import org.jboss.resteasy.api.validation.ResteasyViolationException; import org.jboss.resteasy.cdi.CdiInjectorFactory; import org.jboss.resteasy.cdi.ResteasyCdiExtension; import org.jboss.resteasy.plugins.validation.i18n.LogMessages; import org.jboss.resteasy.plugins.validation.i18n.Messages; import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.InjectorFactory; import org.jboss.resteasy.spi.ResteasyConfiguration; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.validation.GeneralValidatorCDI; import org.jboss.resteasy.util.GetRestful; import com.fasterxml.classmate.Filter; import com.fasterxml.classmate.MemberResolver; import com.fasterxml.classmate.ResolvedType; import com.fasterxml.classmate.ResolvedTypeWithMembers; import com.fasterxml.classmate.TypeResolver; import com.fasterxml.classmate.members.RawMethod; import com.fasterxml.classmate.members.ResolvedMethod; /** * * @author <a href="ron.sigal@jboss.com">Ron Sigal</a> * @version $Revision: 1.1 $ * * Copyright May 23, 2013 */ public class GeneralValidatorImpl implements GeneralValidatorCDI { public static final String SUPPRESS_VIOLATION_PATH = "resteasy.validation.suppress.path"; /** * Used for resolving type parameters. Thread-safe. */ private TypeResolver typeResolver = new TypeResolver(); private ValidatorFactory validatorFactory; private boolean isExecutableValidationEnabled; private ExecutableType[] defaultValidatedExecutableTypes; private boolean suppressPath; private boolean cdiActive; public GeneralValidatorImpl(ValidatorFactory validatorFactory, boolean isExecutableValidationEnabled, Set<ExecutableType> defaultValidatedExecutableTypes) { this.validatorFactory = validatorFactory; this.isExecutableValidationEnabled = isExecutableValidationEnabled; this.defaultValidatedExecutableTypes = defaultValidatedExecutableTypes.toArray(new ExecutableType[]{}); try { cdiActive = ResteasyCdiExtension.isCDIActive(); LogMessages.LOGGER.debug(Messages.MESSAGES.resteasyCdiExtensionOnClasspath()); } catch (Throwable t) { // In case ResteasyCdiExtension is not on the classpath. LogMessages.LOGGER.debug(Messages.MESSAGES.resteasyCdiExtensionNotOnClasspath()); } ResteasyConfiguration context = ResteasyProviderFactory.getContextData(ResteasyConfiguration.class); if (context != null) { String s = context.getParameter(SUPPRESS_VIOLATION_PATH); if (s != null) { suppressPath = Boolean.parseBoolean(s); } } } @Override public void validate(HttpRequest request, Object object, Class<?>... groups) { Validator validator = getValidator(request); Set<ConstraintViolation<Object>> cvs = null; try { cvs = validator.validate(object, groups); } catch (Exception e) { SimpleViolationsContainer violationsContainer = getViolationsContainer(request, object); violationsContainer.setException(e); violationsContainer.setFieldsValidated(true); throw new ResteasyViolationException(violationsContainer); } SimpleViolationsContainer violationsContainer = getViolationsContainer(request, object); violationsContainer.addViolations(cvs); violationsContainer.setFieldsValidated(true); } @Override public void checkViolations(HttpRequest request) { // Called from resteasy-jaxrs only if two argument version of isValidatable() returns true. SimpleViolationsContainer violationsContainer = getViolationsContainer(request, null); Object target = violationsContainer.getTarget(); if (target != null && violationsContainer.isFieldsValidated()) { if (violationsContainer != null && violationsContainer.size() > 0) { throw new ResteasyViolationException(violationsContainer, request.getHttpHeaders().getAcceptableMediaTypes()); } } } @Override public void checkViolationsfromCDI(HttpRequest request) { if (request == null) { return; } SimpleViolationsContainer violationsContainer = SimpleViolationsContainer.class.cast(request.getAttribute(SimpleViolationsContainer.class.getName())); if (violationsContainer != null && violationsContainer.size() > 0) { throw new ResteasyViolationException(violationsContainer, request.getHttpHeaders().getAcceptableMediaTypes()); } } @Override public void validateAllParameters(HttpRequest request, Object object, Method method, Object[] parameterValues, Class<?>... groups) { Validator validator = getValidator(request); SimpleViolationsContainer violationsContainer = getViolationsContainer(request, object); if (method.getParameterTypes().length == 0) { checkViolations(request); return; } Set<ConstraintViolation<Object>> cvs = null; try { cvs = validator.forExecutables().validateParameters(object, method, parameterValues, groups); } catch (Exception e) { violationsContainer.setException(e); throw new ResteasyViolationException(violationsContainer); } violationsContainer.addViolations(cvs); if ((violationsContainer.isFieldsValidated() || !GetRestful.isRootResource(object.getClass()) || hasApplicationScope(object)) && violationsContainer.size() > 0) { throw new ResteasyViolationException(violationsContainer, request.getHttpHeaders().getAcceptableMediaTypes()); } } @Override public void validateReturnValue(HttpRequest request, Object object, Method method, Object returnValue, Class<?>... groups) { Validator validator = getValidator(request); SimpleViolationsContainer violationsContainer = getViolationsContainer(request, object); Set<ConstraintViolation<Object>> cvs = null; try { cvs = validator.forExecutables().validateReturnValue(object, method, returnValue, groups); } catch (Exception e) { violationsContainer.setException(e); throw new ResteasyViolationException(violationsContainer); } violationsContainer.addViolations(cvs); if (violationsContainer.size() > 0) { throw new ResteasyViolationException(violationsContainer, request.getHttpHeaders().getAcceptableMediaTypes()); } } @Override public boolean isValidatable(Class<?> clazz) { // Called from resteasy-jaxrs. if (cdiActive) { return false; } return true; } @Override public boolean isValidatable(Class<?> clazz, InjectorFactory injectorFactory) { try { // Called from resteasy-jaxrs. if (cdiActive && injectorFactory instanceof CdiInjectorFactory) { return false; } } catch (NoClassDefFoundError e) { // Shouldn't get here. Deliberately empty. } return true; } @Override public boolean isValidatableFromCDI(Class<?> clazz) { return true; } @Override public boolean isMethodValidatable(Method m) { if (!isExecutableValidationEnabled) { return false; } ExecutableType[] types = null; List<ExecutableType[]> typesList = getExecutableTypesOnMethodInHierarchy(m); if (typesList.size() > 1) { throw new ValidationException(Messages.MESSAGES.validateOnExceptionOnMultipleMethod()); } if (typesList.size() == 1) { types = typesList.get(0); } else { ValidateOnExecution voe = m.getDeclaringClass().getAnnotation(ValidateOnExecution.class); if (voe == null) { types = defaultValidatedExecutableTypes; } else { if (voe.type().length > 0) { types = voe.type(); } else { types = defaultValidatedExecutableTypes; } } } boolean isGetterMethod = isGetter(m); for (int i = 0; i < types.length; i++) { switch (types[i]) { case IMPLICIT: case ALL: return true; case NONE: continue; case NON_GETTER_METHODS: if (!isGetterMethod) { return true; } continue; case GETTER_METHODS: if (isGetterMethod) { return true; } continue; default: continue; } } return false; } protected List<ExecutableType[]> getExecutableTypesOnMethodInHierarchy(Method method) { Class<?> clazz = method.getDeclaringClass(); List<ExecutableType[]> typesList = new ArrayList<ExecutableType[]>(); while (clazz != null) { // We start by examining the method itself. Method superMethod = getSuperMethod(method, clazz); if (superMethod != null) { ExecutableType[] types = getExecutableTypesOnMethod(superMethod); if (types != null) { typesList.add(types); } } typesList.addAll(getExecutableTypesOnMethodInInterfaces(clazz, method)); clazz = clazz.getSuperclass(); } return typesList; } protected List<ExecutableType[]> getExecutableTypesOnMethodInInterfaces(Class<?> clazz, Method method) { List<ExecutableType[]> typesList = new ArrayList<ExecutableType[]>(); Class<?>[] interfaces = clazz.getInterfaces(); for (int i = 0; i < interfaces.length; i++) { Method interfaceMethod = getSuperMethod(method, interfaces[i]); if (interfaceMethod != null) { ExecutableType[] types = getExecutableTypesOnMethod(interfaceMethod); if (types != null) { typesList.add(types); } } List<ExecutableType[]> superList = getExecutableTypesOnMethodInInterfaces(interfaces[i], method); if (superList.size() > 0) { typesList.addAll(superList); } } return typesList; } static protected ExecutableType[] getExecutableTypesOnMethod(Method method) { ValidateOnExecution voe = method.getAnnotation(ValidateOnExecution.class); if (voe == null || voe.type().length == 0) { return null; } ExecutableType[] types = voe.type(); if (types == null || types.length == 0) { return null; } return types; } static protected boolean isGetter(Method m) { String name = m.getName(); Class<?> returnType = m.getReturnType(); if (returnType.equals(Void.class)) { return false; } if (m.getParameterTypes().length > 0) { return false; } if (name.startsWith("get")) { return true; } if (name.startsWith("is") && returnType.equals(boolean.class)) { return true; } return false; } static protected String convertArrayToString(Object o) { String result = null; if (o instanceof Object[]) { Object[] array = Object[].class.cast(o); StringBuffer sb = new StringBuffer("[").append(convertArrayToString(array[0])); for (int i = 1; i < array.length; i++) { sb.append(", ").append(convertArrayToString(array[i])); } sb.append("]"); result = sb.toString(); } else { result = (o == null ? "" : o.toString()); } return result; } /** * Returns a super method, if any, of a method in a class. * Here, the "super" relationship is reflexive. That is, a method * is a super method of itself. */ protected Method getSuperMethod(Method method, Class<?> clazz) { Method[] methods = clazz.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { if (overrides(method, methods[i])) { return methods[i]; } } return null; } /** * Checks, whether {@code subTypeMethod} overrides {@code superTypeMethod}. * * N.B. "Override" here is reflexive. I.e., a method overrides itself. * * @param subTypeMethod The sub type method (cannot be {@code null}). * @param superTypeMethod The super type method (cannot be {@code null}). * * @return Returns {@code true} if {@code subTypeMethod} overrides {@code superTypeMethod}, {@code false} otherwise. * * Taken from Hibernate Validator */ protected boolean overrides(Method subTypeMethod, Method superTypeMethod) { if (subTypeMethod == null || superTypeMethod == null) { throw new RuntimeException(Messages.MESSAGES.expectTwoNonNullMethods()); } if (!subTypeMethod.getName().equals(superTypeMethod.getName())) { return false; } if (subTypeMethod.getParameterTypes().length != superTypeMethod.getParameterTypes().length) { return false; } if (!superTypeMethod.getDeclaringClass().isAssignableFrom(subTypeMethod.getDeclaringClass())) { return false; } return parametersResolveToSameTypes(subTypeMethod, superTypeMethod); } /** * Taken from Hibernate Validator */ protected boolean parametersResolveToSameTypes(Method subTypeMethod, Method superTypeMethod) { if (subTypeMethod.getParameterTypes().length == 0) { return true; } ResolvedType resolvedSubType = typeResolver.resolve(subTypeMethod.getDeclaringClass()); MemberResolver memberResolver = new MemberResolver(typeResolver); memberResolver.setMethodFilter(new SimpleMethodFilter(subTypeMethod, superTypeMethod)); ResolvedTypeWithMembers typeWithMembers = memberResolver.resolve(resolvedSubType, null, null); ResolvedMethod[] resolvedMethods = typeWithMembers.getMemberMethods(); // The ClassMate doc says that overridden methods are flattened to one // resolved method. But that is the case only for methods without any // generic parameters. if (resolvedMethods.length == 1) { return true; } // For methods with generic parameters I have to compare the argument // types (which are resolved) of the two filtered member methods. for (int i = 0; i < resolvedMethods[0].getArgumentCount(); i++) { if (!resolvedMethods[0].getArgumentType(i).equals(resolvedMethods[1].getArgumentType(i))) { return false; } } return true; } @Override @SuppressWarnings({"rawtypes", "unchecked"}) public void checkForConstraintViolations(HttpRequest request, Exception e) { if (e instanceof InvocationTargetException) { Throwable t = InvocationTargetException.class.cast(e).getTargetException(); if (t instanceof ConstraintViolationException) { e = ConstraintViolationException.class.cast(t); } } if (e instanceof ConstraintViolationException) { SimpleViolationsContainer violationsContainer = getViolationsContainer(request, null); ConstraintViolationException cve = ConstraintViolationException.class.cast(e); Set cvs = cve.getConstraintViolations(); violationsContainer.addViolations(cvs); if (violationsContainer.size() > 0) { throw new ResteasyViolationException(violationsContainer, request.getHttpHeaders().getAcceptableMediaTypes()); } } Throwable t = e.getCause(); while (t != null && !(t instanceof ResteasyViolationException)) { t = t.getCause(); } if (t instanceof ResteasyViolationException) { throw ResteasyViolationException.class.cast(t); } } protected Validator getValidator(HttpRequest request) { Locale locale = getLocale(request); if (locale == null) { return validatorFactory.getValidator(); } MessageInterpolator interpolator = new LocaleSpecificMessageInterpolator(validatorFactory.getMessageInterpolator(), locale); return validatorFactory.usingContext().messageInterpolator(interpolator).getValidator(); } protected SimpleViolationsContainer getViolationsContainer(HttpRequest request, Object target) { if (request == null) { return new SimpleViolationsContainer(target); } SimpleViolationsContainer violationsContainer = SimpleViolationsContainer.class.cast(request.getAttribute(SimpleViolationsContainer.class.getName())); if (violationsContainer == null) { violationsContainer = new SimpleViolationsContainer(target); request.setAttribute(SimpleViolationsContainer.class.getName(), violationsContainer); } return violationsContainer; } private Locale getLocale(HttpRequest request) { if (request == null) { return null; } List<Locale> locales = request.getHttpHeaders().getAcceptableLanguages(); Locale locale = locales == null || locales.isEmpty() ? null : locales.get(0); return locale; } /** * A filter implementation filtering methods matching given methods. * * @author Gunnar Morling * * Taken from Hibernate Validator */ static protected class SimpleMethodFilter implements Filter<RawMethod> { private final Method method1; private final Method method2; private SimpleMethodFilter(Method method1, Method method2) { this.method1 = method1; this.method2 = method2; } @Override public boolean include(RawMethod element) { return element.getRawMember().equals(method1) || element.getRawMember().equals(method2); } } static protected class LocaleSpecificMessageInterpolator implements MessageInterpolator { private final MessageInterpolator interpolator; private final Locale locale; public LocaleSpecificMessageInterpolator(MessageInterpolator interpolator, Locale locale) { this.interpolator = interpolator; this.locale = locale; } @Override public String interpolate(String messageTemplate, Context context) { return interpolator.interpolate(messageTemplate, context, locale); } @Override public String interpolate(String messageTemplate, Context context, Locale locale) { return interpolator.interpolate(messageTemplate, context, locale); } } ResteasyConstraintViolation createResteasyConstraintViolation(ConstraintViolation<?> cv, Type ct) { String path = (suppressPath ? "*" : cv.getPropertyPath().toString()); ResteasyConstraintViolation rcv = new ResteasyConstraintViolation(ct, path, cv.getMessage(), (cv.getInvalidValue() == null ? "null" :cv.getInvalidValue().toString())); return rcv; } private boolean hasApplicationScope(Object o) { Class<?> clazz = o.getClass(); return clazz.getAnnotation(ApplicationScoped.class) != null; } }