package org.qi4j.library.struts2;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import ognl.MethodFailedException;
import ognl.ObjectMethodAccessor;
import ognl.ObjectPropertyAccessor;
import ognl.OgnlContext;
import ognl.OgnlException;
import ognl.OgnlRuntime;
import org.qi4j.api.Qi4j;
import org.qi4j.api.association.Association;
import org.qi4j.api.association.ManyAssociation;
import org.qi4j.api.association.NamedAssociation;
import org.qi4j.api.constraint.ConstraintViolation;
import org.qi4j.api.constraint.ConstraintViolationException;
import org.qi4j.api.injection.scope.Structure;
import org.qi4j.api.property.Property;
import org.qi4j.library.struts2.ConstraintViolationInterceptor.FieldConstraintViolations;
import static com.opensymphony.xwork2.conversion.impl.XWorkConverter.CONVERSION_PROPERTY_FULLNAME;
import static ognl.OgnlRuntime.getConvertedType;
import static ognl.OgnlRuntime.getFieldValue;
import static org.qi4j.library.struts2.ConstraintViolationInterceptor.CONTEXT_CONSTRAINT_VIOLATIONS;
/**
* <p>An implementation of the ObjectPropertyAccessor that provides conversion for Qi4j properties. The typical way that
* OGNL gets/sets object attributes is by finding the corresponding JavaBean getter/setter methods. This
* ObjectPropertyAccessor checks if there is a Qi4j property on the Composite and if there is uses the properties
* get/set methods.</p>
*
* <p>When setting Property values, if a ConstraintViolationException is thrown it is added to the context so that
* it can be processed and by the ConstraintViolationInterceptor, similar to how conversion exceptions are handled by
* the ConversionErrorInterceptor</p>
*
* <p>When setting Association values, we attempt to convert the value to the association type using the normal XWork
* converter mechanisms. If the type is an EntityComposite, we already have a converter registered
* {@link EntityCompositeConverter} to handle conversion from a string identity to an object. If the type is not an
* EntityComposite, but the actual values are EntityComposites, you can register the {@link EntityCompositeConverter}
* for your type in your xwork-conversion.properties file.</p>
*
* <p>NOTE: We can't do this as a regular converter because Qi4j composites doesn't (nor should it be) following the
* JavaBean standard. We might be able to only override the getProperty() method here and have regular converters for
* Property, Association and SetAssociation but I haven't tried that yet so it may not work as expected.</>
*
* <p>TODO: Doesn't yet handle ManyAssociations, but these shouldn't be too hard to add</p>
*/
public class Qi4jPropertyAccessor
extends ObjectPropertyAccessor
{
private static final Object[] BLANK_ARGUMENTS = new Object[0];
private final ObjectMethodAccessor methodAccessor = new ObjectMethodAccessor();
@Structure
Qi4j api;
@Override
public final Object getProperty( Map aContext, Object aTarget, Object aPropertyName )
throws OgnlException
{
String fieldName = aPropertyName.toString();
Object qi4jField = getQi4jField( aContext, aTarget, fieldName );
if( qi4jField != null )
{
Class<?> memberClass = qi4jField.getClass();
if( Property.class.isAssignableFrom( memberClass ) )
{
Property<?> property = (Property) qi4jField;
return property.get();
}
else if( Association.class.isAssignableFrom( memberClass ) )
{
Association<?> association = (Association) qi4jField;
return association.get();
}
else if( ManyAssociation.class.isAssignableFrom( memberClass ) )
{
return qi4jField;
}
else if( NamedAssociation.class.isAssignableFrom( memberClass ) )
{
return qi4jField;
}
}
return super.getProperty( aContext, aTarget, fieldName );
}
@SuppressWarnings( "unchecked" )
private Object getQi4jField( Map aContext, Object aTarget, String aFieldName )
throws OgnlException
{
if( aTarget != null )
{
// Is target#name a method? e.g. cat.name()
try
{
return methodAccessor.callMethod( aContext, aTarget, aFieldName, BLANK_ARGUMENTS );
}
catch( MethodFailedException e )
{
// Means not a property/association
}
// Is target#name a field? e.g. action.field1, where field1 is extracted from a composite
OgnlContext ognlContext = (OgnlContext) aContext;
try
{
return getFieldValue( ognlContext, aTarget, aFieldName, true );
}
catch( NoSuchFieldException e )
{
// Means not a field
}
}
return null;
}
@Override
@SuppressWarnings( "unchecked" )
public final void setProperty( Map aContext, Object aTarget, Object aPropertyName, Object aPropertyValue )
throws OgnlException
{
String fieldName = aPropertyName.toString();
Object qi4jField = getQi4jField( aContext, aTarget, fieldName );
if( qi4jField != null )
{
Class<?> memberClass = qi4jField.getClass();
if( Property.class.isAssignableFrom( memberClass ) )
{
Property property = (Property) qi4jField;
OgnlContext ognlContext = (OgnlContext) aContext;
Class<?> propertyType = (Class) api.propertyDescriptorFor( property ).type();
Object convertedValue = getConvertedType(
ognlContext, aTarget, null, fieldName, aPropertyValue, propertyType );
try
{
property.set( convertedValue );
}
catch( ConstraintViolationException e )
{
Collection<ConstraintViolation> violations = e.constraintViolations();
handleConstraintViolation( aContext, aTarget, fieldName, convertedValue, violations );
}
return;
}
else if( Association.class.isAssignableFrom( memberClass ) )
{
Association association = (Association) qi4jField;
OgnlContext ognlContext = (OgnlContext) aContext;
Class<?> associationType = (Class) api.associationDescriptorFor( association ).type();
Object convertedValue = getConvertedType(
ognlContext, aTarget, null, fieldName, aPropertyValue, associationType );
if( convertedValue == OgnlRuntime.NoConversionPossible )
{
throw new OgnlException( "Could not convert value to association type" );
}
try
{
association.set( convertedValue );
}
catch( ConstraintViolationException e )
{
Collection<ConstraintViolation> violations = e.constraintViolations();
handleConstraintViolation( aContext, aTarget, fieldName, aPropertyValue, violations );
}
return;
}
else if( ManyAssociation.class.isAssignableFrom( memberClass ) )
{
throw new OgnlException( "Setting many association [" + fieldName + "] is impossible." );
}
else if( NamedAssociation.class.isAssignableFrom( memberClass ) )
{
throw new OgnlException( "Setting named association [" + fieldName + "] is impossible." );
}
}
super.setProperty( aContext, aTarget, aPropertyName, aPropertyValue );
}
@SuppressWarnings( "unchecked" )
protected final void handleConstraintViolation(
Map aContext, Object aTarget, String aPropertyName, Object aPropertyValue,
Collection<ConstraintViolation> violations
)
{
Map<String, FieldConstraintViolations> allPropertyConstraintViolations
= (Map<String, FieldConstraintViolations>) aContext.get( CONTEXT_CONSTRAINT_VIOLATIONS );
if( allPropertyConstraintViolations == null )
{
allPropertyConstraintViolations = new HashMap<>();
aContext.put( CONTEXT_CONSTRAINT_VIOLATIONS, allPropertyConstraintViolations );
}
String realFieldName = aPropertyName;
String fieldFullName = (String) aContext.get( CONVERSION_PROPERTY_FULLNAME );
if( fieldFullName != null )
{
realFieldName = fieldFullName;
}
// Add another violation
allPropertyConstraintViolations.put(
realFieldName, new FieldConstraintViolations( aTarget, aPropertyName, aPropertyValue, violations ) );
}
}