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