blob: 8dfb92322b13d4024e03becbf4d9797ecb3d2724 [file] [log] [blame]
/*
* Copyright (c) 2007, 2020 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.jvnet.hk2.config;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TreeSet;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.validation.MessageInterpolator;
import jakarta.validation.Payload;
import jakarta.validation.metadata.ConstraintDescriptor;
/*
* Custom MessageInterpolatorImpl for HK2
*
* This message interpolator is different from the default one in the following
* ways:
*
* 1. It uses a class specified in the "payload" argument to the annotation to
* find the class loader to find the resource bundle for messages. This allows
* classes that are in OSGi modules to specify a resource bundle for bean
* validation messages.
*
* 2. The "LocalStrings" resource bundle within the same package as the payload
* class is used to search for messages. If a message is not found, the message
* is obtained from the default bean validation resource bundle.
*
* This class borrows heavily from the RI implementation of the ResourcBundleMessageInterpolator
* authored by Emmanuel Bernard, Hardy Ferentschik, and Gunnar Morling.
*/
public class MessageInterpolatorImpl 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";
/**
* Regular expression used to do message interpolation.
*/
private static final Pattern MESSAGE_PARAMETER_PATTERN = Pattern.compile("(\\{[^\\}]+?\\})");
/**
* The default locale for the current user.
*/
private final Locale defaultLocale = Locale.getDefault();
/**
* Step 1-3 of message interpolation can be cached. We do this in this map.
*/
private final Map<LocalisedMessage, String> resolvedMessages = new WeakHashMap<>();
/**
* Flag indicating whether this interpolator should chance some of the interpolation steps.
*/
private static final boolean cacheMessages = true;
public MessageInterpolatorImpl() { }
@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 interpolate(message, context, defaultLocale);
}
/**
* 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.
*/
@Override
public String interpolate(String message, Context context, Locale locale) {
Map<String, Object> 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) {
ResourceBundle userResourceBundle = new ContextResourceBundle(context, locale);
ClassLoader cl;
if (System.getSecurityManager()!=null) {
cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
@Override
public ClassLoader run() {
return MessageInterpolator.class.getClassLoader();
}
});
} else {
cl = MessageInterpolator.class.getClassLoader();
}
ResourceBundle defaultResourceBundle = null;
try {
defaultResourceBundle = ResourceBundle.getBundle(DEFAULT_VALIDATION_MESSAGES,
locale,
cl);
}
catch (MissingResourceException mre) {
// defaultResourceBundle is already null
}
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;
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, escapeMetaCharacters(resolvedParameterValue));
}
matcher.appendTail(sb);
return sb.toString();
}
private String replaceAnnotationAttributes(String message, Map<String, Object> 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 = escapeMetaCharacters(variable.toString());
} else {
resolvedParameterValue = parameter;
}
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);
}
/**
* @param s The string in which to replace the meta characters '$' and '\'.
*
* @return A string where meta characters relevant for {@link Matcher#appendReplacement} are escaped.
*/
private String escapeMetaCharacters(String s) {
String escapedString = s.replace("\\", "\\\\");
escapedString = escapedString.replace("$", "\\$");
return escapedString;
}
/*
* A resource bundle that takes strings from the annotation context. This class
* looks for the "LocalStrings" resource bundle in the same package as the
* class that is specified in the payload of the annotation. If a String
* cannot be found there, then it looks in the user resource bundle as defined
* in JSR-303.
*/
private static class ContextResourceBundle extends ResourceBundle {
ResourceBundle contextBundle;
ResourceBundle userBundle;
ContextResourceBundle(Context context, Locale locale) {
ConstraintDescriptor descriptor = context.getConstraintDescriptor();
Set<Class<? extends Payload>> payload = descriptor.getPayload();
// Load the LogStrings.properties for the argument Locale, from the ClassLoader
// of the payload.
if (!payload.isEmpty()) {
final Class payloadClass = payload.iterator().next();
String baseName = payloadClass.getPackage().getName() + ".LocalStrings";
ClassLoader cl;
if (System.getSecurityManager()!=null) {
cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
@Override
public ClassLoader run() {
return payloadClass.getClassLoader();
}
});
} else {
cl = payloadClass.getClassLoader();
}
try {
contextBundle = ResourceBundle.getBundle(baseName, locale, cl);
} catch (MissingResourceException mre) {
contextBundle = null;
}
}
try {
ClassLoader cl = System.getSecurityManager() == null
? Thread.currentThread().getContextClassLoader()
: AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
@Override
public ClassLoader run() {
return Thread.currentThread().getContextClassLoader();
}
});
userBundle = ResourceBundle.getBundle(USER_VALIDATION_MESSAGES, locale, cl);
} catch (MissingResourceException mre) {
userBundle = null;
}
if (userBundle != null) {
setParent(userBundle);
}
}
@Override
protected Object handleGetObject(String key) {
if (contextBundle != null) {
return contextBundle.getObject(key);
}
return null;
}
@Override
public Enumeration<String> getKeys() {
Set<String> keys = new TreeSet<>();
if (contextBundle != null) {
keys.addAll(Collections.list(contextBundle.getKeys()));
}
if (userBundle != null) {
keys.addAll(Collections.list(userBundle.getKeys()));
}
return Collections.enumeration(keys);
}
}
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;
}
}
}