
001/*- 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2025 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.i18n; 021 022import jakarta.annotation.Nonnull; 023 024import java.io.IOException; 025import java.io.InputStream; 026import java.net.URL; 027import java.util.Collections; 028import java.util.Enumeration; 029import java.util.List; 030import java.util.Locale; 031import java.util.Properties; 032import java.util.ResourceBundle; 033 034/** 035 * Finds all properties files on the class path that match the given base name and merges them. 036 * This implementation avoids the unnecessary overhead of writing to and reading from a stream. 037 */ 038public class MultiFileResourceBundleControl extends ResourceBundle.Control { 039 040 /** 041 * Returns a list containing only Locale.ROOT as a candidate. 042 * This forces the loader to look for the base bundle only (e.g., "bundle.properties"). 043 * 044 * @param baseName the base name of the resource bundle 045 * @param locale the locale to load (this parameter is ignored) 046 * @return a singleton list containing Locale.ROOT 047 */ 048 @Override 049 public List<Locale> getCandidateLocales(String baseName, Locale locale) { 050 return Collections.singletonList(Locale.ROOT); 051 } 052 053 /** 054 * Prevents fallback to the default locale if the root bundle isn't found. 055 * 056 * @param baseName the base name of the resource bundle 057 * @param locale the locale where the search is failing 058 * @return null to indicate no fallback should be attempted 059 */ 060 @Override 061 public Locale getFallbackLocale(String baseName, Locale locale) { 062 return null; 063 } 064 065 @Override 066 public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) 067 throws IOException, IllegalAccessException, InstantiationException { 068 069 // We only care about handling the root .properties file 070 if (!"java.properties".equals(format) 071 || (locale != null && !locale.toString().isEmpty())) { 072 return super.newBundle(baseName, locale, format, loader, reload); 073 } 074 075 final String resourceName = toResourceName(toBundleName(baseName, locale), "properties"); 076 final Properties mergedProperties = new Properties(); 077 078 // Load from all matching resources 079 final Enumeration<URL> resources = loader.getResources(resourceName); 080 while (resources.hasMoreElements()) { 081 final URL url = resources.nextElement(); 082 083 try (InputStream is = url.openStream()) { 084 mergedProperties.load(is); 085 } 086 } 087 088 // Directly wrap the merged Properties object instead of performing a stream round-trip. 089 return new PropertiesResourceBundle(mergedProperties); 090 } 091 092 /** 093 * A lightweight, private ResourceBundle implementation that is backed directly by a Properties object. 094 */ 095 private static class PropertiesResourceBundle extends ResourceBundle { 096 private final Properties properties; 097 098 PropertiesResourceBundle(Properties properties) { 099 this.properties = properties; 100 } 101 102 @Override 103 protected Object handleGetObject(@Nonnull String key) { 104 return properties.getProperty(key); 105 } 106 107 @Override 108 @Nonnull 109 public Enumeration<String> getKeys() { 110 return Collections.enumeration(properties.stringPropertyNames()); 111 } 112 } 113}