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