package com.gdpcons.scratch; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.WeakHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.validation.MessageInterpolator; import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator; import org.hibernate.validator.resourceloading.ResourceBundleLocator; /** * Resource bundle backed message interpolator. * * @author Emmanuel Bernard * @author Hardy Ferentschik * @author Gunnar Morling * @author Kevin Pollet - SERLI - (kevin.pollet@serli.com) */ public class ResourceBundleMessageInterpolator implements MessageInterpolator { /** * The name of the default message bundle. */ public static final String DEFAULT_VALIDATION_MESSAGES = "org.hibernate.validator.ValidationMessages"; /** * The name of the user-provided message bundle as defined in the specification. */ public static final String USER_VALIDATION_MESSAGES = "ValidationMessages"; /** * The name of the constraint-provided message bundle. */ public static final String CONSTRAINT_VALIDATION_MESSAGES = "ValidationMessages"; /** * Regular expression used to do message interpolation. */ private static final Pattern MESSAGE_PARAMETER_PATTERN = Pattern.compile( "(\\{[^\\}]+?\\})" ); /** * The default locale in the current JVM. */ private final Locale defaultLocale; /** * Loads user-specified resource bundles. */ private final ResourceBundleLocator userResourceBundleLocator; /** * Loads built-in resource bundles. */ private final ResourceBundleLocator defaultResourceBundleLocator; /** * Step 1-3 of message interpolation can be cached. We do this in this map. */ private final Map resolvedMessages = new WeakHashMap(); /** * Flag indicating whether this interpolator should chance some of the interpolation steps. */ private final boolean cacheMessages; public ResourceBundleMessageInterpolator() { this( null ); } public ResourceBundleMessageInterpolator(ResourceBundleLocator userResourceBundleLocator) { this( userResourceBundleLocator, true ); } public ResourceBundleMessageInterpolator(ResourceBundleLocator userResourceBundleLocator, boolean cacheMessages) { defaultLocale = Locale.getDefault(); if ( userResourceBundleLocator == null ) { this.userResourceBundleLocator = new PlatformResourceBundleLocator( USER_VALIDATION_MESSAGES ); } else { this.userResourceBundleLocator = userResourceBundleLocator; } this.defaultResourceBundleLocator = new PlatformResourceBundleLocator( DEFAULT_VALIDATION_MESSAGES ); this.cacheMessages = cacheMessages; } public String interpolate(String message, Context context) { // probably no need for caching, but it could be done by parameters since the map // is immutable and uniquely built per Validation definition, the comparison has to be based on == and not equals though return interpolateMessage( message, context, defaultLocale ); } public String interpolate(String message, Context context, Locale locale) { return interpolateMessage( message, context, locale ); } /** * Runs the message interpolation according to algorithm specified in JSR 303. *
* Note: *
* Look-ups in user bundles is recursive whereas look-ups in default bundle are not! * * @param message the message to interpolate * @param annotationParameters the parameters of the annotation for which to interpolate this message * @param locale the {@code Locale} to use for the resource bundle. * * @return the interpolated message. */ private String interpolateMessage(String message, Context context, Locale locale) { Map annotationParameters = context.getConstraintDescriptor().getAttributes(); LocalisedMessage localisedMessage = new LocalisedMessage( message, locale ); String resolvedMessage = null; if ( cacheMessages ) { resolvedMessage = resolvedMessages.get( localisedMessage ); } // if the message is not already in the cache we have to run step 1-3 of the message resolution if ( resolvedMessage == null ) { String constraintPackage = context.getConstraintDescriptor().getAnnotation().annotationType().getPackage().getName(); String constraintValidationMessagesResourceBundle = constraintPackage + "." + CONSTRAINT_VALIDATION_MESSAGES; ResourceBundleLocator constraintResourceBundleLocator = new PlatformResourceBundleLocator( constraintValidationMessagesResourceBundle ); ResourceBundle userResourceBundle = userResourceBundleLocator .getResourceBundle( locale ); ResourceBundle constraintResourceBundle = constraintResourceBundleLocator .getResourceBundle( locale ); /* note that we should probably cache the resource bundle in the constraint package for performance reasons; caching key should be the pair eg. something like this: (nota bene: using LocalisedMessage here as the caching key as constraintValidationMessages is a String) // in class declaration... // private final Map constraintResourceBundleLocatorCache = new WeakHashMap(); // then... LocalisedMessage constraintResourceBundleLocatorCacheKey = new LocalisedMessage(constraintValidationMessagesResourceBundle, locale); ResourceBundle constraintResourceBundle = constraintResourceBundleLocatorCache.get(constraintResourceBundleLocatorCacheKey); if (constraintResourceBundle == null) { ResourceBundleLocator constraintResourceBundleLocator = new PlatformResourceBundleLocator( constraintValidationMessagesResourceBundle ); constraintResourceBundle = constraintResourceBundleLocator.getResourceBundle( locale ); // the constraint ResourceBundle might not exist if (constraintResourceBundle != null) { constraintResourceBundleLocatorCache.put(constraintResourceBundleLocatorCacheKey, constraintResourceBundle); } } */ ResourceBundle defaultResourceBundle = defaultResourceBundleLocator .getResourceBundle( locale ); String newIterationResolvedMessage; resolvedMessage = message; boolean evaluatedDefaultBundleOnce = false; do { // search the user bundle recursive (step1) newIterationResolvedMessage = replaceVariables( resolvedMessage, userResourceBundle, locale, true ); // search the constraint bundle recursive (step1.5) newIterationResolvedMessage = replaceVariables( newIterationResolvedMessage, constraintResourceBundle, locale, true ); // exit condition - we have at least tried to validate against the default bundle and there was no // further replacements if ( evaluatedDefaultBundleOnce && !hasReplacementTakenPlace( newIterationResolvedMessage, resolvedMessage ) ) { break; } // search the default bundle non recursive (step2) resolvedMessage = replaceVariables( newIterationResolvedMessage, defaultResourceBundle, locale, false ); evaluatedDefaultBundleOnce = true; if ( cacheMessages ) { resolvedMessages.put( localisedMessage, resolvedMessage ); } } while ( true ); } // resolve annotation attributes (step 4) resolvedMessage = replaceAnnotationAttributes( resolvedMessage, annotationParameters ); // last but not least we have to take care of escaped literals resolvedMessage = resolvedMessage.replace( "\\{", "{" ); resolvedMessage = resolvedMessage.replace( "\\}", "}" ); resolvedMessage = resolvedMessage.replace( "\\\\", "\\" ); return resolvedMessage; } private boolean hasReplacementTakenPlace(String origMessage, String newMessage) { return !origMessage.equals( newMessage ); } private String replaceVariables(String message, ResourceBundle bundle, Locale locale, boolean recurse) { Matcher matcher = MESSAGE_PARAMETER_PATTERN.matcher( message ); StringBuffer sb = new StringBuffer(); String resolvedParameterValue; while ( matcher.find() ) { String parameter = matcher.group( 1 ); resolvedParameterValue = resolveParameter( parameter, bundle, locale, recurse ); matcher.appendReplacement( sb, Matcher.quoteReplacement( resolvedParameterValue ) ); } matcher.appendTail( sb ); return sb.toString(); } private String replaceAnnotationAttributes(String message, Map annotationParameters) { Matcher matcher = MESSAGE_PARAMETER_PATTERN.matcher( message ); StringBuffer sb = new StringBuffer(); while ( matcher.find() ) { String resolvedParameterValue; String parameter = matcher.group( 1 ); Object variable = annotationParameters.get( removeCurlyBrace( parameter ) ); if ( variable != null ) { resolvedParameterValue = variable.toString(); } else { resolvedParameterValue = parameter; } resolvedParameterValue = Matcher.quoteReplacement( resolvedParameterValue ); matcher.appendReplacement( sb, resolvedParameterValue ); } matcher.appendTail( sb ); return sb.toString(); } private String resolveParameter(String parameterName, ResourceBundle bundle, Locale locale, boolean recurse) { String parameterValue; try { if ( bundle != null ) { parameterValue = bundle.getString( removeCurlyBrace( parameterName ) ); if ( recurse ) { parameterValue = replaceVariables( parameterValue, bundle, locale, recurse ); } } else { parameterValue = parameterName; } } catch ( MissingResourceException e ) { // return parameter itself parameterValue = parameterName; } return parameterValue; } private String removeCurlyBrace(String parameter) { return parameter.substring( 1, parameter.length() - 1 ); } private static class LocalisedMessage { private final String message; private final Locale locale; LocalisedMessage(String message, Locale locale) { this.message = message; this.locale = locale; } @Override public boolean equals(Object o) { if ( this == o ) { return true; } if ( o == null || getClass() != o.getClass() ) { return false; } LocalisedMessage that = ( LocalisedMessage ) o; if ( locale != null ? !locale.equals( that.locale ) : that.locale != null ) { return false; } if ( message != null ? !message.equals( that.message ) : that.message != null ) { return false; } return true; } @Override public int hashCode() { int result = message != null ? message.hashCode() : 0; result = 31 * result + ( locale != null ? locale.hashCode() : 0 ); return result; } } }