001// *************************************************************************************************************************** 002// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file * 003// * distributed with this work for additional information regarding copyright ownership. The ASF licenses this file * 004// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * 005// * with the License. You may obtain a copy of the License at * 006// * * 007// * http://www.apache.org/licenses/LICENSE-2.0 * 008// * * 009// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an * 010// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * 011// * specific language governing permissions and limitations under the License. * 012// *************************************************************************************************************************** 013package org.apache.juneau.utils; 014 015import static org.apache.juneau.internal.StringUtils.*; 016import static org.apache.juneau.internal.ThrowableUtils.*; 017 018import java.text.*; 019import java.util.*; 020import java.util.concurrent.*; 021 022import org.apache.juneau.*; 023 024/** 025 * Wraps a {@link ResourceBundle} to provide some useful additional functionality. 026 * 027 * <ul class='spaced-list'> 028 * <li> 029 * Instead of throwing {@link MissingResourceException}, the {@link #getString(String)} method 030 * will return <js>"{!!key}"</js> if the bundle was not found, and <js>"{!key}"</js> if bundle 031 * was found but the key is not in the bundle. 032 * <li> 033 * A client locale can be set as a {@link ThreadLocal} object using the static {@link #setClientLocale(Locale)} 034 * so that client localized messages can be retrieved using the {@link #getClientString(String, Object...)} 035 * method on all instances of this class. 036 * <li> 037 * Resource bundles on parent classes can be added to the search path for this class by using the 038 * {@link #addSearchPath(Class, String)} method. 039 * This allows messages to be retrieved from the resource bundles of parent classes. 040 * <li> 041 * Locale-specific bundles can be retrieved by using the {@link #getBundle(Locale)} method. 042 * <li> 043 * The {@link #getString(Locale, String, Object...)} method can be used to retrieve locale-specific messages. 044 * <li> 045 * Messages in the resource bundle can optionally be prefixed with the simple class name. 046 * For example, if the class is <c>MyClass</c> and the properties file contains <js>"MyClass.myMessage"</js>, 047 * the message can be retrieved using <code>getString(<js>"myMessage"</js>)</code>. 048 * </ul> 049 * 050 * <ul class='notes'> 051 * <li> 052 * This class is thread-safe. 053 * </ul> 054 */ 055public class MessageBundle extends ResourceBundle { 056 057 private static final ThreadLocal<Locale> clientLocale = new ThreadLocal<>(); 058 059 private final ResourceBundle rb; 060 private final String bundlePath, className; 061 private final Class<?> forClass; 062 private final long creationThreadId; 063 064 // A map that contains all keys [shortKeyName->keyName] and [keyName->keyName], where shortKeyName 065 // refers to keys prefixed and stripped of the class name (e.g. "foobar"->"MyClass.foobar") 066 private final Map<String,String> keyMap = new ConcurrentHashMap<>(); 067 068 // Contains all keys present in all bundles in searchBundles. 069 private final ConcurrentSkipListSet<String> allKeys = new ConcurrentSkipListSet<>(); 070 071 // Bundles to search through to find properties. 072 // Typically this will be a list of resource bundles for each class up the class hierarchy chain. 073 private final CopyOnWriteArrayList<MessageBundle> searchBundles = new CopyOnWriteArrayList<>(); 074 075 // Cache of message bundles per locale. 076 private final ConcurrentHashMap<Locale,MessageBundle> localizedBundles = new ConcurrentHashMap<>(); 077 078 /** 079 * Sets the locale for this thread so that calls to {@link #getClientString(String, Object...)} return messages in 080 * that locale. 081 * 082 * @param locale The new client locale. 083 */ 084 public static void setClientLocale(Locale locale) { 085 MessageBundle.clientLocale.set(locale); 086 } 087 088 /** 089 * Constructor. 090 * 091 * <p> 092 * When this method is used, the bundle path is determined by searching for the resource bundle 093 * in the following locations: 094 * <ul> 095 * <li><c>[package].ForClass.properties</c> 096 * <li><c>[package].nls.ForClass.properties</c> 097 * <li><c>[package].i18n.ForClass.properties</c> 098 * </ul> 099 * 100 * @param forClass The class 101 * @return A new message bundle belonging to the class. 102 */ 103 public static final MessageBundle create(Class<?> forClass) { 104 return create(forClass, findBundlePath(forClass)); 105 } 106 107 /** 108 * Constructor. 109 * 110 * <p> 111 * A shortcut for calling <c>new MessageBundle(forClass, bundlePath)</c>. 112 * 113 * @param forClass The class 114 * @param bundlePath The location of the resource bundle. 115 * @return A new message bundle belonging to the class. 116 */ 117 public static final MessageBundle create(Class<?> forClass, String bundlePath) { 118 return new MessageBundle(forClass, bundlePath); 119 } 120 121 private static final String findBundlePath(Class<?> forClass) { 122 String path = forClass.getName(); 123 if (tryBundlePath(forClass, path)) 124 return path; 125 path = forClass.getPackage().getName() + ".nls." + forClass.getSimpleName(); 126 if (tryBundlePath(forClass, path)) 127 return path; 128 path = forClass.getPackage().getName() + ".i18n." + forClass.getSimpleName(); 129 if (tryBundlePath(forClass, path)) 130 return path; 131 return null; 132 } 133 134 private static final boolean tryBundlePath(Class<?> c, String path) { 135 try { 136 path = c.getName(); 137 ResourceBundle.getBundle(path, Locale.getDefault(), c.getClassLoader()); 138 return true; 139 } catch (MissingResourceException e) { 140 return false; 141 } 142 } 143 144 /** 145 * Constructor. 146 * 147 * @param forClass The class using this resource bundle. 148 * @param bundlePath 149 * The path of the resource bundle to wrap. 150 * This can be an absolute path (e.g. <js>"com.foo.MyMessages"</js>) or a path relative to the package of the 151 * <l>forClass</l> (e.g. <js>"MyMessages"</js> if <l>forClass</l> is <js>"com.foo.MyClass"</js>). 152 */ 153 public MessageBundle(Class<?> forClass, String bundlePath) { 154 this(forClass, bundlePath, Locale.getDefault()); 155 } 156 157 private MessageBundle(Class<?> forClass, String bundlePath, Locale locale) { 158 this.forClass = forClass; 159 this.className = forClass.getSimpleName(); 160 if (bundlePath == null) 161 throw new RuntimeException("Bundle path was null."); 162 if (bundlePath.endsWith(".properties")) 163 throw new RuntimeException("Bundle path should not end with '.properties'"); 164 this.bundlePath = bundlePath; 165 this.creationThreadId = Thread.currentThread().getId(); 166 ClassLoader cl = forClass.getClassLoader(); 167 ResourceBundle trb = null; 168 try { 169 trb = ResourceBundle.getBundle(bundlePath, locale, cl); 170 } catch (MissingResourceException e) { 171 try { 172 trb = ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, locale, cl); 173 } catch (MissingResourceException e2) { 174 } 175 } 176 this.rb = trb; 177 if (rb != null) { 178 179 // Populate keyMap with original mappings. 180 for (Enumeration<String> e = getKeys(); e.hasMoreElements();) { 181 String key = e.nextElement(); 182 keyMap.put(key, key); 183 } 184 185 // Override/augment with shortname mappings (e.g. "foobar"->"MyClass.foobar") 186 String c = className + '.'; 187 for (Enumeration<String> e = getKeys(); e.hasMoreElements();) { 188 String key = e.nextElement(); 189 if (key.startsWith(c)) { 190 String shortKey = key.substring(className.length() + 1); 191 keyMap.put(shortKey, key); 192 } 193 } 194 195 allKeys.addAll(keyMap.keySet()); 196 } 197 searchBundles.add(this); 198 } 199 200 201 /** 202 * Add another bundle path to this resource bundle. 203 * 204 * <p> 205 * Order of property lookup is first-to-last. 206 * 207 * <p> 208 * This method must be called from the same thread as the call to the constructor. 209 * This eliminates the need for synchronization. 210 * 211 * @param forClass The class using this resource bundle. 212 * @param bundlePath The bundle path. 213 * @return This object (for method chaining). 214 */ 215 public MessageBundle addSearchPath(Class<?> forClass, String bundlePath) { 216 assertSameThread(creationThreadId, "This method can only be called from the same thread that created the object."); 217 MessageBundle srb = new MessageBundle(forClass, bundlePath); 218 if (srb.rb != null) { 219 allKeys.addAll(srb.keySet()); 220 searchBundles.add(srb); 221 } 222 return this; 223 } 224 225 @Override /* ResourceBundle */ 226 public boolean containsKey(String key) { 227 return allKeys.contains(key); 228 } 229 230 /** 231 * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects. 232 * 233 * @param key The resource bundle key. 234 * @param args Optional {@link MessageFormat}-style arguments. 235 * @return 236 * The resolved value. Never <jk>null</jk>. 237 * <js>"{!!key}"</js> if the bundle is missing. 238 * <js>"{!key}"</js> if the key is missing. 239 */ 240 public String getString(String key, Object...args) { 241 String s = getString(key); 242 if (s.length() > 0 && s.charAt(0) == '{') 243 return s; 244 return format(s, args); 245 } 246 247 /** 248 * Same as {@link #getString(String, Object...)} but allows you to specify the locale. 249 * 250 * @param locale The locale of the resource bundle to retrieve message from. 251 * @param key The resource bundle key. 252 * @param args Optional {@link MessageFormat}-style arguments. 253 * @return 254 * The resolved value. Never <jk>null</jk>. 255 * <js>"{!!key}"</js> if the bundle is missing. 256 * <js>"{!key}"</js> if the key is missing. 257 */ 258 public String getString(Locale locale, String key, Object...args) { 259 if (locale == null) 260 return getString(key, args); 261 return getBundle(locale).getString(key, args); 262 } 263 264 /** 265 * Same as {@link #getString(String, Object...)} but uses the locale specified on the call to {@link #setClientLocale(Locale)}. 266 * 267 * @param key The resource bundle key. 268 * @param args Optional {@link MessageFormat}-style arguments. 269 * @return 270 * The resolved value. Never <jk>null</jk>. 271 * <js>"{!!key}"</js> if the bundle is missing. 272 * <js>"{!key}"</js> if the key is missing. 273 */ 274 public String getClientString(String key, Object...args) { 275 return getString(clientLocale.get(), key, args); 276 } 277 278 /** 279 * Looks for all the specified keys in the resource bundle and returns the first value that exists. 280 * 281 * @param keys The list of possible keys. 282 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 283 */ 284 public String findFirstString(String...keys) { 285 if (rb == null) 286 return null; 287 for (String k : keys) { 288 if (containsKey(k)) 289 return getString(k); 290 } 291 return null; 292 } 293 294 /** 295 * Same as {@link #findFirstString(String...)}, but uses the specified locale. 296 * 297 * @param locale The locale of the resource bundle to retrieve message from. 298 * @param keys The list of possible keys. 299 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 300 */ 301 public String findFirstString(Locale locale, String...keys) { 302 MessageBundle srb = getBundle(locale); 303 return srb.findFirstString(keys); 304 } 305 306 @Override /* ResourceBundle */ 307 public Set<String> keySet() { 308 return Collections.unmodifiableSet(allKeys); 309 } 310 311 /** 312 * Returns all keys in this resource bundle with the specified prefix. 313 * 314 * @param prefix The prefix. 315 * @return The set of all keys in the resource bundle with the prefix. 316 */ 317 public Set<String> keySet(String prefix) { 318 Set<String> set = new HashSet<>(); 319 for (String s : keySet()) { 320 if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.')) 321 set.add(s); 322 } 323 return set; 324 } 325 326 @Override /* ResourceBundle */ 327 public Enumeration<String> getKeys() { 328 if (rb == null) 329 return new Vector<String>(0).elements(); 330 return rb.getKeys(); 331 } 332 333 @Override /* ResourceBundle */ 334 protected Object handleGetObject(String key) { 335 for (MessageBundle srb : searchBundles) { 336 if (srb.rb != null) { 337 String key2 = srb.keyMap.get(key); 338 if (key2 != null) { 339 try { 340 return srb.rb.getObject(key2); 341 } catch (Exception e) { 342 return "{!"+key+"}"; 343 } 344 } 345 } 346 } 347 if (rb == null) 348 return "{!!"+key+"}"; 349 return "{!"+key+"}"; 350 } 351 352 /** 353 * Returns this resource bundle as an {@link ObjectMap}. 354 * 355 * <p> 356 * Useful for debugging purposes. 357 * Note that any class that implements a <c>swap()</c> method will automatically be serialized by 358 * calling this method and serializing the result. 359 * 360 * <p> 361 * This method always constructs a new {@link ObjectMap} on each call. 362 * 363 * @return A new map containing all the keys and values in this bundle. 364 */ 365 public ObjectMap swap() { 366 ObjectMap om = new ObjectMap(); 367 for (String k : allKeys) 368 om.put(k, getString(k)); 369 return om; 370 } 371 372 /** 373 * Returns the resource bundle for the specified locale. 374 * 375 * @param locale The client locale. 376 * @return The resource bundle for the specified locale. Never <jk>null</jk>. 377 */ 378 public MessageBundle getBundle(Locale locale) { 379 MessageBundle mb = localizedBundles.get(locale); 380 if (mb != null) 381 return mb; 382 mb = new MessageBundle(forClass, bundlePath, locale); 383 List<MessageBundle> l = new ArrayList<>(searchBundles.size()-1); 384 for (int i = 1; i < searchBundles.size(); i++) { 385 MessageBundle srb = searchBundles.get(i); 386 srb = new MessageBundle(srb.forClass, srb.bundlePath, locale); 387 l.add(srb); 388 mb.allKeys.addAll(srb.keySet()); 389 } 390 mb.searchBundles.addAll(l); 391 localizedBundles.putIfAbsent(locale, mb); 392 return localizedBundles.get(locale); 393 } 394}