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 this.bundlePath = bundlePath; 162 this.creationThreadId = Thread.currentThread().getId(); 163 ClassLoader cl = forClass.getClassLoader(); 164 ResourceBundle trb = null; 165 try { 166 trb = ResourceBundle.getBundle(bundlePath, locale, cl); 167 } catch (MissingResourceException e) { 168 try { 169 trb = ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, locale, cl); 170 } catch (MissingResourceException e2) { 171 } 172 } 173 this.rb = trb; 174 if (rb != null) { 175 176 // Populate keyMap with original mappings. 177 for (Enumeration<String> e = getKeys(); e.hasMoreElements();) { 178 String key = e.nextElement(); 179 keyMap.put(key, key); 180 } 181 182 // Override/augment with shortname mappings (e.g. "foobar"->"MyClass.foobar") 183 String c = className + '.'; 184 for (Enumeration<String> e = getKeys(); e.hasMoreElements();) { 185 String key = e.nextElement(); 186 if (key.startsWith(c)) { 187 String shortKey = key.substring(className.length() + 1); 188 keyMap.put(shortKey, key); 189 } 190 } 191 192 allKeys.addAll(keyMap.keySet()); 193 } 194 searchBundles.add(this); 195 } 196 197 198 /** 199 * Add another bundle path to this resource bundle. 200 * 201 * <p> 202 * Order of property lookup is first-to-last. 203 * 204 * <p> 205 * This method must be called from the same thread as the call to the constructor. 206 * This eliminates the need for synchronization. 207 * 208 * @param forClass The class using this resource bundle. 209 * @param bundlePath The bundle path. 210 * @return This object (for method chaining). 211 */ 212 public MessageBundle addSearchPath(Class<?> forClass, String bundlePath) { 213 assertSameThread(creationThreadId, "This method can only be called from the same thread that created the object."); 214 MessageBundle srb = new MessageBundle(forClass, bundlePath); 215 if (srb.rb != null) { 216 allKeys.addAll(srb.keySet()); 217 searchBundles.add(srb); 218 } 219 return this; 220 } 221 222 @Override /* ResourceBundle */ 223 public boolean containsKey(String key) { 224 return allKeys.contains(key); 225 } 226 227 /** 228 * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects. 229 * 230 * @param key The resource bundle key. 231 * @param args Optional {@link MessageFormat}-style arguments. 232 * @return 233 * The resolved value. Never <jk>null</jk>. 234 * <js>"{!!key}"</js> if the bundle is missing. 235 * <js>"{!key}"</js> if the key is missing. 236 */ 237 public String getString(String key, Object...args) { 238 String s = getString(key); 239 if (s.length() > 0 && s.charAt(0) == '{') 240 return s; 241 return format(s, args); 242 } 243 244 /** 245 * Same as {@link #getString(String, Object...)} but allows you to specify the locale. 246 * 247 * @param locale The locale of the resource bundle to retrieve message from. 248 * @param key The resource bundle key. 249 * @param args Optional {@link MessageFormat}-style arguments. 250 * @return 251 * The resolved value. Never <jk>null</jk>. 252 * <js>"{!!key}"</js> if the bundle is missing. 253 * <js>"{!key}"</js> if the key is missing. 254 */ 255 public String getString(Locale locale, String key, Object...args) { 256 if (locale == null) 257 return getString(key, args); 258 return getBundle(locale).getString(key, args); 259 } 260 261 /** 262 * Same as {@link #getString(String, Object...)} but uses the locale specified on the call to {@link #setClientLocale(Locale)}. 263 * 264 * @param key The resource bundle key. 265 * @param args Optional {@link MessageFormat}-style arguments. 266 * @return 267 * The resolved value. Never <jk>null</jk>. 268 * <js>"{!!key}"</js> if the bundle is missing. 269 * <js>"{!key}"</js> if the key is missing. 270 */ 271 public String getClientString(String key, Object...args) { 272 return getString(clientLocale.get(), key, args); 273 } 274 275 /** 276 * Looks for all the specified keys in the resource bundle and returns the first value that exists. 277 * 278 * @param keys The list of possible keys. 279 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 280 */ 281 public String findFirstString(String...keys) { 282 if (rb == null) 283 return null; 284 for (String k : keys) { 285 if (containsKey(k)) 286 return getString(k); 287 } 288 return null; 289 } 290 291 /** 292 * Same as {@link #findFirstString(String...)}, but uses the specified locale. 293 * 294 * @param locale The locale of the resource bundle to retrieve message from. 295 * @param keys The list of possible keys. 296 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 297 */ 298 public String findFirstString(Locale locale, String...keys) { 299 MessageBundle srb = getBundle(locale); 300 return srb.findFirstString(keys); 301 } 302 303 @Override /* ResourceBundle */ 304 public Set<String> keySet() { 305 return Collections.unmodifiableSet(allKeys); 306 } 307 308 /** 309 * Returns all keys in this resource bundle with the specified prefix. 310 * 311 * @param prefix The prefix. 312 * @return The set of all keys in the resource bundle with the prefix. 313 */ 314 public Set<String> keySet(String prefix) { 315 Set<String> set = new HashSet<>(); 316 for (String s : keySet()) { 317 if (s.equals(prefix) || (s.startsWith(prefix) && s.charAt(prefix.length()) == '.')) 318 set.add(s); 319 } 320 return set; 321 } 322 323 @Override /* ResourceBundle */ 324 public Enumeration<String> getKeys() { 325 if (rb == null) 326 return new Vector<String>(0).elements(); 327 return rb.getKeys(); 328 } 329 330 @Override /* ResourceBundle */ 331 protected Object handleGetObject(String key) { 332 for (MessageBundle srb : searchBundles) { 333 if (srb.rb != null) { 334 String key2 = srb.keyMap.get(key); 335 if (key2 != null) { 336 try { 337 return srb.rb.getObject(key2); 338 } catch (Exception e) { 339 return "{!"+key+"}"; 340 } 341 } 342 } 343 } 344 if (rb == null) 345 return "{!!"+key+"}"; 346 return "{!"+key+"}"; 347 } 348 349 /** 350 * Returns this resource bundle as an {@link ObjectMap}. 351 * 352 * <p> 353 * Useful for debugging purposes. 354 * Note that any class that implements a <code>swap()</code> method will automatically be serialized by 355 * calling this method and serializing the result. 356 * 357 * <p> 358 * This method always constructs a new {@link ObjectMap} on each call. 359 * 360 * @return A new map containing all the keys and values in this bundle. 361 */ 362 public ObjectMap swap() { 363 ObjectMap om = new ObjectMap(); 364 for (String k : allKeys) 365 om.put(k, getString(k)); 366 return om; 367 } 368 369 /** 370 * Returns the resource bundle for the specified locale. 371 * 372 * @param locale The client locale. 373 * @return The resource bundle for the specified locale. Never <jk>null</jk>. 374 */ 375 public MessageBundle getBundle(Locale locale) { 376 MessageBundle mb = localizedBundles.get(locale); 377 if (mb != null) 378 return mb; 379 mb = new MessageBundle(forClass, bundlePath, locale); 380 List<MessageBundle> l = new ArrayList<>(searchBundles.size()-1); 381 for (int i = 1; i < searchBundles.size(); i++) { 382 MessageBundle srb = searchBundles.get(i); 383 srb = new MessageBundle(srb.forClass, srb.bundlePath, locale); 384 l.add(srb); 385 mb.allKeys.addAll(srb.keySet()); 386 } 387 mb.searchBundles.addAll(l); 388 localizedBundles.putIfAbsent(locale, mb); 389 return localizedBundles.get(locale); 390 } 391}