Skip to main content

Copy files from Classpath to Expansion

During the expansion process, it's a common requirement to include static assets or configuration files from your expansion-resource into the generated output (the expansion directory). This guide details how to achieve this using an ExpansionStep, leveraging Java's ClassLoader to access files packaged with your expansion-resource.

Context: When and Why You Need This

Your plugin might need to copy files for various reasons:

  • Static Assets: HTML templates, CSS files, JavaScript files for a web application.
  • Configuration Files: Default configuration for generated code or external tools.

An ExpansionStep is the ideal place for this logic, as it operates within the controlled environment of the expansion process.

1. Identifying the Expansion Directory

The ExpansionStep needs to know where to copy the files. The expansion directory is available through the ExpansionContext.

When copying static files, the best way is to have the step run on the Application (or whichever program element is applicable). When you do you can expect the expansion-context to be an instance of ProgramExpansionContext.

/**
* An expansion step to copy static files from the plugin's classpath resources.
*/
public class CopyMyFilesStep implements AdditionalExpansionStepImplementation<ProgramExpansionContext<ApplicationComposite>> {

@Override
public void run(ProgramExpansionContext<ApplicationComposite> expansionContext) {
// Retrieve the target directory for the expanded files.
// This is the root output directory for the current program/application.
File expansionDirectory = expansionContext.getExpansionDirectory();

// ... proceed to copy files ...
}
}

By using ProgramExpansionContext<ApplicationComposite>, you access the getExpansionDirectory() method.

2. Copying Files from Classpath Resources

Files under src/main/resources are bundled with the expansion-resource jar and become available on the classpath when it is used to expand. The Java ClassLoader is the standard mechanism to access these resources at runtime.

The most robust way to obtain the ClassLoader within an expansion step is Thread.currentThread().getContextClassLoader(). This ensures you get the ClassLoader that was used to load your expansion-resource and its resources.

If you add a file to the resources directory, you can retrieve that file with the classloader during expansion. The most stable way to get the classloader is by using Thread.currentThread().getContextClassLoader().

public class CopyMyFilesStep implements AdditionalExpansionStepImplementation<ProgramExpansionContext<ApplicationComposite>> {

private static final String RESOURCE_PATH = "path/to/my-file.ext"; // Path inside resources/
private static final String TARGET_FILE = "output/resources/my-file.ext"; // Relative path within expansion directory

@Override
public void run(ProgramExpansionContext<ApplicationComposite> expansionContext) {
File expansionDirectory = expansionContext.getExpansionDirectory();

// Define the full target path where the file will be copied within the expansion directory.
// Example: <expansion_root>/output/resources/my-file.ext
Path targetFile = expansionDirectory.toPath().resolve(TARGET_FILE);

// Get the ClassLoader to access bundled resources.
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

// Retrieve the file content as an InputStream. The path is relative to the root of the classpath (resources/).
try (InputStream inputStream = contextClassLoader.getResourceAsStream(RESOURCE_PATH)) {
if (inputStream == null) {
// Log an error or throw an exception if the resource is not found.
throw new ExpansionException("Resource not found on classpath: " + RESOURCE_PATH);
}

// Ensure that all parent directories leading to the target file exist.
Files.createDirectories(targetFile.getParent());

// Copy the InputStream content to the target file, replacing it if it already exists.
Files.copy(inputStream, targetFile, StandardCopyOption.REPLACE_EXISTING);

} catch (IOException e) {
// Wrap IOExceptions in an ExpansionException to provide context about the failure.
throw new ExpansionException("Failed to copy resource '" + RESOURCE_PATH + "' to '" + targetFile + "'", e);
}
}
}

Important Considerations:

  • Resource Path: The path passed to contextClassLoader.getResourceAsStream() must be the path within the classpath. If your file is src/main/resources/config/my-app.properties, the path would be config/my-app.properties.
  • Target Path: Carefully construct the target Path to ensure the file lands in the correct subdirectory within the expanded project.
  • Error Handling: Always check if getResourceAsStream() returns null (meaning the resource was not found) and wrap IOExceptions in ExpansionException for better error reporting during the expansion process.

3. Testing Your Expansion Step

Thorough testing of expansion steps is crucial. For file copying, you'll want to verify that the files are indeed copied to the correct location within the expansion directory.

You can test this logic with expanders-assert and JUnit 5.

Key elements for testing:

  • @ExpansionStepTest: Annotates your test class, indicating it's an expansion step test.
  • @TempDir Path expansionDirectory: JUnit's way to automatically create and manage a temporary directory, perfect for simulating the expansion output directory.
  • expansion.extendContext(...): This allows you to modify the ExpansionContext for the test run. Here, we inject the temporary directory as the expansionDirectory.
  • afterRunningExpansionStep(): Executes the expansion step.
  • assertThat(expectedFile).exists(): An assertion (from AssertJ, commonly used with expanders-assert) to verify that the file exists in the expected location.
Test example with expanders-assert
@ExpansionStepTest
class CopyMyFilesStepTest {

@TestModel
Spec baseModel() {
return application("testApp");
}

@Test
void testRun(TestExpansion<ApplicationComposite> expansion, @TempDir Path tempExpansionDirectory) {
// 1. Configure the test context:
// We explicitly set the expansion directory for this test run to our temporary directory.
expansion
.extendContext(ctx ->
((ProgramExpansionContext) ctx).setExpansionDirectory(tempExpansionDirectory.toFile()))
// 2. Run the expansion step (and any other configured steps up to this point):
.afterRunningExpansionStep();

// 3. Verify the outcome:
// Construct the expected path within the temporary expansion directory.
Path expectedFile = tempExpansionDirectory.resolve("output/resources/my-file.ext"); // Match TARGET_FILE from step

// Assert that the file actually exists at the expected location.
assertThat(expectedFile)
.withFailMessage("The expected file was not copied to the temporary expansion directory.")
.exists();
}
}

This comprehensive approach ensures your static files are reliably copied during expansion and that your logic is thoroughly tested for correctness.