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