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