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    }