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