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