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}