blob: 0a341a67073b07395ed6aa6578aa2066ec576753 [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.bval.jsr303;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.validation.MessageInterpolator;
import org.apache.bval.jsr303.util.SecureActions;
import org.apache.commons.lang3.ArrayUtils;
/**
* Description: Resource bundle backed message interpolator.
* This message resolver resolve message descriptors
* into human-readable messages. It uses ResourceBundles to find the messages.
* This class is threadsafe.<br/>
*/
public class DefaultMessageInterpolator implements MessageInterpolator {
private static final Logger log = Logger.getLogger(DefaultMessageInterpolator.class.getName());
private static final String DEFAULT_VALIDATION_MESSAGES =
"org.apache.bval.jsr303.ValidationMessages";
private static final String USER_VALIDATION_MESSAGES = "ValidationMessages";
/** Regular expression used to do message interpolation. */
private static final Pattern messageParameterPattern =
Pattern.compile("(\\{[\\w\\.]+\\})");
/** The default locale for the current user. */
private Locale defaultLocale;
/** User specified resource bundles hashed against their locale. */
private final Map<Locale, ResourceBundle> userBundlesMap =
new ConcurrentHashMap<Locale, ResourceBundle>();
/** Builtin resource bundles hashed against their locale. */
private final Map<Locale, ResourceBundle> defaultBundlesMap =
new ConcurrentHashMap<Locale, ResourceBundle>();
/**
* Create a new DefaultMessageInterpolator instance.
*/
public DefaultMessageInterpolator() {
this(null);
}
/**
* Create a new DefaultMessageInterpolator instance.
* @param resourceBundle
*/
public DefaultMessageInterpolator(ResourceBundle resourceBundle) {
defaultLocale = Locale.getDefault();
if (resourceBundle == null) {
ResourceBundle bundle = getFileBasedResourceBundle(defaultLocale);
if (bundle != null) {
userBundlesMap.put(defaultLocale, bundle);
}
} else {
userBundlesMap.put(defaultLocale, resourceBundle);
}
defaultBundlesMap.put(defaultLocale,
ResourceBundle.getBundle(DEFAULT_VALIDATION_MESSAGES, defaultLocale));
}
/** {@inheritDoc} */
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 interpolate(message, context, defaultLocale);
}
/** {@inheritDoc} */
public String interpolate(String message, Context context, Locale locale) {
return interpolateMessage(message,
context.getConstraintDescriptor().getAttributes(), locale);
}
/**
* Runs the message interpolation according to algorithm specified in JSR 303.
* <br/>
* Note:
* <br/>
* Lookups in user bundles are recursive whereas lookups 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</code> to use for the resource bundle.
* @return the interpolated message.
*/
private String interpolateMessage(String message,
Map<String, Object> annotationParameters,
Locale locale) {
ResourceBundle userResourceBundle = findUserResourceBundle(locale);
ResourceBundle defaultResourceBundle = findDefaultResourceBundle(locale);
String userBundleResolvedMessage;
String 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 were 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);
// resolve annotation attributes (step 4)
resolvedMessage =
replaceAnnotationAttributes(resolvedMessage, annotationParameters);
// curly braces need to be scaped in the original msg, so unescape them now
resolvedMessage = resolvedMessage.replace( "\\{", "{" ).replace( "\\}", "}" ).replace( "\\\\", "\\" );
return resolvedMessage;
}
private boolean hasReplacementTakenPlace(String origMessage, String newMessage) {
return !origMessage.equals(newMessage);
}
/**
* Search current thread classloader for the resource bundle. If not found, search validator (this) classloader.
*
* @param locale The locale of the bundle to load.
* @return the resource bundle or <code>null</code> if none is found.
*/
private ResourceBundle getFileBasedResourceBundle(Locale locale) {
ResourceBundle rb = null;
final ClassLoader classLoader = doPrivileged(SecureActions.getContextClassLoader());
if (classLoader != null) {
rb = loadBundle(classLoader, locale,
USER_VALIDATION_MESSAGES + " not found by thread local classloader");
}
// 2011-03-27 jw: No privileged action required.
// A class can always access the classloader of itself and of subclasses.
if (rb == null) {
rb = loadBundle(
getClass().getClassLoader(),
locale,
USER_VALIDATION_MESSAGES + " not found by validator classloader"
);
}
if (rb != null) {
log.log(Level.FINEST, String.format("%s found", USER_VALIDATION_MESSAGES));
} else {
log.log(Level.FINEST, String.format("%s not found. Delegating to %s", USER_VALIDATION_MESSAGES, DEFAULT_VALIDATION_MESSAGES));
}
return rb;
}
private ResourceBundle loadBundle(ClassLoader classLoader, Locale locale,
String message) {
ResourceBundle rb = null;
try {
rb = ResourceBundle.getBundle(USER_VALIDATION_MESSAGES, locale, classLoader);
} catch (MissingResourceException e) {
log.fine(message);
}
return rb;
}
private String replaceVariables(String message, ResourceBundle bundle, Locale locale,
boolean recurse) {
Matcher matcher = messageParameterPattern.matcher(message);
StringBuffer sb = new StringBuffer(64);
String resolvedParameterValue;
while (matcher.find()) {
String parameter = matcher.group(1);
resolvedParameterValue = resolveParameter(parameter, bundle, locale, recurse);
matcher.appendReplacement(sb, sanitizeForAppendReplacement(resolvedParameterValue));
}
matcher.appendTail(sb);
return sb.toString();
}
private String replaceAnnotationAttributes(String message,
Map<String, Object> annotationParameters) {
Matcher matcher = messageParameterPattern.matcher(message);
StringBuffer sb = new StringBuffer(64);
while (matcher.find()) {
String resolvedParameterValue;
String parameter = matcher.group(1);
Object variable = annotationParameters.get(removeCurlyBrace(parameter));
if (variable != null) {
if (variable.getClass().isArray()) {
resolvedParameterValue = ArrayUtils.toString(variable);
} else {
resolvedParameterValue = variable.toString();
}
} else {
resolvedParameterValue = parameter;
}
matcher.appendReplacement(sb, sanitizeForAppendReplacement(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 ResourceBundle findDefaultResourceBundle(Locale locale) {
ResourceBundle bundle = defaultBundlesMap.get(locale);
if (bundle == null)
{
bundle = ResourceBundle.getBundle(DEFAULT_VALIDATION_MESSAGES, locale);
defaultBundlesMap.put(locale, bundle);
}
return bundle;
}
private ResourceBundle findUserResourceBundle(Locale locale) {
ResourceBundle bundle = userBundlesMap.get(locale);
if (bundle == null)
{
bundle = getFileBasedResourceBundle(locale);
if (bundle != null) {
userBundlesMap.put(locale, bundle);
}
}
return bundle;
}
/**
* Set the default locale used by this {@link DefaultMessageInterpolator}.
* @param locale
*/
public void setLocale(Locale locale) {
defaultLocale = locale;
}
/**
* Escapes the string to comply with
* {@link Matcher#appendReplacement(StringBuffer, String)} requirements.
*
* @param src
* The original string.
* @return The sanitized string.
*/
private String sanitizeForAppendReplacement(String src) {
return src.replace("\\", "\\\\").replace("$", "\\$");
}
/**
* Perform action with AccessController.doPrivileged() if a security manager is installed.
*
* @param action
* the action to run
* @return
* result of the action
*/
private static <T> T doPrivileged(final PrivilegedAction<T> action) {
if (System.getSecurityManager() != null) {
return AccessController.doPrivileged(action);
} else {
return action.run();
}
}
}