/* * 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 net.formio.validation; import java.io.Serializable; import java.util.Arrays; import java.util.Locale; import java.util.Map; import java.util.MissingResourceException; import java.util.ResourceBundle; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.validation.MessageInterpolator; /** * Resource bundle backed message interpolator with * explicit passing of default locale. * * @author Emmanuel Bernard * @author Hardy Ferentschik * @author Gunnar Morling * @author Kevin Pollet kevin.pollet@serli.com (C) 2011 * @author Radek Beran (explicit passing of default locale, independent on hibernate classes) */ public class ResBundleMessageInterpolator implements MessageInterpolator { /** * The name of the default message bundle. */ public static final String DEFAULT_VALIDATION_MESSAGES = "ValidationMessages"; /** * Regular expression used to do message interpolation. */ private static final Pattern MESSAGE_PARAMETER_PATTERN = Pattern.compile( "(\\{[^\\}]+?\\})" ); /** * Default locale. */ private final Locale defaultLocale; /** * Loads user-specified resource bundles. */ private final ResBundleLocator userResourceBundleLocator; /** * Loads built-in resource bundles. */ private final ResBundleLocator defaultResourceBundleLocator; /** * Step 1-3 of message interpolation can be cached. We do this in this map. */ private final ConcurrentMap<LocalisedMessage, String> resolvedMessages = new ConcurrentHashMap<LocalisedMessage, String>(); /** * Flag indicating whether this interpolator should chance some of the interpolation steps. */ private final boolean cacheMessages; public ResBundleMessageInterpolator(ResBundleLocator userResourceBundleLocator, Locale defaultLocale) { this(userResourceBundleLocator, defaultLocale, true); } public ResBundleMessageInterpolator(ResBundleLocator userResourceBundleLocator, Locale defaultLocale, boolean cacheMessages) { if (userResourceBundleLocator == null) throw new IllegalArgumentException("userResourceBundleLocator cannot be null"); if (defaultLocale == null) throw new IllegalArgumentException("default locale cannot be null"); this.defaultLocale = defaultLocale; this.userResourceBundleLocator = userResourceBundleLocator; this.defaultResourceBundleLocator = new PlatformResBundleLocator( DEFAULT_VALIDATION_MESSAGES ); this.cacheMessages = cacheMessages; } @Override 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, ConstraintViolationMessage.toSortedSerializableArgs(context.getConstraintDescriptor().getAttributes()), defaultLocale); } @Override public String interpolate(String message, Context context, Locale locale) { return interpolateMessage(message, ConstraintViolationMessage.toSortedSerializableArgs(context.getConstraintDescriptor().getAttributes()), locale); } /** * Runs the message interpolation according to algorithm specified in JSR 303. * <br> * Note: * <br> * 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 or given message unresolved. */ protected String interpolateMessage(String message, Map<String, Serializable> annotationParameters, Locale locale) { 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 ) { ResourceBundle userResourceBundle = tryGetResourceBundle(this.userResourceBundleLocator, locale); ResourceBundle defaultResourceBundle = tryGetResourceBundle(this.defaultResourceBundleLocator, locale); String userBundleResolvedMessage; resolvedMessage = message; boolean evaluatedDefaultBundleOnce = false; do { // search the user bundle recursive (step1) userBundleResolvedMessage = replaceVariables( resolvedMessage, userResourceBundle, locale, true ); // exit condition - we have at least tried to validate against the default bundle and there was no // further replacements if ( evaluatedDefaultBundleOnce && !hasReplacementTakenPlace( userBundleResolvedMessage, resolvedMessage ) ) { break; } // search the default bundle non recursive (step2) resolvedMessage = replaceVariables( userBundleResolvedMessage, defaultResourceBundle, locale, false ); evaluatedDefaultBundleOnce = true; } while ( true ); } // cache resolved message if (cacheMessages) { String cachedResolvedMessage = resolvedMessages.putIfAbsent( localisedMessage, resolvedMessage ); if ( cachedResolvedMessage != null ) { resolvedMessage = cachedResolvedMessage; } } // 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 ResourceBundle tryGetResourceBundle(ResBundleLocator locator, Locale locale) { ResourceBundle rb = null; try { rb = locator.getResourceBundle(locale); } catch (MissingResourceException ex) { // intentionally ignored, message keys will not be translated if no bundle was found } return rb; } private boolean hasReplacementTakenPlace(String origMessage, String newMessage) { if (origMessage == null) return false; return !origMessage.equals( newMessage ); } private String replaceVariables(String message, ResourceBundle bundle, Locale locale, boolean recurse) { if (bundle == null) return message; 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<String, Serializable> annotationParameters) { if (message == null) return null; 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 ) { if ( variable.getClass().isArray() ) { resolvedParameterValue = Arrays.toString( (Object[]) variable ); } else { 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; } } }