/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 1997-2010 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ /* * Portions of this code are subject to the following copyright: * * JBoss, Home of Professional Open Source * Copyright 2009, Red Hat, Inc. and/or its affiliates, and individual contributors * by the @authors tag. See the copyright.txt in the distribution for a * full listing of individual contributors. * * Licensed 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.jvnet.hk2.config; 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 javax.validation.MessageInterpolator; import javax.validation.Payload; import javax.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 resolvedMessages = new WeakHashMap(); /** * Flag indicating whether this interpolator should chance some of the interpolation steps. */ private 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. *
* 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. */ @Override public String interpolate(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) { ResourceBundle userResourceBundle = new ContextResourceBundle(context, locale); ResourceBundle defaultResourceBundle = ResourceBundle.getBundle(DEFAULT_VALIDATION_MESSAGES, locale, Thread.currentThread().getContextClassLoader()); 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 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> payload = descriptor.getPayload(); // Load the LogStrings.properties for the argument Locale, from the ClassLoader // of the payload. if (!payload.isEmpty()) { Class payloadClass = payload.iterator().next(); String baseName = payloadClass.getPackage().getName() + ".LocalStrings"; ClassLoader cl = payloadClass.getClassLoader(); try { contextBundle = ResourceBundle.getBundle(baseName, locale, cl); } catch (MissingResourceException mre) { contextBundle = null; } } try { userBundle = ResourceBundle.getBundle(USER_VALIDATION_MESSAGES, locale, Thread.currentThread().getContextClassLoader()); } 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 getKeys() { Set 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; } } }