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