001 /**
002 * Copyright 2005-2013 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 */
016 package org.kuali.rice.krad.messages.providers;
017
018 import org.apache.commons.lang.StringUtils;
019 import org.kuali.rice.core.api.exception.RiceRuntimeException;
020 import org.kuali.rice.krad.messages.Message;
021 import org.kuali.rice.krad.messages.MessageProvider;
022 import org.kuali.rice.krad.messages.MessageService;
023 import org.kuali.rice.krad.service.KRADServiceLocator;
024 import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
025 import org.kuali.rice.krad.service.ModuleService;
026
027 import java.util.ArrayList;
028 import java.util.Arrays;
029 import java.util.Collection;
030 import java.util.Enumeration;
031 import java.util.HashMap;
032 import java.util.List;
033 import java.util.Locale;
034 import java.util.Map;
035 import 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 */
042 public 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 = KRADServiceLocator.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 }