001/** 002 * Copyright 2005-2016 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.krad.messages.providers; 017 018import org.apache.commons.lang.StringUtils; 019import org.kuali.rice.core.api.CoreApiServiceLocator; 020import org.kuali.rice.core.api.exception.RiceRuntimeException; 021import org.kuali.rice.krad.messages.Message; 022import org.kuali.rice.krad.messages.MessageProvider; 023import org.kuali.rice.krad.messages.MessageService; 024import org.kuali.rice.krad.service.KRADServiceLocatorWeb; 025import org.kuali.rice.krad.service.ModuleService; 026 027import java.util.ArrayList; 028import java.util.Arrays; 029import java.util.Collection; 030import java.util.Enumeration; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Locale; 034import java.util.Map; 035import java.util.ResourceBundle; 036 037/** 038 * Implementation of {@link MessageProvider} that stores messages in resource files 039 * 040 * @author Kuali Rice Team (rice.collab@kuali.org) 041 */ 042public class ResourceMessageProvider implements MessageProvider { 043 044 private static final String COMPONENT_PLACEHOLDER_BEGIN = "@cmp{"; 045 private static final String COMPONENT_PLACEHOLDER_END = "}"; 046 047 protected Map<String, List<ResourceBundle>> cachedResourceBundles; 048 049 public ResourceMessageProvider() { 050 cachedResourceBundles = new HashMap<String, List<ResourceBundle>>(); 051 } 052 053 /** 054 * Iterates through the resource bundles for the give namespace (or the application if namespace is not given) 055 * and finds the message that matches the given key 056 * 057 * <p> 058 * If the message is found in more than one bundle, the text from the bundle that is loaded last will be used 059 * </p> 060 * 061 * <p> 062 * If the given component code is the default component, resource keys that do not have a component defined 063 * and match the given key will also be considered matches 064 * </p> 065 * 066 * @see org.kuali.rice.krad.messages.MessageProvider#getMessage(java.lang.String, java.lang.String, 067 * java.lang.String, java.lang.String) 068 */ 069 public Message getMessage(String namespace, String component, String key, String locale) { 070 Message message = null; 071 072 List<ResourceBundle> bundles = getCachedResourceBundles(namespace, locale); 073 074 // iterate through bundles and find message text, if more than one bundle contains the message 075 // the last one iterated over will be used 076 String messageText = null; 077 for (ResourceBundle bundle : bundles) { 078 // check for key with component first 079 String resourceKey = COMPONENT_PLACEHOLDER_BEGIN + component + COMPONENT_PLACEHOLDER_END + key; 080 if (bundle.containsKey(resourceKey)) { 081 messageText = bundle.getString(resourceKey); 082 } 083 // if component is default then check for key without component code 084 else if (MessageService.DEFAULT_COMPONENT_CODE.equals(component) && bundle.containsKey(key)) { 085 messageText = bundle.getString(key); 086 } 087 } 088 089 // if message text was found build message object 090 if (StringUtils.isNotBlank(messageText)) { 091 message = buildMessage(namespace, component, key, messageText, locale); 092 } 093 094 return message; 095 } 096 097 /** 098 * Iterates through the resource bundles for the give namespace (or the application if namespace is not given) 099 * and finds all messages that match the given namespace 100 * 101 * <p> 102 * If the same resource key is found in more than one bundle, the text from the bundle that is 103 * loaded last will be used 104 * </p> 105 * 106 * <p> 107 * If the given component code is the default component, resource keys that do not have a component defined 108 * and match the given key will also be considered matches 109 * </p> 110 * 111 * @see org.kuali.rice.krad.messages.MessageProvider#getAllMessagesForComponent(java.lang.String, java.lang.String, 112 * java.lang.String) 113 */ 114 public Collection<Message> getAllMessagesForComponent(String namespace, String component, String locale) { 115 List<ResourceBundle> bundles = getCachedResourceBundles(namespace, locale); 116 117 Map<String, Message> messagesByKey = new HashMap<String, Message>(); 118 for (ResourceBundle bundle : bundles) { 119 Enumeration<String> resourceKeys = bundle.getKeys(); 120 while (resourceKeys.hasMoreElements()) { 121 String resourceKey = resourceKeys.nextElement(); 122 123 boolean match = false; 124 if (StringUtils.contains(resourceKey, 125 COMPONENT_PLACEHOLDER_BEGIN + component + COMPONENT_PLACEHOLDER_END)) { 126 match = true; 127 } else if (MessageService.DEFAULT_COMPONENT_CODE.equals(component) && !StringUtils.contains(resourceKey, 128 COMPONENT_PLACEHOLDER_BEGIN)) { 129 match = true; 130 } 131 132 if (match) { 133 String messageText = bundle.getString(resourceKey); 134 135 resourceKey = cleanResourceKey(resourceKey); 136 Message message = buildMessage(namespace, component, resourceKey, messageText, locale); 137 messagesByKey.put(resourceKey, message); 138 } 139 } 140 } 141 142 return messagesByKey.values(); 143 } 144 145 /** 146 * Removes any component declaration within the given resource key 147 * 148 * @param resourceKey resource key to clean 149 * @return String cleaned resource key 150 */ 151 protected String cleanResourceKey(String resourceKey) { 152 String cleanedKey = resourceKey; 153 154 String component = StringUtils.substringBetween(cleanedKey, COMPONENT_PLACEHOLDER_BEGIN, 155 COMPONENT_PLACEHOLDER_END); 156 if (StringUtils.isNotBlank(component)) { 157 cleanedKey = StringUtils.remove(cleanedKey, 158 COMPONENT_PLACEHOLDER_BEGIN + component + COMPONENT_PLACEHOLDER_END); 159 } 160 161 return cleanedKey; 162 } 163 164 /** 165 * Helper method to build a {@link Message} object from the given parameters 166 * 167 * @param namespace namespace for the message 168 * @param component component code for the message 169 * @param key message key 170 * @param messageText text for the message 171 * @param locale locale of the message 172 * @return Message instance populated with parameters 173 */ 174 protected Message buildMessage(String namespace, String component, String key, String messageText, String locale) { 175 Message message = new Message(); 176 177 message.setNamespaceCode(namespace); 178 message.setComponentCode(component); 179 180 key = cleanResourceKey(key); 181 message.setKey(key); 182 183 message.setText(messageText); 184 message.setLocale(locale); 185 186 return message; 187 } 188 189 /** 190 * Retrieves the list of resource bundles for the given namespace or locale from cache if present, otherwise 191 * the list is retrieved and then stored in cache for subsequent calls 192 * 193 * @param namespace namespace to retrieve bundles for 194 * @param localeCode locale code to use in selecting bundles 195 * @return List<ResourceBundle> list of resource bundles for the namespace or empty list if none were found 196 */ 197 protected List<ResourceBundle> getCachedResourceBundles(String namespace, String localeCode) { 198 if (StringUtils.isBlank(namespace)) { 199 namespace = MessageService.DEFAULT_NAMESPACE_CODE; 200 } 201 202 String cacheKey = namespace + "|" + localeCode; 203 if (cachedResourceBundles.containsKey(cacheKey)) { 204 return cachedResourceBundles.get(cacheKey); 205 } 206 207 List<ResourceBundle> bundles = null; 208 if (StringUtils.isBlank(namespace) || MessageService.DEFAULT_NAMESPACE_CODE.equals(namespace)) { 209 bundles = getResourceBundlesForApplication(localeCode); 210 } else { 211 bundles = getResourceBundlesForNamespace(namespace, localeCode); 212 } 213 214 cachedResourceBundles.put(cacheKey, bundles); 215 216 return bundles; 217 } 218 219 /** 220 * Retrieves the configured {@link ResourceBundle} instances for the given namespace and locale 221 * 222 * @param namespace namespace to retrieve bundles for 223 * @param localeCode locale code to use in selecting bundles 224 * @return List<ResourceBundle> list of resource bundles for the namespace or empty list if none were found 225 */ 226 protected List<ResourceBundle> getResourceBundlesForNamespace(String namespace, String localeCode) { 227 List<String> resourceBundleNames = getResourceBundleNamesForNamespace(namespace); 228 229 return getResourceBundles(resourceBundleNames, localeCode); 230 } 231 232 /** 233 * Retrieves the configured {@link ResourceBundle} instances for the application using the given locale code 234 * 235 * @param localeCode locale code to use in selecting bundles 236 * @return List<ResourceBundle> list of resource bundles for the application or empty list if none were found 237 */ 238 protected List<ResourceBundle> getResourceBundlesForApplication(String localeCode) { 239 List<String> resourceBundleNames = getResourceBundleNamesForApplication(); 240 241 return getResourceBundles(resourceBundleNames, localeCode); 242 } 243 244 /** 245 * Helper method to build a list of resource bundles for the given list of bundle names and locale code 246 * 247 * <p> 248 * For details on how resource bundles are selected given a bundle name and locale code see 249 * {@link ResourceBundle} 250 * </p> 251 * 252 * @param resourceBundleNames list of bundle names to get bundles for 253 * @param localeCode locale code to use when selecting bundles 254 * @return List<ResourceBundle> list of resource bundles (one for each bundle name if found) 255 */ 256 protected List<ResourceBundle> getResourceBundles(List<String> resourceBundleNames, String localeCode) { 257 List<ResourceBundle> resourceBundles = new ArrayList<ResourceBundle>(); 258 259 String[] localeIdentifiers = StringUtils.split(localeCode, "-"); 260 if ((localeIdentifiers == null) || (localeIdentifiers.length != 2)) { 261 throw new RiceRuntimeException("Invalid locale code: " + (localeCode == null ? "Null" : localeCode)); 262 } 263 264 Locale locale = new Locale(localeIdentifiers[0], localeIdentifiers[1]); 265 266 if (resourceBundleNames != null) { 267 for (String bundleName : resourceBundleNames) { 268 ResourceBundle bundle = ResourceBundle.getBundle(bundleName, locale); 269 if (bundle != null) { 270 resourceBundles.add(bundle); 271 } 272 } 273 } 274 275 return resourceBundles; 276 } 277 278 /** 279 * Retrieves the list of configured bundle names for the namespace 280 * 281 * <p> 282 * Resource bundle names are configured for a namespace using the property <code>resourceBundleNames</code> 283 * on the corresponding {@link org.kuali.rice.krad.bo.ModuleConfiguration} 284 * </p> 285 * 286 * @param namespace namespace to retrieve configured bundle names for 287 * @return List<String> list of bundle names or null if module was not found for given namespace 288 */ 289 protected List<String> getResourceBundleNamesForNamespace(String namespace) { 290 ModuleService moduleService = KRADServiceLocatorWeb.getKualiModuleService().getModuleServiceByNamespaceCode( 291 namespace); 292 if (moduleService != null) { 293 return moduleService.getModuleConfiguration().getResourceBundleNames(); 294 } 295 296 return null; 297 } 298 299 /** 300 * Retrieves the list of configured bundle names for the application 301 * 302 * <p> 303 * Resource bundle names are configured for the application using the configuration property 304 * <code>resourceBundleNames</code> 305 * </p> 306 * 307 * @return List<String> list of bundle names configured for the application 308 */ 309 protected List<String> getResourceBundleNamesForApplication() { 310 String resourceBundleNamesConfig = CoreApiServiceLocator.getKualiConfigurationService().getPropertyValueAsString( 311 "resourceBundleNames"); 312 if (StringUtils.isNotBlank(resourceBundleNamesConfig)) { 313 String[] resourceBundleNames = StringUtils.split(resourceBundleNamesConfig, ","); 314 315 return Arrays.asList(resourceBundleNames); 316 } 317 318 return null; 319 } 320}