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