/* * $Id$ * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.struts.beanvalidation.validation.interceptor; import com.opensymphony.xwork2.ActionInvocation; import com.opensymphony.xwork2.ActionProxy; import com.opensymphony.xwork2.ModelDriven; import com.opensymphony.xwork2.TextProviderFactory; import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor; import com.opensymphony.xwork2.util.AnnotationUtils; import com.opensymphony.xwork2.validator.DelegatingValidatorContext; import com.opensymphony.xwork2.validator.ValidatorContext; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts.beanvalidation.validation.constant.ValidatorConstants; import org.apache.struts2.interceptor.validation.SkipValidation; import javax.validation.ConstraintViolation; import javax.validation.Validator; import java.lang.reflect.Method; import java.util.Set; /** * <p> * Bean Validation interceptor. This Interceptor does not itself provide any Bean validation functionality but * works as a bridge between Bean validation implementations like Apache Bval or Hibernate Validator and Struts2 validation mechanism. * </p> * <p> * Interceptor will create a Validation Factory based on the provider class and will validate requested method or Action * class. Hibernate bean validator will be used as a default validator in case of no provider class will be supplied to * the interceptor. * </p> */ public class BeanValidationInterceptor extends MethodFilterInterceptor { private static final Logger LOG = LogManager.getLogger(BeanValidationInterceptor.class); protected BeanValidationManager beanValidationManager; protected TextProviderFactory textProviderFactory; protected boolean convertToUtf8 = false; protected String convertFromEncoding = "ISO-8859-1"; @Inject() public void setBeanValidationManager(BeanValidationManager beanValidationManager) { this.beanValidationManager = beanValidationManager; } @Inject public void setTextProviderFactory(TextProviderFactory textProviderFactory) { this.textProviderFactory = textProviderFactory; } @Inject(value = ValidatorConstants.CONVERT_MESSAGE_TO_UTF8, required = false) public void setConvertToUtf8(String convertToUtf8) { this.convertToUtf8 = BooleanUtils.toBoolean(convertToUtf8); } @Inject(value = ValidatorConstants.CONVERT_MESSAGE_FROM, required = false) public void setConvertFromEncoding(String convertFromEncoding) { this.convertFromEncoding = convertFromEncoding; } @Override protected String doIntercept(ActionInvocation invocation) throws Exception { Validator validator = this.beanValidationManager.getValidator(); if (validator == null) { LOG.debug("There is no Bean Validator configured in class path. Skipping Bean validation.."); return invocation.invoke(); } LOG.debug("Starting bean validation using validator: {}", validator.getClass()); Object action = invocation.getAction(); ActionProxy actionProxy = invocation.getProxy(); String methodName = actionProxy.getMethod(); if (LOG.isDebugEnabled()) { LOG.debug("Validating [{}/{}] with method [{}]", invocation.getProxy().getNamespace(), invocation.getProxy().getActionName(), methodName); } if (null == AnnotationUtils.findAnnotation(getActionMethod(action.getClass(), methodName), SkipValidation.class)) { // performing bean validation on action performBeanValidation(action, validator); } return invocation.invoke(); } protected void performBeanValidation(Object action, Validator validator) { LOG.trace("Initiating bean validation.."); Set<ConstraintViolation<Object>> constraintViolations; if (action instanceof ModelDriven) { LOG.trace("Performing validation on model.."); Object model = (Object)((ModelDriven<?>) action).getModel(); constraintViolations = validator.validate(model); } else { LOG.trace("Performing validation on action.."); constraintViolations = validator.validate(action); } addBeanValidationErrors(constraintViolations, action); } @SuppressWarnings("nls") private void addBeanValidationErrors(Set<ConstraintViolation<Object>> constraintViolations, Object action) { if (constraintViolations != null) { ValidatorContext validatorContext = new DelegatingValidatorContext(action, textProviderFactory); for (ConstraintViolation<Object> constraintViolation : constraintViolations) { String key = constraintViolation.getMessage(); String message = key; try { message = validatorContext.getText(key); if (convertToUtf8 && StringUtils.isNotBlank(message)) { message = new String(message.getBytes(convertFromEncoding), "UTF-8"); } } catch (Exception e) { LOG.error("Error while trying to fetch message: {}", key, e); } if (isActionError(constraintViolation)) { LOG.debug("Adding action error [{}]", message); validatorContext.addActionError(message); } else { ValidationError validationError = buildBeanValidationError(constraintViolation, message); String fieldName = validationError.getFieldName(); if (action instanceof ModelDriven && fieldName.startsWith(ValidatorConstants.MODELDRIVEN_PREFIX)) { fieldName = fieldName.replace("model.", ValidatorConstants.EMPTY_SPACE); } if (LOG.isDebugEnabled()) { LOG.debug("Adding field error [{}] with message [{}]", fieldName, validationError.getMessage()); } validatorContext.addFieldError(fieldName, validationError.getMessage()); } } } } protected ValidationError buildBeanValidationError(ConstraintViolation<Object> violation, String message) { if (violation.getPropertyPath().iterator().next().getName() != null) { String fieldName = violation.getPropertyPath().toString(); String finalMessage = StringUtils.removeStart(message, fieldName + ValidatorConstants.FIELD_SEPERATOR); return new ValidationError(fieldName, finalMessage); } return null; } /** * Decide if a violation should be added to the fieldErrors or actionErrors * * @param violation the violation * * @return true if violation should be added to the fieldErrors or actionErrors */ protected boolean isActionError(ConstraintViolation<Object> violation) { return violation.getLeafBean() == violation.getInvalidValue(); } /** * This is copied from DefaultActionInvocation * * @param actionClass the action class * @param methodName the method name * * @return Method * * @throws NoSuchMethodException if no method with this name was found */ protected Method getActionMethod(Class<?> actionClass, String methodName) throws NoSuchMethodException { Method method; method = actionClass.getMethod(methodName); return method; } /** * Inner class for validation error * Nice concept taken from Oval plugin. */ class ValidationError { private final String fieldName; private final String message; ValidationError(String fieldName, String message) { this.fieldName = fieldName; this.message = message; } public String getFieldName() { return this.fieldName; } public String getMessage() { return this.message; } } }