• 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

78.92
today-context/src/main/java/infra/context/support/ReloadableResourceBundleMessageSource.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.io.IOException;
23
import java.io.InputStream;
24
import java.io.InputStreamReader;
25
import java.text.MessageFormat;
26
import java.util.ArrayList;
27
import java.util.Collections;
28
import java.util.List;
29
import java.util.Locale;
30
import java.util.Map;
31
import java.util.Properties;
32
import java.util.concurrent.ConcurrentHashMap;
33
import java.util.concurrent.ConcurrentMap;
34
import java.util.concurrent.locks.ReentrantLock;
35

36
import infra.context.ResourceLoaderAware;
37
import infra.core.io.DefaultResourceLoader;
38
import infra.core.io.Resource;
39
import infra.core.io.ResourceLoader;
40
import infra.lang.Assert;
41
import infra.util.CollectionUtils;
42
import infra.util.DefaultPropertiesPersister;
43
import infra.util.PropertiesPersister;
44
import infra.util.StringUtils;
45

46
/**
47
 * Framework-specific {@link infra.context.MessageSource} implementation
48
 * that accesses resource bundles using specified basenames, participating in the
49
 * Framework {@link infra.context.ApplicationContext}'s resource loading.
50
 *
51
 * <p>In contrast to the JDK-based {@link ResourceBundleMessageSource}, this class uses
52
 * {@link java.util.Properties} instances as its custom data structure for messages,
53
 * loading them via a {@link PropertiesPersister} strategy
54
 * from Framework {@link Resource} handles. This strategy is not only capable of
55
 * reloading files based on timestamp changes, but also of loading properties files
56
 * with a specific character encoding. It will detect XML property files as well.
57
 *
58
 * <p>Note that the basenames set as {@link #setBasenames "basenames"} property
59
 * are treated in a slightly different fashion than the "basenames" property of
60
 * {@link ResourceBundleMessageSource}. It follows the basic ResourceBundle rule of not
61
 * specifying file extension or language codes, but can refer to any Framework resource
62
 * location (instead of being restricted to classpath resources). With a "classpath:"
63
 * prefix, resources can still be loaded from the classpath, but "cacheSeconds" values
64
 * other than "-1" (caching forever) might not work reliably in this case.
65
 *
66
 * <p>For a typical web application, message files could be placed in {@code WEB-INF}:
67
 * e.g. a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties",
68
 * "WEB-INF/messages_en.properties" etc arrangement as well as "WEB-INF/messages.xml",
69
 * "WEB-INF/messages_en.xml" etc. Note that message definitions in a <i>previous</i>
70
 * resource bundle will override ones in a later bundle, due to sequential lookup.
71
 *
72
 * <p>This MessageSource can easily be used outside of an
73
 * {@link infra.context.ApplicationContext}: it will use a
74
 * {@link DefaultResourceLoader} as default,
75
 * simply getting overridden with the ApplicationContext's resource loader
76
 * if running in a context. It does not have any other specific dependencies.
77
 *
78
 * <p>Thanks to Thomas Achleitner for providing the initial implementation of
79
 * this message source!
80
 *
81
 * @author Juergen Hoeller
82
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
83
 * @see #setCacheSeconds
84
 * @see #setBasenames
85
 * @see #setDefaultEncoding
86
 * @see #setFileEncodings
87
 * @see #setPropertiesPersister
88
 * @see #setResourceLoader
89
 * @see DefaultResourceLoader
90
 * @see ResourceBundleMessageSource
91
 * @see java.util.ResourceBundle
92
 * @since 4.0
93
 */
94
public class ReloadableResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements ResourceLoaderAware {
2✔
95

96
  private static final String XML_EXTENSION = ".xml";
97

98
  private List<String> fileExtensions = List.of(".properties", XML_EXTENSION);
5✔
99

100
  @Nullable
101
  private Properties fileEncodings;
102

103
  private boolean concurrentRefresh = true;
3✔
104

105
  private PropertiesPersister propertiesPersister = DefaultPropertiesPersister.INSTANCE;
3✔
106

107
  private ResourceLoader resourceLoader = new DefaultResourceLoader();
5✔
108

109
  // Cache to hold filename lists per Locale
110
  private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();
5✔
111

112
  // Cache to hold already loaded properties per filename
113
  private final ConcurrentMap<String, PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
5✔
114

115
  // Cache to hold already loaded properties per filename
116
  private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>();
6✔
117

118
  /**
119
   * Set the list of supported file extensions.
120
   * <p>The default is a list containing {@code .properties} and {@code .xml}.
121
   *
122
   * @param fileExtensions the file extensions (starts with a dot)
123
   */
124
  public void setFileExtensions(List<String> fileExtensions) {
125
    Assert.isTrue(CollectionUtils.isNotEmpty(fileExtensions), "At least one file extension is required");
4✔
126
    for (String extension : fileExtensions) {
10✔
127
      if (!extension.startsWith(".")) {
4✔
128
        throw new IllegalArgumentException("File extension '" + extension + "' should start with '.'");
6✔
129
      }
130
    }
1✔
131
    this.fileExtensions = Collections.unmodifiableList(fileExtensions);
4✔
132
  }
1✔
133

134
  /**
135
   * Set per-file charsets to use for parsing properties files.
136
   * <p>Only applies to classic properties files, not to XML files.
137
   *
138
   * @param fileEncodings a Properties with filenames as keys and charset
139
   * names as values. Filenames have to match the basename syntax,
140
   * with optional locale-specific components: e.g. "WEB-INF/messages"
141
   * or "WEB-INF/messages_en".
142
   * @see #setBasenames
143
   * @see PropertiesPersister#load
144
   */
145
  public void setFileEncodings(Properties fileEncodings) {
146
    this.fileEncodings = fileEncodings;
3✔
147
  }
1✔
148

149
  /**
150
   * Specify whether to allow for concurrent refresh behavior, i.e. one thread
151
   * locked in a refresh attempt for a specific cached properties file whereas
152
   * other threads keep returning the old properties for the time being, until
153
   * the refresh attempt has completed.
154
   * <p>Default is "true"
155
   *
156
   * @see #setCacheSeconds
157
   */
158
  public void setConcurrentRefresh(boolean concurrentRefresh) {
159
    this.concurrentRefresh = concurrentRefresh;
3✔
160
  }
1✔
161

162
  /**
163
   * Set the PropertiesPersister to use for parsing properties files.
164
   * <p>The default is {@code DefaultPropertiesPersister}.
165
   *
166
   * @see DefaultPropertiesPersister#INSTANCE
167
   */
168
  public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersister) {
169
    this.propertiesPersister =
×
170
            (propertiesPersister != null ? propertiesPersister : DefaultPropertiesPersister.INSTANCE);
×
171
  }
×
172

173
  /**
174
   * Set the ResourceLoader to use for loading bundle properties files.
175
   * <p>The default is a DefaultResourceLoader. Will get overridden by the
176
   * ApplicationContext if running in a context, as it implements the
177
   * ResourceLoaderAware interface. Can be manually overridden when
178
   * running outside an ApplicationContext.
179
   *
180
   * @see DefaultResourceLoader
181
   * @see ResourceLoaderAware
182
   */
183
  @Override
184
  public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
185
    this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader());
6!
186
  }
1✔
187

188
  /**
189
   * Resolves the given message code as key in the retrieved bundle files,
190
   * returning the value found in the bundle as-is (without MessageFormat parsing).
191
   */
192
  @Override
193
  @Nullable
194
  protected String resolveCodeWithoutArguments(String code, Locale locale) {
195
    if (getCacheMillis() < 0) {
5✔
196
      PropertiesHolder propHolder = getMergedProperties(locale);
4✔
197
      return propHolder.getProperty(code);
4✔
198
    }
199
    else {
200
      for (String basename : getBasenameSet()) {
11!
201
        List<String> filenames = calculateAllFilenames(basename, locale);
5✔
202
        for (String filename : filenames) {
10!
203
          PropertiesHolder propHolder = getProperties(filename);
4✔
204
          String result = propHolder.getProperty(code);
4✔
205
          if (result != null) {
2✔
206
            return result;
2✔
207
          }
208
        }
1✔
209
      }
×
210
    }
211
    return null;
×
212
  }
213

214
  /**
215
   * Resolves the given message code as key in the retrieved bundle files,
216
   * using a cached MessageFormat instance per message code.
217
   */
218
  @Override
219
  @Nullable
220
  protected MessageFormat resolveCode(String code, Locale locale) {
221
    if (getCacheMillis() < 0) {
5!
222
      PropertiesHolder propHolder = getMergedProperties(locale);
4✔
223
      return propHolder.getMessageFormat(code, locale);
5✔
224
    }
225
    else {
226
      for (String basename : getBasenameSet()) {
×
227
        List<String> filenames = calculateAllFilenames(basename, locale);
×
228
        for (String filename : filenames) {
×
229
          PropertiesHolder propHolder = getProperties(filename);
×
230
          MessageFormat result = propHolder.getMessageFormat(code, locale);
×
231
          if (result != null) {
×
232
            return result;
×
233
          }
234
        }
×
235
      }
×
236
    }
237
    return null;
×
238
  }
239

240
  /**
241
   * Get a PropertiesHolder that contains the actually visible properties
242
   * for a Locale, after merging all specified resource bundles.
243
   * Either fetches the holder from the cache or freshly loads it.
244
   * <p>Only used when caching resource bundle contents forever, i.e.
245
   * with cacheSeconds &lt; 0. Therefore, merged properties are always
246
   * cached forever.
247
   */
248
  protected PropertiesHolder getMergedProperties(Locale locale) {
249
    PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
6✔
250
    if (mergedHolder != null) {
2✔
251
      return mergedHolder;
2✔
252
    }
253
    mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
6✔
254
    PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
7✔
255
    if (existing != null) {
2!
256
      mergedHolder = existing;
×
257
    }
258
    return mergedHolder;
2✔
259
  }
260

261
  /**
262
   * Determine the properties to merge based on the specified basenames.
263
   *
264
   * @param locale the locale
265
   * @return the list of properties holders
266
   * @see #getBasenameSet()
267
   * @see #calculateAllFilenames
268
   * @see #mergeProperties
269
   */
270
  protected List<PropertiesHolder> collectPropertiesToMerge(Locale locale) {
271
    String[] basenames = StringUtils.toStringArray(getBasenameSet());
4✔
272
    ArrayList<PropertiesHolder> holders = new ArrayList<>(basenames.length);
6✔
273
    for (int i = basenames.length - 1; i >= 0; i--) {
9✔
274
      List<String> filenames = calculateAllFilenames(basenames[i], locale);
7✔
275
      for (int j = filenames.size() - 1; j >= 0; j--) {
9✔
276
        String filename = filenames.get(j);
5✔
277
        PropertiesHolder propHolder = getProperties(filename);
4✔
278
        if (propHolder.getProperties() != null) {
3✔
279
          holders.add(propHolder);
4✔
280
        }
281
      }
282
    }
283
    return holders;
2✔
284
  }
285

286
  /**
287
   * Merge the given properties holders into a single holder.
288
   *
289
   * @param holders the list of properties holders
290
   * @return a single merged properties holder
291
   * @see #newProperties()
292
   * @see #getMergedProperties
293
   * @see #collectPropertiesToMerge
294
   */
295
  protected PropertiesHolder mergeProperties(List<PropertiesHolder> holders) {
296
    Properties mergedProps = newProperties();
3✔
297
    long latestTimestamp = -1;
2✔
298
    for (PropertiesHolder holder : holders) {
10✔
299
      mergedProps.putAll(holder.getProperties());
4✔
300
      if (holder.getFileTimestamp() > latestTimestamp) {
5!
301
        latestTimestamp = holder.getFileTimestamp();
×
302
      }
303
    }
1✔
304
    return new PropertiesHolder(mergedProps, latestTimestamp);
7✔
305
  }
306

307
  /**
308
   * Calculate all filenames for the given bundle basename and Locale.
309
   * Will calculate filenames for the given Locale, the system Locale
310
   * (if applicable), and the default file.
311
   *
312
   * @param basename the basename of the bundle
313
   * @param locale the locale
314
   * @return the List of filenames to check
315
   * @see #setFallbackToSystemLocale
316
   * @see #calculateFilenamesForLocale
317
   */
318
  protected List<String> calculateAllFilenames(String basename, Locale locale) {
319
    Map<Locale, List<String>> localeMap = this.cachedFilenames.get(basename);
6✔
320
    if (localeMap != null) {
2✔
321
      List<String> filenames = localeMap.get(locale);
5✔
322
      if (filenames != null) {
2✔
323
        return filenames;
2✔
324
      }
325
    }
326

327
    // Filenames for given Locale
328
    List<String> filenames = new ArrayList<>(7);
5✔
329
    filenames.addAll(calculateFilenamesForLocale(basename, locale));
7✔
330

331
    // Filenames for default Locale, if any
332
    Locale defaultLocale = getDefaultLocale();
3✔
333
    if (defaultLocale != null && !defaultLocale.equals(locale)) {
6✔
334
      List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
5✔
335
      for (String fallbackFilename : fallbackFilenames) {
10✔
336
        if (!filenames.contains(fallbackFilename)) {
4✔
337
          // Entry for fallback locale that isn't already in filenames list.
338
          filenames.add(fallbackFilename);
4✔
339
        }
340
      }
1✔
341
    }
342

343
    // Filename for default bundle file
344
    filenames.add(basename);
4✔
345

346
    if (localeMap == null) {
2✔
347
      localeMap = new ConcurrentHashMap<>();
4✔
348
      Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
7✔
349
      if (existing != null) {
2!
350
        localeMap = existing;
×
351
      }
352
    }
353
    localeMap.put(locale, filenames);
5✔
354
    return filenames;
2✔
355
  }
356

357
  /**
358
   * Calculate the filenames for the given bundle basename and Locale,
359
   * appending language code, country code, and variant code.
360
   * <p>For example, basename "messages", Locale "de_AT_oo" &rarr; "messages_de_AT_OO",
361
   * "messages_de_AT", "messages_de".
362
   * <p>Follows the rules defined by {@link java.util.Locale#toString()}.
363
   *
364
   * @param basename the basename of the bundle
365
   * @param locale the locale
366
   * @return the List of filenames to check
367
   */
368
  protected List<String> calculateFilenamesForLocale(String basename, Locale locale) {
369
    ArrayList<String> result = new ArrayList<>(3);
5✔
370
    String language = locale.getLanguage();
3✔
371
    String country = locale.getCountry();
3✔
372
    String variant = locale.getVariant();
3✔
373
    StringBuilder temp = new StringBuilder(basename);
5✔
374

375
    temp.append('_');
4✔
376
    if (!language.isEmpty()) {
3✔
377
      temp.append(language);
4✔
378
      result.add(0, temp.toString());
5✔
379
    }
380

381
    temp.append('_');
4✔
382
    if (!country.isEmpty()) {
3✔
383
      temp.append(country);
4✔
384
      result.add(0, temp.toString());
5✔
385
    }
386

387
    if (!variant.isEmpty() && (!language.isEmpty() || !country.isEmpty())) {
9✔
388
      temp.append('_').append(variant);
6✔
389
      result.add(0, temp.toString());
5✔
390
    }
391

392
    return result;
2✔
393
  }
394

395
  /**
396
   * Get a PropertiesHolder for the given filename, either from the
397
   * cache or freshly loaded.
398
   *
399
   * @param filename the bundle filename (basename + Locale)
400
   * @return the current PropertiesHolder for the bundle
401
   */
402
  protected PropertiesHolder getProperties(String filename) {
403
    PropertiesHolder propHolder = this.cachedProperties.get(filename);
6✔
404
    long originalTimestamp = -2;
2✔
405

406
    if (propHolder != null) {
2✔
407
      originalTimestamp = propHolder.getRefreshTimestamp();
3✔
408
      if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - getCacheMillis()) {
11✔
409
        // Up to date
410
        return propHolder;
2✔
411
      }
412
    }
413
    else {
414
      propHolder = new PropertiesHolder();
5✔
415
      PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(filename, propHolder);
7✔
416
      if (existingHolder != null) {
2!
417
        propHolder = existingHolder;
×
418
      }
419
    }
420

421
    // At this point, we need to refresh...
422
    if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
8✔
423
      // A populated but stale holder -> could keep using it.
424
      if (!propHolder.refreshLock.tryLock()) {
4!
425
        // Getting refreshed by another thread already ->
426
        // let's return the existing properties for the time being.
427
        return propHolder;
×
428
      }
429
    }
430
    else {
431
      propHolder.refreshLock.lock();
3✔
432
    }
433
    try {
434
      PropertiesHolder existingHolder = this.cachedProperties.get(filename);
6✔
435
      if (existingHolder != null && existingHolder.getRefreshTimestamp() > originalTimestamp) {
7!
436
        return existingHolder;
×
437
      }
438
      return refreshProperties(filename, propHolder);
7✔
439
    }
440
    finally {
441
      propHolder.refreshLock.unlock();
3✔
442
    }
443
  }
444

445
  /**
446
   * Refresh the PropertiesHolder for the given bundle filename.
447
   * <p>The holder can be {@code null} if not cached before, or a timed-out cache entry
448
   * (potentially getting re-validated against the current last-modified timestamp).
449
   *
450
   * @param filename the bundle filename (basename + Locale)
451
   * @param propHolder the current PropertiesHolder for the bundle
452
   * @see #resolveResource(String)
453
   */
454
  protected PropertiesHolder refreshProperties(String filename, @Nullable PropertiesHolder propHolder) {
455
    long refreshTimestamp = (getCacheMillis() < 0 ? -1 : System.currentTimeMillis());
9✔
456

457
    Resource resource = resolveResource(filename);
4✔
458
    if (resource != null) {
2✔
459
      long fileTimestamp = -1;
2✔
460
      if (getCacheMillis() >= 0) {
5✔
461
        // Last-modified timestamp of file will just be read if caching with timeout.
462
        try {
463
          fileTimestamp = resource.lastModified();
3✔
464
          if (propHolder != null && propHolder.getFileTimestamp() == fileTimestamp) {
7!
465
            if (logger.isDebugEnabled()) {
4!
466
              logger.debug("Re-caching properties for filename [{}] - file hasn't been modified", filename);
×
467
            }
468
            propHolder.setRefreshTimestamp(refreshTimestamp);
3✔
469
            return propHolder;
2✔
470
          }
471
        }
472
        catch (IOException ex) {
×
473
          // Probably a class path resource: cache it forever.
474
          logger.debug("{} could not be resolved in the file system - assuming that it hasn't changed", resource, ex);
×
475
          fileTimestamp = -1;
×
476
        }
1✔
477
      }
478
      try {
479
        Properties props = loadProperties(resource, filename);
5✔
480
        propHolder = new PropertiesHolder(props, fileTimestamp);
7✔
481
      }
482
      catch (IOException ex) {
×
483
        if (logger.isWarnEnabled()) {
×
484
          logger.warn("Could not parse properties file [{}]", resource.getName(), ex);
×
485
        }
486
        // Empty holder representing "not valid".
487
        propHolder = new PropertiesHolder();
×
488
      }
1✔
489
    }
1✔
490

491
    else {
492
      // Resource does not exist.
493
      if (logger.isDebugEnabled()) {
4!
494
        logger.debug("No properties file found for [{}] - neither plain properties nor XML", filename);
×
495
      }
496
      // Empty holder representing "not found".
497
      propHolder = new PropertiesHolder();
5✔
498
    }
499

500
    propHolder.setRefreshTimestamp(refreshTimestamp);
3✔
501
    this.cachedProperties.put(filename, propHolder);
6✔
502
    return propHolder;
2✔
503
  }
504

505
  /**
506
   * Resolve the specified bundle {@code filename} into a concrete {@link Resource},
507
   * potentially checking multiple sources or file extensions.
508
   * <p>If no suitable concrete {@code Resource} can be resolved, this method
509
   * returns a {@code Resource} for which {@link Resource#exists()} returns
510
   * {@code false}, which gets subsequently ignored.
511
   * <p>This can be leveraged to check the last modification timestamp or to load
512
   * properties from alternative sources &mdash; for example, from an XML BLOB
513
   * in a database, or from properties serialized using a custom format such as
514
   * JSON.
515
   * <p>The default implementation delegates to the configured
516
   * {@link #setResourceLoader(ResourceLoader) ResourceLoader} to resolve
517
   * resources, checking in order for existing {@code Resource} with extensions defined
518
   * by {@link #setFileExtensions(List)} ({@code .properties} and {@code .xml}
519
   * by default).
520
   * <p>When overriding this method, {@link #loadProperties(Resource, String)}
521
   * <strong>must</strong> be capable of loading properties from any type of
522
   * {@code Resource} returned by this method. As a consequence, implementors
523
   * are strongly encouraged to also override {@code loadProperties()}.
524
   * <p>As an alternative to overriding this method, you can configure a
525
   * {@link #setPropertiesPersister(PropertiesPersister) PropertiesPersister}
526
   * that is capable of dealing with all resources returned by this method.
527
   * Please note, however, that the default {@code loadProperties()} implementation
528
   * uses {@link PropertiesPersister#loadFromXml(Properties, InputStream) loadFromXml}
529
   * for XML resources and otherwise uses the two
530
   * {@link PropertiesPersister#load(Properties, InputStream) load} methods
531
   * for other types of resources.
532
   *
533
   * @param filename the bundle filename (basename + Locale)
534
   * @return the {@code Resource} to use
535
   */
536
  @Nullable
537
  protected Resource resolveResource(String filename) {
538
    for (String fileExtension : this.fileExtensions) {
11✔
539
      Resource resource = this.resourceLoader.getResource(filename + fileExtension);
7✔
540
      if (resource.exists()) {
3✔
541
        return resource;
2✔
542
      }
543
    }
1✔
544
    return null;
2✔
545
  }
546

547
  /**
548
   * Load the properties from the given resource.
549
   *
550
   * @param resource the resource to load from
551
   * @param filename the original bundle filename (basename + Locale)
552
   * @return the populated Properties instance
553
   * @throws IOException if properties loading failed
554
   */
555
  protected Properties loadProperties(Resource resource, String filename) throws IOException {
556
    Properties props = newProperties();
3✔
557
    try (InputStream is = resource.getInputStream()) {
3✔
558
      String resourceFilename = resource.getName();
3✔
559
      if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) {
6!
560
        if (logger.isDebugEnabled()) {
4!
561
          logger.debug("Loading properties [{}]", resource.getName());
×
562
        }
563
        this.propertiesPersister.loadFromXml(props, is);
6✔
564
      }
565
      else {
566
        String encoding = null;
2✔
567
        if (this.fileEncodings != null) {
3✔
568
          encoding = this.fileEncodings.getProperty(filename);
5✔
569
        }
570
        if (encoding == null) {
2✔
571
          encoding = getDefaultEncoding();
3✔
572
        }
573
        if (encoding != null) {
2✔
574
          if (logger.isDebugEnabled()) {
4!
575
            logger.debug("Loading properties [{}] with encoding '{}'", resource.getName(), encoding);
×
576
          }
577
          this.propertiesPersister.load(props, new InputStreamReader(is, encoding));
10✔
578
        }
579
        else {
580
          if (logger.isDebugEnabled()) {
4!
581
            logger.debug("Loading properties [{}]", resource.getName());
×
582
          }
583
          this.propertiesPersister.load(props, is);
5✔
584
        }
585
      }
586
      return props;
4✔
587
    }
588
  }
589

590
  /**
591
   * Template method for creating a plain new {@link Properties} instance.
592
   * The default implementation simply calls {@link Properties#Properties()}.
593
   * <p>Allows for returning a custom {@link Properties} extension in subclasses.
594
   * Overriding methods should just instantiate a custom {@link Properties} subclass,
595
   * with no further initialization or population to be performed at that point.
596
   *
597
   * @return a plain Properties instance
598
   */
599
  protected Properties newProperties() {
600
    return new Properties();
4✔
601
  }
602

603
  /**
604
   * Clear the resource bundle cache.
605
   * Subsequent resolve calls will lead to reloading of the properties files.
606
   */
607
  public void clearCache() {
608
    logger.debug("Clearing entire resource bundle cache");
×
609
    this.cachedProperties.clear();
×
610
    this.cachedMergedProperties.clear();
×
611
  }
×
612

613
  /**
614
   * Clear the resource bundle caches of this MessageSource and all its ancestors.
615
   *
616
   * @see #clearCache
617
   */
618
  public void clearCacheIncludingAncestors() {
619
    clearCache();
×
620
    if (getParentMessageSource() instanceof ReloadableResourceBundleMessageSource reloadableMsgSrc) {
×
621
      reloadableMsgSrc.clearCacheIncludingAncestors();
×
622
    }
623
  }
×
624

625
  @Override
626
  public String toString() {
627
    return getClass().getName() + ": basenames=" + getBasenameSet();
×
628
  }
629

630
  /**
631
   * PropertiesHolder for caching.
632
   * Stores the last-modified timestamp of the source file for efficient
633
   * change detection, and the timestamp of the last refresh attempt
634
   * (updated every time the cache entry gets re-validated).
635
   */
636
  protected class PropertiesHolder {
637

638
    @Nullable
639
    private final Properties properties;
640

641
    private final long fileTimestamp;
642

643
    private volatile long refreshTimestamp = -2;
6✔
644

645
    private final ReentrantLock refreshLock = new ReentrantLock();
10✔
646

647
    /** Cache to hold already generated MessageFormats per message code. */
648
    private final ConcurrentMap<String, Map<Locale, MessageFormat>> cachedMessageFormats =
10✔
649
            new ConcurrentHashMap<>();
650

651
    public PropertiesHolder() {
5✔
652
      this.properties = null;
3✔
653
      this.fileTimestamp = -1;
3✔
654
    }
1✔
655

656
    public PropertiesHolder(Properties properties, long fileTimestamp) {
5✔
657
      this.properties = properties;
3✔
658
      this.fileTimestamp = fileTimestamp;
3✔
659
    }
1✔
660

661
    @Nullable
662
    public Properties getProperties() {
663
      return this.properties;
3✔
664
    }
665

666
    public long getFileTimestamp() {
667
      return this.fileTimestamp;
3✔
668
    }
669

670
    public void setRefreshTimestamp(long refreshTimestamp) {
671
      this.refreshTimestamp = refreshTimestamp;
3✔
672
    }
1✔
673

674
    public long getRefreshTimestamp() {
675
      return this.refreshTimestamp;
3✔
676
    }
677

678
    @Nullable
679
    public String getProperty(String code) {
680
      if (this.properties == null) {
3✔
681
        return null;
2✔
682
      }
683
      return this.properties.getProperty(code);
5✔
684
    }
685

686
    @Nullable
687
    public MessageFormat getMessageFormat(String code, Locale locale) {
688
      if (this.properties == null) {
3!
689
        return null;
×
690
      }
691
      Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
6✔
692
      if (localeMap != null) {
2✔
693
        MessageFormat result = localeMap.get(locale);
5✔
694
        if (result != null) {
2!
695
          return result;
2✔
696
        }
697
      }
698
      String msg = this.properties.getProperty(code);
5✔
699
      if (msg != null) {
2✔
700
        if (localeMap == null) {
2!
701
          localeMap = new ConcurrentHashMap<>();
4✔
702
          Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
7✔
703
          if (existing != null) {
2!
704
            localeMap = existing;
×
705
          }
706
        }
707
        MessageFormat result = createMessageFormat(msg, locale);
6✔
708
        localeMap.put(locale, result);
5✔
709
        return result;
2✔
710
      }
711
      return null;
2✔
712
    }
713
  }
714

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