001/*
002 * #%L
003 * HAPI FHIR - Core Library
004 * %%
005 * Copyright (C) 2014 - 2023 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
044/**
045 * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with caution
046 */
047public class HapiLocalizer {
048
049        @SuppressWarnings("WeakerAccess")
050        public static final String UNKNOWN_I18N_KEY_MESSAGE = "!MESSAGE!";
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 }
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
152                for (int i = 0; i < (pattern.length()-1); i++) {
153                        if (pattern.charAt(i) == '{') {
154                                char nextChar = pattern.charAt(i+1);
155                                if (nextChar >= '0' && nextChar <= '9') {
156                                        continue;
157                                }
158
159                                pattern.replace(i, i+1, "'{'");
160                                int closeBraceIndex = pattern.indexOf("}", i);
161                                if (closeBraceIndex > 0) {
162                                        i = closeBraceIndex;
163                                        pattern.replace(i, i+1, "'}'");
164                                }
165                        }
166                }
167
168                return new MessageFormat(pattern.toString());
169        }
170
171        protected void init(String[] theBundleNames) {
172                myBundle = new ArrayList<>();
173                for (String nextName : theBundleNames) {
174                        myBundle.add(ResourceBundle.getBundle(nextName));
175                }
176        }
177
178    public Locale getLocale() {
179                return myLocale;
180    }
181
182    /**
183         * This <b>global setting</b> causes the localizer to fail if any attempts
184         * are made to retrieve a key that does not exist. This method is primarily for
185         * unit tests.
186         */
187        public static void setOurFailOnMissingMessage(boolean ourFailOnMissingMessage) {
188                HapiLocalizer.ourFailOnMissingMessage = ourFailOnMissingMessage;
189        }
190
191        public static String toKey(Class<?> theType, String theKey) {
192                return theType.getName() + '.' + theKey;
193        }
194
195}