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/25 19:02:19 UTC

[juneau] branch master updated: Improvements to org.apache.juneau.cp package.

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 b9a37fa  Improvements to org.apache.juneau.cp package.
b9a37fa is described below

commit b9a37fa2261e197ce9b6e233d84b6b2f240301f6
Author: JamesBognar <ja...@salesforce.com>
AuthorDate: Sat Jul 25 15:02:12 2020 -0400

    Improvements to org.apache.juneau.cp package.
---
 .../juneau-core-utest/files/Test3.properties       |  15 ++
 .../juneau-core-utest/files/Test3_ja.properties    |  15 ++
 .../juneau-core-utest/files/Test3_ja_JP.properties |  15 ++
 juneau-core/juneau-core-utest/files/test2.txt      |   1 +
 .../apache/juneau/cp/BasicResourceFinder_Test.java | 191 +++++++++++++++++++++
 .../org/apache/juneau/cp/MessageBundle_Test.java   |  52 +++---
 .../juneau/cp/RecursiveResourceFinder_Test.java    |  69 ++++++++
 .../org/apache/juneau/cp/ResourceFinder_Test.java  |  45 +----
 .../org/apache/juneau/cp/ResourceManager_Test.java | 123 +++++++++++++
 .../juneau/cp/SimpleResourceFinder_Test.java       |  56 ++++++
 .../apache/juneau/cp/test1/MessageBundleTest1.java |  40 +----
 .../java/org/apache/juneau/cp/test1/Test1.java     |  40 +----
 .../java/org/apache/juneau/cp/test2/Test2.java     |  40 +----
 .../juneau/cp/test1/MessageBundleTest1.properties  |  15 ++
 .../cp/test1/MessageBundleTest1_ja.properties      |  15 ++
 .../cp/test1/MessageBundleTest1_ja_JP.properties   |  15 ++
 .../apache/juneau/cp/test1/files/Test1.properties  |  15 ++
 .../juneau/cp/test1/files/Test1_ja.properties      |  15 ++
 .../juneau/cp/test1/files/Test1_ja_JP.properties   |  15 ++
 .../org/apache/juneau/cp/test2/Test2.properties    |  15 ++
 .../org/apache/juneau/cp/test2/Test2_ja.properties |  15 ++
 .../apache/juneau/cp/test2/Test2_ja_JP.properties  |  15 ++
 .../resources/org/apache/juneau/cp/test2/Test4     |  15 ++
 .../resources/org/apache/juneau/cp/test2/Test4_ja  |  15 ++
 .../org/apache/juneau/cp/test2/Test4_ja_JP         |  15 ++
 .../juneau/assertions/FluentStringAssertion.java   |  30 ++++
 .../org/apache/juneau/cp/BasicResourceFinder.java  |  17 +-
 .../java/org/apache/juneau/cp/MessageBundle.java   | 100 +++++------
 .../java/org/apache/juneau/cp/ResourceFinder.java  |   2 +-
 .../java/org/apache/juneau/cp/ResourceManager.java |  76 ++------
 .../apache/juneau/microservice/Microservice.java   |   2 +-
 .../juneau/microservice/console/ConfigCommand.java |   2 +-
 .../juneau/microservice/console/ExitCommand.java   |   2 +-
 .../juneau/microservice/console/HelpCommand.java   |   2 +-
 .../microservice/console/RestartCommand.java       |   2 +-
 .../microservice/jetty/JettyMicroservice.java      |   2 +-
 .../java/org/apache/juneau/rest/RestContext.java   | 107 +-----------
 .../java/org/apache/juneau/rest/RestServlet.java   |   2 +-
 .../java/org/apache/juneau/rest/StaticFiles.java   |   4 +-
 .../juneau/rest/reshandlers/DefaultHandler.java    |   2 +-
 40 files changed, 821 insertions(+), 413 deletions(-)

diff --git a/juneau-core/juneau-core-utest/files/Test3.properties b/juneau-core/juneau-core-utest/files/Test3.properties
new file mode 100644
index 0000000..90a3dcd
--- /dev/null
+++ b/juneau-core/juneau-core-utest/files/Test3.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test3.properties
diff --git a/juneau-core/juneau-core-utest/files/Test3_ja.properties b/juneau-core/juneau-core-utest/files/Test3_ja.properties
new file mode 100644
index 0000000..4ffd31e
--- /dev/null
+++ b/juneau-core/juneau-core-utest/files/Test3_ja.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test3_ja.properties
diff --git a/juneau-core/juneau-core-utest/files/Test3_ja_JP.properties b/juneau-core/juneau-core-utest/files/Test3_ja_JP.properties
new file mode 100644
index 0000000..ed1139f
--- /dev/null
+++ b/juneau-core/juneau-core-utest/files/Test3_ja_JP.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test3_ja_JP.properties
diff --git a/juneau-core/juneau-core-utest/files/test2.txt b/juneau-core/juneau-core-utest/files/test2.txt
index 917e02b..d12cd3c 100644
--- a/juneau-core/juneau-core-utest/files/test2.txt
+++ b/juneau-core/juneau-core-utest/files/test2.txt
@@ -10,3 +10,4 @@
 * "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.                                              *
 ***************************************************************************************************************************
+test2.txt
\ No newline at end of file
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/BasicResourceFinder_Test.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/BasicResourceFinder_Test.java
new file mode 100644
index 0000000..6edc881
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/BasicResourceFinder_Test.java
@@ -0,0 +1,191 @@
+// ***************************************************************************************************************************
+// * 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 static java.util.Locale.*;
+
+import org.apache.juneau.cp.test1.*;
+import org.apache.juneau.cp.test2.*;
+import org.junit.*;
+
+@FixMethodOrder(NAME_ASCENDING)
+public class BasicResourceFinder_Test {
+
+	@Test
+	public void a01_basic() throws Exception {
+		ResourceFinder x = BasicResourceFinder.INSTANCE;
+
+		assertStream(x.findResource(null,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",CHINA)).doesNotExist();
+
+		assertStream(x.findResource(null,"Test2.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"Test2.properties",null)).string().contains("Test2.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",JAPANESE)).string().contains("Test2_ja.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",JAPAN)).string().contains("Test2_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",CHINA)).string().contains("Test2.properties");
+
+		assertStream(x.findResource(null,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+
+		assertStream(x.findResource(null,"Test4",null)).doesNotExist();
+		assertStream(x.findResource(null,"Test4",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"Test4",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"Test4",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test4",null)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test4",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test4",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test4",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"Test4",null)).string().contains("Test4");
+		assertStream(x.findResource(Test2.class,"Test4",JAPANESE)).string().contains("Test4_ja");
+		assertStream(x.findResource(Test2.class,"Test4",JAPAN)).string().contains("Test4_ja_JP");
+		assertStream(x.findResource(Test2.class,"Test4",CHINA)).string().contains("Test4");
+	}
+
+	@Test
+	public void a02_noFileSystem() throws Exception {
+		ResourceFinder x = new BasicResourceFinder(false, false);
+
+		assertStream(x.findResource(null,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",CHINA)).doesNotExist();
+
+		assertStream(x.findResource(null,"files/Test3.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test3.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test3.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test3.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",CHINA)).doesNotExist();
+	}
+
+	@Test
+	public void a03_recursive() throws Exception {
+		ResourceFinder x = new BasicResourceFinder(true, true);
+
+		assertStream(x.findResource(null,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+
+		assertStream(x.findResource(null,"Test2.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"Test2.properties",null)).string().contains("Test2.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",JAPANESE)).string().contains("Test2_ja.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",JAPAN)).string().contains("Test2_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",CHINA)).string().contains("Test2.properties");
+
+		assertStream(x.findResource(null,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+	}
+
+	@Test
+	public void a04_invalidNames() throws Exception {
+		ResourceFinder x = BasicResourceFinder.INSTANCE;
+
+		for (String s : new String[]{"bad.properties","",null,"files/../files/Test1.properties"}) {
+			assertStream(x.findResource(null,s,null)).doesNotExist();
+			assertStream(x.findResource(null,s,JAPANESE)).doesNotExist();
+			assertStream(x.findResource(null,s,JAPAN)).doesNotExist();
+			assertStream(x.findResource(null,s,CHINA)).doesNotExist();
+			assertStream(x.findResource(Test1.class,s,null)).doesNotExist();
+			assertStream(x.findResource(Test1.class,s,JAPANESE)).doesNotExist();
+			assertStream(x.findResource(Test1.class,s,JAPAN)).doesNotExist();
+			assertStream(x.findResource(Test1.class,s,CHINA)).doesNotExist();
+			assertStream(x.findResource(Test2.class,s,null)).doesNotExist();
+			assertStream(x.findResource(Test2.class,s,JAPANESE)).doesNotExist();
+			assertStream(x.findResource(Test2.class,s,JAPAN)).doesNotExist();
+			assertStream(x.findResource(Test2.class,s,CHINA)).doesNotExist();
+		}
+
+		String s = ".";
+		assertStream(x.findResource(null,s,null)).doesNotExist();
+		assertStream(x.findResource(null,s,JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,s,JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,s,CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,s,null)).string().contains("Test1.class");
+		assertStream(x.findResource(Test1.class,s,JAPANESE)).string().contains("Test1.class");
+		assertStream(x.findResource(Test1.class,s,JAPAN)).string().contains("Test1.class");
+		assertStream(x.findResource(Test1.class,s,CHINA)).string().contains("Test1.class");
+		assertStream(x.findResource(Test2.class,s,null)).string().contains("Test2.class");
+		assertStream(x.findResource(Test2.class,s,JAPANESE)).string().contains("Test2.class");
+		assertStream(x.findResource(Test2.class,s,JAPAN)).string().contains("Test2.class");
+		assertStream(x.findResource(Test2.class,s,CHINA)).string().contains("Test2.class");
+	}
+}
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/MessageBundle_Test.java
similarity index 57%
copy from juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
copy to juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/MessageBundle_Test.java
index 5cb2216..e5c74f8 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/MessageBundle_Test.java
@@ -10,44 +10,34 @@
 // * "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.microservice.console;
+package org.apache.juneau.cp;
 
-import java.io.*;
-import java.util.*;
+import static org.apache.juneau.assertions.Assertions.*;
+import static org.junit.runners.MethodSorters.*;
 
-import org.apache.juneau.collections.*;
-import org.apache.juneau.microservice.*;
-import org.apache.juneau.cp.*;
+import java.util.*;
 
-/**
- * Implements the 'exit' console command to gracefully shut down the microservice and JVM.
- */
-public class ExitCommand extends ConsoleCommand {
+import static java.util.Locale.*;
 
-	private final MessageBundle mb = MessageBundle.create(ExitCommand.class, "Messages");
+import org.apache.juneau.cp.test1.*;
+import org.junit.*;
 
-	@Override /* ConsoleCommand */
-	public String getName() {
-		return "exit";
-	}
-
-	@Override /* ConsoleCommand */
-	public String getInfo() {
-		return mb.getString("info");
-	}
+@FixMethodOrder(NAME_ASCENDING)
+public class MessageBundle_Test {
 
-	@Override /* ConsoleCommand */
-	public String getDescription() {
-		return mb.getString("description");
+	@Test
+	public void a01_nonExistent() throws Exception {
+		assertThrown(()->MessageBundle.of(Test1.class)).contains("Could not find bundle path for class");
+		assertThrown(()->MessageBundle.of(Test1.class,"bad.properties")).contains("Bundle path should not end with '.properties'");
 	}
 
-	@Override /* ConsoleCommand */
-	public boolean execute(Scanner in, PrintWriter out, Args args) {
-		try {
-			Microservice.getInstance().stop().exit();
-		} catch (Exception e) {
-			e.printStackTrace();
-		}
-		return true;
+	@Test
+	public void a02_sameDirectory() throws Exception {
+		MessageBundle x = MessageBundle.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");
 	}
 }
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/RecursiveResourceFinder_Test.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/RecursiveResourceFinder_Test.java
new file mode 100644
index 0000000..6907e66
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/RecursiveResourceFinder_Test.java
@@ -0,0 +1,69 @@
+// ***************************************************************************************************************************
+// * 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 static java.util.Locale.*;
+
+import org.apache.juneau.cp.test1.*;
+import org.apache.juneau.cp.test2.*;
+import org.junit.*;
+
+@FixMethodOrder(NAME_ASCENDING)
+public class RecursiveResourceFinder_Test {
+
+	@Test
+	public void a01_basic() throws Exception {
+		ResourceFinder x = RecursiveResourceFinder.INSTANCE;
+
+		assertStream(x.findResource(null,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+
+		assertStream(x.findResource(null,"Test2.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"Test2.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"Test2.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"Test2.properties",null)).string().contains("Test2.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",JAPANESE)).string().contains("Test2_ja.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",JAPAN)).string().contains("Test2_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"Test2.properties",CHINA)).string().contains("Test2.properties");
+
+		assertStream(x.findResource(null,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(null,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",null)).string().contains("Test3.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPANESE)).string().contains("Test3_ja.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPAN)).string().contains("Test3_ja_JP.properties");
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",CHINA)).string().contains("Test3.properties");
+	}
+}
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/ResourceFinder_Test.java
similarity index 62%
copy from juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
copy to juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/ResourceFinder_Test.java
index 5cb2216..b6c656a 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/ResourceFinder_Test.java
@@ -10,44 +10,17 @@
 // * "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.microservice.console;
+package org.apache.juneau.cp;
 
-import java.io.*;
-import java.util.*;
+import static org.apache.juneau.assertions.Assertions.*;
+import static org.junit.runners.MethodSorters.*;
+import org.junit.*;
 
-import org.apache.juneau.collections.*;
-import org.apache.juneau.microservice.*;
-import org.apache.juneau.cp.*;
+@FixMethodOrder(NAME_ASCENDING)
+public class ResourceFinder_Test {
 
-/**
- * Implements the 'exit' console command to gracefully shut down the microservice and JVM.
- */
-public class ExitCommand extends ConsoleCommand {
-
-	private final MessageBundle mb = MessageBundle.create(ExitCommand.class, "Messages");
-
-	@Override /* ConsoleCommand */
-	public String getName() {
-		return "exit";
-	}
-
-	@Override /* ConsoleCommand */
-	public String getInfo() {
-		return mb.getString("info");
-	}
-
-	@Override /* ConsoleCommand */
-	public String getDescription() {
-		return mb.getString("description");
-	}
-
-	@Override /* ConsoleCommand */
-	public boolean execute(Scanner in, PrintWriter out, Args args) {
-		try {
-			Microservice.getInstance().stop().exit();
-		} catch (Exception e) {
-			e.printStackTrace();
-		}
-		return true;
+	@Test
+	public void a01_basic_Null() throws Exception {
+		assertObject(new ResourceFinder.Null().findResource(null, null, null)).isNull();
 	}
 }
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/ResourceManager_Test.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/ResourceManager_Test.java
new file mode 100644
index 0000000..9f2897b
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/ResourceManager_Test.java
@@ -0,0 +1,123 @@
+// ***************************************************************************************************************************
+// * 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 static java.util.Locale.*;
+
+import org.apache.juneau.cp.test2.*;
+import org.apache.juneau.parser.*;
+import org.apache.juneau.plaintext.*;
+import org.junit.*;
+
+@FixMethodOrder(NAME_ASCENDING)
+public class ResourceManager_Test {
+
+	private static Parser PARSER = PlainTextParser.DEFAULT;
+
+	@Test
+	public void a01_basic_BasicResourceFinder_nocache() throws Exception {
+		ResourceManager rm = of(Test2.class);
+		String f, fja, fjp;
+
+		f = "Test2.properties"; fja = "Test2_ja.properties"; fjp = "Test2_ja_JP.properties";
+		assertStream(rm.getStream(f)).string().contains(f);
+		assertStream(rm.getStream(f,JAPANESE)).string().contains(fja);
+		assertStream(rm.getStream(f,JAPAN)).string().contains(fjp);
+		assertStream(rm.getStream(f,CHINA)).string().contains(f);
+		assertString(rm.getString(f)).contains(f);
+		assertString(rm.getString(f,JAPANESE)).contains(fja);
+		assertString(rm.getString(f,JAPAN)).contains(fjp);
+		assertString(rm.getString(f,CHINA)).contains(f);
+		assertObject(rm.getResource(String.class,PARSER,f)).string().contains(f);
+		assertObject(rm.getResource(String.class,PARSER,f,JAPANESE)).string().contains(fja);
+		assertObject(rm.getResource(String.class,PARSER,f,JAPAN)).string().contains(fjp);
+		assertObject(rm.getResource(String.class,PARSER,f,CHINA)).string().contains(f);
+
+		f = "bad.properties"; fja = "bad.properties"; fjp = "bad.properties";
+		assertStream(rm.getStream(f)).doesNotExist();
+		assertStream(rm.getStream(f,JAPANESE)).doesNotExist();
+		assertStream(rm.getStream(f,JAPAN)).doesNotExist();
+		assertStream(rm.getStream(f,CHINA)).doesNotExist();
+		assertString(rm.getString(f)).doesNotExist();
+		assertString(rm.getString(f,JAPANESE)).doesNotExist();
+		assertString(rm.getString(f,JAPAN)).doesNotExist();
+		assertString(rm.getString(f,CHINA)).doesNotExist();
+		assertObject(rm.getResource(String.class,PARSER,f)).doesNotExist();
+		assertObject(rm.getResource(String.class,PARSER,f,JAPANESE)).doesNotExist();
+		assertObject(rm.getResource(String.class,PARSER,f,JAPAN)).doesNotExist();
+		assertObject(rm.getResource(String.class,PARSER,f,CHINA)).doesNotExist();
+	}
+
+	@Test
+	public void a02_basic_BasicResourceFinder_cache() throws Exception {
+		ResourceManager rm = of(Test2.class, BasicResourceFinder.INSTANCE, true);
+		String f, fja, fjp;
+
+		for (int i = 0; i <= 1; i++) {
+			f = "Test2.properties"; fja = "Test2_ja.properties"; fjp = "Test2_ja_JP.properties";
+			assertStream(rm.getStream(f)).string().contains(f);
+			assertStream(rm.getStream(f,JAPANESE)).string().contains(fja);
+			assertStream(rm.getStream(f,JAPAN)).string().contains(fjp);
+			assertStream(rm.getStream(f,CHINA)).string().contains(f);
+			assertString(rm.getString(f)).contains(f);
+			assertString(rm.getString(f,JAPANESE)).contains(fja);
+			assertString(rm.getString(f,JAPAN)).contains(fjp);
+			assertString(rm.getString(f,CHINA)).contains(f);
+			assertObject(rm.getResource(String.class,PARSER,f)).string().contains(f);
+			assertObject(rm.getResource(String.class,PARSER,f,JAPANESE)).string().contains(fja);
+			assertObject(rm.getResource(String.class,PARSER,f,JAPAN)).string().contains(fjp);
+			assertObject(rm.getResource(String.class,PARSER,f,CHINA)).string().contains(f);
+
+			f = "bad.properties"; fja = "bad.properties"; fjp = "bad.properties";
+			assertStream(rm.getStream(f)).doesNotExist();
+			assertStream(rm.getStream(f,JAPANESE)).doesNotExist();
+			assertStream(rm.getStream(f,JAPAN)).doesNotExist();
+			assertStream(rm.getStream(f,CHINA)).doesNotExist();
+			assertString(rm.getString(f)).doesNotExist();
+			assertString(rm.getString(f,JAPANESE)).doesNotExist();
+			assertString(rm.getString(f,JAPAN)).doesNotExist();
+			assertString(rm.getString(f,CHINA)).doesNotExist();
+			assertObject(rm.getResource(String.class,PARSER,f)).doesNotExist();
+			assertObject(rm.getResource(String.class,PARSER,f,JAPANESE)).doesNotExist();
+			assertObject(rm.getResource(String.class,PARSER,f,JAPAN)).doesNotExist();
+			assertObject(rm.getResource(String.class,PARSER,f,CHINA)).doesNotExist();
+		}
+	}
+
+	@Test
+	public void a03_basic_other() throws Exception {
+		ResourceManager rm = of(Test2.class);
+
+		assertStream(rm.getStream(null)).doesNotExist();
+		assertString(rm.getString(null)).doesNotExist();
+		assertObject(rm.getResource(String.class,PARSER,null)).doesNotExist();
+		assertStream(rm.getStream("")).doesNotExist();
+		assertString(rm.getString("")).doesNotExist();
+		assertObject(rm.getResource(String.class,PARSER,"")).doesNotExist();
+	}
+
+
+	//------------------------------------------------------------------------------------------------------------------
+	// Utility methods
+	//------------------------------------------------------------------------------------------------------------------
+
+	private ResourceManager of(Class<?> baseClass, ResourceFinder resourceFinder, boolean useCache) {
+		return new ResourceManager(baseClass, resourceFinder, useCache);
+	}
+
+	private ResourceManager of(Class<?> baseClass) {
+		return new ResourceManager(baseClass);
+	}
+}
diff --git a/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/SimpleResourceFinder_Test.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/SimpleResourceFinder_Test.java
new file mode 100644
index 0000000..fb1cb48
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/SimpleResourceFinder_Test.java
@@ -0,0 +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 static java.util.Locale.*;
+
+import org.apache.juneau.cp.test1.*;
+import org.apache.juneau.cp.test2.*;
+import org.junit.*;
+
+@FixMethodOrder(NAME_ASCENDING)
+public class SimpleResourceFinder_Test {
+
+	@Test
+	public void a01_basic() throws Exception {
+		ResourceFinder x = SimpleResourceFinder.INSTANCE;
+
+		assertStream(x.findResource(null,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test1.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",null)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPANESE)).string().contains("Test1_ja.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",JAPAN)).string().contains("Test1_ja_JP.properties");
+		assertStream(x.findResource(Test1.class,"files/Test1.properties",CHINA)).string().contains("Test1.properties");
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test1.properties",CHINA)).doesNotExist();
+
+		assertStream(x.findResource(null,"files/Test3.properties",null)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test3.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test3.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(null,"files/Test3.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test1.class,"files/Test3.properties",CHINA)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",null)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPANESE)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",JAPAN)).doesNotExist();
+		assertStream(x.findResource(Test2.class,"files/Test3.properties",CHINA)).doesNotExist();
+	}
+}
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test1/MessageBundleTest1.java
similarity index 61%
copy from juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
copy to juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test1/MessageBundleTest1.java
index 5cb2216..cd3b40c 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test1/MessageBundleTest1.java
@@ -10,44 +10,8 @@
 // * "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.microservice.console;
+package org.apache.juneau.cp.test1;
 
-import java.io.*;
-import java.util.*;
+public class MessageBundleTest1 {
 
-import org.apache.juneau.collections.*;
-import org.apache.juneau.microservice.*;
-import org.apache.juneau.cp.*;
-
-/**
- * Implements the 'exit' console command to gracefully shut down the microservice and JVM.
- */
-public class ExitCommand extends ConsoleCommand {
-
-	private final MessageBundle mb = MessageBundle.create(ExitCommand.class, "Messages");
-
-	@Override /* ConsoleCommand */
-	public String getName() {
-		return "exit";
-	}
-
-	@Override /* ConsoleCommand */
-	public String getInfo() {
-		return mb.getString("info");
-	}
-
-	@Override /* ConsoleCommand */
-	public String getDescription() {
-		return mb.getString("description");
-	}
-
-	@Override /* ConsoleCommand */
-	public boolean execute(Scanner in, PrintWriter out, Args args) {
-		try {
-			Microservice.getInstance().stop().exit();
-		} catch (Exception e) {
-			e.printStackTrace();
-		}
-		return true;
-	}
 }
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test1/Test1.java
similarity index 61%
copy from juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
copy to juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test1/Test1.java
index 5cb2216..36757f6 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test1/Test1.java
@@ -10,44 +10,8 @@
 // * "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.microservice.console;
+package org.apache.juneau.cp.test1;
 
-import java.io.*;
-import java.util.*;
+public class Test1 {
 
-import org.apache.juneau.collections.*;
-import org.apache.juneau.microservice.*;
-import org.apache.juneau.cp.*;
-
-/**
- * Implements the 'exit' console command to gracefully shut down the microservice and JVM.
- */
-public class ExitCommand extends ConsoleCommand {
-
-	private final MessageBundle mb = MessageBundle.create(ExitCommand.class, "Messages");
-
-	@Override /* ConsoleCommand */
-	public String getName() {
-		return "exit";
-	}
-
-	@Override /* ConsoleCommand */
-	public String getInfo() {
-		return mb.getString("info");
-	}
-
-	@Override /* ConsoleCommand */
-	public String getDescription() {
-		return mb.getString("description");
-	}
-
-	@Override /* ConsoleCommand */
-	public boolean execute(Scanner in, PrintWriter out, Args args) {
-		try {
-			Microservice.getInstance().stop().exit();
-		} catch (Exception e) {
-			e.printStackTrace();
-		}
-		return true;
-	}
 }
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test2/Test2.java
similarity index 61%
copy from juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
copy to juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test2/Test2.java
index 5cb2216..e433353 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
+++ b/juneau-core/juneau-core-utest/src/test/java/org/apache/juneau/cp/test2/Test2.java
@@ -10,44 +10,10 @@
 // * "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.microservice.console;
+package org.apache.juneau.cp.test2;
 
-import java.io.*;
-import java.util.*;
+import org.apache.juneau.cp.test1.*;
 
-import org.apache.juneau.collections.*;
-import org.apache.juneau.microservice.*;
-import org.apache.juneau.cp.*;
+public class Test2 extends Test1 {
 
-/**
- * Implements the 'exit' console command to gracefully shut down the microservice and JVM.
- */
-public class ExitCommand extends ConsoleCommand {
-
-	private final MessageBundle mb = MessageBundle.create(ExitCommand.class, "Messages");
-
-	@Override /* ConsoleCommand */
-	public String getName() {
-		return "exit";
-	}
-
-	@Override /* ConsoleCommand */
-	public String getInfo() {
-		return mb.getString("info");
-	}
-
-	@Override /* ConsoleCommand */
-	public String getDescription() {
-		return mb.getString("description");
-	}
-
-	@Override /* ConsoleCommand */
-	public boolean execute(Scanner in, PrintWriter out, Args args) {
-		try {
-			Microservice.getInstance().stop().exit();
-		} catch (Exception e) {
-			e.printStackTrace();
-		}
-		return true;
-	}
 }
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
new file mode 100644
index 0000000..7c463ea
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=MessageBundleTest1.properties
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
new file mode 100644
index 0000000..927ee6d
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=MessageBundleTest1_ja.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja_JP.properties b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja_JP.properties
new file mode 100644
index 0000000..99c16a2
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/MessageBundleTest1_ja_JP.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=MessageBundleTest1_ja_JP.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1.properties b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1.properties
new file mode 100644
index 0000000..54fa0f5
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test1.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1_ja.properties b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1_ja.properties
new file mode 100644
index 0000000..fdaeca0
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1_ja.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test1_ja.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1_ja_JP.properties b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1_ja_JP.properties
new file mode 100644
index 0000000..6ee8fdb
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test1/files/Test1_ja_JP.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test1_ja_JP.properties
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
new file mode 100644
index 0000000..80affa9
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=Test2.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2_ja.properties b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2_ja.properties
new file mode 100644
index 0000000..0431d1c
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2_ja.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=Test2_ja.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2_ja_JP.properties b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2_ja_JP.properties
new file mode 100644
index 0000000..a0e86c7
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test2_ja_JP.properties
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=Test2_ja_JP.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4 b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4
new file mode 100644
index 0000000..b4721db
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test4.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4_ja b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4_ja
new file mode 100644
index 0000000..24cdc09
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4_ja
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test4_ja.properties
diff --git a/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4_ja_JP b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4_ja_JP
new file mode 100644
index 0000000..416ba5c
--- /dev/null
+++ b/juneau-core/juneau-core-utest/src/test/resources/org/apache/juneau/cp/test2/Test4_ja_JP
@@ -0,0 +1,15 @@
+# ***************************************************************************************************************************
+# * 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.                                              *
+# *                                                                                                                         *
+# ***************************************************************************************************************************
+
+file=files/Test4_ja_JP.properties
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
index 31156e9..38efbfa 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/assertions/FluentStringAssertion.java
@@ -482,6 +482,36 @@ public class FluentStringAssertion<R> extends FluentObjectAssertion<R> {
 		return returns();
 	}
 
+	/**
+	 * Asserts that the text starts with the specified string.
+	 *
+	 * @param string The string to test for.
+	 * @return The response object (for method chaining).
+	 * @throws AssertionError If assertion failed.
+	 */
+	public R startsWith(String string) {
+		exists();
+		assertNotNull("string", string);
+		if (! text.startsWith(string))
+			throw error("Text did not start with expected string.\n\tString=[{0}]\n\tText=[{1}]", fix(string), fix(text));
+		return returns();
+	}
+
+	/**
+	 * Asserts that the text ends with the specified string.
+	 *
+	 * @param string The string to test for.
+	 * @return The response object (for method chaining).
+	 * @throws AssertionError If assertion failed.
+	 */
+	public R endsWith(String string) {
+		exists();
+		assertNotNull("string", string);
+		if (! text.endsWith(string))
+			throw error("Text did not end with expected string.\n\tString=[{0}]\n\tText=[{1}]", fix(string), fix(text));
+		return returns();
+	}
+
 	//------------------------------------------------------------------------------------------------------------------
 	// Utility methods
 	//------------------------------------------------------------------------------------------------------------------
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicResourceFinder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicResourceFinder.java
index e21be54..9834581 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicResourceFinder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/BasicResourceFinder.java
@@ -13,6 +13,7 @@
 package org.apache.juneau.cp;
 
 import static org.apache.juneau.internal.FileUtils.*;
+import static org.apache.juneau.internal.StringUtils.*;
 
 import java.io.*;
 import java.util.*;
@@ -74,6 +75,8 @@ public class BasicResourceFinder implements ResourceFinder {
 	@SuppressWarnings("resource")
 	@Override /* ClasspathResourceFinder */
 	public InputStream findResource(Class<?> baseClass, String name, Locale locale) throws IOException {
+		if (isInvalidName(name))
+			return null;
 		InputStream is = null;
 		if (includeFileSystem)
 			is = findFileSystemResource(name, locale);
@@ -125,12 +128,10 @@ public class BasicResourceFinder implements ResourceFinder {
 	 * @throws IOException Thrown by underlying stream.
 	 */
 	protected InputStream findFileSystemResource(String name, Locale locale) throws IOException {
-		if (name.indexOf("..") == -1) {
-			for (String n2 : getCandidateFileNames(name, locale)) {
-				File f = new File(n2);
-				if (f.exists() && f.canRead() && ! f.isAbsolute()) {
-					return new FileInputStream(f);
-				}
+		for (String n2 : getCandidateFileNames(name, locale)) {
+			File f = new File(n2);
+			if (f.exists() && f.isFile() && f.canRead() && ! f.isAbsolute()) {
+				return new FileInputStream(f);
 			}
 		}
 		return null;
@@ -209,4 +210,8 @@ public class BasicResourceFinder implements ResourceFinder {
 			return ROOT_LOCALE;
 		return RB_CONTROL.getCandidateLocales("", locale);
 	}
+
+	private static boolean isInvalidName(String name) {
+		return isEmpty(name) || name.contains("..");
+	}
 }
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessageBundle.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessageBundle.java
index f755185..c2ae09d 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessageBundle.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/MessageBundle.java
@@ -88,57 +88,22 @@ public class MessageBundle extends ResourceBundle {
 	/**
 	 * Constructor.
 	 *
-	 * <p>
-	 * When this method is used, the bundle path is determined by searching for the resource bundle
-	 * in 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 forClass The class
 	 * @return A new message bundle belonging to the class.
 	 */
-	public static final MessageBundle create(Class<?> forClass) {
-		return create(forClass, findBundlePath(forClass));
+	public static final MessageBundle of(Class<?> forClass) {
+		return new MessageBundle(forClass, null, null);
 	}
 
 	/**
 	 * Constructor.
 	 *
-	 * <p>
-	 * A shortcut for calling <c>new MessageBundle(forClass, bundlePath)</c>.
-	 *
 	 * @param forClass The class
 	 * @param bundlePath The location of the resource bundle.
 	 * @return A new message bundle belonging to the class.
 	 */
-	public static final MessageBundle create(Class<?> forClass, String bundlePath) {
-		return new MessageBundle(forClass, bundlePath);
-	}
-
-	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;
-		return 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;
-		}
+	public static final MessageBundle of(Class<?> forClass, String bundlePath) {
+		return new MessageBundle(forClass, bundlePath, null);
 	}
 
 	/**
@@ -147,21 +112,32 @@ public class MessageBundle extends ResourceBundle {
 	 * @param forClass The class using this resource bundle.
 	 * @param bundlePath
 	 * 	The path of the resource bundle to wrap.
-	 * 	This can be an absolute path (e.g. <js>"com.foo.MyMessages"</js>) or a path relative to the package of the
+	 * 	<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 MessageBundle(Class<?> forClass, String bundlePath) {
-		this(forClass, bundlePath, Locale.getDefault());
-	}
-
-	private MessageBundle(Class<?> forClass, String bundlePath, Locale locale) {
+	public MessageBundle(Class<?> forClass, String bundlePath, Locale locale) throws MissingResourceException {
 		this.forClass = forClass;
 		this.className = forClass.getSimpleName();
+
 		if (bundlePath == null)
-			throw new RuntimeException("Bundle path was 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;
@@ -214,7 +190,7 @@ public class MessageBundle extends ResourceBundle {
 	 */
 	public MessageBundle addSearchPath(Class<?> forClass, String bundlePath) {
 		assertSameThread(creationThreadId, "This method can only be called from the same thread that created the object.");
-		MessageBundle srb = new MessageBundle(forClass, bundlePath);
+		MessageBundle srb = new MessageBundle(forClass, bundlePath, null);
 		if (srb.rb != null) {
 			allKeys.addAll(srb.keySet());
 			searchBundles.add(srb);
@@ -372,10 +348,15 @@ public class MessageBundle extends ResourceBundle {
 	/**
 	 * Returns the resource bundle for the specified locale.
 	 *
-	 * @param locale The client 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 MessageBundle getBundle(Locale locale) {
+		if (locale == null)
+			locale = Locale.getDefault();
+
 		MessageBundle mb = localizedBundles.get(locale);
 		if (mb != null)
 			return mb;
@@ -391,4 +372,27 @@ public class MessageBundle extends ResourceBundle {
 		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
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceFinder.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceFinder.java
index 2749250..a41f3a7 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceFinder.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceFinder.java
@@ -40,7 +40,7 @@ public interface ResourceFinder {
 	public static final class Null implements ResourceFinder {
 		@Override
 		public InputStream findResource(Class<?> baseClass, String name, Locale locale) throws IOException {
-			throw new NoSuchMethodError();
+			return null;
 		}
 	}
 
diff --git a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceManager.java b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceManager.java
index 6506cd3..e3527bc 100644
--- a/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceManager.java
+++ b/juneau-core/juneau-marshall/src/main/java/org/apache/juneau/cp/ResourceManager.java
@@ -64,7 +64,7 @@ public final class ResourceManager {
 	 * @param baseClass The default class to use for retrieving resources from the classpath.
 	 */
 	public ResourceManager(Class<?> baseClass) {
-		this(baseClass, new BasicResourceFinder(), false);
+		this(baseClass, BasicResourceFinder.INSTANCE, false);
 	}
 
 	/**
@@ -87,24 +87,9 @@ public final class ResourceManager {
 	 * @throws IOException Thrown by underlying stream.
 	 */
 	public InputStream getStream(String name, Locale locale) throws IOException {
-		return getStream(baseClass, name, locale);
-	}
-
-	/**
-	 * Finds the resource with the given name for the specified locale and returns it as an input stream.
-	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the base class passed in through the constructor of this class.
-	 * @param name Name of the desired resource.
-	 * @param locale The locale.  Can be <jk>null</jk>.
-	 * @return An input stream to the object, or <jk>null</jk> if the resource could not be found.
-	 * @throws IOException Thrown by underlying stream.
-	 */
-	public InputStream getStream(Class<?> baseClass, String name, Locale locale) throws IOException {
 
-		if (baseClass == null)
-			baseClass = this.baseClass;
+		if (isEmpty(name))
+			return null;
 
 		if (! useCache)
 			return resourceFinder.findResource(baseClass, name, locale);
@@ -131,21 +116,7 @@ public final class ResourceManager {
 	 * @throws IOException Thrown by underlying stream.
 	 */
 	public String getString(String name) throws IOException {
-		return getString(baseClass, name, null);
-	}
-
-	/**
-	 * Finds the resource with the given name and converts it to a simple string.
-	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the base class passed in through the constructor of this class.
-	 * @param name Name of the desired resource.
-	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
-	 * @throws IOException Thrown by underlying stream.
-	 */
-	public String getString(Class<?> baseClass, String name) throws IOException {
-		return getString(baseClass, name, null);
+		return getString(name, null);
 	}
 
 	/**
@@ -157,24 +128,9 @@ public final class ResourceManager {
 	 * @throws IOException Thrown by underlying stream.
 	 */
 	public String getString(String name, Locale locale) throws IOException {
-		return getString(baseClass, name, locale);
-	}
 
-	/**
-	 * Finds the resource with the given name and converts it to a simple string.
-	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the base class passed in through the constructor of this class.
-	 * @param name Name of the desired resource.
-	 * @param locale The locale.  Can be <jk>null</jk>.
-	 * @return The resource converted to a string, or <jk>null</jk> if the resource could not be found.
-	 * @throws IOException Thrown by underlying stream.
-	 */
-	public String getString(Class<?> baseClass, String name, Locale locale) throws IOException {
-
-		if (baseClass == null)
-			baseClass = this.baseClass;
+		if (isEmpty(name))
+			return null;
 
 		if (! useCache) {
 			try (InputStream is = resourceFinder.findResource(baseClass, name, locale)) {
@@ -201,24 +157,17 @@ public final class ResourceManager {
 	 * @param c The class type of the POJO to create.
 	 * @param parser The parser to use to parse the stream.
 	 * @param name The resource name (e.g. "htdocs/styles.css").
-	 * @param locale
-	 * 	Optional locale.
-	 * 	<br>If <jk>null</jk>, won't look for localized file names.
 	 * @return The parsed resource, or <jk>null</jk> if the resource could not be found.
 	 * @throws IOException Thrown by underlying stream.
 	 * @throws ParseException If stream could not be parsed using the specified parser.
 	 */
-	public <T> T getResource(Class<T> c, Parser parser, String name, Locale locale) throws IOException, ParseException {
-		return getResource(null, c, parser, name, locale);
+	public <T> T getResource(Class<T> c, Parser parser, String name) throws IOException, ParseException {
+		return getResource(c, parser, name, null);
 	}
 
 	/**
-	 * Same as {@link #getResource(Class, Parser, String, Locale)}, except overrides the class used
-	 * for retrieving the classpath resource.
+	 * Reads the input stream and parses it into a POJO using the specified parser.
 	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the REST resource class.
 	 * @param c The class type of the POJO to create.
 	 * @param parser The parser to use to parse the stream.
 	 * @param name The resource name (e.g. "htdocs/styles.css").
@@ -229,8 +178,9 @@ public final class ResourceManager {
 	 * @throws IOException Thrown by underlying stream.
 	 * @throws ParseException If stream could not be parsed using the specified parser.
 	 */
-	public <T> T getResource(Class<?> baseClass, Class<T> c, Parser parser, String name, Locale locale) throws IOException, ParseException {
-		InputStream is = getStream(baseClass, name, locale);
+	public <T> T getResource(Class<T> c, Parser parser, String name, Locale locale) throws IOException, ParseException {
+
+		InputStream is = getStream(name, locale);
 		if (is == null)
 			return null;
 		try (Closeable in = parser.isReaderParser() ? new InputStreamReader(is, UTF8) : is) {
@@ -254,7 +204,7 @@ public final class ResourceManager {
 
 		@Override
 		public boolean equals(Object o) {
-			return (o instanceof ResourceKey) && eq(this, (ResourceKey)o, (x,y)->eq(x.name, y.name) && eq(x.locale, y.locale));
+			return eq(this, (ResourceKey)o, (x,y)->eq(x.name, y.name) && eq(x.locale, y.locale));
 		}
 	}
 }
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
index 4724eeb..b54aea9 100755
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
+++ b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/Microservice.java
@@ -111,7 +111,7 @@ public class Microservice implements ConfigEventListener {
 	}
 
 
-	final MessageBundle messages = MessageBundle.create(Microservice.class);
+	final MessageBundle messages = MessageBundle.of(Microservice.class);
 
 	//-----------------------------------------------------------------------------------------------------------------
 	// Properties set in constructor
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ConfigCommand.java b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ConfigCommand.java
index 33232e6..c5d2a47 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ConfigCommand.java
+++ b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ConfigCommand.java
@@ -25,7 +25,7 @@ import org.apache.juneau.cp.*;
  */
 public class ConfigCommand extends ConsoleCommand {
 
-	private final MessageBundle mb = MessageBundle.create(ConfigCommand.class, "Messages");
+	private final MessageBundle mb = MessageBundle.of(ConfigCommand.class, "Messages");
 
 	@Override /* ConsoleCommand */
 	public String getName() {
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
index 5cb2216..03914c9 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
+++ b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/ExitCommand.java
@@ -24,7 +24,7 @@ import org.apache.juneau.cp.*;
  */
 public class ExitCommand extends ConsoleCommand {
 
-	private final MessageBundle mb = MessageBundle.create(ExitCommand.class, "Messages");
+	private final MessageBundle mb = MessageBundle.of(ExitCommand.class, "Messages");
 
 	@Override /* ConsoleCommand */
 	public String getName() {
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/HelpCommand.java b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/HelpCommand.java
index 34610b6..f4f7318 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/HelpCommand.java
+++ b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/HelpCommand.java
@@ -24,7 +24,7 @@ import org.apache.juneau.cp.*;
  */
 public class HelpCommand extends ConsoleCommand {
 
-	private final MessageBundle mb = MessageBundle.create(HelpCommand.class, "Messages");
+	private final MessageBundle mb = MessageBundle.of(HelpCommand.class, "Messages");
 
 	@Override /* ConsoleCommand */
 	public String getName() {
diff --git a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/RestartCommand.java b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/RestartCommand.java
index 64108f0..4a4bc4e 100644
--- a/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/RestartCommand.java
+++ b/juneau-microservice/juneau-microservice-core/src/main/java/org/apache/juneau/microservice/console/RestartCommand.java
@@ -24,7 +24,7 @@ import org.apache.juneau.cp.*;
  */
 public class RestartCommand extends ConsoleCommand {
 
-	private final MessageBundle mb = MessageBundle.create(RestartCommand.class, "Messages");
+	private final MessageBundle mb = MessageBundle.of(RestartCommand.class, "Messages");
 
 	@Override /* ConsoleCommand */
 	public String getName() {
diff --git a/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/JettyMicroservice.java b/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/JettyMicroservice.java
index 14f16a5..538f020 100644
--- a/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/JettyMicroservice.java
+++ b/juneau-microservice/juneau-microservice-jetty/src/main/java/org/apache/juneau/microservice/jetty/JettyMicroservice.java
@@ -88,7 +88,7 @@ public class JettyMicroservice extends Microservice {
 			.join();
 	}
 
-	final MessageBundle messages = MessageBundle.create(JettyMicroservice.class);
+	final MessageBundle messages = MessageBundle.of(JettyMicroservice.class);
 
 	//-----------------------------------------------------------------------------------------------------------------
 	// Properties set in constructor
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 62f727f..386e4f3 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
@@ -801,11 +801,8 @@ public final class RestContext extends BeanContext {
 	 * 	<li class='jc'>{@link RestContext}
 	 * 	<ul>
 	 * 		<li class='jm'>{@link #getClasspathResource(String,Locale) getClasspathResource(String,Locale)}
-	 * 		<li class='jm'>{@link #getClasspathResource(Class,String,Locale) getClasspathResource(Class,String,Locale)}
 	 * 		<li class='jm'>{@link #getClasspathResource(Class,MediaType,String,Locale) getClasspathResource(Class,MediaType,String,Locale)}
-	 * 		<li class='jm'>{@link #getClasspathResource(Class,Class,MediaType,String,Locale) getClasspathResource(Class,Class,MediaType,String,Locale)}
 	 * 		<li class='jm'>{@link #getClasspathResourceAsString(String,Locale) getClasspathResourceAsString(String,Locale)}
-	 * 		<li class='jm'>{@link #getClasspathResourceAsString(Class,String,Locale) getClasspathResourceAsString(Class,String,Locale)}
 	 * 		<li class='jm'>{@link #resolveStaticFile(String) resolveStaticFile(String)}
 	 * 	</ul>
 	 * 	<li class='jc'>{@link RestRequest}
@@ -3855,9 +3852,9 @@ public final class RestContext extends BeanContext {
 
 			MessageBundleLocation[] mbl = getInstanceArrayProperty(REST_messages, MessageBundleLocation.class, new MessageBundleLocation[0]);
 			if (mbl.length == 0)
-				msgs = new MessageBundle(rci.inner(), "");
+				msgs = new MessageBundle(rci.inner(), "", null);
 			else {
-				msgs = new MessageBundle(mbl[0] != null ? mbl[0].baseClass : rci.inner(), mbl[0].bundlePath);
+				msgs = new MessageBundle(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);
 			}
@@ -4335,38 +4332,6 @@ public final class RestContext extends BeanContext {
 	}
 
 	/**
-	 * Same as {@link #getClasspathResource(String, Locale)}, but allows you to override the class used for looking
-	 * up the classpath resource.
-	 *
-	 * <h5 class='section'>Example:</h5>
-	 * <p class='bcode w800'>
-	 * 	<jc>// A rest method that (unsafely!) returns the contents of a localized file </jc>
-	 *	<jc>// from the classpath.</jc>
-	 * 	<ja>@RestMethod</ja>(path=<js>"/foo"</js>)
-	 * 	<jk>public</jk> Object myMethod(RestRequest req, <ja>@Query</ja>(<js>"file"</js>) String file) {
-	 * 		<jk>return</jk> getContext().getClasspathResource(SomeOtherClass.<jk>class</jk>, file, req.getLocale());
-	 * 	}
-	 * </p>
-	 *
-	 * <ul class='seealso'>
-	 * 	<li class='jf'>{@link #REST_classpathResourceFinder}
-	 * </ul>
-	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the REST resource class.
-	 * @param name The resource name.
-	 * @param locale
-	 * 	Optional locale.
-	 * 	<br>If <jk>null</jk>, won't look for localized file names.
-	 * @return An input stream of the resource, or <jk>null</jk> if the resource could not be found.
-	 * @throws IOException Thrown by underlying stream.
-	 */
-	public InputStream getClasspathResource(Class<?> baseClass, String name, Locale locale) throws IOException {
-		return staticResourceManager.getStream(baseClass, name, locale);
-	}
-
-	/**
 	 * Reads the input stream from {@link #getClasspathResource(String, Locale)} into a String.
 	 *
 	 * <h5 class='section'>Example:</h5>
@@ -4394,37 +4359,6 @@ public final class RestContext extends BeanContext {
 		return staticResourceManager.getString(name, locale);
 	}
 
-	/**
-	 * Same as {@link #getClasspathResourceAsString(String, Locale)}, but allows you to override the class used for looking
-	 * up the classpath resource.
-	 *
-	 * <h5 class='section'>Example:</h5>
-	 * <p class='bcode w800'>
-	 * 	<jc>// A rest method that (unsafely!) returns the contents of a localized file </jc>
-	 *	<jc>// from the classpath.</jc>
-	 * 	<ja>@RestMethod</ja>(path=<js>"/foo"</js>)
-	 * 	<jk>public</jk> String myMethod(RestRequest req, <ja>@Query</ja>(<js>"file"</js>) String file) {
-	 * 		<jk>return</jk> getContext().getClasspathResourceAsString(SomeOtherClass.<jk>class</jk>, file, req.getLocale());
-	 * 	}
-	 * </p>
-	 *
-	 * <ul class='seealso'>
-	 * 	<li class='jf'>{@link #REST_classpathResourceFinder}
-	 * </ul>
-	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the REST resource class.
-	 * @param name The resource name.
-	 * @param locale
-	 * 	Optional locale.
-	 * 	<br>If <jk>null</jk>, won't look for localized file names.
-	 * @return The contents of the stream as a string, or <jk>null</jk> if the resource could not be found.
-	 * @throws IOException If resource could not be found.
-	 */
-	public String getClasspathResourceAsString(Class<?> baseClass, String name, Locale locale) throws IOException {
-		return staticResourceManager.getString(baseClass, name, locale);
-	}
 
 	/**
 	 * Reads the input stream from {@link #getClasspathResource(String, Locale)} and parses it into a POJO using the parser
@@ -4458,42 +4392,7 @@ public final class RestContext extends BeanContext {
 	 * @throws ServletException If the media type was unknown or the input could not be parsed into a POJO.
 	 */
 	public <T> T getClasspathResource(Class<T> c, MediaType mediaType, String name, Locale locale) throws IOException, ServletException {
-		return getClasspathResource(null, c, mediaType, name, locale);
-	}
-
-	/**
-	 * Same as {@link #getClasspathResource(Class, MediaType, String, Locale)}, except overrides the class used
-	 * for retrieving the classpath resource.
-	 *
-	 * <ul class='seealso'>
-	 * 	<li class='jf'>{@link #REST_classpathResourceFinder}
-	 * </ul>
-	 *
-	 * <h5 class='section'>Example:</h5>
-	 * <p class='bcode w800'>
-	 * 	<jc>// A rest method that (unsafely!) returns the contents of a localized file </jc>
-	 *	<jc>// from the classpath parsed as an array of beans.</jc>
-	 * 	<ja>@RestMethod</ja>(path=<js>"/foo"</js>)
-	 * 	<jk>public</jk> MyBean[] myMethod(RestRequest req, <ja>@Query</ja>(<js>"file"</js>) String file) {
-	 * 		<jk>return</jk> getContext().getClasspathResource(SomeOtherClass.<jk>class</jk>, MyBean[].<jk>class</jk>, <jsf>JSON</jsf>, file, req.getLocale());
-	 * 	}
-	 * </p>
-	 *
-	 * @param baseClass
-	 * 	Overrides the default class to use for retrieving the classpath resource.
-	 * 	<br>If <jk>null</jk>, uses the REST resource class.
-	 * @param c The class type of the POJO to create.
-	 * @param mediaType The media type of the data in the stream (e.g. <js>"text/json"</js>)
-	 * @param name The resource name (e.g. "htdocs/styles.css").
-	 * @param locale
-	 * 	Optional locale.
-	 * 	<br>If <jk>null</jk>, won't look for localized file names.
-	 * @return The parsed resource, or <jk>null</jk> if the resource could not be found.
-	 * @throws IOException Thrown by underlying stream.
-	 * @throws ServletException If the media type was unknown or the input could not be parsed into a POJO.
-	 */
-	public <T> T getClasspathResource(Class<?> baseClass, Class<T> c, MediaType mediaType, String name, Locale locale) throws IOException, ServletException {
-		InputStream is = getClasspathResource(baseClass, name, locale);
+		InputStream is = getClasspathResource(name, locale);
 		if (is == null)
 			return null;
 		try {
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServlet.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServlet.java
index 1270102..0e5074a 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServlet.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/RestServlet.java
@@ -101,7 +101,7 @@ public abstract class RestServlet extends HttpServlet implements RestCallHandler
 		callHandler = new BasicRestCallHandler(context);
 		infoProvider = new BasicRestInfoProvider(context);
 		callLogger = new BasicRestCallLogger(context);
-		resourceFinder = new BasicResourceFinder();
+		resourceFinder = new RecursiveResourceFinder();
 		context.postInit();
 	}
 
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
index 82bfa40..3987b83 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/StaticFiles.java
@@ -24,7 +24,6 @@ import org.apache.juneau.internal.*;
  * The static file resource resolver for a single {@link StaticFileMapping}.
  */
 class StaticFiles {
-	private final Class<?> resourceClass;
 	private final String path, location;
 	private final Map<String,Object> responseHeaders;
 
@@ -32,7 +31,6 @@ class StaticFiles {
 	private final MimetypesFileTypeMap mimetypesFileTypeMap;
 
 	StaticFiles(StaticFileMapping sfm, ResourceManager staticResourceManager, MimetypesFileTypeMap mimetypesFileTypeMap, Map<String,Object> staticFileResponseHeaders) {
-		this.resourceClass = sfm.resourceClass;
 		this.path = sfm.path;
 		this.location = sfm.location;
 		this.responseHeaders = sfm.responseHeaders != null ? sfm.responseHeaders : staticFileResponseHeaders;
@@ -49,7 +47,7 @@ class StaticFiles {
 			String remainder = (p.equals(path) ? "" : p.substring(path.length()));
 			if (remainder.isEmpty() || remainder.startsWith("/")) {
 				String p2 = location + remainder;
-				try (InputStream is = staticResourceManager.getStream(resourceClass, p2, null)) {
+				try (InputStream is = staticResourceManager.getStream(p2, null)) {
 					if (is != null) {
 						int i = p2.lastIndexOf('/');
 						String name = (i == -1 ? p2 : p2.substring(i+1));
diff --git a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
index cf55519..130ace6 100644
--- a/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
+++ b/juneau-rest/juneau-rest-server/src/main/java/org/apache/juneau/rest/reshandlers/DefaultHandler.java
@@ -216,7 +216,7 @@ public class DefaultHandler implements ResponseHandler {
 		}
 
 		// Non-existent Accept or plain/text can just be serialized as-is.
-		if (o != null && (isEmpty(accept) || accept.startsWith("text/plain") || accept.equals("*/*"))) {
+		if (o != null && (isEmpty(accept) || accept.startsWith("text/plain") || accept.contains("*/*"))) {
 			String out = null;
 			if (isEmpty(res.getContentType()))
 				res.setContentType("text/plain");