blob: 0eecf04a3e57c249cee9a494e513f7a814e65d08 [file] [log] [blame]
/*
* 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.polygene.api.constraint;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.polygene.api.composite.CompositeDescriptor;
import org.apache.polygene.api.entity.EntityDescriptor;
import org.apache.polygene.api.identity.Identity;
import org.apache.polygene.api.service.ServiceDescriptor;
import org.apache.polygene.api.util.Classes;
/**
* 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 boolean longNames = Boolean.getBoolean( "polygene.constraints.longNames" );
private static final String DEFAULT_PATTERN = "\n\tConstraint Violation(s) in {0} of types [{3}].\n";
private static final String ENTITY_DEFAULT_PATTERN = "\n\tConstraint Violation(s) in entity {0} with id=[{2}].\n";
private static final String SERVICE_DEFAULT_PATTERN = "\n\tConstraint Violation(s) in service {0} with id=[{2}].\n";
private static final String MIXIN_DEFAULT_PATTERN = "\t\t@{2}({3}) on {0}.{1}(). Parameter [{4}] does not allow value [{5}].\n";
private String instanceToString; // arg {0}
private Class<?> primaryType; // arg {1}
private List<? extends Type> instanceTypes; // arg {2}
private Collection<ValueConstraintViolation> constraintViolations; // arg {4} and {5}
private String identity; // arg {6}
private boolean isService;
private boolean isEntity;
public ConstraintViolationException( Collection<ValueConstraintViolation> violations )
{
this.constraintViolations = new ArrayList<>();
this.constraintViolations.addAll( violations );
}
public Collection<ValueConstraintViolation> constraintViolations()
{
return constraintViolations;
}
/**
* Creates localized message of all the constraint violations that has occured.
* <p>
* Each ConstraintViolationException concerns one Composite instance, but may have many violations on that
* instance. For the composite instance related message following entries in the ResourceBundle will be searched
* for a pattern in the following order;
* </p>
* <ol>
* <li><code>polygene.constraint.<i><strong>CompositeType</strong></i></code></li>
* <li><code>polygene.constraint.composite</code></li>
* </ol>
* <p>
* where <strong><code><i>CompositeType</i></code></strong> is the
* class name of the Composite instance. If such key does not exist, or if the resourceBundle argument is null,
* then the default patterns will be used;
* </p>
* <table summary="Default localization of constraint violations for composite.">
* <tr><th>Type of Composite</th><th>Pattern used</th></tr>
* <tr><td>Composite</td>
* <td><code>\tConstraint Violation(s) in {0} with types {3}\n</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>
* The ResourceBundle arguments are defined as;
* <p>
* <p>
* 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>Primary Type of Composite</td>
* </tr>
* <tr>
* <td>{1}</td>
* <td>Composite instance toString()</td>
* </tr>
* <tr>
* <td>{2}</td>
* <td>Identity if composite implements HasIdentity</td>
* </tr>
* <tr>
* <td>{3}</td>
* <td>Comma-separeated list of types implemented by Composite</td>
* </tr>
* </table>
* <p>
* Once the message at the composite type level has been established, the message will contain each of the found
* violations. For each such violation, the resource bundle will be searched in the following order;
* <ol>
* <li><code>polygene.constraint.<i><strong>MixinType</strong></i>.<i><strong>member</strong></i></code></li>
* <li><code>polygene.constraint.<i><strong>MixinType</strong></i></code></li>
* <li><code>polygene.constraint.mixin</code></li>
* </ol>
* where <code><i><strong>MixinType</strong></i></code> refers to the mixin type of the member (method, field or
* constructor) and the <code><i><strong>member</strong></i></code> is the name of such Member.
* <table summary="Default localization of constraint violations for mixin.">
* <tr><th>Type of Composite</th><th>Pattern used</th></tr>
* <tr><td>Mixin</td>
* <td><code>\t\t@{2} {0}.{1} does not allow value [{4}]</code></td>
* </tr>
* </table>
* For these the ResourceBundle arguments are;
* <table summary="List of arguments available."><tr><th>Arg</th><th>Value</th></tr>
* <tr>
* <td>{0}</td>
* <td>Mixin Type Name</td>
* </tr>
* <tr>
* <td>{1}</td>
* <td>Mixin Member Name</td>
* </tr>
* <tr>
* <td>{2}</td>
* <td>Annotation type</td>
* </tr>
* <tr>
* <td>{3}</td>
* <td>Annotation toString</td>
* </tr>
* <tr>
* <td>{4}</td>
* <td>Name of the Member, see {@link Name}</td>
* </tr>
* <tr>
* <td>{5}</td>
* <td>Value attempted</td>
* </tr>
* </table>
*
* @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 localizedMessageFrom( ResourceBundle bundle )
{
Locale locale;
if( bundle != null )
{
locale = bundle.getLocale();
}
else
{
locale = Locale.getDefault();
}
StringBuffer message = new StringBuffer();
{
String[] searchKeys = new String[]{ "polygene.constraint." + primaryType, "polygene.constraint.composite" };
String compositePattern = findPattern( bundle, searchKeys, defaultPattern() );
String types = instanceTypes == null
? null
: instanceTypes.stream()
.map( this::nameOf )
.collect( Collectors.joining( "," ) );
String name;
if( longNames )
{
name = primaryType.getName();
}
else
{
name = primaryType.getSimpleName();
}
Object[] args = new Object[]{ name, instanceToString, identity, types };
MessageFormat formatter = new MessageFormat( compositePattern, locale );
formatter.format( args, message, null );
}
for( ValueConstraintViolation violation : constraintViolations )
{
String[] searchKeys = new String[]{ "polygene.constraint." + primaryType, "polygene.constraint.composite" };
String mixinPattern = findPattern( bundle, searchKeys, MIXIN_DEFAULT_PATTERN );
Annotation annotation = violation.constraint();
Class<? extends Annotation> annotatioType = annotation.annotationType();
Class<?> mixinType = violation.mixinType();
Object[] args = new Object[]
{
longNames ? mixinType.getName() : mixinType.getSimpleName(),
violation.methodName(),
longNames ? annotatioType.getName() : annotatioType.getSimpleName(),
annotation.toString(),
violation.name(),
violation.value()
};
MessageFormat formatter = new MessageFormat( mixinPattern, locale );
formatter.format( args, message, null );
}
String result = message.toString();
message.setLength( 0 ); // TODO: is this still needed to avoid JVM memory leak??
return result;
}
private String nameOf( Type type )
{
Class<?> clazz = Classes.RAW_CLASS.apply( type );
if( longNames )
{
return clazz.getName();
}
else
{
return clazz.getSimpleName();
}
}
@Override
public String getMessage()
{
return localizedMessageFrom( null );
}
private String findPattern( ResourceBundle bundle, String[] searchKeys, String defaultPattern )
{
String compositePattern;
if( bundle != null )
{
compositePattern = Stream.of( searchKeys )
.map( name -> findPattern( bundle, name ) )
.filter( Objects::nonNull )
.findFirst().orElse( defaultPattern );
}
else
{
compositePattern = defaultPattern;
}
return compositePattern;
}
private String findPattern( ResourceBundle bundle, String name )
{
try
{
return bundle.getString( name );
}
catch( Exception e )
{
return null;
}
}
private String defaultPattern()
{
if( isEntity )
{
return ENTITY_DEFAULT_PATTERN;
}
if( isService )
{
return SERVICE_DEFAULT_PATTERN;
}
return DEFAULT_PATTERN;
}
public void setCompositeDescriptor( CompositeDescriptor descriptor )
{
this.primaryType = descriptor.primaryType();
this.instanceTypes = descriptor.mixinTypes().collect( Collectors.toList() );
this.isEntity = descriptor instanceof EntityDescriptor;
this.isService = descriptor instanceof ServiceDescriptor;
}
public void setIdentity( Identity identity )
{
if( identity == null )
{
return;
}
this.identity = identity.toString();
}
public void setInstanceString( String instanceString )
{
instanceToString = instanceString;
}
}