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}