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}