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}