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 * #%L 015 * HAPI FHIR - Core Library 016 * %% 017 * Copyright (C) 2014 - 2019 University Health Network 018 * %% 019 * Licensed under the Apache License, Version 2.0 (the "License"); 020 * you may not use this file except in compliance with the License. 021 * You may obtain a copy of the License at 022 * 023 * http://www.apache.org/licenses/LICENSE-2.0 024 * 025 * Unless required by applicable law or agreed to in writing, software 026 * distributed under the License is distributed on an "AS IS" BASIS, 027 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 028 * See the License for the specific language governing permissions and 029 * limitations under the License. 030 * #L% 031 */ 032 033/** 034 * This feature is not yet in its final state and should be considered an internal part of HAPI for now - use with caution 035 */ 036public class HapiLocalizer { 037 038 @SuppressWarnings("WeakerAccess") 039 public static final String UNKNOWN_I18N_KEY_MESSAGE = "!MESSAGE!"; 040 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HapiLocalizer.class); 041 private static boolean ourFailOnMissingMessage; 042 private final Map<String, MessageFormat> myKeyToMessageFormat = new ConcurrentHashMap<>(); 043 private List<ResourceBundle> myBundle = new ArrayList<>(); 044 private final Map<String, String> myHardcodedMessages = new HashMap<>(); 045 private String[] myBundleNames; 046 047 public HapiLocalizer() { 048 this(HapiLocalizer.class.getPackage().getName() + ".hapi-messages"); 049 } 050 051 public HapiLocalizer(String... theBundleNames) { 052 myBundleNames = theBundleNames; 053 init(); 054 addMessage("hapi.version", VersionUtil.getVersion()); 055 } 056 057 /** 058 * Subclasses may use this to add hardcoded messages 059 */ 060 @SuppressWarnings("WeakerAccess") 061 protected void addMessage(String theKey, String theMessage) { 062 myHardcodedMessages.put(theKey, theMessage); 063 } 064 065 public Set<String> getAllKeys() { 066 HashSet<String> retVal = new HashSet<>(); 067 for (ResourceBundle nextBundle : myBundle) { 068 Enumeration<String> keysEnum = nextBundle.getKeys(); 069 while (keysEnum.hasMoreElements()) { 070 retVal.add(keysEnum.nextElement()); 071 } 072 } 073 return retVal; 074 } 075 076 /** 077 * @return Returns the raw message format string for the given key, or returns {@link #UNKNOWN_I18N_KEY_MESSAGE} if not found 078 */ 079 @SuppressWarnings("WeakerAccess") 080 public String getFormatString(String theQualifiedKey) { 081 String formatString = myHardcodedMessages.get(theQualifiedKey); 082 if (isBlank(formatString)) { 083 for (ResourceBundle nextBundle : myBundle) { 084 if (nextBundle.containsKey(theQualifiedKey)) { 085 formatString = nextBundle.getString(theQualifiedKey); 086 formatString = trim(formatString); 087 } 088 if (isNotBlank(formatString)) { 089 break; 090 } 091 } 092 } 093 094 if (formatString == null) { 095 ourLog.warn("Unknown localization key: {}", theQualifiedKey); 096 if (ourFailOnMissingMessage) { 097 throw new ConfigurationException("Unknown localization key: " + theQualifiedKey); 098 } 099 formatString = UNKNOWN_I18N_KEY_MESSAGE; 100 } 101 return formatString; 102 } 103 104 public String getMessage(Class<?> theType, String theKey, Object... theParameters) { 105 return getMessage(toKey(theType, theKey), theParameters); 106 } 107 108 /** 109 * Create the message and sanitize parameters using {@link } 110 */ 111 public String getMessageSanitized(Class<?> theType, String theKey, Object... theParameters) { 112 if (theParameters != null) { 113 for (int i = 0; i < theParameters.length; i++) { 114 if (theParameters[i] instanceof CharSequence) { 115 theParameters[i] = UrlUtil.sanitizeUrlPart((CharSequence) theParameters[i]); 116 } 117 } 118 } 119 return getMessage(toKey(theType, theKey), theParameters); 120 } 121 122 public String getMessage(String theQualifiedKey, Object... theParameters) { 123 if (theParameters != null && theParameters.length > 0) { 124 MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey); 125 if (format != null) { 126 return format.format(theParameters); 127 } 128 129 String formatString = getFormatString(theQualifiedKey); 130 131 format = newMessageFormat(formatString); 132 myKeyToMessageFormat.put(theQualifiedKey, format); 133 return format.format(theParameters); 134 } 135 return getFormatString(theQualifiedKey); 136 } 137 138 MessageFormat newMessageFormat(String theFormatString) { 139 StringBuilder pattern = new StringBuilder(theFormatString.trim()); 140 141 142 for (int i = 0; i < (pattern.length()-1); i++) { 143 if (pattern.charAt(i) == '{') { 144 char nextChar = pattern.charAt(i+1); 145 if (nextChar >= '0' && nextChar <= '9') { 146 continue; 147 } 148 149 pattern.replace(i, i+1, "'{'"); 150 int closeBraceIndex = pattern.indexOf("}", i); 151 if (closeBraceIndex > 0) { 152 i = closeBraceIndex; 153 pattern.replace(i, i+1, "'}'"); 154 } 155 } 156 } 157 158 return new MessageFormat(pattern.toString()); 159 } 160 161 protected void init() { 162 for (String nextName : myBundleNames) { 163 myBundle.add(ResourceBundle.getBundle(nextName)); 164 } 165 } 166 167 /** 168 * This <b>global setting</b> causes the localizer to fail if any attempts 169 * are made to retrieve a key that does not exist. This method is primarily for 170 * unit tests. 171 */ 172 public static void setOurFailOnMissingMessage(boolean ourFailOnMissingMessage) { 173 HapiLocalizer.ourFailOnMissingMessage = ourFailOnMissingMessage; 174 } 175 176 public static String toKey(Class<?> theType, String theKey) { 177 return theType.getName() + '.' + theKey; 178 } 179 180}