• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

TAKETODAY / today-infrastructure / 16671461024

01 Aug 2025 09:25AM UTC coverage: 81.784% (+0.006%) from 81.778%
16671461024

push

github

TAKETODAY
:white_check_mark:

59515 of 77725 branches covered (76.57%)

Branch coverage included in aggregate %.

140858 of 167278 relevant lines covered (84.21%)

3.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.74
today-context/src/main/java/infra/context/support/AbstractMessageSource.java
1
/*
2
 * Copyright 2017 - 2025 the original author or authors.
3
 *
4
 * This program is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see [https://www.gnu.org/licenses/]
16
 */
17

18
package infra.context.support;
19

20
import java.text.MessageFormat;
21
import java.util.ArrayList;
22
import java.util.List;
23
import java.util.Locale;
24
import java.util.Properties;
25

26
import infra.beans.factory.config.PropertiesFactoryBean;
27
import infra.context.HierarchicalMessageSource;
28
import infra.context.MessageSource;
29
import infra.context.MessageSourceResolvable;
30
import infra.context.NoSuchMessageException;
31
import infra.lang.Nullable;
32
import infra.util.ObjectUtils;
33
import infra.validation.FieldError;
34

35
/**
36
 * Abstract implementation of the {@link HierarchicalMessageSource} interface,
37
 * implementing common handling of message variants, making it easy
38
 * to implement a specific strategy for a concrete MessageSource.
39
 *
40
 * <p>Subclasses must implement the abstract {@link #resolveCode}
41
 * method. For efficient resolution of messages without arguments, the
42
 * {@link #resolveCodeWithoutArguments} method should be overridden
43
 * as well, resolving messages without a MessageFormat being involved.
44
 *
45
 * <p><b>Note:</b> By default, message texts are only parsed through
46
 * MessageFormat if arguments have been passed in for the message. In case
47
 * of no arguments, message texts will be returned as-is. As a consequence,
48
 * you should only use MessageFormat escaping for messages with actual
49
 * arguments, and keep all other messages unescaped. If you prefer to
50
 * escape all messages, set the "alwaysUseMessageFormat" flag to "true".
51
 *
52
 * <p>Supports not only MessageSourceResolvables as primary messages
53
 * but also resolution of message arguments that are in turn
54
 * MessageSourceResolvables themselves.
55
 *
56
 * <p>This class does not implement caching of messages per code, thus
57
 * subclasses can dynamically change messages over time. Subclasses are
58
 * encouraged to cache their messages in a modification-aware fashion,
59
 * allowing for hot deployment of updated messages.
60
 *
61
 * @author Juergen Hoeller
62
 * @author Rod Johnson
63
 * @see #resolveCode(String, java.util.Locale)
64
 * @see #resolveCodeWithoutArguments(String, java.util.Locale)
65
 * @see #setAlwaysUseMessageFormat
66
 * @see java.text.MessageFormat
67
 */
68
public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource {
2✔
69

70
  @Nullable
71
  private MessageSource parentMessageSource;
72

73
  @Nullable
74
  private Properties commonMessages;
75

76
  private boolean useCodeAsDefaultMessage = false;
4✔
77

78
  @Override
79
  public void setParentMessageSource(@Nullable MessageSource parent) {
80
    this.parentMessageSource = parent;
3✔
81
  }
1✔
82

83
  @Override
84
  @Nullable
85
  public MessageSource getParentMessageSource() {
86
    return this.parentMessageSource;
3✔
87
  }
88

89
  /**
90
   * Specify locale-independent common messages, with the message code as key
91
   * and the full message String (may contain argument placeholders) as value.
92
   * <p>May also link to an externally defined Properties object, e.g. defined
93
   * through a {@link PropertiesFactoryBean}.
94
   */
95
  public void setCommonMessages(@Nullable Properties commonMessages) {
96
    this.commonMessages = commonMessages;
3✔
97
  }
1✔
98

99
  /**
100
   * Return a Properties object defining locale-independent common messages, if any.
101
   */
102
  @Nullable
103
  protected Properties getCommonMessages() {
104
    return this.commonMessages;
3✔
105
  }
106

107
  /**
108
   * Set whether to use the message code as default message instead of
109
   * throwing a NoSuchMessageException. Useful for development and debugging.
110
   * Default is "false".
111
   * <p>Note: In case of a MessageSourceResolvable with multiple codes
112
   * (like a FieldError) and a MessageSource that has a parent MessageSource,
113
   * do <i>not</i> activate "useCodeAsDefaultMessage" in the <i>parent</i>:
114
   * Else, you'll get the first code returned as message by the parent,
115
   * without attempts to check further codes.
116
   * <p>To be able to work with "useCodeAsDefaultMessage" turned on in the parent,
117
   * AbstractMessageSource and AbstractApplicationContext contain special checks
118
   * to delegate to the internal {@link #getMessageInternal} method if available.
119
   * In general, it is recommended to just use "useCodeAsDefaultMessage" during
120
   * development and not rely on it in production in the first place, though.
121
   *
122
   * @see #getMessage(String, Object[], Locale)
123
   * @see FieldError
124
   */
125
  public void setUseCodeAsDefaultMessage(boolean useCodeAsDefaultMessage) {
126
    this.useCodeAsDefaultMessage = useCodeAsDefaultMessage;
3✔
127
  }
1✔
128

129
  /**
130
   * Return whether to use the message code as default message instead of
131
   * throwing a NoSuchMessageException. Useful for development and debugging.
132
   * Default is "false".
133
   * <p>Alternatively, consider overriding the {@link #getDefaultMessage}
134
   * method to return a custom fallback message for an unresolvable code.
135
   *
136
   * @see #getDefaultMessage(String)
137
   */
138
  protected boolean isUseCodeAsDefaultMessage() {
139
    return this.useCodeAsDefaultMessage;
3✔
140
  }
141

142
  @Nullable
143
  @Override
144
  public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, @Nullable Locale locale) {
145
    String msg = getMessageInternal(code, args, locale);
6✔
146
    if (msg != null) {
2✔
147
      return msg;
2✔
148
    }
149
    if (defaultMessage == null) {
2✔
150
      return getDefaultMessage(code);
4✔
151
    }
152
    return renderDefaultMessage(defaultMessage, args, locale);
6✔
153
  }
154

155
  @Override
156
  public final String getMessage(String code, @Nullable Object[] args, @Nullable Locale locale) throws NoSuchMessageException {
157
    String msg = getMessageInternal(code, args, locale);
6✔
158
    if (msg != null) {
2✔
159
      return msg;
2✔
160
    }
161
    String fallback = getDefaultMessage(code);
4✔
162
    if (fallback != null) {
2✔
163
      return fallback;
2✔
164
    }
165
    if (locale == null) {
2!
166
      throw new NoSuchMessageException(code);
×
167
    }
168
    else {
169
      throw new NoSuchMessageException(code, locale);
6✔
170
    }
171
  }
172

173
  @Override
174
  public final String getMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) throws NoSuchMessageException {
175
    String[] codes = resolvable.getCodes();
3✔
176
    if (codes != null) {
2✔
177
      for (String code : codes) {
16✔
178
        String message = getMessageInternal(code, resolvable.getArguments(), locale);
7✔
179
        if (message != null) {
2✔
180
          return message;
2✔
181
        }
182
      }
183
    }
184
    String defaultMessage = getDefaultMessage(resolvable, locale);
5✔
185
    if (defaultMessage != null) {
2✔
186
      return defaultMessage;
2✔
187
    }
188
    String code = ObjectUtils.isNotEmpty(codes) ? codes[codes.length - 1] : "";
11!
189
    if (locale == null) {
2!
190
      throw new NoSuchMessageException(code);
×
191
    }
192
    else {
193
      throw new NoSuchMessageException(code, locale);
6✔
194
    }
195
  }
196

197
  /**
198
   * Resolve the given code and arguments as message in the given Locale,
199
   * returning {@code null} if not found. Does <i>not</i> fall back to
200
   * the code as default message. Invoked by {@code getMessage} methods.
201
   *
202
   * @param code the code to lookup up, such as 'calculator.noRateSet'
203
   * @param args array of arguments that will be filled in for params
204
   * within the message
205
   * @param locale the locale in which to do the lookup
206
   * @return the resolved message, or {@code null} if not found
207
   * @see #getMessage(String, Object[], String, Locale)
208
   * @see #getMessage(String, Object[], Locale)
209
   * @see #getMessage(MessageSourceResolvable, Locale)
210
   * @see #setUseCodeAsDefaultMessage
211
   */
212
  @Nullable
213
  protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) {
214
    if (code == null) {
2✔
215
      return null;
2✔
216
    }
217
    if (locale == null) {
2!
218
      locale = Locale.getDefault();
×
219
    }
220
    Object[] argsToUse = args;
2✔
221

222
    if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
6✔
223
      // Optimized resolution: no arguments to apply,
224
      // therefore no MessageFormat needs to be involved.
225
      // Note that the default implementation still uses MessageFormat;
226
      // this can be overridden in specific subclasses.
227
      String message = resolveCodeWithoutArguments(code, locale);
5✔
228
      if (message != null) {
2✔
229
        return message;
2✔
230
      }
231
    }
1✔
232

233
    else {
234
      // Resolve arguments eagerly, for the case where the message
235
      // is defined in a parent MessageSource but resolvable arguments
236
      // are defined in the child MessageSource.
237
      argsToUse = resolveArguments(args, locale);
5✔
238

239
      MessageFormat messageFormat = resolveCode(code, locale);
5✔
240
      if (messageFormat != null) {
2✔
241
        synchronized(messageFormat) {
4✔
242
          return messageFormat.format(argsToUse);
6✔
243
        }
244
      }
245
    }
246

247
    // Check locale-independent common messages for the given message code.
248
    Properties commonMessages = getCommonMessages();
3✔
249
    if (commonMessages != null) {
2✔
250
      String commonMessage = commonMessages.getProperty(code);
4✔
251
      if (commonMessage != null) {
2!
252
        return formatMessage(commonMessage, args, locale);
6✔
253
      }
254
    }
255

256
    // Not found -> check parent, if any.
257
    return getMessageFromParent(code, argsToUse, locale);
6✔
258
  }
259

260
  /**
261
   * Try to retrieve the given message from the parent {@code MessageSource}, if any.
262
   *
263
   * @param code the code to lookup up, such as 'calculator.noRateSet'
264
   * @param args array of arguments that will be filled in for params
265
   * within the message
266
   * @param locale the locale in which to do the lookup
267
   * @return the resolved message, or {@code null} if not found
268
   * @see #getParentMessageSource()
269
   */
270
  @Nullable
271
  protected String getMessageFromParent(String code, @Nullable Object[] args, Locale locale) {
272
    MessageSource parent = getParentMessageSource();
3✔
273
    if (parent != null) {
2✔
274
      if (parent instanceof AbstractMessageSource) {
3✔
275
        // Call internal method to avoid getting the default code back
276
        // in case of "useCodeAsDefaultMessage" being activated.
277
        return ((AbstractMessageSource) parent).getMessageInternal(code, args, locale);
7✔
278
      }
279
      else {
280
        // Check parent MessageSource, returning null if not found there.
281
        // Covers custom MessageSource impls and DelegatingMessageSource.
282
        return parent.getMessage(code, args, null, locale);
7✔
283
      }
284
    }
285
    // Not found in parent either.
286
    return null;
2✔
287
  }
288

289
  /**
290
   * Get a default message for the given {@code MessageSourceResolvable}.
291
   * <p>This implementation fully renders the default message if available,
292
   * or just returns the plain default message {@code String} if the primary
293
   * message code is being used as a default message.
294
   *
295
   * @param resolvable the value object to resolve a default message for
296
   * @param locale the current locale
297
   * @return the default message, or {@code null} if none
298
   * @see #renderDefaultMessage(String, Object[], Locale)
299
   * @see #getDefaultMessage(String)
300
   */
301
  @Nullable
302
  protected String getDefaultMessage(MessageSourceResolvable resolvable, @Nullable Locale locale) {
303
    String defaultMessage = resolvable.getDefaultMessage();
3✔
304
    String[] codes = resolvable.getCodes();
3✔
305
    if (defaultMessage != null) {
2✔
306
      if (resolvable instanceof DefaultMessageSourceResolvable defaultResolvable
7✔
307
              && !defaultResolvable.shouldRenderDefaultMessage()) {
2✔
308
        // Given default message does not contain any argument placeholders
309
        // (and isn't escaped for alwaysUseMessageFormat either) -> return as-is.
310
        return defaultMessage;
2✔
311
      }
312
      if (ObjectUtils.isNotEmpty(codes) && defaultMessage.equals(codes[0])) {
9✔
313
        // Never format a code-as-default-message, even with alwaysUseMessageFormat=true
314
        return defaultMessage;
2✔
315
      }
316
      return renderDefaultMessage(defaultMessage, resolvable.getArguments(), locale);
7✔
317
    }
318
    return ObjectUtils.isNotEmpty(codes) ? getDefaultMessage(codes[0]) : null;
10!
319
  }
320

321
  /**
322
   * Return a fallback default message for the given code, if any.
323
   * <p>Default is to return the code itself if "useCodeAsDefaultMessage" is activated,
324
   * or return no fallback else. In case of no fallback, the caller will usually
325
   * receive a {@code NoSuchMessageException} from {@code getMessage}.
326
   *
327
   * @param code the message code that we couldn't resolve
328
   * and that we didn't receive an explicit default message for
329
   * @return the default message to use, or {@code null} if none
330
   * @see #setUseCodeAsDefaultMessage
331
   */
332
  @Nullable
333
  protected String getDefaultMessage(String code) {
334
    if (isUseCodeAsDefaultMessage()) {
3✔
335
      return code;
2✔
336
    }
337
    return null;
2✔
338
  }
339

340
  /**
341
   * Searches through the given array of objects, finds any MessageSourceResolvable
342
   * objects and resolves them.
343
   * <p>Allows for messages to have MessageSourceResolvables as arguments.
344
   *
345
   * @param args array of arguments for a message
346
   * @param locale the locale to resolve through
347
   * @return an array of arguments with any MessageSourceResolvables resolved
348
   */
349
  @Override
350
  protected Object[] resolveArguments(@Nullable Object[] args, @Nullable Locale locale) {
351
    if (ObjectUtils.isEmpty(args)) {
3✔
352
      return super.resolveArguments(args, locale);
5✔
353
    }
354
    List<Object> resolvedArgs = new ArrayList<>(args.length);
6✔
355
    for (Object arg : args) {
16✔
356
      if (arg instanceof MessageSourceResolvable) {
3✔
357
        resolvedArgs.add(getMessage((MessageSourceResolvable) arg, locale));
9✔
358
      }
359
      else {
360
        resolvedArgs.add(arg);
4✔
361
      }
362
    }
363
    return resolvedArgs.toArray();
3✔
364
  }
365

366
  /**
367
   * Subclasses can override this method to resolve a message without arguments
368
   * in an optimized fashion, i.e. to resolve without involving a MessageFormat.
369
   * <p>The default implementation <i>does</i> use MessageFormat, through
370
   * delegating to the {@link #resolveCode} method. Subclasses are encouraged
371
   * to replace this with optimized resolution.
372
   * <p>Unfortunately, {@code java.text.MessageFormat} is not implemented
373
   * in an efficient fashion. In particular, it does not detect that a message
374
   * pattern doesn't contain argument placeholders in the first place. Therefore,
375
   * it is advisable to circumvent MessageFormat for messages without arguments.
376
   *
377
   * @param code the code of the message to resolve
378
   * @param locale the locale to resolve the code for
379
   * (subclasses are encouraged to support internationalization)
380
   * @return the message String, or {@code null} if not found
381
   * @see #resolveCode
382
   * @see java.text.MessageFormat
383
   */
384
  @Nullable
385
  protected String resolveCodeWithoutArguments(String code, Locale locale) {
386
    MessageFormat messageFormat = resolveCode(code, locale);
×
387
    if (messageFormat != null) {
×
388
      synchronized(messageFormat) {
×
389
        return messageFormat.format(new Object[0]);
×
390
      }
391
    }
392
    return null;
×
393
  }
394

395
  /**
396
   * Subclasses must implement this method to resolve a message.
397
   * <p>Returns a MessageFormat instance rather than a message String,
398
   * to allow for appropriate caching of MessageFormats in subclasses.
399
   * <p><b>Subclasses are encouraged to provide optimized resolution
400
   * for messages without arguments, not involving MessageFormat.</b>
401
   * See the {@link #resolveCodeWithoutArguments} javadoc for details.
402
   *
403
   * @param code the code of the message to resolve
404
   * @param locale the locale to resolve the code for
405
   * (subclasses are encouraged to support internationalization)
406
   * @return the MessageFormat for the message, or {@code null} if not found
407
   * @see #resolveCodeWithoutArguments(String, java.util.Locale)
408
   */
409
  @Nullable
410
  protected abstract MessageFormat resolveCode(String code, Locale locale);
411

412
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc