diff --git a/src/main/java/emissary/util/io/GreedyResourceReader.java b/src/main/java/emissary/util/io/GreedyResourceReader.java
new file mode 100644
index 0000000000..309ebe48f5
--- /dev/null
+++ b/src/main/java/emissary/util/io/GreedyResourceReader.java
@@ -0,0 +1,134 @@
+package emissary.util.io;
+
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+import static org.apache.commons.lang3.StringUtils.substringBeforeLast;
+
+/**
+ * A {@link ResourceReader} extended to find data files based purely on their location, ignoring the default naming
+ * convention used by the {@link ResourceReader#findDataResourcesFor(Class)} method.
+ *
+ * This class is primarily used to find payload files for Identification tests that can benefit from more
+ * content-representative file names.
+ *
+ */
+public class GreedyResourceReader extends ResourceReader {
+ private static final Logger logger = LoggerFactory.getLogger(GreedyResourceReader.class);
+
+ public static final String PAYLOADS_FOLDER = "payloads";
+ public static final String ANSWERS_FOLDER = "answers";
+
+ public static final Predicate IS_XML_FILE = filename -> "xml".equals(FilenameUtils.getExtension(filename));
+
+ /**
+ * Returns the project-relative paths of test files for the specified test class. The files should be underneath a
+ * "payloads" subdirectory of the test class directory. Additional subdirectories can exist within the payloads
+ * directory itself, and any files found within will be included in the results.
+ *
+ * @param c test class for which to perform the search
+ * @return list of project-relative test file paths
+ */
+ public List findAllPayloadFilesFor(Class> c) {
+ URL url = this.which(c);
+ if (url == null || !url.getProtocol().equals("file")) {
+ return Collections.emptyList();
+ }
+
+ List results = new ArrayList<>(findAllFilesUnderClassNameSubDir(c, url, PAYLOADS_FOLDER));
+ Collections.sort(results);
+ return results;
+ }
+
+ /**
+ * Returns the project-relative paths of test files for the specified test class. The files should be underneath a
+ * "payloads" subdirectory of the test class directory. Additional subdirectories can exist within the payloads
+ * directory itself, and any files found within will be included in the results.
+ *
+ * @param c test class for which to perform the search
+ * @return list of project-relative test file paths
+ */
+ public List findAllAnswerFilesFor(Class> c) {
+ URL url = this.which(c);
+ if (url == null || !url.getProtocol().equals("file")) {
+ return Collections.emptyList();
+ }
+
+ List results = findAllFilesUnderClassNameSubDir(c, url, ANSWERS_FOLDER, IS_XML_FILE);
+ Collections.sort(results);
+ return results;
+ }
+
+ /**
+ * Finds all files beneath the specified subdirectory of the test class resource folder
+ *
+ * @param c test class for which the resource files exist
+ * @param url location from which the classLoader looded the test class
+ * @param subDirName subdirectory that contains the files
+ * @return List of test resource file paths
+ */
+ private List findAllFilesUnderClassNameSubDir(Class> c, URL url, final String subDirName) {
+ return findAllFilesUnderClassNameSubDir(c, url, subDirName, StringUtils::isNotBlank);
+ }
+
+ /**
+ * Finds the files beneath a given test class resource folder, filtered by a provided {@link Predicate}
+ *
+ * @param c test class for which the resource files exist
+ * @param url location from which the classLoader loaded the test class
+ * @param subDirName subdirectory that contains the files
+ * @param fileFilter Predicate used to filter the list of discovered files
+ * @return List of test resource file paths
+ */
+ private List findAllFilesUnderClassNameSubDir(Class> c, URL url, final String subDirName, final Predicate fileFilter) {
+ String classNameInPathFormat = getResourceName(c);
+ Path subDir = Path.of(getFullPathOfTestClassResourceFolder(url, c), subDirName);
+ File testClassDir = subDir.toFile();
+ if (testClassDir.exists() && testClassDir.isDirectory()) {
+ try (Stream theList = Files.walk(testClassDir.toPath())) {
+ return theList.filter(Files::isRegularFile)
+ .map(testClassDir.toPath()::relativize)
+ .map(filePath -> classNameInPathFormat + "/" + subDirName + "/" + filePath)
+ .filter(fileFilter::test)
+ .collect(Collectors.toList());
+
+ } catch (IOException e) {
+ logger.debug("Failed to retrieve files for class {}", c.getName(), e);
+ }
+ }
+
+ return Collections.emptyList();
+ }
+
+ /**
+ * Gets the absolute path of a test class runtime resource folder
+ *
+ * @param url URL from which the ClassLoader loaded the test class
+ * @param c test class
+ * @return test class folder path
+ */
+ protected String getFullPathOfTestClassResourceFolder(URL url, Class> c) {
+ String classNameInPathFormat = getResourceName(c);
+ if (url.getPath().contains(CLASS_SUFFIX)) {
+ // return the URL minus the ".class" suffix
+ return StringUtils.substringBeforeLast(url.getPath(), CLASS_SUFFIX);
+ }
+
+ return StringUtils.join(url.getPath(), "/", classNameInPathFormat);
+ }
+}
diff --git a/src/test/java/emissary/util/io/GreedyResourceReaderTest.java b/src/test/java/emissary/util/io/GreedyResourceReaderTest.java
new file mode 100644
index 0000000000..f4139895a1
--- /dev/null
+++ b/src/test/java/emissary/util/io/GreedyResourceReaderTest.java
@@ -0,0 +1,86 @@
+package emissary.util.io;
+
+import emissary.test.core.junit5.UnitTest;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+public class GreedyResourceReaderTest extends UnitTest {
+
+
+ @Test
+ void testPayloadFileLocation() {
+
+ // files in the "payloads" subdirectory should be found as resources
+ List testFileNames = Arrays.asList("payloads/File1.txt", "payloads/subdir/sample.md");
+ // Non-payload.txt is in the test class directory, but not beneath its "payloads" subdirectory
+ final String NON_PAYLOAD = "Non-payload.txt";
+
+ GreedyResourceReader grr = new GreedyResourceReader();
+ String testClassDir = grr.getResourceName(this.getClass());
+
+ List resources = grr.findAllPayloadFilesFor(this.getClass());
+ assertNotNull(resources, "Resources must not be null");
+ assertEquals(testFileNames.size(), resources.size(), "All data resources not found");
+
+ testFileNames.stream()
+ .map(t -> Path.of(testClassDir, t))
+ .forEach(p -> assertTrue(resources.contains(p.toString())));
+
+ assertFalse(resources.contains(Path.of(testClassDir, NON_PAYLOAD).toString()));
+
+ for (String rez : resources) {
+ try (InputStream is = grr.getResourceAsStream(rez)) {
+ assertNotNull(is, "Failed to open " + rez);
+ } catch (IOException e) {
+ fail("Failed to open " + rez, e);
+ }
+ }
+ }
+
+
+ @Test
+ void testAnswerFileLocation() {
+
+ // files in the "payloads" subdirectory should be found as resources
+ List testAnswerFileNames = Arrays.asList("answers/File1.txt.xml", "answers/subdir/sample.md.xml");
+
+ // files that should NOT be detected as "answer" files based on their locations
+ List misplacedAnswerFileNames = Arrays.asList("Non-answer.xml", "answers/README");
+
+ GreedyResourceReader grr = new GreedyResourceReader();
+ String testClassDir = grr.getResourceName(this.getClass());
+
+ List answerFiles = grr.findAllAnswerFilesFor(this.getClass());
+ assertNotNull(answerFiles, "Resources must not be null");
+ assertEquals(testAnswerFileNames.size(), answerFiles.size(), "Not all answer files not found");
+
+ testAnswerFileNames.stream()
+ .map(t -> Path.of(testClassDir, t))
+ .forEach(p -> assertTrue(answerFiles.contains(p.toString())));
+
+ misplacedAnswerFileNames.stream()
+ .map(t -> Path.of(testClassDir, t))
+ .forEach(p -> assertFalse(answerFiles.contains(p.toString())));
+
+
+ for (String file : answerFiles) {
+ try (InputStream is = grr.getResourceAsStream(file)) {
+ assertNotNull(is, "Failed to open " + file);
+ } catch (IOException e) {
+ fail("Failed to open " + file, e);
+ }
+ }
+ }
+}
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/Non-answer.xml b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/Non-answer.xml
new file mode 100644
index 0000000000..8967709a37
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/Non-answer.xml
@@ -0,0 +1 @@
+file is in wrong directory
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/Non-payload.txt b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/Non-payload.txt
new file mode 100644
index 0000000000..999b5844c8
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/Non-payload.txt
@@ -0,0 +1 @@
+ignore me
\ No newline at end of file
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/File1.txt.xml b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/File1.txt.xml
new file mode 100644
index 0000000000..a0e4df0f35
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/File1.txt.xml
@@ -0,0 +1 @@
+ignore the file content
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/README b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/README
new file mode 100644
index 0000000000..aa108bece2
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/README
@@ -0,0 +1 @@
+not an answer file
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/subdir/sample.md.xml b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/subdir/sample.md.xml
new file mode 100644
index 0000000000..a0e4df0f35
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/answers/subdir/sample.md.xml
@@ -0,0 +1 @@
+ignore the file content
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/payloads/File1.txt b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/payloads/File1.txt
new file mode 100644
index 0000000000..51816bdbac
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/payloads/File1.txt
@@ -0,0 +1 @@
+Ignore the content
\ No newline at end of file
diff --git a/src/test/resources/emissary/util/io/GreedyResourceReaderTest/payloads/subdir/sample.md b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/payloads/subdir/sample.md
new file mode 100644
index 0000000000..e44d594c78
--- /dev/null
+++ b/src/test/resources/emissary/util/io/GreedyResourceReaderTest/payloads/subdir/sample.md
@@ -0,0 +1 @@
+## this is a sample file
\ No newline at end of file