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