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}