package org.qi4j.library.struts2;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.ValueStack;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.qi4j.api.Qi4j;
import org.qi4j.api.composite.Composite;
import org.qi4j.api.constraint.ConstraintViolation;
import org.qi4j.library.struts2.util.ClassNameMapper;
import static java.util.Collections.emptyMap;
import static org.qi4j.library.struts2.util.ClassNameFilters.removeSuffixes;
import static org.qi4j.library.struts2.util.ClassNames.classNameInDotNotation;
/**
* <p>ConstraintViolationInterceptor adds constraint violations from the ActionContext to the Action's field errors.</p>
*
* <p>This interceptor adds any error found in the {@link ActionContext}'s constraint violations map as a field error
* (provided that the action implements {@link ValidationAware}). In addition, any field that contains a constraint
* violation has its original value saved such that any subsequent requests for that value return the original value
* rather than the value in the action. This is important because if the value "abc" is submitted and can't be set
* on a property requiring at least 5 characters, we want to display the original string ("abc") again rather than the
* original value (likely an empty string, which would make very little sense to the user).</p>
*
* <p>This is similar, in principle, to the XWork ConversionErrorInterceptor and much of the code is reflects that.</p>
*/
public class ConstraintViolationInterceptor
extends AbstractInterceptor
{
static final long serialVersionUID = 1L;
public static final String CONTEXT_CONSTRAINT_VIOLATIONS = ConstraintViolationInterceptor.class.getName() + ".constraintViolations";
protected Object getOverrideExpr( ActionInvocation invocation, FieldConstraintViolations violations )
{
return "'" + violations.value() + "'";
}
@Override
public String intercept( ActionInvocation invocation )
throws Exception
{
ActionContext invocationContext = invocation.getInvocationContext();
ValueStack stack = invocationContext.getValueStack();
Object action = invocation.getAction();
if( action instanceof ValidationAware )
{
ValidationAware va = (ValidationAware) action;
HashMap<Object, Object> propertyOverrides = new HashMap<Object, Object>();
for( Map.Entry<String, FieldConstraintViolations> fieldViolations : fieldConstraintViolations( invocationContext )
.entrySet() )
{
addConstraintViolationFieldErrors( stack, va, fieldViolations.getKey(), fieldViolations.getValue() );
propertyOverrides.put( fieldViolations.getKey(), getOverrideExpr( invocation, fieldViolations.getValue() ) );
}
// if there were some errors, put the original (fake) values in place right before the result
if( !propertyOverrides.isEmpty() )
{
overrideActionValues( invocation, stack, propertyOverrides );
}
}
return invocation.invoke();
}
private void overrideActionValues(
ActionInvocation invocation, ValueStack stack, final HashMap<Object, Object> propertyOverrides
)
{
invocation.addPreResultListener( new PreResultListener()
{
@Override
public void beforeResult( ActionInvocation invocation, String resultCode )
{
invocation.getStack().setExprOverrides( propertyOverrides );
}
} );
}
private void addConstraintViolationFieldErrors(
ValueStack stack, ValidationAware va, String fieldName, FieldConstraintViolations violations
)
{
for( ConstraintViolation constraintViolation : violations.constraintViolations() )
{
Object target = violations.target();
String message = message( target, violations.propertyName(), constraintViolation, stack );
va.addFieldError( fieldName, message );
}
}
@SuppressWarnings( "unchecked" )
private Map<String, FieldConstraintViolations> fieldConstraintViolations( ActionContext context )
{
Map<String, FieldConstraintViolations> violations =
(Map<String, FieldConstraintViolations>) context.get( CONTEXT_CONSTRAINT_VIOLATIONS );
if( violations == null )
{
return emptyMap();
}
return violations;
}
protected String message( Object target,
String propertyName,
ConstraintViolation constraintViolation,
ValueStack stack
)
{
String messageKey = messageKey( target, propertyName, constraintViolation );
String getTextExpression = "getText('" + messageKey + "')";
String message = (String) stack.findValue( getTextExpression );
if( message == null )
{
message = messageKey;
}
return message;
}
/**
* <p>The message key is generated based on the type of the target, the name of the property and the type of the
* constraint violation. So, if the target has type ItemEntity with a name property that has a not empty constraint
* and the user doesn't enter anything for the value, the corresponding message key would be
* 'item.name.not.empty.constraint.violated'.</p>
*
* <p>Note that if the type name of the target ends with 'Composite' or 'Entity', those will be removed and the
* rest of the name will be converted from camel-case to a dot notation. This is true of the constraint types as
* well. So a constraint named NotEmpty will be converted to not.empty as in the example above.</p>
* @param target JAVADOC
* @param propertyName JAVADOC
* @param violation JAVADOC
* @return JAVADOC
*/
protected String messageKey( Object target, String propertyName, ConstraintViolation violation )
{
Iterable<Class<?>> types;
if( target instanceof Composite )
{
Composite composite = (Composite) target;
types = Qi4j.FUNCTION_DESCRIPTOR_FOR.map( composite ).types();
}
else
{
ArrayList<Class<?>> list = new ArrayList<Class<?>>( 1 );
list.add(target.getClass());
types = list;
}
return classNameInDotNotation( types, withoutCompositeOrEntitySuffix )
+ "." + propertyName
+ "." + constraintKeyPart( violation )
+ ".constraint.violated";
}
private static final ClassNameMapper withoutCompositeOrEntitySuffix = removeSuffixes( "Composite", "Entity" );
private String constraintKeyPart( ConstraintViolation constraintViolation )
{
return classNameInDotNotation( constraintViolation.constraint().annotationType() );
}
public static class FieldConstraintViolations
{
private final Object target;
private final String propertyName;
private final Object value;
private final Collection<ConstraintViolation> constraintViolations;
public FieldConstraintViolations(
Object aTarget,
String aPropertyName,
Object aValue,
Collection<ConstraintViolation> constraintViolations
)
{
target = aTarget;
propertyName = aPropertyName;
value = aValue;
this.constraintViolations = constraintViolations;
}
public Object target()
{
return target;
}
public String propertyName()
{
return propertyName;
}
public Object value()
{
return value;
}
public Collection<ConstraintViolation> constraintViolations()
{
return constraintViolations;
}
}
}