001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.context.support; 018 019import java.text.MessageFormat; 020import java.util.ArrayList; 021import java.util.List; 022import java.util.Locale; 023import java.util.Properties; 024 025import org.springframework.context.HierarchicalMessageSource; 026import org.springframework.context.MessageSource; 027import org.springframework.context.MessageSourceResolvable; 028import org.springframework.context.NoSuchMessageException; 029import org.springframework.util.ObjectUtils; 030 031/** 032 * Abstract implementation of the {@link HierarchicalMessageSource} interface, 033 * implementing common handling of message variants, making it easy 034 * to implement a specific strategy for a concrete MessageSource. 035 * 036 * <p>Subclasses must implement the abstract {@link #resolveCode} 037 * method. For efficient resolution of messages without arguments, the 038 * {@link #resolveCodeWithoutArguments} method should be overridden 039 * as well, resolving messages without a MessageFormat being involved. 040 * 041 * <p><b>Note:</b> By default, message texts are only parsed through 042 * MessageFormat if arguments have been passed in for the message. In case 043 * of no arguments, message texts will be returned as-is. As a consequence, 044 * you should only use MessageFormat escaping for messages with actual 045 * arguments, and keep all other messages unescaped. If you prefer to 046 * escape all messages, set the "alwaysUseMessageFormat" flag to "true". 047 * 048 * <p>Supports not only MessageSourceResolvables as primary messages 049 * but also resolution of message arguments that are in turn 050 * MessageSourceResolvables themselves. 051 * 052 * <p>This class does not implement caching of messages per code, thus 053 * subclasses can dynamically change messages over time. Subclasses are 054 * encouraged to cache their messages in a modification-aware fashion, 055 * allowing for hot deployment of updated messages. 056 * 057 * @author Juergen Hoeller 058 * @author Rod Johnson 059 * @see #resolveCode(String, java.util.Locale) 060 * @see #resolveCodeWithoutArguments(String, java.util.Locale) 061 * @see #setAlwaysUseMessageFormat 062 * @see java.text.MessageFormat 063 */ 064public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { 065 066 private MessageSource parentMessageSource; 067 068 private Properties commonMessages; 069 070 private boolean useCodeAsDefaultMessage = false; 071 072 073 @Override 074 public void setParentMessageSource(MessageSource parent) { 075 this.parentMessageSource = parent; 076 } 077 078 @Override 079 public MessageSource getParentMessageSource() { 080 return this.parentMessageSource; 081 } 082 083 /** 084 * Specify locale-independent common messages, with the message code as key 085 * and the full message String (may contain argument placeholders) as value. 086 * <p>May also link to an externally defined Properties object, e.g. defined 087 * through a {@link org.springframework.beans.factory.config.PropertiesFactoryBean}. 088 */ 089 public void setCommonMessages(Properties commonMessages) { 090 this.commonMessages = commonMessages; 091 } 092 093 /** 094 * Return a Properties object defining locale-independent common messages, if any. 095 */ 096 protected Properties getCommonMessages() { 097 return this.commonMessages; 098 } 099 100 /** 101 * Set whether to use the message code as default message instead of 102 * throwing a NoSuchMessageException. Useful for development and debugging. 103 * Default is "false". 104 * <p>Note: In case of a MessageSourceResolvable with multiple codes 105 * (like a FieldError) and a MessageSource that has a parent MessageSource, 106 * do <i>not</i> activate "useCodeAsDefaultMessage" in the <i>parent</i>: 107 * Else, you'll get the first code returned as message by the parent, 108 * without attempts to check further codes. 109 * <p>To be able to work with "useCodeAsDefaultMessage" turned on in the parent, 110 * AbstractMessageSource and AbstractApplicationContext contain special checks 111 * to delegate to the internal {@link #getMessageInternal} method if available. 112 * In general, it is recommended to just use "useCodeAsDefaultMessage" during 113 * development and not rely on it in production in the first place, though. 114 * @see #getMessage(String, Object[], Locale) 115 * @see org.springframework.validation.FieldError 116 */ 117 public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) { 118 this.useCodeAsDefaultMessage = useCodeAsDefaultMessage; 119 } 120 121 /** 122 * Return whether to use the message code as default message instead of 123 * throwing a NoSuchMessageException. Useful for development and debugging. 124 * Default is "false". 125 * <p>Alternatively, consider overriding the {@link #getDefaultMessage} 126 * method to return a custom fallback message for an unresolvable code. 127 * @see #getDefaultMessage(String) 128 */ 129 protected boolean isUseCodeAsDefaultMessage() { 130 return this.useCodeAsDefaultMessage; 131 } 132 133 134 @Override 135 public final String getMessage(String code, Object[] args, String defaultMessage, Locale locale) { 136 String msg = getMessageInternal(code, args, locale); 137 if (msg != null) { 138 return msg; 139 } 140 if (defaultMessage == null) { 141 String fallback = getDefaultMessage(code); 142 if (fallback != null) { 143 return fallback; 144 } 145 } 146 return renderDefaultMessage(defaultMessage, args, locale); 147 } 148 149 @Override 150 public final String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException { 151 String msg = getMessageInternal(code, args, locale); 152 if (msg != null) { 153 return msg; 154 } 155 String fallback = getDefaultMessage(code); 156 if (fallback != null) { 157 return fallback; 158 } 159 throw new NoSuchMessageException(code, locale); 160 } 161 162 @Override 163 public final String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException { 164 String[] codes = resolvable.getCodes(); 165 if (codes != null) { 166 for (String code : codes) { 167 String message = getMessageInternal(code, resolvable.getArguments(), locale); 168 if (message != null) { 169 return message; 170 } 171 } 172 } 173 String defaultMessage = getDefaultMessage(resolvable, locale); 174 if (defaultMessage != null) { 175 return defaultMessage; 176 } 177 throw new NoSuchMessageException(!ObjectUtils.isEmpty(codes) ? codes[codes.length - 1] : null, locale); 178 } 179 180 181 /** 182 * Resolve the given code and arguments as message in the given Locale, 183 * returning {@code null} if not found. Does <i>not</i> fall back to 184 * the code as default message. Invoked by {@code getMessage} methods. 185 * @param code the code to lookup up, such as 'calculator.noRateSet' 186 * @param args array of arguments that will be filled in for params 187 * within the message 188 * @param locale the locale in which to do the lookup 189 * @return the resolved message, or {@code null} if not found 190 * @see #getMessage(String, Object[], String, Locale) 191 * @see #getMessage(String, Object[], Locale) 192 * @see #getMessage(MessageSourceResolvable, Locale) 193 * @see #setUseCodeAsDefaultMessage 194 */ 195 protected String getMessageInternal(String code, Object[] args, Locale locale) { 196 if (code == null) { 197 return null; 198 } 199 if (locale == null) { 200 locale = Locale.getDefault(); 201 } 202 Object[] argsToUse = args; 203 204 if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { 205 // Optimized resolution: no arguments to apply, 206 // therefore no MessageFormat needs to be involved. 207 // Note that the default implementation still uses MessageFormat; 208 // this can be overridden in specific subclasses. 209 String message = resolveCodeWithoutArguments(code, locale); 210 if (message != null) { 211 return message; 212 } 213 } 214 215 else { 216 // Resolve arguments eagerly, for the case where the message 217 // is defined in a parent MessageSource but resolvable arguments 218 // are defined in the child MessageSource. 219 argsToUse = resolveArguments(args, locale); 220 221 MessageFormat messageFormat = resolveCode(code, locale); 222 if (messageFormat != null) { 223 synchronized (messageFormat) { 224 return messageFormat.format(argsToUse); 225 } 226 } 227 } 228 229 // Check locale-independent common messages for the given message code. 230 Properties commonMessages = getCommonMessages(); 231 if (commonMessages != null) { 232 String commonMessage = commonMessages.getProperty(code); 233 if (commonMessage != null) { 234 return formatMessage(commonMessage, args, locale); 235 } 236 } 237 238 // Not found -> check parent, if any. 239 return getMessageFromParent(code, argsToUse, locale); 240 } 241 242 /** 243 * Try to retrieve the given message from the parent {@code MessageSource}, if any. 244 * @param code the code to lookup up, such as 'calculator.noRateSet' 245 * @param args array of arguments that will be filled in for params 246 * within the message 247 * @param locale the locale in which to do the lookup 248 * @return the resolved message, or {@code null} if not found 249 * @see #getParentMessageSource() 250 */ 251 protected String getMessageFromParent(String code, Object[] args, Locale locale) { 252 MessageSource parent = getParentMessageSource(); 253 if (parent != null) { 254 if (parent instanceof AbstractMessageSource) { 255 // Call internal method to avoid getting the default code back 256 // in case of "useCodeAsDefaultMessage" being activated. 257 return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale); 258 } 259 else { 260 // Check parent MessageSource, returning null if not found there. 261 return parent.getMessage(code, args, null, locale); 262 } 263 } 264 // Not found in parent either. 265 return null; 266 } 267 268 /** 269 * Get a default message for the given {@code MessageSourceResolvable}. 270 * <p>This implementation fully renders the default message if available, 271 * or just returns the plain default message {@code String} if the primary 272 * message code is being used as a default message. 273 * @param resolvable the value object to resolve a default message for 274 * @param locale the current locale 275 * @return the default message, or {@code null} if none 276 * @since 4.3.6 277 * @see #renderDefaultMessage(String, Object[], Locale) 278 * @see #getDefaultMessage(String) 279 */ 280 protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) { 281 String defaultMessage = resolvable.getDefaultMessage(); 282 String[] codes = resolvable.getCodes(); 283 if (defaultMessage != null) { 284 if (!ObjectUtils.isEmpty(codes) && defaultMessage.equals(codes[0])) { 285 // Never format a code-as-default-message, even with alwaysUseMessageFormat=true 286 return defaultMessage; 287 } 288 return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale); 289 } 290 return (!ObjectUtils.isEmpty(codes) ? getDefaultMessage(codes[0]) : null); 291 } 292 293 /** 294 * Return a fallback default message for the given code, if any. 295 * <p>Default is to return the code itself if "useCodeAsDefaultMessage" is activated, 296 * or return no fallback else. In case of no fallback, the caller will usually 297 * receive a {@code NoSuchMessageException} from {@code getMessage}. 298 * @param code the message code that we couldn't resolve 299 * and that we didn't receive an explicit default message for 300 * @return the default message to use, or {@code null} if none 301 * @see #setUseCodeAsDefaultMessage 302 */ 303 protected String getDefaultMessage(String code) { 304 if (isUseCodeAsDefaultMessage()) { 305 return code; 306 } 307 return null; 308 } 309 310 311 /** 312 * Searches through the given array of objects, finds any MessageSourceResolvable 313 * objects and resolves them. 314 * <p>Allows for messages to have MessageSourceResolvables as arguments. 315 * @param args array of arguments for a message 316 * @param locale the locale to resolve through 317 * @return an array of arguments with any MessageSourceResolvables resolved 318 */ 319 @Override 320 protected Object[] resolveArguments(Object[] args, Locale locale) { 321 if (args == null) { 322 return new Object[0]; 323 } 324 List<Object> resolvedArgs = new ArrayList<Object>(args.length); 325 for (Object arg : args) { 326 if (arg instanceof MessageSourceResolvable) { 327 resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale)); 328 } 329 else { 330 resolvedArgs.add(arg); 331 } 332 } 333 return resolvedArgs.toArray(); 334 } 335 336 /** 337 * Subclasses can override this method to resolve a message without arguments 338 * in an optimized fashion, i.e. to resolve without involving a MessageFormat. 339 * <p>The default implementation <i>does</i> use MessageFormat, through 340 * delegating to the {@link #resolveCode} method. Subclasses are encouraged 341 * to replace this with optimized resolution. 342 * <p>Unfortunately, {@code java.text.MessageFormat} is not implemented 343 * in an efficient fashion. In particular, it does not detect that a message 344 * pattern doesn't contain argument placeholders in the first place. Therefore, 345 * it is advisable to circumvent MessageFormat for messages without arguments. 346 * @param code the code of the message to resolve 347 * @param locale the locale to resolve the code for 348 * (subclasses are encouraged to support internationalization) 349 * @return the message String, or {@code null} if not found 350 * @see #resolveCode 351 * @see java.text.MessageFormat 352 */ 353 protected String resolveCodeWithoutArguments(String code, Locale locale) { 354 MessageFormat messageFormat = resolveCode(code, locale); 355 if (messageFormat != null) { 356 synchronized (messageFormat) { 357 return messageFormat.format(new Object[0]); 358 } 359 } 360 return null; 361 } 362 363 /** 364 * Subclasses must implement this method to resolve a message. 365 * <p>Returns a MessageFormat instance rather than a message String, 366 * to allow for appropriate caching of MessageFormats in subclasses. 367 * <p><b>Subclasses are encouraged to provide optimized resolution 368 * for messages without arguments, not involving MessageFormat.</b> 369 * See the {@link #resolveCodeWithoutArguments} javadoc for details. 370 * @param code the code of the message to resolve 371 * @param locale the locale to resolve the code for 372 * (subclasses are encouraged to support internationalization) 373 * @return the MessageFormat for the message, or {@code null} if not found 374 * @see #resolveCodeWithoutArguments(String, java.util.Locale) 375 */ 376 protected abstract MessageFormat resolveCode(String code, Locale locale); 377 378}