You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@juneau.apache.org by ja...@apache.org on 2020/07/26 20:52:45 UTC

[juneau] branch master updated: Rewrite Messages API.

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 <ja...@salesforce.com>
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());
 	}
 
 	/**