Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a new class that can extract files from a classloader classpath #554

Merged
merged 5 commits into from
May 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
Expand Down
183 changes: 183 additions & 0 deletions src/main/java/edu/hm/hafner/util/ResourceExtractor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package edu.hm.hafner.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;

/**
* A proxy for resources. Extracts a given collection of files from the classpath and copies them to a target path.
*
* @author Ullrich Hafner
*/
public class ResourceExtractor {
private final boolean readingFromJarFile;
private final Extractor extractor;
private String resourcePath;

/**
* Creates a new {@link ResourceExtractor} that extracts resources from the classloader of the specified class.
*
* @param targetClass
* the target class to use the classloader from
*/
public ResourceExtractor(final Class<?> targetClass) {
this(targetClass, targetClass.getProtectionDomain());
}

@VisibleForTesting
ResourceExtractor(final Class<?> targetClass, final ProtectionDomain protectionDomain) {
CodeSource codeSource = protectionDomain.getCodeSource();
if (codeSource == null) {
throw new IllegalArgumentException("There is no CodeSource for " + targetClass);
}
URL location = codeSource.getLocation();
if (location == null) {
throw new IllegalArgumentException("There is no CodeSource location for " + targetClass);
}
String locationPath = location.getPath();
if (StringUtils.isBlank(locationPath)) {
throw new IllegalArgumentException("The CodeSource location path is not set for " + targetClass);
}
Path entryPoint = new File(locationPath).toPath();
readingFromJarFile = Files.isRegularFile(entryPoint);
if (readingFromJarFile) {
extractor = new JarExtractor(entryPoint);
}
else {
extractor = new FolderExtractor(entryPoint);
}
resourcePath = entryPoint.toString();
}

public String getResourcePath() {
return resourcePath;
}

public boolean isReadingFromJarFile() {
return readingFromJarFile;
}

/**
* Extracts the specified source files from the classloader and saves them to the specified target folder.
*
* @param targetDirectory
* the target path that will be the parent folder of all extracted files
* @param source
* the source file to extract
* @param sources
* the additional source files to extract
*/
public void extract(final Path targetDirectory, final String source, final String... sources) {
if (!Files.isDirectory(targetDirectory)) {
throw new IllegalArgumentException(
"Target directory must be an existing directory: " + targetDirectory); // implement
}
String[] allSources = Arrays.copyOf(sources, sources.length + 1);
allSources[sources.length] = source;
extractor.extractFiles(targetDirectory, allSources);
}

/**
* Extracts a collection of files and copies them to a given target path.
*/
private abstract static class Extractor {
private final Path entryPoint;

Extractor(final Path entryPoint) {
this.entryPoint = entryPoint;
}

Path getEntryPoint() {
return entryPoint;
}

abstract void extractFiles(Path targetDirectory, String... sources);
}

/**
* Extracts files from a folder, typically provided by the development environment or build system.
*/
private static class FolderExtractor extends Extractor {
FolderExtractor(final Path entryPoint) {
super(entryPoint);
}

@Override
public void extractFiles(final Path targetDirectory, final String... sources) {
try {
for (String source : sources) {
Path targetFile = targetDirectory.resolve(source);
Files.createDirectories(targetFile);
copy(targetFile, source);
}
}
catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}

private void copy(final Path target, final String source) {
try {
Files.copy(getEntryPoint().resolve(source), target, StandardCopyOption.REPLACE_EXISTING);
}
catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
}

/**
* Extracts files from a deployed jar file.
*/
private static class JarExtractor extends Extractor {
JarExtractor(final Path entryPoint) {
super(entryPoint);
}

@Override
public void extractFiles(final Path targetDirectory, final String... sources) {
Set<String> remaining = Arrays.stream(sources).collect(Collectors.toSet());
try (JarFile jar = new JarFile(getEntryPoint().toFile())) {
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (remaining.contains(name)) {
Path targetFile = targetDirectory.resolve(name);
if (!targetFile.normalize().startsWith(targetDirectory)) {
throw new IllegalArgumentException("Corrupt jar structure, contains invalid path: " + name);
}
Files.createDirectories(targetFile.getParent());
try (InputStream inputStream = jar.getInputStream(entry); OutputStream outputStream = Files.newOutputStream(targetFile)) {
IOUtils.copy(inputStream, outputStream);
}
remaining.remove(name);
}
}
}
catch (IOException exception) {
throw new UncheckedIOException(exception);
}
if (!remaining.isEmpty()) {
throw new NoSuchElementException("The following files have not been found: " + remaining);
}
}
}
}
138 changes: 138 additions & 0 deletions src/test/java/edu/hm/hafner/util/ResourceExtractorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package edu.hm.hafner.util;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.CodeSource;
import java.security.ProtectionDomain;

import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

/**
* Tests the class {@link ResourceExtractor}.
*
* @author Ullrich Hafner
*/
class ResourceExtractorTest {
private static final String ASSERTJ_TEMPLATES = "assertj-templates/has_assertion_template.txt";
private static final String JENKINS_FILE = "Jenkinsfile.reference";
private static final String MANIFEST_MF = "META-INF/MANIFEST.MF";

@Test
void shouldLocateResourcesInFolder() {
ResourceExtractor folderExtractor = new ResourceExtractor(ResourceExtractor.class);
assertThat(folderExtractor.isReadingFromJarFile()).isFalse();
assertThat(normalizePath(folderExtractor.getResourcePath())).endsWith("target/classes");
}

private String normalizePath(final String resourcePath) {
return new PathUtil().getAbsolutePath(resourcePath);
}

@Test
void shouldLocateResourcesInJarFile() {
ResourceExtractor jarExtractor = new ResourceExtractor(StringUtils.class);
assertThat(jarExtractor.isReadingFromJarFile()).isTrue();
assertThat(jarExtractor.getResourcePath()).matches(".*commons-lang3-\\d+\\.\\d+\\.\\d+.jar");
}

@Test
void shouldExtractFromFolder(@TempDir final Path targetFolder) {
ResourceExtractor proxy = new ResourceExtractor(ResourceExtractor.class);

proxy.extract(targetFolder, ASSERTJ_TEMPLATES, JENKINS_FILE,
"edu/hm/hafner/util/ResourceExtractor.class");

assertThat(readToString(targetFolder.resolve(ASSERTJ_TEMPLATES)))
.contains("has${Property}(${propertyType} ${property_safe})");
assertThat(readToString(targetFolder.resolve(JENKINS_FILE)))
.contains("node", "stage ('Build and Static Analysis')");
}

@Test
void shouldThrowExceptionIfTargetIsFileInFolder() throws IOException {
ResourceExtractor proxy = new ResourceExtractor(ResourceExtractor.class);

Path tempFile = Files.createTempFile("tmp", "tmp");
assertThatIllegalArgumentException().isThrownBy(() -> proxy.extract(tempFile, MANIFEST_MF));
}

@Test
void shouldThrowExceptionIfFileDoesNotExistInFolder(@TempDir final Path targetFolder) {
ResourceExtractor proxy = new ResourceExtractor(ResourceExtractor.class);

assertThatExceptionOfType(UncheckedIOException.class).isThrownBy(() ->
proxy.extract(targetFolder, "does-not-exist"));
}

@Test
void shouldExtractFromJar(@TempDir final Path targetFolder) {
ResourceExtractor proxy = new ResourceExtractor(StringUtils.class);

proxy.extract(targetFolder, MANIFEST_MF,
"org/apache/commons/lang3/StringUtils.class");

assertThat(readToString(targetFolder.resolve(MANIFEST_MF))).contains("Manifest-Version: 1.0",
"Bundle-SymbolicName: org.apache.commons.lang3");
}

@Test
void shouldThrowExceptionIfTargetIsFileInJar() throws IOException {
ResourceExtractor proxy = new ResourceExtractor(StringUtils.class);

Path tempFile = Files.createTempFile("tmp", "tmp");
assertThatIllegalArgumentException().isThrownBy(() -> proxy.extract(tempFile, MANIFEST_MF));
}

@Test
void shouldThrowExceptionIfFileDoesNotExistInJar(@TempDir final Path targetFolder) {
ResourceExtractor proxy = new ResourceExtractor(StringUtils.class);

assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() ->
proxy.extract(targetFolder, "does-not-exist"));
}

@Test
void shouldHandleClassloaderProblems() {
ProtectionDomain protectionDomain = mock(ProtectionDomain.class);

assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceExtractor(ResourceExtractor.class, protectionDomain))
.withMessageContainingAll("CodeSource for", "ResourceExtractor");

CodeSource codeSource = mock(CodeSource.class);
when(protectionDomain.getCodeSource()).thenReturn(codeSource);

assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceExtractor(ResourceExtractor.class, protectionDomain))
.withMessageContaining("CodeSource location for", "ResourceExtractor");

URL url = mock(URL.class);
when(codeSource.getLocation()).thenReturn(url);
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceExtractor(ResourceExtractor.class, protectionDomain))
.withMessageContaining("CodeSource location path", "ResourceExtractor");

when(url.getPath()).thenReturn("file.jar");
ResourceExtractor extractor = new ResourceExtractor(ResourceExtractor.class, protectionDomain);

assertThat(extractor.getResourcePath()).isEqualTo("file.jar");
}

private String readToString(final Path output) {
try {
return new String(Files.readAllBytes(output), StandardCharsets.UTF_8);
}
catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
}