
001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2025 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; 025import com.google.common.annotations.VisibleForTesting; 026 027import java.text.MessageFormat; 028import java.util.ArrayList; 029import java.util.Enumeration; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Locale; 034import java.util.Map; 035import java.util.ResourceBundle; 036import java.util.Set; 037import java.util.concurrent.ConcurrentHashMap; 038 039import static org.apache.commons.lang3.StringUtils.isBlank; 040import static org.apache.commons.lang3.StringUtils.isNotBlank; 041import static org.apache.commons.lang3.StringUtils.trim; 042 043/** 044 * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with caution 045 */ 046public class HapiLocalizer { 047 048 @SuppressWarnings("WeakerAccess") 049 public static final String UNKNOWN_I18N_KEY_MESSAGE = "!MESSAGE!"; 050 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 UrlUtil#sanitizeUrlPart(CharSequence)} 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 for (int i = 0; i < (pattern.length() - 1); i++) { 152 if (pattern.charAt(i) == '{') { 153 char nextChar = pattern.charAt(i + 1); 154 if (nextChar >= '0' && nextChar <= '9') { 155 continue; 156 } 157 158 pattern.replace(i, i + 1, "'{'"); 159 int closeBraceIndex = pattern.indexOf("}", i); 160 if (closeBraceIndex > 0) { 161 i = closeBraceIndex; 162 pattern.replace(i, i + 1, "'}'"); 163 } 164 } 165 } 166 167 return new MessageFormat(pattern.toString()); 168 } 169 170 protected void init(String[] theBundleNames) { 171 myBundle = new ArrayList<>(); 172 for (String nextName : theBundleNames) { 173 myBundle.add(ResourceBundle.getBundle(nextName, new MultiFileResourceBundleControl())); 174 } 175 } 176 177 public Locale getLocale() { 178 return myLocale; 179 } 180 181 /** 182 * This <b>global setting</b> causes the localizer to fail if any attempts 183 * are made to retrieve a key that does not exist. This method is primarily for 184 * unit tests. 185 */ 186 public static void setOurFailOnMissingMessage(boolean ourFailOnMissingMessage) { 187 HapiLocalizer.ourFailOnMissingMessage = ourFailOnMissingMessage; 188 } 189 190 public static String toKey(Class<?> theType, String theKey) { 191 return theType.getName() + '.' + theKey; 192 } 193 194 @VisibleForTesting 195 List<ResourceBundle> getBundles() { 196 return myBundle; 197 } 198}