001/* 002 * #%L 003 * HAPI FHIR - Core Library 004 * %% 005 * Copyright (C) 2014 - 2024 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 * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with caution 044 */ 045public class HapiLocalizer { 046 047 @SuppressWarnings("WeakerAccess") 048 public static final String UNKNOWN_I18N_KEY_MESSAGE = "!MESSAGE!"; 049 050 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HapiLocalizer.class); 051 private static boolean ourFailOnMissingMessage; 052 private final Map<String, MessageFormat> myKeyToMessageFormat = new ConcurrentHashMap<>(); 053 private List<ResourceBundle> myBundle; 054 private final Map<String, String> myHardcodedMessages = new HashMap<>(); 055 private Locale myLocale = Locale.getDefault(); 056 057 public HapiLocalizer() { 058 this(HapiLocalizer.class.getPackage().getName() + ".hapi-messages"); 059 } 060 061 public HapiLocalizer(String... theBundleNames) { 062 init(theBundleNames); 063 addMessage("hapi.version", VersionUtil.getVersion()); 064 } 065 066 /** 067 * Subclasses may use this to add hardcoded messages 068 */ 069 @SuppressWarnings("WeakerAccess") 070 protected void addMessage(String theKey, String theMessage) { 071 myHardcodedMessages.put(theKey, theMessage); 072 } 073 074 public Set<String> getAllKeys() { 075 HashSet<String> retVal = new HashSet<>(); 076 for (ResourceBundle nextBundle : myBundle) { 077 Enumeration<String> keysEnum = nextBundle.getKeys(); 078 while (keysEnum.hasMoreElements()) { 079 retVal.add(keysEnum.nextElement()); 080 } 081 } 082 return retVal; 083 } 084 085 /** 086 * @return Returns the raw message format string for the given key, or returns {@link #UNKNOWN_I18N_KEY_MESSAGE} if not found 087 */ 088 @SuppressWarnings("WeakerAccess") 089 public String getFormatString(String theQualifiedKey) { 090 String formatString = myHardcodedMessages.get(theQualifiedKey); 091 if (isBlank(formatString)) { 092 for (ResourceBundle nextBundle : myBundle) { 093 if (nextBundle.containsKey(theQualifiedKey)) { 094 formatString = nextBundle.getString(theQualifiedKey); 095 formatString = trim(formatString); 096 } 097 if (isNotBlank(formatString)) { 098 break; 099 } 100 } 101 } 102 103 if (formatString == null) { 104 ourLog.warn("Unknown localization key: {}", theQualifiedKey); 105 if (ourFailOnMissingMessage) { 106 throw new ConfigurationException(Msg.code(1908) + "Unknown localization key: " + theQualifiedKey); 107 } 108 formatString = UNKNOWN_I18N_KEY_MESSAGE; 109 } 110 return formatString; 111 } 112 113 public String getMessage(Class<?> theType, String theKey, Object... theParameters) { 114 return getMessage(toKey(theType, theKey), theParameters); 115 } 116 117 /** 118 * Create the message and sanitize parameters using {@link } 119 */ 120 public String getMessageSanitized(Class<?> theType, String theKey, Object... theParameters) { 121 if (theParameters != null) { 122 for (int i = 0; i < theParameters.length; i++) { 123 if (theParameters[i] instanceof CharSequence) { 124 theParameters[i] = UrlUtil.sanitizeUrlPart((CharSequence) theParameters[i]); 125 } 126 } 127 } 128 return getMessage(toKey(theType, theKey), theParameters); 129 } 130 131 public String getMessage(String theQualifiedKey, Object... theParameters) { 132 if (theParameters != null && theParameters.length > 0) { 133 MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey); 134 if (format != null) { 135 return format.format(theParameters); 136 } 137 138 String formatString = getFormatString(theQualifiedKey); 139 140 format = newMessageFormat(formatString); 141 myKeyToMessageFormat.put(theQualifiedKey, format); 142 return format.format(theParameters); 143 } 144 return getFormatString(theQualifiedKey); 145 } 146 147 MessageFormat newMessageFormat(String theFormatString) { 148 StringBuilder pattern = new StringBuilder(theFormatString.trim()); 149 150 for (int i = 0; i < (pattern.length() - 1); i++) { 151 if (pattern.charAt(i) == '{') { 152 char nextChar = pattern.charAt(i + 1); 153 if (nextChar >= '0' && nextChar <= '9') { 154 continue; 155 } 156 157 pattern.replace(i, i + 1, "'{'"); 158 int closeBraceIndex = pattern.indexOf("}", i); 159 if (closeBraceIndex > 0) { 160 i = closeBraceIndex; 161 pattern.replace(i, i + 1, "'}'"); 162 } 163 } 164 } 165 166 return new MessageFormat(pattern.toString()); 167 } 168 169 protected void init(String[] theBundleNames) { 170 myBundle = new ArrayList<>(); 171 for (String nextName : theBundleNames) { 172 myBundle.add(ResourceBundle.getBundle(nextName)); 173 } 174 } 175 176 public Locale getLocale() { 177 return myLocale; 178 } 179 180 /** 181 * This <b>global setting</b> causes the localizer to fail if any attempts 182 * are made to retrieve a key that does not exist. This method is primarily for 183 * unit tests. 184 */ 185 public static void setOurFailOnMissingMessage(boolean ourFailOnMissingMessage) { 186 HapiLocalizer.ourFailOnMissingMessage = ourFailOnMissingMessage; 187 } 188 189 public static String toKey(Class<?> theType, String theKey) { 190 return theType.getName() + '.' + theKey; 191 } 192}