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.commons.utils.CollectionUtils.*; 020import static org.apache.juneau.commons.utils.ResourceBundleUtils.*; 021import static org.apache.juneau.commons.utils.StringUtils.*; 022import static org.apache.juneau.commons.utils.ThrowableUtils.*; 023import static org.apache.juneau.commons.utils.Utils.*; 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.commons.collections.*; 032import org.apache.juneau.commons.function.*; 033import org.apache.juneau.commons.utils.*; 034import org.apache.juneau.marshaller.*; 035import org.apache.juneau.parser.ParseException; 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 */ 114public class Messages extends ResourceBundle { 115 /** 116 * Builder class. 117 */ 118 public static class Builder extends BeanBuilder<Messages> { 119 120 private static class MessagesString { 121 public String name; 122 public String[] baseNames; 123 public String locale; 124 } 125 126 Class<?> forClass; 127 Locale locale; 128 String name; 129 Messages parent; 130 131 List<Tuple2<Class<?>,String>> locations; 132 133 private String[] baseNames = { "{package}.{name}", "{package}.i18n.{name}", "{package}.nls.{name}", "{package}.messages.{name}" }; 134 135 /** 136 * Constructor. 137 * 138 * @param forClass The base class. 139 */ 140 protected Builder(Class<?> forClass) { 141 super(Messages.class, BeanStore.INSTANCE); 142 this.forClass = forClass; 143 this.name = cns(forClass); 144 locations = list(); 145 locale = Locale.getDefault(); 146 } 147 148 /** 149 * Specifies the base name patterns to use for finding the resource bundle. 150 * 151 * @param baseNames 152 * The bundle base names. 153 * <br>The default is the following: 154 * <ul> 155 * <li><js>"{package}.{name}"</js> 156 * <li><js>"{package}.i18n.{name}"</js> 157 * <li><js>"{package}.nls.{name}"</js> 158 * <li><js>"{package}.messages.{name}"</js> 159 * </ul> 160 * @return This object. 161 */ 162 public Builder baseNames(String...baseNames) { 163 this.baseNames = baseNames == null ? a() : baseNames; 164 return this; 165 } 166 167 @Override /* Overridden from BeanBuilder */ 168 public Builder impl(Object value) { 169 super.impl(value); 170 return this; 171 } 172 173 /** 174 * Specifies the locale. 175 * 176 * @param value 177 * The locale. 178 * If <jk>null</jk>, the default locale is used. 179 * @return This object. 180 */ 181 public Builder locale(Locale value) { 182 this.locale = value == null ? Locale.getDefault() : value; 183 return this; 184 } 185 186 /** 187 * Specifies the locale. 188 * 189 * @param value 190 * The locale. 191 * If <jk>null</jk>, the default locale is used. 192 * @return This object. 193 */ 194 public Builder locale(String value) { 195 return locale(value == null ? null : Locale.forLanguageTag(value)); 196 } 197 198 /** 199 * Specifies a location of where to look for messages. 200 * 201 * @param baseClass The base class. 202 * @param bundlePath The bundle path. 203 * @return This object. 204 */ 205 public Builder location(Class<?> baseClass, String bundlePath) { 206 this.locations.add(0, Tuple2.of(baseClass, bundlePath)); 207 return this; 208 } 209 210 /** 211 * Specifies a location of where to look for messages. 212 * 213 * @param bundlePath The bundle path. 214 * @return This object. 215 */ 216 public Builder location(String bundlePath) { 217 this.locations.add(0, Tuple2.of(forClass, bundlePath)); 218 return this; 219 } 220 221 /** 222 * Specifies the bundle name (e.g. <js>"Messages"</js>). 223 * 224 * @param value 225 * The bundle name. 226 * <br>If <jk>null</jk>, the forClass class name is used. 227 * @return This object. 228 */ 229 public Builder name(String value) { 230 this.name = isEmpty(value) ? cns(forClass) : value; 231 return this; 232 } 233 234 /** 235 * Adds a parent bundle. 236 * 237 * @param value The parent bundle. Can be <jk>null</jk>. 238 * @return This object. 239 */ 240 public Builder parent(Messages value) { 241 parent = value; 242 return this; 243 } 244 245 @Override /* Overridden from BeanBuilder */ 246 public Builder type(Class<?> value) { 247 super.type(value); 248 return this; 249 } 250 251 @SuppressWarnings("unchecked") 252 @Override /* Overridden from BeanBuilder */ 253 protected Messages buildDefault() { 254 255 if (! locations.isEmpty()) { 256 Tuple2<Class<?>,String>[] mbl = locations.toArray(new Tuple2[0]); 257 258 var x = (Builder)null; 259 260 for (var i = mbl.length - 1; i >= 0; i--) { 261 var c = firstNonNull(mbl[i].getA(), forClass); 262 var value = mbl[i].getB(); 263 if (isProbablyJsonObject(value, true)) { 264 MessagesString ms; 265 try { 266 ms = Json5.DEFAULT.read(value, MessagesString.class); 267 } catch (ParseException e) { 268 throw toRex(e); 269 } 270 x = Messages.create(c).name(ms.name).baseNames(StringUtils.splita(ms.baseNames, ',')).locale(ms.locale).parent(x == null ? null : x.build()); 271 } else { 272 x = Messages.create(c).name(value).parent(x == null ? null : x.build()); 273 } 274 } 275 276 return x == null ? null : x.build(); // Shouldn't be null. 277 } 278 279 return new Messages(this); 280 } 281 282 ResourceBundle getBundle() { 283 var cl = forClass.getClassLoader(); 284 var m = JsonMap.of("name", name, "package", forClass.getPackage().getName()); 285 for (var bn : baseNames) { 286 bn = StringUtils.formatNamed(bn, m); 287 var rb = findBundle(bn, locale, cl); 288 if (nn(rb)) 289 return rb; 290 } 291 return null; 292 } 293 } 294 295 /** 296 * Static creator. 297 * 298 * @param forClass 299 * The class we're creating this object for. 300 * @return A new builder. 301 */ 302 public static final Builder create(Class<?> forClass) { 303 return new Builder(forClass); 304 } 305 306 /** 307 * Constructor. 308 * 309 * @param forClass 310 * The class we're creating this object for. 311 * @return A new message bundle belonging to the class. 312 */ 313 public static final Messages of(Class<?> forClass) { 314 return create(forClass).build(); 315 } 316 317 /** 318 * Constructor. 319 * 320 * @param forClass 321 * The class we're creating this object for. 322 * @param name 323 * The bundle name (e.g. <js>"Messages"</js>). 324 * <br>If <jk>null</jk>, uses the class name. 325 * @return A new message bundle belonging to the class. 326 */ 327 public static final Messages of(Class<?> forClass, String name) { 328 return create(forClass).name(name).build(); 329 } 330 331 private ResourceBundle rb; 332 private Class<?> c; 333 private Messages parent; 334 private Locale locale; 335 336 // Cache of message bundles per locale. 337 private final ConcurrentHashMap<Locale,Messages> localizedMessages = new ConcurrentHashMap<>(); 338 339 // Cache of virtual keys to actual keys. 340 private final Map<String,String> keyMap; 341 342 private final Set<String> rbKeys; 343 344 /** 345 * Constructor. 346 * 347 * @param builder 348 * The builder for this object. 349 */ 350 protected Messages(Builder builder) { 351 this(builder.forClass, builder.getBundle(), builder.locale, builder.parent); 352 } 353 354 Messages(Class<?> forClass, ResourceBundle rb, Locale locale, Messages parent) { 355 this.c = forClass; 356 this.rb = rb; 357 this.parent = parent; 358 if (nn(parent)) 359 setParent(parent); 360 this.locale = locale == null ? Locale.getDefault() : locale; 361 362 var keyMap = new TreeMap<String,String>(); 363 364 var cn = cns(c) + '.'; 365 if (nn(rb)) { 366 rb.keySet().forEach(x -> { 367 keyMap.put(x, x); 368 if (x.startsWith(cn)) { 369 var shortKey = x.substring(cn.length()); 370 keyMap.put(shortKey, x); 371 } 372 }); 373 } 374 if (nn(parent)) { 375 parent.keySet().forEach(x -> { 376 keyMap.put(x, x); 377 if (x.startsWith(cn)) { 378 var shortKey = x.substring(cn.length()); 379 keyMap.put(shortKey, x); 380 } 381 }); 382 } 383 384 this.keyMap = u(copyOf(keyMap)); 385 this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet(); 386 } 387 388 @Override /* Overridden from ResourceBundle */ 389 public boolean containsKey(String key) { 390 return keyMap.containsKey(key); 391 } 392 393 /** 394 * Looks for all the specified keys in the resource bundle and returns the first value that exists. 395 * 396 * @param keys The list of possible keys. 397 * @return The resolved value, or <jk>null</jk> if no value is found or the resource bundle is missing. 398 */ 399 public String findFirstString(String...keys) { 400 for (var k : keys) { 401 if (containsKey(k)) 402 return getString(k); 403 } 404 return null; 405 } 406 407 /** 408 * Returns this message bundle for the specified locale. 409 * 410 * @param locale The locale to get the messages for. 411 * @return A new {@link Messages} object. Never <jk>null</jk>. 412 */ 413 public Messages forLocale(Locale locale) { 414 if (locale == null) 415 locale = Locale.getDefault(); 416 if (this.locale.equals(locale)) 417 return this; 418 var mb = localizedMessages.get(locale); 419 if (mb == null) { 420 var parent = this.parent == null ? null : this.parent.forLocale(locale); 421 var rb = this.rb == null ? null : findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader()); 422 mb = new Messages(c, rb, locale, parent); 423 localizedMessages.put(locale, mb); 424 } 425 return mb; 426 } 427 428 @Override /* Overridden from ResourceBundle */ 429 public Enumeration<String> getKeys() { return Collections.enumeration(keySet()); } 430 431 /** 432 * Similar to {@link ResourceBundle#getString(String)} except allows you to pass in {@link MessageFormat} objects. 433 * 434 * @param key The resource bundle key. 435 * @param args Optional {@link MessageFormat}-style arguments. 436 * @return 437 * The resolved value. Never <jk>null</jk>. 438 * <js>"{!key}"</js> if the key is missing. 439 */ 440 public String getString(String key, Object...args) { 441 var s = getString(key); 442 if (s.startsWith("{!")) 443 return s; 444 return f(s, args); 445 } 446 447 @Override /* Overridden from ResourceBundle */ 448 public Set<String> keySet() { 449 return keyMap.keySet(); 450 } 451 452 /** 453 * Returns all keys in this resource bundle with the specified prefix. 454 * 455 * <p> 456 * Keys are returned in alphabetical order. 457 * 458 * @param prefix The prefix. 459 * @return The set of all keys in the resource bundle with the prefix. 460 */ 461 public Set<String> keySet(String prefix) { 462 Set<String> set = set(); 463 keySet().forEach(x -> { 464 if (x.equals(prefix) || (x.startsWith(prefix) && x.charAt(prefix.length()) == '.')) 465 set.add(x); 466 }); 467 return set; 468 } 469 470 protected FluentMap<String,Object> properties() { 471 // @formatter:off 472 var m = filteredBeanPropertyMap(); 473 keySet().stream().forEach(x -> m.a(x, getString(x))); 474 return m; 475 // @formatter:on 476 } 477 478 @Override /* Overridden from Object */ 479 public String toString() { 480 return r(properties()); 481 } 482 483 @Override /* Overridden from ResourceBundle */ 484 protected Object handleGetObject(String key) { 485 var k = keyMap.get(key); 486 if (k == null) 487 return "{!" + key + "}"; 488 try { 489 if (rbKeys.contains(k)) 490 return rb.getObject(k); 491 } catch (@SuppressWarnings("unused") MissingResourceException e) { /* Shouldn't happen */ } 492 return parent.handleGetObject(key); 493 } 494}