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

TAKETODAY / today-infrastructure / 18154768944

01 Oct 2025 07:26AM UTC coverage: 81.882% (-0.005%) from 81.887%
18154768944

push

github

web-flow
Merge pull request #290 from TAKETODAY/dev/jspecify

jspecify

59788 of 78013 branches covered (76.64%)

Branch coverage included in aggregate %.

141239 of 167496 relevant lines covered (84.32%)

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 org.jspecify.annotations.Nullable;
21

22
import java.text.MessageFormat;
23
import java.util.ArrayList;
24
import java.util.List;
25
import java.util.Locale;
26
import java.util.Properties;
27

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

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

71
  @Nullable
72
  private MessageSource parentMessageSource;
73

74
  @Nullable
75
  private Properties commonMessages;
76

77
  private boolean useCodeAsDefaultMessage = false;
4✔
78

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

413
}
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