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