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;
039import java.util.concurrent.ConcurrentHashMap;
040
041import org.apache.commons.logging.Log;
042
043import com.google.common.base.Optional;
044
045/**
046 * Provides access to the localized messages for several resource bundles simultaneously.<p>
047 *
048 * Messages are cached for faster lookup. If a localized key is contained in more then one resource bundle,
049 * it will be used only from the resource bundle where it was first found in. The resource bundle order is undefined. It is therefore
050 * recommended to ensure the uniqueness of all module keys by placing a special prefix in front of all keys of a resource bundle.<p>
051 *
052 * @since 6.0.0
053 */
054public class CmsMultiMessages extends CmsMessages {
055
056    /**
057     * Interface to provide fallback keys to be used when the message for a key is not found.<p>
058     */
059    public interface I_KeyFallbackHandler {
060
061        /**
062         * Gets the fallback key for the given key, or the absent value if there is no fallback key.<p>
063         *
064         * @param key the original key
065         *
066         * @return the fallback key
067         */
068        Optional<String> getFallbackKey(String key);
069    }
070
071    /** Constant for the multi bundle name. */
072    public static final String MULTI_BUNDLE_NAME = CmsMultiMessages.class.getName();
073
074    /** Null String value for caching of null message results. */
075    public static final String NULL_STRING = "null";
076
077    /** Static reference to the log. */
078    private static final Log LOG = CmsLog.getLog(CmsMultiMessages.class);
079
080    /** The key fallback handler. */
081    private I_KeyFallbackHandler m_keyFallbackHandler;
082
083    /** For successfully found message keys, indicates the position in the bundle list where they were last found. */
084    private Map<String, Integer> m_lastBundleIndexForKey = new ConcurrentHashMap<>();
085
086    /** A cache for the messages to prevent multiple lookups in many bundles. */
087    private Map<String, String> m_messageCache;
088
089    /** List of resource bundles from the installed modules. */
090    private List<CmsMessages> m_messages;
091
092    /**
093     * Constructor for creating a new messages object initialized with the given locale.<p>
094     *
095     * @param locale the locale to use for localization of the messages
096     */
097    public CmsMultiMessages(Locale locale) {
098
099        super();
100        // set the bundle name and the locale
101        setBundleName(CmsMultiMessages.MULTI_BUNDLE_NAME);
102        setLocale(locale);
103        // generate array for the messages
104        m_messages = new ArrayList<CmsMessages>();
105        // use "old" Hashtable since it is the most efficient synchronized HashMap implementation
106        m_messageCache = new Hashtable<String, String>();
107    }
108
109    /**
110     * Adds a bundle instance to this multi message bundle.<p>
111     *
112     * The added bundle will be localized with the locale of this multi message bundle.<p>
113     *
114     * @param bundle the bundle instance to add
115     */
116    public void addBundle(I_CmsMessageBundle bundle) {
117
118        // add the localized bundle to the messages
119        addMessages(bundle.getBundle(getLocale()));
120    }
121
122    /**
123     * Adds a messages instance to this multi message bundle.<p>
124     *
125     * The messages instance should have been initialized with the same locale as this multi bundle,
126     * if not, the locale of the messages instance is automatically replaced. However, this will not work
127     * if the added messages instance is in face also of type <code>{@link CmsMultiMessages}</code>.<p>
128     *
129     * @param messages the messages instance to add
130     *
131     * @throws CmsIllegalArgumentException if the locale of the given <code>{@link CmsMultiMessages}</code> does not match the locale of this multi messages
132     */
133    public void addMessages(CmsMessages messages) throws CmsIllegalArgumentException {
134
135        Locale locale = messages.getLocale();
136        if (!getLocale().equals(locale)) {
137            // not the same locale, try to change the locale if this is a simple CmsMessage object
138            if (!(messages instanceof CmsMultiMessages)) {
139                // match locale of multi bundle
140                String bundleName = messages.getBundleName();
141                messages = new CmsMessages(bundleName, getLocale());
142            } else {
143                // multi bundles with wrong locales can't be added this way
144                throw new CmsIllegalArgumentException(
145                    Messages.get().container(
146                        Messages.ERR_MULTIMSG_LOCALE_DOES_NOT_MATCH_2,
147                        messages.getLocale(),
148                        getLocale()));
149            }
150        }
151        if (!m_messages.contains(messages)) {
152            if ((m_messageCache != null) && (m_messageCache.size() > 0)) {
153                // cache has already been used, must flush because of newly added keys
154                m_messageCache = new Hashtable<String, String>();
155            }
156            m_messages.add(messages);
157        }
158    }
159
160    /**
161     * Adds a list a messages instances to this multi message bundle.<p>
162     *
163     * @param messages the messages instance to add
164     */
165    public void addMessages(List<CmsMessages> messages) {
166
167        if (messages == null) {
168            throw new CmsIllegalArgumentException(Messages.get().container(Messages.ERR_MULTIMSG_EMPTY_LIST_0));
169        }
170
171        Iterator<CmsMessages> i = messages.iterator();
172        while (i.hasNext()) {
173            addMessages(i.next());
174        }
175    }
176
177    /**
178     * Returns the list of all individual message objects in this multi message instance.<p>
179     *
180     * @return the list of all individual message objects in this multi message instance
181     */
182    public List<CmsMessages> getMessages() {
183
184        return m_messages;
185    }
186
187    /**
188     * @see org.opencms.i18n.CmsMessages#getString(java.lang.String)
189     */
190    @Override
191    public String getString(String keyName) {
192
193        return resolveKeyWithFallback(keyName);
194    }
195
196    /**
197     * @see org.opencms.i18n.CmsMessages#isInitialized()
198     */
199    @Override
200    public boolean isInitialized() {
201
202        return (m_messages != null) && !m_messages.isEmpty();
203    }
204
205    /**
206     * @see org.opencms.i18n.CmsMessages#key(java.lang.String, boolean)
207     */
208    @Override
209    public String key(String keyName, boolean allowNull) {
210
211        // special implementation since we uses several bundles for the messages
212        String result = resolveKeyWithFallback(keyName);
213        if ((result == null) && !allowNull) {
214            result = formatUnknownKey(keyName);
215        }
216        return result;
217    }
218
219    /**
220     * Sets the key fallback handler.<p>
221     *
222     * @param fallbackHandler the new key fallback handler
223     */
224    public void setFallbackHandler(I_KeyFallbackHandler fallbackHandler) {
225
226        m_keyFallbackHandler = fallbackHandler;
227    }
228
229    /**
230     * Returns the localized resource string for a given message key,
231     * checking the workplace default resources and all module bundles.<p>
232     *
233     * If the key was not found, <code>null</code> is returned.<p>
234     *
235     * @param keyName the key for the desired string
236     * @return the resource string for the given key or null if not found
237     */
238    private String resolveKey(String keyName) {
239
240        if (LOG.isDebugEnabled()) {
241            LOG.debug(Messages.get().getBundle().key(Messages.LOG_RESOLVE_MESSAGE_KEY_1, keyName));
242        }
243
244        String result = m_messageCache.get(keyName);
245        if (result == NULL_STRING) {
246            // key was already checked and not found
247            return null;
248        }
249        boolean noCache = false;
250        Integer indexHint = m_lastBundleIndexForKey.get(keyName);
251        // Look at the position where the key was last found.
252        // This may not be successful, because VFS-based bundles are mutable and keys could have been removed.
253        if (indexHint != null) {
254            int i = indexHint.intValue();
255            if (i < m_messages.size()) {
256                try {
257                    result = m_messages.get(i).getString(keyName);
258                    noCache = true;
259                } catch (CmsMessageException e) {
260                    if (LOG.isDebugEnabled()) {
261                        LOG.debug(e.getMessage(), e);
262                    }
263                }
264            }
265        }
266        if (result == null) {
267            // so far not in the cache
268            for (int i = 0; (result == null) && (i < m_messages.size()); i++) {
269                try {
270                    result = (m_messages.get(i)).getString(keyName);
271                    m_lastBundleIndexForKey.put(keyName, Integer.valueOf(i));
272                    // if no exception is thrown here we have found the result
273                    noCache |= m_messages.get(i).isUncacheable();
274                } catch (CmsMessageException e) {
275                    // can usually be ignored
276                    if (LOG.isDebugEnabled()) {
277                        LOG.debug(e.getMessage(), e);
278                    }
279                }
280            }
281        } else {
282            // result was found in cache
283            if (LOG.isDebugEnabled()) {
284                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MESSAGE_KEY_FOUND_CACHED_2, keyName, result));
285            }
286            return result;
287        }
288        if (result == null) {
289            // key was not found in "regular" bundle as well as module messages
290            if (LOG.isDebugEnabled()) {
291                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MESSAGE_KEY_NOT_FOUND_1, keyName));
292            }
293            // ensure null values are also cached
294            m_messageCache.put(keyName, NULL_STRING);
295        } else {
296            // optional debug output
297            if (LOG.isDebugEnabled()) {
298                LOG.debug(Messages.get().getBundle().key(Messages.LOG_MESSAGE_KEY_FOUND_2, keyName, result));
299            }
300            if (!noCache) {
301                // cache the result
302                m_messageCache.put(keyName, result);
303            }
304        }
305        // return the result
306        return result;
307    }
308
309    /**
310     * Resolves a message key, using the key fallback handler if it is set.<p>
311     *
312     * @param keyName the key to resolve
313     *
314     * @return the resolved key
315     */
316    private String resolveKeyWithFallback(String keyName) {
317
318        String result = resolveKey(keyName);
319        if ((result == null) && (m_keyFallbackHandler != null)) {
320            Optional<String> fallback = m_keyFallbackHandler.getFallbackKey(keyName);
321            if (fallback.isPresent()) {
322                result = resolveKey(fallback.get());
323            }
324        }
325        return result;
326    }
327}