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, 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.util.CmsFileUtil; 031 032import java.io.File; 033import java.io.FileInputStream; 034import java.io.IOException; 035import java.io.InputStream; 036import java.net.URL; 037import java.security.AccessControlException; 038import java.util.HashSet; 039import java.util.List; 040import java.util.Locale; 041import java.util.Map; 042import java.util.MissingResourceException; 043import java.util.ResourceBundle; 044import java.util.Set; 045import java.util.concurrent.ConcurrentHashMap; 046 047/** 048 * Resource bundle loader for property based resource bundles from OpenCms that has a flushable cache.<p> 049 * 050 * The main reason for implementing this is that the Java default resource bundle loading mechanism 051 * provided by {@link java.util.ResourceBundle#getBundle(java.lang.String, java.util.Locale)} uses a 052 * cache that can NOT be flushed by any standard means. This means for every simple change in a resource 053 * bundle, the Java VM (and the webapp container that runs OpenCms) must be restarted. 054 * This non-standard resource bundle loader avoids this by providing a flushable cache.<p> 055 * 056 * In case the requested bundle can not be found, a fallback mechanism to 057 * {@link java.util.ResourceBundle#getBundle(java.lang.String, java.util.Locale)} is used to look up 058 * the resource bundle with the Java default resource bundle loading mechanism.<p> 059 * 060 * @see java.util.ResourceBundle 061 * @see java.util.PropertyResourceBundle 062 * @see org.opencms.i18n.CmsPropertyResourceBundle 063 * 064 * @since 6.2.0 065 */ 066public final class CmsResourceBundleLoader { 067 068 /** 069 * Cache key for the ResourceBundle cache.<p> 070 * 071 * Resource bundles are keyed by the combination of bundle name, locale, and class loader. 072 */ 073 private static class BundleKey { 074 075 /** The base bundle name. */ 076 private String m_baseName; 077 078 /** The hash code. */ 079 private int m_hashcode; 080 081 /** The locale. */ 082 private Locale m_locale; 083 084 /** 085 * Create an initialized bundle key.<p> 086 * 087 * @param s the base name 088 * @param l the locale 089 */ 090 BundleKey(String s, Locale l) { 091 092 set(s, l); 093 } 094 095 /** 096 * @see java.lang.Object#equals(java.lang.Object) 097 */ 098 @Override 099 public boolean equals(Object o) { 100 101 if (!(o instanceof BundleKey)) { 102 return false; 103 } 104 BundleKey key = (BundleKey)o; 105 return (m_hashcode == key.m_hashcode) && m_baseName.equals(key.m_baseName) && m_locale.equals(key.m_locale); 106 } 107 108 /** 109 * @see java.lang.Object#hashCode() 110 */ 111 @Override 112 public int hashCode() { 113 114 return m_hashcode; 115 } 116 117 /** 118 * Checks if the given base name is identical to the base name of this bundle key.<p> 119 * 120 * @param baseName the base name to compare 121 * 122 * @return <code>true</code> if the given base name is identical to the base name of this bundle key 123 */ 124 public boolean isSameBase(String baseName) { 125 126 return m_baseName.equals(baseName); 127 } 128 129 /** 130 * @see java.lang.Object#toString() 131 */ 132 @Override 133 public String toString() { 134 135 return m_baseName + "_" + m_locale; 136 } 137 138 /** 139 * Initialize this bundle key.<p> 140 * 141 * @param s the base name 142 * @param l the locale 143 */ 144 void set(String s, Locale l) { 145 146 m_baseName = s; 147 m_locale = l; 148 m_hashcode = m_baseName.hashCode() ^ m_locale.hashCode(); 149 } 150 } 151 152 /** The resource bundle cache. */ 153 private static Map<BundleKey, ResourceBundle> m_bundleCache; 154 155 /** The last default Locale we saw, if this ever changes then we have to reset our caches. */ 156 private static Locale m_lastDefaultLocale; 157 158 /** Cache lookup key to avoid having to a new one for every getBundle() call. */ 159 // private static BundleKey m_lookupKey = new BundleKey(); 160 161 /** The permanent list resource bundle cache. */ 162 private static Map<String, I_CmsResourceBundle> m_permanentCache; 163 164 /** Singleton cache entry to represent previous failed lookups. */ 165 private static final ResourceBundle NULL_ENTRY = new CmsListResourceBundle(); 166 167 static { 168 m_bundleCache = new ConcurrentHashMap<BundleKey, ResourceBundle>(); 169 m_lastDefaultLocale = Locale.getDefault(); 170 m_permanentCache = new ConcurrentHashMap<String, I_CmsResourceBundle>(); 171 } 172 173 /** 174 * Hides the public constructor.<p> 175 */ 176 private CmsResourceBundleLoader() { 177 178 // noop 179 } 180 181 /** 182 * Adds the specified resource bundle to the permanent cache.<p> 183 * 184 * @param baseName the raw bundle name, without locale qualifiers 185 * @param locale the locale 186 * @param bundle the bundle to cache 187 */ 188 public static void addBundleToCache(String baseName, Locale locale, I_CmsResourceBundle bundle) { 189 190 String key = baseName; 191 if (locale != null) { 192 key += "_" + locale; 193 } 194 m_permanentCache.put(key, bundle); 195 } 196 197 /** 198 * Flushes the complete resource bundle cache.<p> 199 */ 200 public static void flushBundleCache() { 201 202 synchronized (m_bundleCache) { 203 m_bundleCache.clear(); 204 } 205 // We are not flushing the permanent cache on clear! 206 // Reason: It's not 100% clear if the cache would be filled correctly from the XML after a flush. 207 // For example if a reference to an XML content object is held, than after a clear cache, this 208 // object would not have a working localization since the schema and handler would not be initialized again. 209 // For XML contents that are unmarshalled after the clear cache the localization would work, but it 210 // seems likely that old references are held. 211 // On the other hand, if something is changed in the XML, the cache is updated anyway, so we won't be 212 // stuck with "old" resource bundles that require a server restart. 213 214 // m_permanentCache.clear(); 215 } 216 217 /** 218 * Flushes all variations for the provided bundle from the cache.<p> 219 * 220 * @param baseName the bundle base name to flush the variations for 221 * @param flushPermanent if true, the cache for additional message bundles will be flushed, too 222 */ 223 public static void flushBundleCache(String baseName, boolean flushPermanent) { 224 225 if (baseName != null) { 226 synchronized (m_bundleCache) { 227 228 // first check and clear the bundle cache 229 Map<BundleKey, ResourceBundle> bundleCacheNew = new ConcurrentHashMap<BundleKey, ResourceBundle>( 230 m_bundleCache.size()); 231 for (Map.Entry<BundleKey, ResourceBundle> entry : m_bundleCache.entrySet()) { 232 if (!entry.getKey().isSameBase(baseName)) { 233 // entry has a different base name, keep it 234 bundleCacheNew.put(entry.getKey(), entry.getValue()); 235 } 236 } 237 if (bundleCacheNew.size() < m_bundleCache.size()) { 238 // switch caches if only if at least one entry was removed 239 m_bundleCache = bundleCacheNew; 240 } 241 if (flushPermanent) { 242 flushPermanentCache(baseName); 243 } 244 } 245 } 246 } 247 248 /** 249 * Removes bundles with the given base name from the permanent cache.<p> 250 * 251 * @param baseName the bundle base name 252 */ 253 public static void flushPermanentCache(String baseName) { 254 255 Set<String> keys = new HashSet<String>(m_permanentCache.keySet()); 256 for (String key : keys) { 257 if ((key.startsWith(baseName) 258 && ((key.length() == baseName.length()) || (key.charAt(baseName.length()) == '_')))) { 259 // entry has a the same base name, remove it 260 m_permanentCache.remove(key); 261 } 262 } 263 } 264 265 /** 266 * Get the appropriate ResourceBundle for the given locale. The following 267 * strategy is used: 268 * 269 * <p>A sequence of candidate bundle names are generated, and tested in 270 * this order, where the suffix 1 means the string from the specified 271 * locale, and the suffix 2 means the string from the default locale:</p> 272 * 273 * <ul> 274 * <li>baseName + "_" + language1 + "_" + country1 + "_" + variant1</li> 275 * <li>baseName + "_" + language1 + "_" + country1</li> 276 * <li>baseName + "_" + language1</li> 277 * <li>baseName + "_" + language2 + "_" + country2 + "_" + variant2</li> 278 * <li>baseName + "_" + language2 + "_" + country2</li> 279 * <li>baseName + "_" + language2</li> 280 * <li>baseName</li> 281 * </ul> 282 * 283 * <p>In the sequence, entries with an empty string are ignored. Next, 284 * <code>getBundle</code> tries to instantiate the resource bundle:</p> 285 * 286 * <ul> 287 * <li>This implementation only resolves property based resource bundles. 288 * Class based resource bundles are nor found.</li> 289 * <li>A search is made for a property resource file, by replacing 290 * '.' with '/' and appending ".properties", and using 291 * ClassLoader.getResource(). If a file is found, then a 292 * PropertyResourceBundle is created from the file's contents.</li> 293 * </ul> 294 * 295 * <p>If no resource bundle was found, the default resource bundle loader 296 * is used to look for the resource bundle. Class based resource bundles 297 * will be found now.<p> 298 * 299 * @param baseName the name of the ResourceBundle 300 * @param locale A locale 301 * @return the desired resource bundle 302 */ 303 // This method is synchronized so that the cache is properly 304 // handled. 305 public static ResourceBundle getBundle(String baseName, Locale locale) { 306 307 // If the default locale changed since the last time we were called, 308 // all cache entries are invalidated. 309 Locale defaultLocale = Locale.getDefault(); 310 if (defaultLocale != m_lastDefaultLocale) { 311 synchronized (m_bundleCache) { 312 if (defaultLocale != m_lastDefaultLocale) { 313 m_bundleCache = new ConcurrentHashMap<BundleKey, ResourceBundle>(); 314 m_lastDefaultLocale = defaultLocale; 315 } 316 } 317 } 318 319 // This will throw NullPointerException if any arguments are null. 320 BundleKey m_lookupKey = new BundleKey(baseName, locale); 321 322 Object obj = m_bundleCache.get(m_lookupKey); 323 324 if (obj instanceof ResourceBundle) { 325 return (ResourceBundle)obj; 326 } else if (obj == NULL_ENTRY) { 327 // Lookup has failed previously. Fall through. 328 } else { 329 synchronized (m_bundleCache) { 330 obj = m_bundleCache.get(m_lookupKey); 331 if (obj instanceof ResourceBundle) { 332 // check the bundle again 333 return (ResourceBundle)obj; 334 } 335 // First, look for a bundle for the specified locale. We don't want 336 // the base bundle this time. 337 boolean wantBase = locale.equals(m_lastDefaultLocale); 338 ResourceBundle bundle = tryBundle(baseName, locale, wantBase); 339 340 // Try the default locale if necessary 341 if ((bundle == null) && !locale.equals(m_lastDefaultLocale)) { 342 bundle = tryBundle(baseName, m_lastDefaultLocale, true); 343 } 344 345 BundleKey key = new BundleKey(baseName, locale); 346 if (bundle != null) { 347 // Cache the result and return it. 348 m_bundleCache.put(key, bundle); 349 return bundle; 350 } 351 } 352 } 353 354 // unable to find the resource bundle with this implementation 355 // use default Java mechanism to look up the bundle again 356 return ResourceBundle.getBundle(baseName, locale); 357 } 358 359 /** 360 * Tries to load a property file with the specified name. 361 * 362 * @param localizedName the name 363 * @return the resource bundle if it was loaded, otherwise the backup 364 */ 365 private static I_CmsResourceBundle tryBundle(String localizedName) { 366 367 I_CmsResourceBundle result = null; 368 369 try { 370 371 String resourceName = localizedName.replace('.', '/') + ".properties"; 372 URL url = CmsResourceBundleLoader.class.getClassLoader().getResource(resourceName); 373 374 I_CmsResourceBundle additionalBundle = m_permanentCache.get(localizedName); 375 if (additionalBundle != null) { 376 result = additionalBundle.getClone(); 377 } else if (url != null) { 378 // the resource was found on the file system 379 InputStream is = null; 380 String path = CmsFileUtil.normalizePath(url); 381 File file = new File(path); 382 try { 383 // try to load the resource bundle from a file, NOT with the resource loader first 384 // this is important since using #getResourceAsStream() may return cached results, 385 // for example Tomcat by default does cache all resources loaded by the class loader 386 // this means a changed resource bundle file is not loaded 387 is = new FileInputStream(file); 388 } catch (IOException ex) { 389 // this will happen if the resource is contained for example in a .jar file 390 is = CmsResourceBundleLoader.class.getClassLoader().getResourceAsStream(resourceName); 391 } catch (AccessControlException acex) { 392 // fixed bug #1550 393 // this will happen if the resource is contained for example in a .jar file 394 // and security manager is turned on. 395 is = CmsResourceBundleLoader.class.getClassLoader().getResourceAsStream(resourceName); 396 } 397 if (is != null) { 398 result = new CmsPropertyResourceBundle(is); 399 } 400 } 401 } catch (IOException ex) { 402 // can't localized these message since this may lead to a chicken-egg problem 403 MissingResourceException mre = new MissingResourceException( 404 "Failed to load bundle '" + localizedName + "'", 405 localizedName, 406 ""); 407 mre.initCause(ex); 408 throw mre; 409 } 410 411 return result; 412 } 413 414 /** 415 * Tries to load a the bundle for a given locale, also loads the backup 416 * locales with the same language. 417 * 418 * @param baseName the raw bundle name, without locale qualifiers 419 * @param locale the locale 420 * @param wantBase whether a resource bundle made only from the base name 421 * (with no locale information attached) should be returned. 422 * @return the resource bundle if it was loaded, otherwise the backup 423 */ 424 private static ResourceBundle tryBundle(String baseName, Locale locale, boolean wantBase) { 425 426 I_CmsResourceBundle first = null; // The most specialized bundle. 427 I_CmsResourceBundle last = null; // The least specialized bundle. 428 429 List<String> bundleNames = CmsLocaleManager.getLocaleVariants(baseName, locale, true, true); 430 for (String bundleName : bundleNames) { 431 // break if we would try the base bundle, but we do not want it directly 432 if (bundleName.equals(baseName) && !wantBase && (first == null)) { 433 break; 434 } 435 I_CmsResourceBundle foundBundle = tryBundle(bundleName); 436 if (foundBundle != null) { 437 if (first == null) { 438 first = foundBundle; 439 } 440 441 if (last != null) { 442 last.setParent((ResourceBundle)foundBundle); 443 } 444 foundBundle.setLocale(locale); 445 446 last = foundBundle; 447 } 448 } 449 return (ResourceBundle)first; 450 } 451}