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