This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 224fafb  Rewrite Messages API.
224fafb is described below

commit 224fafb055cf149b7617e1b3a0730c1d839f087b
Author: JamesBognar <[email protected]>
AuthorDate: Sun Jul 26 16:52:37 2020 -0400

    Rewrite Messages API.
---
 .../java/org/apache/juneau/cp/Messages_Test.java   | 149 ++++-
 .../juneau/cp/test1/MessageBundleTest1.properties  |   8 +
 .../cp/test1/MessageBundleTest1_ja.properties      |   5 +
 .../org/apache/juneau/cp/test2/Test2.properties    |   2 +
 .../main/java/org/apache/juneau/cp/Messages.java   | 653 ++++++++-------------
 .../java/org/apache/juneau/cp/MessagesBuilder.java | 116 ++++
 .../juneau/internal/ResourceBundleUtils.java}      |  99 ++--
 .../test/java/org/apache/juneau/rest/NlsTest.java  |   2 +-
 .../rest/annotation/RestResourceMessagesTest.java  |   2 +-
 .../java/org/apache/juneau/rest/RestContext.java   |  10 +-
 .../java/org/apache/juneau/rest/RestRequest.java   |   2 +-
 11 files changed, 591 insertions(+), 457 deletions(-)

diff --git 
a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
 
b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
index 8c6c6f9..60f8cb4 100644
--- 
a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
+++ 
b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
@@ -20,24 +20,155 @@ import java.util.*;
 import static java.util.Locale.*;
 
 import org.apache.juneau.cp.test1.*;
+import org.apache.juneau.cp.test2.*;
 import org.junit.*;
 
 @FixMethodOrder(NAME_ASCENDING)
 public class Messages_Test {
 
        @Test
-       public void a01_nonExistent() throws Exception {
-               assertThrown(()->Messages.of(Test1.class)).contains("Could not 
find bundle path for class");
-               
assertThrown(()->Messages.of(Test1.class,"bad.properties")).contains("Bundle 
path should not end with '.properties'");
+       public void a01_sameDirectory() throws Exception {
+               Messages x1 = Messages.of(MessageBundleTest1.class);
+               
assertString(x1.getString("file")).is("MessageBundleTest1.properties");
+               assertString(x1.getString(JAPANESE, 
"file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x1.forLocale(JAPANESE).getString("file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x1.forLocale(JAPAN).getString("file")).is("MessageBundleTest1_ja_JP.properties");
+               
assertString(x1.forLocale(CHINA).getString("file")).is("MessageBundleTest1.properties");
+               
assertString(x1.forLocale((Locale)null).getString("file")).is("MessageBundleTest1.properties");
        }
 
        @Test
-       public void a02_sameDirectory() throws Exception {
+       public void a02_customName() throws Exception {
+               Messages x1 = Messages.of(MessageBundleTest1.class, 
"files/Test1");
+               assertString(x1.getString("file")).is("files/Test1.properties");
+               assertString(x1.getString(JAPANESE, 
"file")).is("files/Test1_ja.properties");
+               
assertString(x1.forLocale(JAPANESE).getString("file")).is("files/Test1_ja.properties");
+               
assertString(x1.forLocale(JAPAN).getString("file")).is("files/Test1_ja_JP.properties");
+               
assertString(x1.forLocale(CHINA).getString("file")).is("files/Test1.properties");
+               
assertString(x1.forLocale((Locale)null).getString("file")).is("files/Test1.properties");
+
+               Messages x2 = 
Messages.create(MessageBundleTest1.class).name(null).build();
+               
assertString(x2.getString("file")).is("MessageBundleTest1.properties");
+               assertString(x2.getString(JAPANESE, 
"file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x2.forLocale(JAPANESE).getString("file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x2.forLocale(JAPAN).getString("file")).is("MessageBundleTest1_ja_JP.properties");
+               
assertString(x2.forLocale(CHINA).getString("file")).is("MessageBundleTest1.properties");
+               
assertString(x2.forLocale((Locale)null).getString("file")).is("MessageBundleTest1.properties");
+       }
+
+       @Test
+       public void a03_customSearchPaths() throws Exception {
+               Messages x = 
Messages.create(MessageBundleTest1.class).name("Test1").baseNames("{package}.files.{name}").build();
+               assertString(x.getString("file")).is("files/Test1.properties");
+               assertString(x.getString(JAPANESE, 
"file")).is("files/Test1_ja.properties");
+               
assertString(x.forLocale(JAPANESE).getString("file")).is("files/Test1_ja.properties");
+               
assertString(x.forLocale(JAPAN).getString("file")).is("files/Test1_ja_JP.properties");
+               
assertString(x.forLocale(CHINA).getString("file")).is("files/Test1.properties");
+               
assertString(x.forLocale((Locale)null).getString("file")).is("files/Test1.properties");
+
+               Messages x2 = 
Messages.create(MessageBundleTest1.class).name("Test1").baseNames((String[])null).build();
+               assertString(x2.getString("file")).is("{!file}");
+       }
+
+       @Test
+       public void a04_customLocale() throws Exception {
+               Messages x1 = 
Messages.create(MessageBundleTest1.class).locale(Locale.JAPAN).build();
+               
assertString(x1.getString("file")).is("MessageBundleTest1_ja_JP.properties");
+               assertString(x1.getString(JAPANESE, 
"file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x1.forLocale(JAPANESE).getString("file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x1.forLocale(JAPAN).getString("file")).is("MessageBundleTest1_ja_JP.properties");
+               
assertString(x1.forLocale(CHINA).getString("file")).is("MessageBundleTest1.properties");
+               
assertString(x1.forLocale((Locale)null).getString("file")).is("MessageBundleTest1.properties");
+
+               Messages x2 = 
Messages.create(MessageBundleTest1.class).locale(null).build();
+               
assertString(x2.getString("file")).is("MessageBundleTest1.properties");
+               assertString(x2.getString(JAPANESE, 
"file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x2.forLocale(JAPANESE).getString("file")).is("MessageBundleTest1_ja.properties");
+               
assertString(x2.forLocale(JAPAN).getString("file")).is("MessageBundleTest1_ja_JP.properties");
+               
assertString(x2.forLocale(CHINA).getString("file")).is("MessageBundleTest1.properties");
+               
assertString(x2.forLocale((Locale)null).getString("file")).is("MessageBundleTest1.properties");
+       }
+
+       @Test
+       public void a05_nonExistentBundle() throws Exception {
+               Messages x1 = Messages.of(MessageBundleTest1.class, "Bad");
+               assertString(x1.getString("file")).is("{!file}");
+               assertString(x1.getString(JAPANESE, "file")).is("{!file}");
+               
assertString(x1.forLocale(JAPANESE).getString("file")).is("{!file}");
+               
assertString(x1.forLocale(JAPAN).getString("file")).is("{!file}");
+               
assertString(x1.forLocale(CHINA).getString("file")).is("{!file}");
+               
assertString(x1.forLocale((Locale)null).getString("file")).is("{!file}");
+
+               Messages x2 = x1.forLocale(JAPANESE);
+               assertString(x2.getString("file")).is("{!file}");
+       }
+
+       @Test
+       public void a06_parent() throws Exception {
+               Messages x1 = 
Messages.create(MessageBundleTest1.class).name("Bad").parent(Messages.of(Test2.class)).build();
+               assertString(x1.getString("file")).is("Test2.properties");
+               assertString(x1.getString(JAPANESE, 
"file")).is("Test2_ja.properties");
+               
assertString(x1.forLocale(JAPANESE).getString("file")).is("Test2_ja.properties");
+               
assertString(x1.forLocale(JAPAN).getString("file")).is("Test2_ja_JP.properties");
+               
assertString(x1.forLocale(CHINA).getString("file")).is("Test2.properties");
+               
assertString(x1.forLocale((Locale)null).getString("file")).is("Test2.properties");
+
+               Messages x2 = 
Messages.create(MessageBundleTest1.class).parent(Messages.of(Test2.class)).build();
+               
assertString(x2.getString("file")).is("MessageBundleTest1.properties");
+               assertString(x2.getString("yyy")).is("bar");
+       }
+
+       @Test
+       public void a07_nonExistentMessage() throws Exception {
+               Messages x = 
Messages.create(MessageBundleTest1.class).name("Bad").parent(Messages.of(Test2.class)).build();
+               assertString(x.getString("bad")).is("{!bad}");
+       }
+
+       @Test
+       public void a08_nonExistentMessage() throws Exception {
+               Messages x = 
Messages.create(MessageBundleTest1.class).name("Bad").parent(Messages.of(Test2.class)).build();
+               assertString(x.getString("bad")).is("{!bad}");
+       }
+
+       @Test
+       public void a09_keySet_prefix() throws Exception {
+               Messages x = Messages.of(MessageBundleTest1.class);
+               assertObject(new 
TreeSet<>(x.keySet("xx"))).json().is("['xx','xx.','xx.foo']");
+       }
+
+       @Test
+       public void a10_getString() throws Exception {
                Messages x = Messages.of(MessageBundleTest1.class);
-               
assertString(x.getString("file")).is("MessageBundleTest1.properties");
-               
assertString(x.getBundle(JAPANESE).getString("file")).is("MessageBundleTest1_ja.properties");
-               
assertString(x.getBundle(JAPAN).getString("file")).is("MessageBundleTest1_ja_JP.properties");
-               
assertString(x.getBundle(CHINA).getString("file")).is("MessageBundleTest1.properties");
-               
assertString(x.getBundle((Locale)null).getString("file")).is("MessageBundleTest1.properties");
+               assertString(x.getString("foo","bar")).is("foo bar");
+               assertString(x.getString("bar","bar")).is("bar bar");
+               assertString(x.getString("baz","bar")).is("{!baz}");
+               assertString(x.getString(JAPAN, "foo","bar")).is("fooja bar");
+               assertString(x.getString(CHINA, "foo","bar")).is("foo bar");
+               assertString(x.getString((Locale)null, "foo","bar")).is("foo 
bar");
+               assertString(x.getString(JAPAN, "baz")).is("baz");
+               assertString(x.getString(CHINA, "baz")).is("{!baz}");
+               assertString(x.getString((Locale)null, "baz")).is("{!baz}");
+       }
+
+       @Test
+       public void a11_findFirstString() throws Exception {
+               Messages x = Messages.of(MessageBundleTest1.class);
+               assertString(x.findFirstString("baz","foo")).is("foo {0}");
+               assertString(x.findFirstString("baz","baz")).isNull();
+               assertString(x.findFirstString(JAPAN,"baz","foo")).is("baz");
+               assertString(x.findFirstString(CHINA,"baz","baz")).isNull();
+               
assertString(x.findFirstString((Locale)null,"baz","baz")).isNull();
+       }
+
+       @Test
+       public void a12_getKeys() throws Exception {
+               Messages x = Messages.of(Test2.class);
+               assertObject(x.getKeys()).json().is("['file','yyy']");
+       }
+
+       @Test
+       public void a13_toString() throws Exception {
+               Messages x = Messages.of(Test2.class);
+               assertString(x).is("{file:'Test2.properties',yyy:'bar'}");
        }
 }
diff --git 
a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1.properties
 
b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1.properties
index 7c463ea..48f5b24 100644
--- 
a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1.properties
+++ 
b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1.properties
@@ -13,3 +13,11 @@
 # 
***************************************************************************************************************************
 
 file=MessageBundleTest1.properties
+
+MessageBundleTest1.foo = foo {0}
+bar = bar {0}
+
+xx = foo
+xx. = foo
+xxx = foo
+xx.foo = foo
\ No newline at end of file
diff --git 
a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja.properties
 
b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja.properties
index 927ee6d..5516332 100644
--- 
a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja.properties
+++ 
b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja.properties
@@ -13,3 +13,8 @@
 # 
***************************************************************************************************************************
 
 file=MessageBundleTest1_ja.properties
+
+MessageBundleTest1.foo = fooja {0}
+
+baz = baz
+
diff --git 
a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2.properties
 
b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2.properties
index 80affa9..89556df 100644
--- 
a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2.properties
+++ 
b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2.properties
@@ -13,3 +13,5 @@
 # 
***************************************************************************************************************************
 
 file=Test2.properties
+
+yyy = bar
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/Messages.java 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/Messages.java
index 88804b6..7cadfad 100644
--- 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/Messages.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/Messages.java
@@ -1,398 +1,255 @@
-// 
***************************************************************************************************************************
-// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
-// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
-// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
-// * with the License.  You may obtain a copy of the License at                
                                              *
-// *                                                                           
                                              *
-// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
-// *                                                                           
                                              *
-// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
-// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
-// * specific language governing permissions and limitations under the 
License.                                              *
-// 
***************************************************************************************************************************
-package org.apache.juneau.cp;
-
-import static org.apache.juneau.internal.StringUtils.*;
-import static org.apache.juneau.internal.ThrowableUtils.*;
-
-import java.text.*;
-import java.util.*;
-import java.util.concurrent.*;
-
-import org.apache.juneau.collections.*;
-
-/**
- * Wraps a {@link ResourceBundle} to provide some useful additional 
functionality.
- *
- * <ul class='spaced-list'>
- *     <li>
- *             Instead of throwing {@link MissingResourceException}, the 
{@link #getString(String)} method
- *             will return <js>"{!!key}"</js> if the bundle was not found, and 
<js>"{!key}"</js> if bundle
- *             was found but the key is not in the bundle.
- *     <li>
- *             A client locale can be set as a {@link ThreadLocal} object 
using the static {@link #setClientLocale(Locale)}
- *             so that client localized messages can be retrieved using the 
{@link #getClientString(String, Object...)}
- *             method on all instances of this class.
- *     <li>
- *             Resource bundles on parent classes can be added to the search 
path for this class by using the
- *             {@link #addSearchPath(Class, String)} method.
- *             This allows messages to be retrieved from the resource bundles 
of parent classes.
- *     <li>
- *             Locale-specific bundles can be retrieved by using the {@link 
#getBundle(Locale)} method.
- *     <li>
- *             The {@link #getString(Locale, String, Object...)} method can be 
used to retrieve locale-specific messages.
- *     <li>
- *             Messages in the resource bundle can optionally be prefixed with 
the simple class name.
- *             For example, if the class is <c>MyClass</c> and the properties 
file contains <js>"MyClass.myMessage"</js>,
- *             the message can be retrieved using 
<code>getString(<js>"myMessage"</js>)</code>.
- * </ul>
- *
- * <ul class='notes'>
- *     <li>
- *             This class is thread-safe.
- * </ul>
- */
-public class Messages extends ResourceBundle {
-
-       private static final ThreadLocal<Locale> clientLocale = new 
ThreadLocal<>();
-
-       private final ResourceBundle rb;
-       private final String bundlePath, className;
-       private final Class<?> forClass;
-       private final long creationThreadId;
-
-       // A map that contains all keys [shortKeyName->keyName] and 
[keyName->keyName], where shortKeyName
-       // refers to keys prefixed and stripped of the class name (e.g. 
"foobar"->"MyClass.foobar")
-       private final Map<String,String> keyMap = new ConcurrentHashMap<>();
-
-       // Contains all keys present in all bundles in searchBundles.
-       private final ConcurrentSkipListSet<String> allKeys = new 
ConcurrentSkipListSet<>();
-
-       // Bundles to search through to find properties.
-       // Typically this will be a list of resource bundles for each class up 
the class hierarchy chain.
-       private final CopyOnWriteArrayList<Messages> searchBundles = new 
CopyOnWriteArrayList<>();
-
-       // Cache of message bundles per locale.
-       private final ConcurrentHashMap<Locale,Messages> localizedBundles = new 
ConcurrentHashMap<>();
-
-       /**
-        * Sets the locale for this thread so that calls to {@link 
#getClientString(String, Object...)} return messages in
-        * that locale.
-        *
-        * @param locale The new client locale.
-        */
-       public static void setClientLocale(Locale locale) {
-               Messages.clientLocale.set(locale);
-       }
-
-       /**
-        * Constructor.
-        *
-        * @param forClass The class
-        * @return A new message bundle belonging to the class.
-        */
-       public static final Messages of(Class<?> forClass) {
-               return new Messages(forClass, null, null);
-       }
-
-       /**
-        * Constructor.
-        *
-        * @param forClass The class
-        * @param bundlePath The location of the resource bundle.
-        * @return A new message bundle belonging to the class.
-        */
-       public static final Messages of(Class<?> forClass, String bundlePath) {
-               return new Messages(forClass, bundlePath, null);
-       }
-
-       /**
-        * Constructor.
-        *
-        * @param forClass The class using this resource bundle.
-        * @param bundlePath
-        *      The path of the resource bundle to wrap.
-        *      <br>This can be an absolute path (e.g. 
<js>"com.foo.MyMessages"</js>) or a path relative to the package of the
-        *      <l>forClass</l> (e.g. <js>"MyMessages"</js> if <l>forClass</l> 
is <js>"com.foo.MyClass"</js>).
-        *      <br>If <jk>null</jk>, searches for the following locations:
-        *      <ul>
-        *              <li><c>[package].ForClass.properties</c>
-        *              <li><c>[package].nls.ForClass.properties</c>
-        *              <li><c>[package].i18n.ForClass.properties</c>
-        *      </ul>
-        * @param locale
-        *      The locale.
-        *      <br>If <jk>null</jk>, uses the default locale.
-        * @throws MissingResourceException If resource bundle could not be 
found.
-        */
-       public Messages(Class<?> forClass, String bundlePath, Locale locale) 
throws MissingResourceException {
-               this.forClass = forClass;
-               this.className = forClass.getSimpleName();
-
-               if (bundlePath == null)
-                       bundlePath = findBundlePath(forClass);
-               if (bundlePath.endsWith(".properties"))
-                       throw new RuntimeException("Bundle path should not end 
with '.properties'");
-               this.bundlePath = bundlePath;
-
-               if (locale == null)
-                       locale = Locale.getDefault();
-
-               this.creationThreadId = Thread.currentThread().getId();
-               ClassLoader cl = forClass.getClassLoader();
-               ResourceBundle trb = null;
-               try {
-                       trb = ResourceBundle.getBundle(bundlePath, locale, cl);
-               } catch (MissingResourceException e) {
-                       try {
-                               trb = 
ResourceBundle.getBundle(forClass.getPackage().getName() + '.' + bundlePath, 
locale, cl);
-                       } catch (MissingResourceException e2) {
-                       }
-               }
-               this.rb = trb;
-               if (rb != null) {
-
-                       // Populate keyMap with original mappings.
-                       for (Enumeration<String> e = getKeys(); 
e.hasMoreElements();) {
-                               String key = e.nextElement();
-                               keyMap.put(key, key);
-                       }
-
-                       // Override/augment with shortname mappings (e.g. 
"foobar"->"MyClass.foobar")
-                       String c = className + '.';
-                       for (Enumeration<String> e = getKeys(); 
e.hasMoreElements();) {
-                               String key = e.nextElement();
-                               if (key.startsWith(c)) {
-                                       String shortKey = 
key.substring(className.length() + 1);
-                                       keyMap.put(shortKey, key);
-                               }
-                       }
-
-                       allKeys.addAll(keyMap.keySet());
-               }
-               searchBundles.add(this);
-       }
-
-
-       /**
-        * Add another bundle path to this resource bundle.
-        *
-        * <p>
-        * Order of property lookup is first-to-last.
-        *
-        * <p>
-        * This method must be called from the same thread as the call to the 
constructor.
-        * This eliminates the need for synchronization.
-        *
-        * @param forClass The class using this resource bundle.
-        * @param bundlePath The bundle path.
-        * @return This object (for method chaining).
-        */
-       public Messages addSearchPath(Class<?> forClass, String bundlePath) {
-               assertSameThread(creationThreadId, "This method can only be 
called from the same thread that created the object.");
-               Messages srb = new Messages(forClass, bundlePath, null);
-               if (srb.rb != null) {
-                       allKeys.addAll(srb.keySet());
-                       searchBundles.add(srb);
-               }
-               return this;
-       }
-
-       @Override /* ResourceBundle */
-       public boolean containsKey(String key) {
-               return allKeys.contains(key);
-       }
-
-       /**
-        * Similar to {@link ResourceBundle#getString(String)} except allows 
you to pass in {@link MessageFormat} objects.
-        *
-        * @param key The resource bundle key.
-        * @param args Optional {@link MessageFormat}-style arguments.
-        * @return
-        *      The resolved value.  Never <jk>null</jk>.
-        *      <js>"{!!key}"</js> if the bundle is missing.
-        *      <js>"{!key}"</js> if the key is missing.
-        */
-       public String getString(String key, Object...args) {
-               String s = getString(key);
-               if (s.length() > 0 && s.charAt(0) == '{')
-                       return s;
-               return format(s, args);
-       }
-
-       /**
-        * Same as {@link #getString(String, Object...)} but allows you to 
specify the locale.
-        *
-        * @param locale The locale of the resource bundle to retrieve message 
from.
-        * @param key The resource bundle key.
-        * @param args Optional {@link MessageFormat}-style arguments.
-        * @return
-        *      The resolved value.  Never <jk>null</jk>.
-        *      <js>"{!!key}"</js> if the bundle is missing.
-        *      <js>"{!key}"</js> if the key is missing.
-        */
-       public String getString(Locale locale, String key, Object...args) {
-               if (locale == null)
-                       return getString(key, args);
-               return getBundle(locale).getString(key, args);
-       }
-
-       /**
-        * Same as {@link #getString(String, Object...)} but uses the locale 
specified on the call to {@link #setClientLocale(Locale)}.
-        *
-        * @param key The resource bundle key.
-        * @param args Optional {@link MessageFormat}-style arguments.
-        * @return
-        *      The resolved value.  Never <jk>null</jk>.
-        *      <js>"{!!key}"</js> if the bundle is missing.
-        *      <js>"{!key}"</js> if the key is missing.
-        */
-       public String getClientString(String key, Object...args) {
-               return getString(clientLocale.get(), key, args);
-       }
-
-       /**
-        * Looks for all the specified keys in the resource bundle and returns 
the first value that exists.
-        *
-        * @param keys The list of possible keys.
-        * @return The resolved value, or <jk>null</jk> if no value is found or 
the resource bundle is missing.
-        */
-       public String findFirstString(String...keys) {
-               if (rb == null)
-                       return null;
-               for (String k : keys) {
-                       if (containsKey(k))
-                               return getString(k);
-               }
-               return null;
-       }
-
-       /**
-        * Same as {@link #findFirstString(String...)}, but uses the specified 
locale.
-        *
-        * @param locale The locale of the resource bundle to retrieve message 
from.
-        * @param keys The list of possible keys.
-        * @return The resolved value, or <jk>null</jk> if no value is found or 
the resource bundle is missing.
-        */
-       public String findFirstString(Locale locale, String...keys) {
-               Messages srb = getBundle(locale);
-               return srb.findFirstString(keys);
-       }
-
-       @Override /* ResourceBundle */
-       public Set<String> keySet() {
-               return Collections.unmodifiableSet(allKeys);
-       }
-
-       /**
-        * Returns all keys in this resource bundle with the specified prefix.
-        *
-        * @param prefix The prefix.
-        * @return The set of all keys in the resource bundle with the prefix.
-        */
-       public Set<String> keySet(String prefix) {
-               Set<String> set = new HashSet<>();
-               for (String s : keySet()) {
-                       if (s.equals(prefix) || (s.startsWith(prefix) && 
s.charAt(prefix.length()) == '.'))
-                               set.add(s);
-               }
-               return set;
-       }
-
-       @Override /* ResourceBundle */
-       public Enumeration<String> getKeys() {
-               if (rb == null)
-                       return new Vector<String>(0).elements();
-               return rb.getKeys();
-       }
-
-       @Override /* ResourceBundle */
-       protected Object handleGetObject(String key) {
-               for (Messages srb : searchBundles) {
-                       if (srb.rb != null) {
-                               String key2 = srb.keyMap.get(key);
-                               if (key2 != null) {
-                                       try {
-                                               return srb.rb.getObject(key2);
-                                       } catch (Exception e) {
-                                               return "{!"+key+"}";
-                                       }
-                               }
-                       }
-               }
-               if (rb == null)
-                       return "{!!"+key+"}";
-               return "{!"+key+"}";
-       }
-
-       /**
-        * Returns this resource bundle as an {@link OMap}.
-        *
-        * <p>
-        * Useful for debugging purposes.
-        * Note that any class that implements a <c>swap()</c> method will 
automatically be serialized by
-        * calling this method and serializing the result.
-        *
-        * <p>
-        * This method always constructs a new {@link OMap} on each call.
-        *
-        * @return A new map containing all the keys and values in this bundle.
-        */
-       public OMap swap() {
-               OMap om = new OMap();
-               for (String k : allKeys)
-                       om.put(k, getString(k));
-               return om;
-       }
-
-       /**
-        * Returns the resource bundle for the specified locale.
-        *
-        * @param locale
-        *      The client locale.
-        *      <br>If <jk>null</jk>, assumes the default locale.
-        * @return The resource bundle for the specified locale.  Never 
<jk>null</jk>.
-        */
-       public Messages getBundle(Locale locale) {
-               if (locale == null)
-                       locale = Locale.getDefault();
-
-               Messages mb = localizedBundles.get(locale);
-               if (mb != null)
-                       return mb;
-               mb = new Messages(forClass, bundlePath, locale);
-               List<Messages> l = new ArrayList<>(searchBundles.size()-1);
-               for (int i = 1; i < searchBundles.size(); i++) {
-                       Messages srb = searchBundles.get(i);
-                       srb = new Messages(srb.forClass, srb.bundlePath, 
locale);
-                       l.add(srb);
-                       mb.allKeys.addAll(srb.keySet());
-               }
-               mb.searchBundles.addAll(l);
-               localizedBundles.putIfAbsent(locale, mb);
-               return localizedBundles.get(locale);
-       }
-
-       private static final String findBundlePath(Class<?> forClass) {
-               String path = forClass.getName();
-               if (tryBundlePath(forClass, path))
-                       return path;
-               path = forClass.getPackage().getName() + ".nls." + 
forClass.getSimpleName();
-               if (tryBundlePath(forClass, path))
-                       return path;
-               path = forClass.getPackage().getName() + ".i18n." + 
forClass.getSimpleName();
-               if (tryBundlePath(forClass, path))
-                       return path;
-               throw new MissingResourceException("Could not find bundle path 
for class ", forClass.getName(), null);
-       }
-
-       private static final boolean tryBundlePath(Class<?> c, String path) {
-               try {
-                       path = c.getName();
-                       ResourceBundle.getBundle(path, Locale.getDefault(), 
c.getClassLoader());
-                       return true;
-               } catch (MissingResourceException e) {
-                       return false;
-               }
-       }
-}
\ No newline at end of file
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.cp;
+
+import static org.apache.juneau.internal.ResourceBundleUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
+
+import java.text.*;
+import java.util.*;
+import java.util.concurrent.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.marshall.*;
+
+/**
+ * A wrapper around a {@link ResourceBundle}.
+ *
+ * <p>
+ * Adds support for non-existent resource bundles and associating class 
loaders.
+ */
+public class Messages extends ResourceBundle {
+
+       private ResourceBundle rb;
+       private Class<?> c;
+       private Messages parent;
+
+       // Cache of message bundles per locale.
+       private final ConcurrentHashMap<Locale,Messages> localizedMessages = 
new ConcurrentHashMap<>();
+
+       // Cache of virtual keys to actual keys.
+       private final Map<String,String> keyMap;
+
+       private final Set<String> rbKeys;
+
+       /**
+        * Creator.
+        *
+        * @param forClass
+        *      The class we're creating this object for.
+        * @return A new builder.
+        */
+       public static final MessagesBuilder create(Class<?> forClass) {
+               return new MessagesBuilder(forClass);
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param forClass
+        *      The class we're creating this object for.
+        * @return A new message bundle belonging to the class.
+        */
+       public static final Messages of(Class<?> forClass) {
+               return create(forClass).build();
+       }
+
+       /**
+        * Constructor.
+        *
+        * @param forClass
+        *      The class we're creating this object for.
+        * @param name
+        *      The bundle name (e.g. <js>"Messages"</js>).
+        *      <br>If <jk>null</jk>, uses the class name.
+        * @return A new message bundle belonging to the class.
+        */
+       public static final Messages of(Class<?> forClass, String name) {
+               return create(forClass).name(name).build();
+       }
+
+
+       /**
+        * Constructor.
+        *
+        * @param forClass
+        *      The class we're creating this object for.
+        * @param rb
+        *      The resource bundle we're encapsulating.  Can be <jk>null</jk>.
+        * @param parent
+        *      The parent resource.  Can be <jk>null</jk>.
+        */
+       public Messages(Class<?> forClass, ResourceBundle rb, Messages parent) {
+               this.c = forClass;
+               this.rb = rb;
+               this.parent = parent;
+               if (parent != null)
+                       setParent(parent);
+
+               Map<String,String> keyMap = new TreeMap<>();
+
+               String cn = c.getSimpleName() + '.';
+               if (rb != null) {
+                       for (String key : rb.keySet()) {
+                               keyMap.put(key, key);
+                               if (key.startsWith(cn)) {
+                                       String shortKey = 
key.substring(cn.length());
+                                       keyMap.put(shortKey, key);
+                               }
+                       }
+               }
+               if (parent != null) {
+                       for (String key : parent.keySet()) {
+                               keyMap.put(key, key);
+                               if (key.startsWith(cn)) {
+                                       String shortKey = 
key.substring(cn.length());
+                                       keyMap.put(shortKey, key);
+                               }
+                       }
+               }
+
+               this.keyMap = Collections.unmodifiableMap(new 
LinkedHashMap<>(keyMap));
+               this.rbKeys = rb == null ? Collections.emptySet() : rb.keySet();
+       }
+
+       /**
+        * Returns this message bundle for the specified locale.
+        *
+        * @param locale The locale to get the messages for.
+        * @return A new {@link Messages} object.  Never <jk>null</jk>.
+        */
+       public Messages forLocale(Locale locale) {
+               if (locale == null)
+                       locale = Locale.getDefault();
+               Messages mb = localizedMessages.get(locale);
+               if (mb == null) {
+                       Messages parent = this.parent == null ? null : 
this.parent.forLocale(locale);
+                       ResourceBundle rb = this.rb == null ? null : 
findBundle(this.rb.getBaseBundleName(), locale, c.getClassLoader());
+                       mb = new Messages(c, rb, parent);
+                       localizedMessages.put(locale, mb);
+               }
+               return mb;
+       }
+
+       /**
+        * Returns all keys in this resource bundle with the specified prefix.
+        *
+        * <p>
+        * Keys are returned in alphabetical order.
+        *
+        * @param prefix The prefix.
+        * @return The set of all keys in the resource bundle with the prefix.
+        */
+       public Set<String> keySet(String prefix) {
+               Set<String> set = new LinkedHashSet<>();
+               for (String s : keySet()) {
+                       if (s.equals(prefix) || (s.startsWith(prefix) && 
s.charAt(prefix.length()) == '.'))
+                               set.add(s);
+               }
+               return set;
+       }
+
+       /**
+        * Similar to {@link ResourceBundle#getString(String)} except allows 
you to pass in {@link MessageFormat} objects.
+        *
+        * @param key The resource bundle key.
+        * @param args Optional {@link MessageFormat}-style arguments.
+        * @return
+        *      The resolved value.  Never <jk>null</jk>.
+        *      <js>"{!key}"</js> if the key is missing.
+        */
+       public String getString(String key, Object...args) {
+               String s = getString(key);
+               if (s.startsWith("{!"))
+                       return s;
+               return format(s, args);
+       }
+
+       /**
+        * Same as {@link #getString(String, Object...)} but allows you to 
specify the locale.
+        *
+        * @param locale The locale of the resource bundle to retrieve message 
from.
+        * @param key The resource bundle key.
+        * @param args Optional {@link MessageFormat}-style arguments.
+        * @return
+        *      The resolved value.  Never <jk>null</jk>.
+        *      <js>"{!!key}"</js> if the bundle is missing.
+        *      <js>"{!key}"</js> if the key is missing.
+        */
+       public String getString(Locale locale, String key, Object...args) {
+               if (locale == null)
+                       return getString(key, args);
+               return forLocale(locale).getString(key, args);
+       }
+
+       /**
+        * Looks for all the specified keys in the resource bundle and returns 
the first value that exists.
+        *
+        * @param keys The list of possible keys.
+        * @return The resolved value, or <jk>null</jk> if no value is found or 
the resource bundle is missing.
+        */
+       public String findFirstString(String...keys) {
+               for (String k : keys) {
+                       if (containsKey(k))
+                               return getString(k);
+               }
+               return null;
+       }
+
+       /**
+        * Same as {@link #findFirstString(String...)}, but uses the specified 
locale.
+        *
+        * @param locale The locale of the resource bundle to retrieve message 
from.
+        * @param keys The list of possible keys.
+        * @return The resolved value, or <jk>null</jk> if no value is found or 
the resource bundle is missing.
+        */
+       public String findFirstString(Locale locale, String...keys) {
+               Messages srb = forLocale(locale);
+               return srb.findFirstString(keys);
+       }
+
+       @Override /* ResourceBundle */
+       protected Object handleGetObject(String key) {
+               String k = keyMap.get(key);
+               if (k == null)
+                       return "{!" + key + "}";
+               try {
+                       if (rbKeys.contains(k))
+                               return rb.getObject(k);
+               } catch (MissingResourceException e) { /* Shouldn't happen */ }
+               return parent.handleGetObject(key);
+       }
+
+       @Override /* ResourceBundle */
+       public boolean containsKey(String key) {
+               return keyMap.containsKey(key);
+       }
+
+       @Override /* ResourceBundle */
+       public Set<String> keySet() {
+               return keyMap.keySet();
+       }
+
+       @Override /* ResourceBundle */
+       public Enumeration<String> getKeys() {
+               return Collections.enumeration(keySet());
+       }
+
+       @Override
+       public String toString() {
+               OMap om = new OMap();
+               for (String k : new TreeSet<>(keySet()))
+                       om.put(k, getString(k));
+               return SimpleJson.DEFAULT.toString(om);
+       }
+}
diff --git 
a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessagesBuilder.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessagesBuilder.java
new file mode 100644
index 0000000..5a60e01
--- /dev/null
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessagesBuilder.java
@@ -0,0 +1,116 @@
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.cp;
+
+import static org.apache.juneau.internal.StringUtils.*;
+import static org.apache.juneau.internal.ResourceBundleUtils.*;
+
+import java.util.*;
+
+import org.apache.juneau.collections.*;
+import org.apache.juneau.internal.*;
+
+/**
+ * Builder for {@link Messages} objects.
+ */
+public class MessagesBuilder {
+
+       private Class<?> forClass;
+       private Locale locale = Locale.getDefault();
+       private String name;
+       private Messages parent;
+
+       private String[] baseNames = 
{"{package}.{name}","{package}.i18n.{name}","{package}.nls.{name}","{package}.messages.{name}"};
+
+       MessagesBuilder(Class<?> forClass) {
+               this.forClass = forClass;
+               this.name = forClass.getSimpleName();
+       }
+
+       /**
+        * Adds a parent bundle.
+        *
+        * @param parent The parent bundle.  Can be <jk>null</jk>.
+        * @return This object (for method chaining).
+        */
+       public MessagesBuilder parent(Messages parent) {
+               this.parent = parent;
+               return this;
+       }
+
+       /**
+        * Specifies the bundle name (e.g. <js>"Messages"</js>).
+        *
+        * @param name
+        *      The bundle name.
+        *      <br>If <jk>null</jk>, the forClass class name is used.
+        * @return This object (for method chaining).
+        */
+       public MessagesBuilder name(String name) {
+               this.name = isEmpty(name) ? forClass.getSimpleName() : name;
+               return this;
+       }
+
+       /**
+        * Specifies the base name patterns to use for finding the resource 
bundle.
+        *
+        * @param baseNames
+        *      The bundle base names.
+        *      <br>The default is the following:
+        *      <ul>
+        *              <li><js>"{package}.{name}"</js>
+        *              <li><js>"{package}.i18n.{name}"</js>
+        *              <li><js>"{package}.nls.{name}"</js>
+        *              <li><js>"{package}.messages.{name}"</js>
+        *      </ul>
+        * @return This object (for method chaining).
+        */
+       public MessagesBuilder baseNames(String...baseNames) {
+               this.baseNames = baseNames == null ? new String[]{} : baseNames;
+               return this;
+       }
+
+       /**
+        * Specifies the locale.
+        *
+        * @param locale
+        *      The locale.
+        *      If <jk>null</jk>, the default locale is used.
+        * @return This object (for method chaining).
+        */
+       public MessagesBuilder locale(Locale locale) {
+               this.locale = locale == null ? Locale.getDefault() : locale;
+               return this;
+       }
+
+       /**
+        * Creates a new {@link Messages} based on the setting of this builder.
+        *
+        * @return A new {@link Messages} object.
+        */
+       public Messages build() {
+               return new Messages(forClass, getBundle(), parent);
+       }
+
+       private ResourceBundle getBundle() {
+               ClassLoader cl = forClass.getClassLoader();
+               OMap m = OMap.of("name", name, "package", 
forClass.getPackage().getName());
+               for (String bn : baseNames) {
+                       bn = StringUtils.replaceVars(bn, m);
+                       ResourceBundle rb = findBundle(bn, locale, cl);
+                       if (rb != null)
+                               return rb;
+               }
+               return null;
+       }
+}
diff --git 
a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ResourceBundleUtils.java
similarity index 55%
copy from 
juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
copy to 
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ResourceBundleUtils.java
index 8c6c6f9..98279d2 100644
--- 
a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/Messages_Test.java
+++ 
b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/internal/ResourceBundleUtils.java
@@ -1,43 +1,56 @@
-// 
***************************************************************************************************************************
-// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
-// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
-// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
-// * with the License.  You may obtain a copy of the License at                
                                              *
-// *                                                                           
                                              *
-// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
-// *                                                                           
                                              *
-// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
-// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
-// * specific language governing permissions and limitations under the 
License.                                              *
-// 
***************************************************************************************************************************
-package org.apache.juneau.cp;
-
-import static org.apache.juneau.assertions.Assertions.*;
-import static org.junit.runners.MethodSorters.*;
-
-import java.util.*;
-
-import static java.util.Locale.*;
-
-import org.apache.juneau.cp.test1.*;
-import org.junit.*;
-
-@FixMethodOrder(NAME_ASCENDING)
-public class Messages_Test {
-
-       @Test
-       public void a01_nonExistent() throws Exception {
-               assertThrown(()->Messages.of(Test1.class)).contains("Could not 
find bundle path for class");
-               
assertThrown(()->Messages.of(Test1.class,"bad.properties")).contains("Bundle 
path should not end with '.properties'");
-       }
-
-       @Test
-       public void a02_sameDirectory() throws Exception {
-               Messages x = Messages.of(MessageBundleTest1.class);
-               
assertString(x.getString("file")).is("MessageBundleTest1.properties");
-               
assertString(x.getBundle(JAPANESE).getString("file")).is("MessageBundleTest1_ja.properties");
-               
assertString(x.getBundle(JAPAN).getString("file")).is("MessageBundleTest1_ja_JP.properties");
-               
assertString(x.getBundle(CHINA).getString("file")).is("MessageBundleTest1.properties");
-               
assertString(x.getBundle((Locale)null).getString("file")).is("MessageBundleTest1.properties");
-       }
-}
+// 
***************************************************************************************************************************
+// * Licensed to the Apache Software Foundation (ASF) under one or more 
contributor license agreements.  See the NOTICE file *
+// * distributed with this work for additional information regarding copyright 
ownership.  The ASF licenses this file        *
+// * to you under the Apache License, Version 2.0 (the "License"); you may not 
use this file except in compliance            *
+// * with the License.  You may obtain a copy of the License at                
                                              *
+// *                                                                           
                                              *
+// *  http://www.apache.org/licenses/LICENSE-2.0                               
                                              *
+// *                                                                           
                                              *
+// * Unless required by applicable law or agreed to in writing, software 
distributed under the License is distributed on an  *
+// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
express or implied.  See the License for the        *
+// * specific language governing permissions and limitations under the 
License.                                              *
+// 
***************************************************************************************************************************
+package org.apache.juneau.internal;
+
+import java.util.*;
+
+/**
+ * Class-related utility methods.
+ */
+public final class ResourceBundleUtils {
+
+       private static final ResourceBundle EMPTY = new ResourceBundle() {
+               @Override
+               protected Object handleGetObject(String key) {
+                       return null;
+               }
+               @Override
+               public Enumeration<String> getKeys() {
+                       return Collections.emptyEnumeration();
+               }
+       };
+
+       /**
+        * Same as {@link ResourceBundle#getBundle(String, Locale, 
ClassLoader)} but never throws a {@link MissingResourceException}.
+        *
+        * @param baseName The base name of the resource bundle, a fully 
qualified class name.
+        * @param locale The locale for which a resource bundle is desired.
+        * @param loader The class loader from which to load the resource 
bundle.
+        * @return The matching resource bundle, or <jk>null</jk> if it could 
not be found.
+        */
+       public static ResourceBundle findBundle(String baseName, Locale locale, 
ClassLoader loader) {
+               try {
+                       return ResourceBundle.getBundle(baseName, locale, 
loader);
+               } catch (MissingResourceException e) {}
+               return null;
+       }
+
+       /**
+        * Returns an empty resource bundle.
+        *
+        * @return An empty resource bundle.
+        */
+       public static ResourceBundle empty() {
+               return EMPTY;
+       }
+}
diff --git 
a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/NlsTest.java
 
b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/NlsTest.java
index ec52440..b09c9f1 100644
--- 
a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/NlsTest.java
+++ 
b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/NlsTest.java
@@ -109,6 +109,6 @@ public class NlsTest {
 
        @Test
        public void c01_missingResourceBundle() throws Exception {
-               c.get("/test").run().assertBody().is("{!!bad}");
+               c.get("/test").run().assertBody().is("{!bad}");
        }
 }
diff --git 
a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceMessagesTest.java
 
b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceMessagesTest.java
index 60af1d3..2807fcc 100644
--- 
a/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceMessagesTest.java
+++ 
b/juneau-rest/juneau-rest-server-utest/src/test/java/org/apache/juneau/rest/annotation/RestResourceMessagesTest.java
@@ -29,7 +29,7 @@ public class RestResourceMessagesTest {
 
        static OMap convertToMap(ResourceBundle rb) {
                OMap m = new OMap();
-               for (String k : rb.keySet())
+               for (String k : new TreeSet<>(rb.keySet()))
                        m.put(k, rb.getString(k));
                return m;
        }
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
index bc69ce1..e9bfea6 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestContext.java
@@ -3852,11 +3852,13 @@ public final class RestContext extends BeanContext {
 
                        MessageBundleLocation[] mbl = 
getInstanceArrayProperty(REST_messages, MessageBundleLocation.class, new 
MessageBundleLocation[0]);
                        if (mbl.length == 0)
-                               msgs = new Messages(rci.inner(), "", null);
+                               msgs = Messages.of(rci.inner());
                        else {
-                               msgs = new Messages(mbl[0] != null ? 
mbl[0].baseClass : rci.inner(), mbl[0].bundlePath, null);
-                               for (int i = 1; i < mbl.length; i++)
-                                       msgs.addSearchPath(mbl[i] != null ? 
mbl[i].baseClass : rci.inner(), mbl[i].bundlePath);
+                               Messages msgs = null;
+                               for (int i = mbl.length-1; i >= 0; i--)
+                                       if (mbl[i] != null)
+                                               msgs = 
Messages.create(mbl[i].baseClass == null ? rci.inner() : 
mbl[i].baseClass).name(mbl[i].bundlePath).parent(msgs).build();
+                               this.msgs = msgs;
                        }
 
                        this.fullPath = (builder.parentContext == null ? "" : 
(builder.parentContext.fullPath + '/')) + builder.getPath();
diff --git 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
index 91971c6..ad4ae46 100644
--- 
a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
+++ 
b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestRequest.java
@@ -1250,7 +1250,7 @@ public final class RestRequest extends 
HttpServletRequestWrapper {
         *      <br>Never <jk>null</jk>.
         */
        public Messages getMessageBundle() {
-               return context.getMessages().getBundle(getLocale());
+               return context.getMessages().forLocale(getLocale());
        }
 
        /**

Reply via email to