001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software GmbH & Co. KG, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.i18n;
029
030import org.opencms.main.CmsIllegalArgumentException;
031import org.opencms.main.CmsLog;
032
033import java.util.ArrayList;
034import java.util.Hashtable;
035import java.util.Iterator;
036import java.util.List;
037import java.util.Locale;
038import java.util.Map;
039
040import org.apache.commons.logging.Log;
041
042import com.google.common.base.Optional;
043
044/**
045 * Provides access to the localized messages for several resource bundles simultaneously.<p>
046 *
047 * Messages are cached for faster lookup. If a localized key is contained in more then one resource bundle,
048 * it will be used only from the resource bundle where it was first found in. The resource bundle order is undefined. It is therefore
049 * recommended to ensure the uniqueness of all module keys by placing a special prefix in front of all keys of a resource bundle.<p>
050 *
051 * @since 6.0.0
052 */
053public class CmsMultiMessages extends CmsMessages {
054
055    /**
056     * Interface to provide fallback keys to be used when the message for a key is not found.<p>
057     */
058    public interface I_KeyFallbackHandler {
059
060        /**
061         * Gets the fallback key for the given key, or the absent value if there is no fallback key.<p>
062         *
063         * @param key the original key
064         *
065         * @return the fallback key
066         */
067        Optional<String> getFallbackKey(String key);
068    }
069
070    /** Constant for the multi bundle name. */
071    public static final String MULTI_BUNDLE_NAME = CmsMultiMessages.class.getName();
072
073    /** Null String value for caching of null message results. */
074    public static final String NULL_STRING = "null";
075
076    /** Static reference to the log. */
077    private static final Log LOG = CmsLog.getLog(CmsMultiMessages.class);
078
079    /** The key fallback handler. */
080    private I_KeyFallbackHandler m_keyFallbackHandler;
081
082    /** A cache for the messages to prevent multiple lookups in many bundles. */
083    private Map<String, String> m_messageCache;
084
085    /** List of resource bundles from the installed modules. */
086    private List<CmsMessages> m_messages;
087
088    /**
089     * Constructor for creating a new messages object initialized with the given locale.<p>
090     *
091     * @param locale the locale to use for localization of the messages
092     */
093    public CmsMultiMessages(Locale locale) {
094
095        super();
096        // set the bundle name and the locale
097        setBundleName(CmsMultiMessages.MULTI_BUNDLE_NAME);
098        setLocale(locale);
099        // generate array for the messages
100        m_messages = new ArrayList<CmsMessages>();
101        // use "old" Hashtable since it is the most efficient synchronized HashMap implementation
102        m_messageCache = new Hashtable<String, String>();
103    }
104
105    /**
106     * Adds a bundle instance to this multi message bundle.<p>
107     *
108     * The added bundle will be localized with the locale of this multi message bundle.<p>
109     *
110     * @param bundle the bundle instance to add
111     */
112    public void addBundle(I_CmsMessageBundle bundle) {
113
114        // add the localized bundle to the messages
115        addMessages(bundle.getBundle(getLocale()));
116    }
117
118    /**
119     * Adds a messages instance to this multi message bundle.<p>
120     *
121     * The messages instance should have been initialized with the same locale as this multi bundle,
122     * if not, the locale of the messages instance is automatically replaced. However, this will not work
123     * if the added messages instance is in face also of type <code>{@link CmsMultiMessages}</code>.<p>
124     *
125     * @param messages the messages instance to add
126     *
127     * @throws CmsIllegalArgumentException if the locale of the given <code>{@link CmsMultiMessages}</code> does not match the locale of this multi messages
128     */
129    public void addMessages(CmsMessages messages) throws CmsIllegalArgumentException {
130
131        Locale locale = messages.getLocale();
132        if (!getLocale().equals(locale)) {
133            // not the same locale, try to change the locale if this is a simple CmsMessage object
134            if (!(messages instanceof CmsMultiMessages)) {
135                // match locale of multi bundle
136                String bundleName = messages.getBundleName();
137                messages = new CmsMessages(bundleName, getLocale());
138            } else {
139                // multi bundles with wrong locales can't be added this way
140                throw new CmsIllegalArgumentException(Messages.get().container(
141                    Messages.ERR_MULTIMSG_LOCALE_DOES_NOT_MATCH_2,
142                    messages.getLocale(),
143                    getLocale()));
144            }
145        }
146        if (!m_messages.contains(messages)) {
147            if ((m_messageCache != null) && (m_messageCache.size() > 0)) {
148                // cache has already been used, must flush because of newly added keys
149                m_messageCache = new Hashtable<String, String>();
150            }
151            m_messages.add(messages);
152        }
153    }
154
155    /**
156     * Adds a list a messages instances to this multi message bundle.<p>
157     *
158     * @param messages the messages instance to add
159     */
160    public void addMessages(List<CmsMessages> messages) {
161
162        if (messages == null) {
163            throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_MULTIMSG_EMPTY_LIST_0));
164        }
165
166        Iterator<CmsMessages> i = messages.iterator();
167        while (i.hasNext()) {
168            addMessages(i.next());
169        }
170    }
171
172    /**
173     * Returns the list of all individual message objects in this multi message instance.<p>
174     *
175     * @return the list of all individual message objects in this multi message instance
176     */
177    public List<CmsMessages> getMessages() {
178
179        return m_messages;
180    }
181
182    /**
183     * @see org.opencms.i18n.CmsMessages#getString(java.lang.String)
184     */
185    @Override
186    public String getString(String keyName) {
187
188        return resolveKeyWithFallback(keyName);
189    }
190
191    /**
192     * @see org.opencms.i18n.CmsMessages#isInitialized()
193     */
194    @Override
195    public boolean isInitialized() {
196
197        return (m_messages != null) && !m_messages.isEmpty();
198    }
199
200    /**
201     * @see org.opencms.i18n.CmsMessages#key(java.lang.String, boolean)
202     */
203    @Override
204    public String key(String keyName, boolean allowNull) {
205
206        // special implementation since we uses several bundles for the messages
207        String result = resolveKeyWithFallback(keyName);
208        if ((result == null) && !allowNull) {
209            result = formatUnknownKey(keyName);
210        }
211        return result;
212    }
213
214    /**
215     * Sets the key fallback handler.<p>
216     *
217     * @param fallbackHandler the new key fallback handler
218     */
219    public void setFallbackHandler(I_KeyFallbackHandler fallbackHandler) {
220
221        m_keyFallbackHandler = fallbackHandler;
222    }
223
224    /**
225     * Returns the localized resource string for a given message key,
226     * checking the workplace default resources and all module bundles.<p>
227     *
228     * If the key was not found, <code>null</code> is returned.<p>
229     *
230     * @param keyName the key for the desired string
231     * @return the resource string for the given key or null if not found
232     */
233    private String resolveKey(String keyName) {
234
235        if (LOG.isDebugEnabled()) {
236            LOG.debug(Messages.get().getBundle().key(Messages.LOG_RESOLVE_MESSAGE_KEY_1, keyName));
237        }
238
239        String result = m_messageCache.get(keyName);
240        if (result == NULL_STRING) {
241            // key was already checked and not found
242            return null;
243        }
244        boolean noCache = false;
245        if (result == null) {
246            // so far not in the cache
247            for (int i = 0; (result == null) && (i < m_messages.size()); i++) {
248                try {
249                    result = (m_messages.get(i)).getString(keyName);
250                    // if no exception is thrown here we have found the result
251                    noCache |= m_messages.get(i).isUncacheable();
252                } catch (CmsMessageException e) {
253                    // can usually be ignored
254                    if (LOG.isDebugEnabled()) {
255                        LOG.debug(e.getMessage(), e);
256                    }
257                }
258            }
259        } else {
260            // result was found in cache
261            if (LOG.isDebugEnabled()) {
262                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MESSAGE_KEY_FOUND_CACHED_2, keyName, result));
263            }
264            return result;
265        }
266        if (result == null) {
267            // key was not found in "regular" bundle as well as module messages
268            if (LOG.isDebugEnabled()) {
269                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MESSAGE_KEY_NOT_FOUND_1, keyName));
270            }
271            // ensure null values are also cached
272            m_messageCache.put(keyName, NULL_STRING);
273        } else {
274            // optional debug output
275            if (LOG.isDebugEnabled()) {
276                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MESSAGE_KEY_FOUND_2, keyName, result));
277            }
278            if (!noCache) {
279                // cache the result
280                m_messageCache.put(keyName, result);
281            }
282        }
283        // return the result
284        return result;
285    }
286
287    /**
288     * Resolves a message key, using the key fallback handler if it is set.<p>
289     *
290     * @param keyName the key to resolve
291     *
292     * @return the resolved key
293     */
294    private String resolveKeyWithFallback(String keyName) {
295
296        String result = resolveKey(keyName);
297        if ((result == null) && (m_keyFallbackHandler != null)) {
298            Optional<String> fallback = m_keyFallbackHandler.getFallbackKey(keyName);
299            if (fallback.isPresent()) {
300                result = resolveKey(fallback.get());
301            }
302        }
303        return result;
304    }
305}