
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}