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 ca.uhn.fhir.context.ConfigurationException;
023import ca.uhn.fhir.util.UrlUtil;
024import ca.uhn.fhir.util.VersionUtil;
025import com.google.common.annotations.VisibleForTesting;
026
027import java.text.MessageFormat;
028import java.util.ArrayList;
029import java.util.Enumeration;
030import java.util.HashMap;
031import java.util.HashSet;
032import java.util.List;
033import java.util.Locale;
034import java.util.Map;
035import java.util.ResourceBundle;
036import java.util.Set;
037import java.util.concurrent.ConcurrentHashMap;
038
039import static org.apache.commons.lang3.StringUtils.isBlank;
040import static org.apache.commons.lang3.StringUtils.isNotBlank;
041import static org.apache.commons.lang3.StringUtils.trim;
042
043/**
044 * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with caution
045 */
046public class HapiLocalizer {
047
048        @SuppressWarnings("WeakerAccess")
049        public static final String UNKNOWN_I18N_KEY_MESSAGE = "!MESSAGE!";
050
051        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HapiLocalizer.class);
052        private static boolean ourFailOnMissingMessage;
053        private final Map<String, MessageFormat> myKeyToMessageFormat = new ConcurrentHashMap<>();
054        private List<ResourceBundle> myBundle;
055        private final Map<String, String> myHardcodedMessages = new HashMap<>();
056        private Locale myLocale = Locale.getDefault();
057
058        public HapiLocalizer() {
059                this(HapiLocalizer.class.getPackage().getName() + ".hapi-messages");
060        }
061
062        public HapiLocalizer(String... theBundleNames) {
063                init(theBundleNames);
064                addMessage("hapi.version", VersionUtil.getVersion());
065        }
066
067        /**
068         * Subclasses may use this to add hardcoded messages
069         */
070        @SuppressWarnings("WeakerAccess")
071        protected void addMessage(String theKey, String theMessage) {
072                myHardcodedMessages.put(theKey, theMessage);
073        }
074
075        public Set<String> getAllKeys() {
076                HashSet<String> retVal = new HashSet<>();
077                for (ResourceBundle nextBundle : myBundle) {
078                        Enumeration<String> keysEnum = nextBundle.getKeys();
079                        while (keysEnum.hasMoreElements()) {
080                                retVal.add(keysEnum.nextElement());
081                        }
082                }
083                return retVal;
084        }
085
086        /**
087         * @return Returns the raw message format string for the given key, or returns {@link #UNKNOWN_I18N_KEY_MESSAGE} if not found
088         */
089        @SuppressWarnings("WeakerAccess")
090        public String getFormatString(String theQualifiedKey) {
091                String formatString = myHardcodedMessages.get(theQualifiedKey);
092                if (isBlank(formatString)) {
093                        for (ResourceBundle nextBundle : myBundle) {
094                                if (nextBundle.containsKey(theQualifiedKey)) {
095                                        formatString = nextBundle.getString(theQualifiedKey);
096                                        formatString = trim(formatString);
097                                }
098                                if (isNotBlank(formatString)) {
099                                        break;
100                                }
101                        }
102                }
103
104                if (formatString == null) {
105                        ourLog.warn("Unknown localization key: {}", theQualifiedKey);
106                        if (ourFailOnMissingMessage) {
107                                throw new ConfigurationException(Msg.code(1908) + "Unknown localization key: " + theQualifiedKey);
108                        }
109                        formatString = UNKNOWN_I18N_KEY_MESSAGE;
110                }
111                return formatString;
112        }
113
114        public String getMessage(Class<?> theType, String theKey, Object... theParameters) {
115                return getMessage(toKey(theType, theKey), theParameters);
116        }
117
118        /**
119         * Create the message and sanitize parameters using {@link UrlUtil#sanitizeUrlPart(CharSequence)}
120         */
121        public String getMessageSanitized(Class<?> theType, String theKey, Object... theParameters) {
122                if (theParameters != null) {
123                        for (int i = 0; i < theParameters.length; i++) {
124                                if (theParameters[i] instanceof CharSequence) {
125                                        theParameters[i] = UrlUtil.sanitizeUrlPart((CharSequence) theParameters[i]);
126                                }
127                        }
128                }
129                return getMessage(toKey(theType, theKey), theParameters);
130        }
131
132        public String getMessage(String theQualifiedKey, Object... theParameters) {
133                if (theParameters != null && theParameters.length > 0) {
134                        MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey);
135                        if (format != null) {
136                                return format.format(theParameters);
137                        }
138
139                        String formatString = getFormatString(theQualifiedKey);
140
141                        format = newMessageFormat(formatString);
142                        myKeyToMessageFormat.put(theQualifiedKey, format);
143                        return format.format(theParameters);
144                }
145                return getFormatString(theQualifiedKey);
146        }
147
148        MessageFormat newMessageFormat(String theFormatString) {
149                StringBuilder pattern = new StringBuilder(theFormatString.trim());
150
151                for (int i = 0; i < (pattern.length() - 1); i++) {
152                        if (pattern.charAt(i) == '{') {
153                                char nextChar = pattern.charAt(i + 1);
154                                if (nextChar >= '0' && nextChar <= '9') {
155                                        continue;
156                                }
157
158                                pattern.replace(i, i + 1, "'{'");
159                                int closeBraceIndex = pattern.indexOf("}", i);
160                                if (closeBraceIndex > 0) {
161                                        i = closeBraceIndex;
162                                        pattern.replace(i, i + 1, "'}'");
163                                }
164                        }
165                }
166
167                return new MessageFormat(pattern.toString());
168        }
169
170        protected void init(String[] theBundleNames) {
171                myBundle = new ArrayList<>();
172                for (String nextName : theBundleNames) {
173                        myBundle.add(ResourceBundle.getBundle(nextName, new MultiFileResourceBundleControl()));
174                }
175        }
176
177        public Locale getLocale() {
178                return myLocale;
179        }
180
181        /**
182         * This <b>global setting</b> causes the localizer to fail if any attempts
183         * are made to retrieve a key that does not exist. This method is primarily for
184         * unit tests.
185         */
186        public static void setOurFailOnMissingMessage(boolean ourFailOnMissingMessage) {
187                HapiLocalizer.ourFailOnMissingMessage = ourFailOnMissingMessage;
188        }
189
190        public static String toKey(Class<?> theType, String theKey) {
191                return theType.getName() + '.' + theKey;
192        }
193
194        @VisibleForTesting
195        List<ResourceBundle> getBundles() {
196                return myBundle;
197        }
198}