001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.cp; 018 019import static org.apache.juneau.common.utils.StringUtils.*; 020import static org.apache.juneau.common.utils.ThrowableUtils.*; 021import static org.apache.juneau.common.utils.Utils.*; 022import static org.apache.juneau.internal.CollectionUtils.*; 023import static org.apache.juneau.internal.ResourceBundleUtils.*; 024 025import java.text.*; 026import java.util.*; 027import java.util.concurrent.*; 028 029import org.apache.juneau.*; 030import org.apache.juneau.collections.*; 031import org.apache.juneau.common.utils.*; 032import org.apache.juneau.internal.*; 033import org.apache.juneau.marshaller.*; 034import org.apache.juneau.parser.ParseException; 035import org.apache.juneau.utils.*; 036 037/** 038 * An enhanced {@link ResourceBundle}. 039 * 040 * <p> 041 * Wraps a ResourceBundle to provide some useful additional functionality. 042 * 043 * <ul> 044 * <li> 045 * Instead of throwing {@link MissingResourceException}, the {@link ResourceBundle#getString(String)} method 046 * will return <js>"{!key}"</js> if the message could not be found. 047 * <li> 048 * Supported hierarchical lookup of resources from parent parent classes. 049 * <li> 050 * Support for easy retrieval of localized bundles. 051 * <li> 052 * Support for generalized resource bundles (e.g. properties files containing keys for several classes). 053 * </ul> 054 * 055 * <p> 056 * The following example shows the basic usage of this class for retrieving localized messages: 057 * 058 * <p class='bini'> 059 * <cc># Contents of MyClass.properties</cc> 060 * <ck>foo</ck> = <cv>foo {0}</cv> 061 * <ck>MyClass.bar</ck> = <cv>bar {0}</cv> 062 * </p> 063 * <p class='bjava'> 064 * <jk>public class</jk> MyClass { 065 * <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>); 066 * 067 * <jk>public void</jk> doFoo() { 068 * 069 * <jc>// A normal property.</jc> 070 * String <jv>foo</jv> = <jsf>MESSAGES</jsf>.getString(<js>"foo"</js>,<js>"x"</js>); <jc>// == "foo x"</jc> 071 * 072 * <jc>// A property prefixed by class name.</jc> 073 * String <jv>bar</jv> = <jsf>MESSAGES</jsf>.getString(<js>"bar"</js>,<js>"x"</js>); <jc>// == "bar x"</jc> 074 * 075 * <jc>// A non-existent property.</jc> 076 * String <jv>baz</jv> = <jsf>MESSAGES</jsf>.getString(<js>"baz"</js>,<js>"x"</js>); <jc>// == "{!baz}"</jc> 077 * } 078 * } 079 * </p> 080 * 081 * <p> 082 * The ability to resolve keys prefixed by class name allows you to place all your messages in a single file such 083 * as a common <js>"Messages.properties"</js> file along with those for other classes. 084 * <p> 085 * The following shows how to retrieve messages from a common bundle: 086 * 087 * <p class='bjava'> 088 * <jk>public class</jk> MyClass { 089 * <jk>private static final</jk> Messages <jsf>MESSAGES</jsf> = Messages.<jsm>of</jsm>(MyClass.<jk>class</jk>, <js>"Messages"</js>); 090 * } 091 * </p> 092 * 093 * <p> 094 * Resource bundles are searched using the following base name patterns: 095 * <ul> 096 * <li><js>"{package}.{name}"</js> 097 * <li><js>"{package}.i18n.{name}"</js> 098 * <li><js>"{package}.nls.{name}"</js> 099 * <li><js>"{package}.messages.{name}"</js> 100 * </ul> 101 * 102 * <p> 103 * These patterns can be customized using the {@link Builder#baseNames(String...)} method. 104 * 105 * <p> 106 * Localized messages can be retrieved in the following way: 107 * 108 * <p class='bjava'> 109 * <jc>// Return value from Japan locale bundle.</jc> 110 * String <jv>foo</jv> = <jsf>MESSAGES</jsf>.forLocale(Locale.<jsf>JAPAN</jsf>).getString(<js>"foo"</js>); 111 * </p> 112 * 113 * <h5 class='section'>See Also:</h5><ul> 114 * </ul> 115 */ 116public class Messages extends ResourceBundle { 117 118 //----------------------------------------------------------------------------------------------------------------- 119 // Static 120 //----------------------------------------------------------------------------------------------------------------- 121 122 /** 123 * Static creator. 124 * 125 * @param forClass 126 * The class we're creating this object for. 127 * @return A new builder. 128 */ 129 public static final Builder create(Class<?> forClass) { 130 return new Builder(forClass); 131 } 132 133 /** 134 * Constructor. 135 * 136 * @param forClass 137 * The class we're creating this object for. 138 * @return A new message bundle belonging to the class. 139 */ 140 public static final Messages of(Class<?> forClass) { 141 return create(forClass).build(); 142 } 143 144 /** 145 * Constructor. 146 * 147 * @param forClass 148 * The class we're creating this object for. 149 * @param name 150 * The bundle name (e.g. <js>"Messages"</js>). 151 * <br>If <jk>null</jk>, uses the class name. 152 * @return A new message bundle belonging to the class. 153 */ 154 public static final Messages of(Class<?> forClass, String name) { 155 return create(forClass).name(name).build(); 156 } 157 158 //----------------------------------------------------------------------------------------------------------------- 159 // Builder 160 //----------------------------------------------------------------------------------------------------------------- 161 162 /** 163 * Builder class. 164 */ 165 public static class Builder extends BeanBuilder<Messages> { 166 167 Class<?> forClass; 168 Locale locale; 169 String name; 170 Messages parent; 171 List<Tuple2<Class<?>,String>> locations; 172 173 private String[] baseNames = {"{package}.{name}","{package}.i18n.{name}","{package}.nls.{name}","{package}.messages.{name}"}; 174 175 /** 176 * Constructor. 177 * 178 * @param forClass The base class. 179 */ 180 protected Builder(Class<?> forClass) { 181 super(Messages.class, BeanStore.INSTANCE); 182 this.forClass = forClass; 183 this.name = forClass.getSimpleName(); 184 locations = Utils.list(); 185 locale = Locale.getDefault(); 186 } 187 188 @Override /* BeanBuilder */ 189 protected Messages buildDefault() { 190 191 if (! locations.isEmpty()) { 192 Tuple2<Class<?>,String>[] mbl = locations.toArray(new Tuple2[0]); 193 194 Builder x = null; 195 196 for (int i = mbl.length-1; i >= 0; i--) { 197 Class<?> c = Utils.firstNonNull(mbl[i].getA(), forClass); 198 String value = mbl[i].getB(); 199 if (isJsonObject(value, true)) { 200 MessagesString ms; 201 try { 202 ms = Json5.DEFAULT.read(value, MessagesString.class); 203 } catch (ParseException e) { 204 throw asRuntimeException(e); 205 } 206 x = Messages.create(c).name(ms.name).baseNames(Utils.splita(ms.baseNames, ',')).locale(ms.locale).parent(x == null ? null : x.build()); 207 } else { 208 x = Messages.create(c).name(value).parent(x == null ? null : x.build()); 209 } 210 } 211 212 return x == null ? null : x.build(); // Shouldn't be null. 213 } 214 215 return new Messages(this); 216 } 217 218 private static class MessagesString { 219 public String name; 220 public String[] baseNames; 221 public String locale; 222 } 223 224 //------------------------------------------------------------------------------------------------------------- 225 // Properties 226 //------------------------------------------------------------------------------------------------------------- 227 228 /** 229 * Adds a parent bundle. 230 * 231 * @param parent The parent bundle. Can be <jk>null</jk>. 232 * @return This object. 233 */ 234 public Builder parent(Messages parent) { 235 this.parent = parent; 236 return this; 237 } 238 239 /** 240 * Specifies the bundle name (e.g. <js>"Messages"</js>). 241 * 242 * @param name 243 * The bundle name. 244 * <br>If <jk>null</jk>, the forClass class name is used. 245 * @return This object. 246 */ 247 public Builder name(String name) { 248 this.name = Utils.isEmpty(name) ? forClass.getSimpleName() : name; 249 return this; 250 } 251 252 /** 253 * Specifies the base name patterns to use for finding the resource bundle. 254 * 255 * @param baseNames 256 * The bundle base names. 257 * <br>The default is the following: 258 * <ul> 259 * <li><js>"{package}.{name}"</js> 260 * <li><js>"{package}.i18n.{name}"</js> 261 * <li><js>"{package}.nls.{name}"</js> 262 * <li><js>"{package}.messages.{name}"</js> 263 * </ul> 264 * @return This object. 265 */ 266 public Builder baseNames(String...baseNames) { 267 this.baseNames = baseNames == null ? new String[]{} : baseNames; 268 return this; 269 } 270 271 /** 272 * Specifies the locale. 273 * 274 * @param locale 275 * The locale. 276 * If <jk>null</jk>, the default locale is used. 277 * @return This object. 278 */ 279 public Builder locale(Locale locale) { 280 this.locale = locale == null ? Locale.getDefault() : locale; 281 return this; 282 } 283 284 /** 285 * Specifies the locale. 286 * 287 * @param locale 288 * The locale. 289 * If <jk>null</jk>, the default locale is used. 290 * @return This object. 291 */ 292 public Builder locale(String locale) { 293 return locale(locale == null ? null : Locale.forLanguageTag(locale)); 294 } 295 296 /** 297 * Specifies a location of where to look for messages. 298 * 299 * @param baseClass The base class. 300 * @param bundlePath The bundle path. 301 * @return This object. 302 */ 303 public Builder location(Class<?> baseClass, String bundlePath) { 304 this.locations.add(0, Tuple2.of(baseClass, bundlePath)); 305 return this; 306 } 307 308 /** 309 * Specifies a location of where to look for messages. 310 * 311 * @param bundlePath The bundle path. 312 * @return This object. 313 */ 314 public Builder location(String bundlePath) { 315 this.locations.add(0, Tuple2.of(forClass, bundlePath)); 316 return this; 317 } 318 @Override /* Overridden from BeanBuilder */ 319 public Builder impl(Object value) { 320 super.impl(value); 321 return this; 322 } 323 324 @Override /* Overridden from BeanBuilder */ 325 public Builder type(Class<?> value) { 326 super.type(value); 327 return this; 328 } 329 //------------------------------------------------------------------------------------------------------------- 330 // Other methods 331 //------------------------------------------------------------------------------------------------------------- 332 333 ResourceBundle getBundle() { 334 ClassLoader cl = forClass.getClassLoader(); 335 JsonMap m = JsonMap.of("name", name, "package", forClass.getPackage().getName()); 336 for (String bn : baseNames) { 337 bn = StringUtils.replaceVars(bn, m); 338 ResourceBundle rb = findBundle(bn, locale, cl); 339 if (rb != null) 340 return rb; 341 } 342 return null; 343 } 344 } 345 346 //----------------------------------------------------------------------------------------------------------------- 347 // Instance 348 //----------------------------------------------------------------------------------------------------------------- 349 350 private ResourceBundle rb; 351 private Class<?> c; 352 private Messages parent; 353 private Locale locale; 354 355 // Cache of message bundles per locale. 356 private final ConcurrentHashMap<Locale,Messages> localizedMessages = new ConcurrentHashMap<>(); 357 358 // Cache of virtual keys to actual keys. 359 private final Map<String,String> keyMap; 360 361 private final Set<String> rbKeys; 362 363 364 /** 365 * Constructor. 366 * 367 * @param builder 368 * The builder for this object. 369 */ 370 protected Messages(Builder builder) { 371 this(builder.forClass, builder.getBundle(), builder.locale, builder.parent); 372 } 373 374 Messages(Class<?> forClass, ResourceBundle rb, Locale locale, Messages parent) { 375 this.c = forClass; 376 this.rb = rb; 377 this.parent = parent; 378 if (parent != null) 379 setParent(parent); 380 this.locale = locale == null ? Locale.getDefault() : locale; 381 382 Map<String,String> keyMap = new TreeMap<>(); 383 384 String cn = c.getSimpleName() + '.'; 385 if (rb != null) { 386 rb.keySet().forEach(x -> { 387 keyMap.put(x, x); 388 if (x.startsWith(cn)) { 389 String shortKey = x.substring(cn.length()); 390 keyMap.put(shortKey, x); 391 } 392 }); 393 } 394 if (parent != null) { 395 parent.keySet().forEach(x -> { 396 keyMap.put(x, x); 397 if (x.startsWith(cn)) { 398 String shortKey = x.substring(cn.length()); 399 keyMap.put(shortKey, x); 400 } 401 }); 402 } 403 404 this.keyMap = u(copyOf(keyMap)); 405 this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet(); 406 } 407 408 /** 409 * Returns this message bundle for the specified locale. 410 * 411 * @param locale The locale to get the messages for. 412 * @return A new {@link Messages} object. Never <jk>null</jk>. 413 */ 414 public Messages forLocale(Locale locale) { 415 if (locale == null) 416 locale = Locale.getDefault(); 417 if (this.locale.equals(locale)) 418 return this; 419 Messages mb = localizedMessages.get(locale); 420 if (mb == null) { 421 Messages parent = this.parent == null ? null : this.parent.forLocale(locale); 422 ResourceBundle rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader()); 423 mb = new Messages(c, rb, locale, parent); 424 localizedMessages.put(locale, mb); 425 } 426 return mb; 427 } 428 429 /** 430 * Returns all keys in this resource bundle with the specified prefix. 431 * 432 * <p> 433 * Keys are returned in alphabetical order. 434 * 435 * @param prefix The prefix. 436 * @return The set of all keys in the resource bundle with the prefix. 437 */ 438 public Set<String> keySet(String prefix) { 439 Set<String> set = Utils.set(); 440 keySet().forEach(x -> { 441 if (x.equals(prefix) || (x.startsWith(prefix) && x.charAt(prefix.length()) == '.')) 442 set.add(x); 443 }); 444 return set; 445 } 446 447 /** 448 * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects. 449 * 450 * @param key The resource bundle key. 451 * @param args Optional {@link MessageFormat}-style arguments. 452 * @return 453 * The resolved value. Never <jk>null</jk>. 454 * <js>"{!key}"</js> if the key is missing. 455 */ 456 public String getString(String key, Object...args) { 457 String s = getString(key); 458 if (s.startsWith("{!")) 459 return s; 460 return format(s, args); 461 } 462 463 /** 464 * Looks for all the specified keys in the resource bundle and returns the first value that exists. 465 * 466 * @param keys The list of possible keys. 467 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 468 */ 469 public String findFirstString(String...keys) { 470 for (String k : keys) { 471 if (containsKey(k)) 472 return getString(k); 473 } 474 return null; 475 } 476 477 @Override /* ResourceBundle */ 478 protected Object handleGetObject(String key) { 479 String k = keyMap.get(key); 480 if (k == null) 481 return "{!" + key + "}"; 482 try { 483 if (rbKeys.contains(k)) 484 return rb.getObject(k); 485 } catch (MissingResourceException e) { /* Shouldn't happen */ } 486 return parent.handleGetObject(key); 487 } 488 489 @Override /* ResourceBundle */ 490 public boolean containsKey(String key) { 491 return keyMap.containsKey(key); 492 } 493 494 @Override /* ResourceBundle */ 495 public Set<String> keySet() { 496 return keyMap.keySet(); 497 } 498 499 @Override /* ResourceBundle */ 500 public Enumeration<String> getKeys() { 501 return Collections.enumeration(keySet()); 502 } 503 504 @Override /* Object */ 505 public String toString() { 506 JsonMap m = new JsonMap(); 507 for (String k : new TreeSet<>(keySet())) 508 m.put(k, getString(k)); 509 return Json5.of(m); 510 } 511}