ConstraintViolationException.java

/*
 * Copyright 2008 Niclas Hedhman. All rights Reserved.
 *
 * Licensed  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.qi4j.api.constraint;

import java.lang.annotation.Annotation;
import java.lang.reflect.Member;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import org.qi4j.api.Qi4j;
import org.qi4j.api.composite.Composite;
import org.qi4j.functional.Function;
import org.qi4j.functional.Iterables;

/**
 * This Exception is thrown when there is one or more Constraint Violations in a method
 * call.
 * <p>
 * The Constraint Violations are aggregated per method, and this exception will contain those
 * violations, together with the Composite instance it happened on as well as the Method that
 * was invoked. The Exception also has support for localized messages of these violations.
 * </p>
 */
public class ConstraintViolationException
    extends IllegalArgumentException
{
    private static final long serialVersionUID = 1L;

    private final Collection<ConstraintViolation> constraintViolations;
    private String methodName;
    private String mixinTypeName;
    private String instanceToString;
    private Iterable<Class<?>> instanceTypes;

    public ConstraintViolationException( Composite instance, Member method,
                                         Collection<ConstraintViolation> constraintViolations
    )
    {
        this( instance.toString(), Qi4j.FUNCTION_DESCRIPTOR_FOR.map( instance ).types(), method, constraintViolations );
    }

    public ConstraintViolationException( String instanceToString,
                                         Iterable<Class<?>> instanceTypes,
                                         Member method,
                                         Collection<ConstraintViolation> violations
    )
    {
        this.instanceToString = instanceToString;
        this.instanceTypes = instanceTypes;
        mixinTypeName = method.getDeclaringClass().getName();
        methodName = method.getName();
        this.constraintViolations = violations;
    }

    public ConstraintViolationException( String instanceToString,
                                         Iterable<Class<?>> instanceTypes,
                                         String mixinTypeName,
                                         String methodName,
                                         Collection<ConstraintViolation> violations
    )
    {
        this.instanceToString = instanceToString;
        this.instanceTypes = instanceTypes;
        this.mixinTypeName = mixinTypeName;
        this.methodName = methodName;
        this.constraintViolations = violations;
    }

    public Collection<ConstraintViolation> constraintViolations()
    {
        return constraintViolations;
    }

    /**
     * Creates localized messages of all the constraint violations that has occured.
     * <p>
     * The key "<code>Qi4j_ConstraintViolation_<i><strong>CompositeType</strong></i></code>" will be used to lookup the text formatting
     * pattern from the ResourceBundle, where <strong><code><i>CompositeType</i></code></strong> is the
     * class name of the Composite where the constraint was violated. If such key does not exist, then the
     * key &nbsp;"<code>Qi4j_ConstraintViolation</code>" will be used, and if that one also doesn't exist, or
     * the resourceBundle argument is null, then the default patterns will be used;
     * </p>
     * <table summary="Localization of constraint vioations.">
     * <tr><th>Type of Composite</th><th>Pattern used</th></tr>
     * <tr><td>Composite</td>
     * <td><code>Constraint Violation in {2}.{3} with constraint {4}, in composite \n{0} of type {1}</code></td>
     * </tr>
     * <tr><td>EntityComposite</td>
     * <td><code>Constraint Violation in {2}.{3} with constraint {4}, in entity {1}[id={0}]</code></td>
     * </tr>
     * <tr><td>ServiceComposite</td>
     * <td><code>Constraint Violation in {2}.{3} with constraint {4}, in service {0}</code></td>
     * </tr>
     * </table>
     * Then format each ConstraintViolation according to such pattern, where the following argument are passed;
     * <table summary="List of arguments available."><tr><th>Arg</th><th>Value</th></tr>
     * <tr>
     * <td>{0}</td>
     * <td>Composite instance toString()</td>
     * </tr>
     * <tr>
     * <td>{1}</td>
     * <td>CompositeType class name</td>
     * </tr>
     * <tr>
     * <td>{2}</td>
     * <td>MixinType class name</td>
     * </tr>
     * <tr>
     * <td>{3}</td>
     * <td>MixinType method name</td>
     * </tr>
     * <tr>
     * <td>{4}</td>
     * <td>Annotation toString()</td>
     * </tr>
     * <tr>
     * <td>{5}</td>
     * <td>toString() of value passed as the argument, or "null" text if argument was null.</td>
     * </tr>
     * </table>
     * <p>
     * <b>NOTE!!!</b> This class is still under construction and will be modified further.
     * </p>
     *
     * @param bundle The ResourceBundle for Localization, or null if default formatting and locale to be used.
     *
     * @return An array of localized messages of the violations incurred.
     */
    public String[] localizedMessagesFrom( ResourceBundle bundle )
    {
        String pattern = "Constraint violation in {0}.{1} for method ''{3}'' with constraint \"{4}({6})\", for value ''{5}''";

        ArrayList<String> list = new ArrayList<String>();
        for( ConstraintViolation violation : constraintViolations )
        {
            Locale locale;
            if( bundle != null )
            {
                try
                {
                    pattern = bundle.getString( "qi4j.constraint." + mixinTypeName + "." + methodName );
                }
                catch( MissingResourceException e1 )
                {
                    try
                    {
                        pattern = bundle.getString( "qi4j.constraint" );
                    }
                    catch( MissingResourceException e2 )
                    {
                        // ignore. The default pattern will be used.
                    }
                }
                locale = bundle.getLocale();
            }
            else
            {
                locale = Locale.getDefault();
            }
            MessageFormat format = new MessageFormat( pattern, locale );

            Annotation annotation = violation.constraint();
            String name = violation.name();
            Object value = violation.value();
            String classes;
            if( Iterables.count( instanceTypes ) == 1 )
            {
                classes = Iterables.first( instanceTypes ).getSimpleName();
            }
            else
            {
                classes = "[" + Iterables.<Class<?>>toString( instanceTypes, new Function<Class<?>, String>()
                {
                    @Override
                    public String map( Class<?> from )
                    {
                        return from.getSimpleName();
                    }
                }, "," ) + "]";
            }
            Object[] args = new Object[]
                {
                    instanceToString,
                    classes,
                    mixinTypeName,
                    methodName,
                    annotation.toString(),
                    "" + value,
                    name
                };
            StringBuffer text = new StringBuffer();
            format.format( args, text, null );
            list.add( text.toString() );
        }
        String[] result = new String[ list.size() ];
        list.toArray( result );
        return result;
    }

    public String localizedMessage()
    {
        String[] messages = localizedMessagesFrom( null );
        StringBuilder result = new StringBuilder();
        boolean first = true;
        for( String message : messages )
        {
            if( !first )
            {
                result.append( ',' );
            }
            first = false;
            result.append( message );
        }
        return result.toString();
    }

    @Override
    public String getLocalizedMessage()
    {
        return localizedMessage();
    }

    @Override
    public String getMessage()
    {
        return localizedMessage();
    }

    public String methodName()
    {
        return methodName;
    }

    public String mixinTypeName()
    {
        return mixinTypeName;
    }
}