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