Skip to content

Commit

Permalink
feat: native lib jar can be downloaded from remote repo
Browse files Browse the repository at this point in the history
  • Loading branch information
manusa committed Jan 23, 2024
1 parent a5eb4c2 commit 782ceaa
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 2 deletions.
74 changes: 74 additions & 0 deletions helm-java/src/test/java/com/marcnuri/helm/NativeLibraryTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.marcnuri.helm;

import com.marcnuri.helm.jni.NativeLibrary;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import java.net.URLClassLoader;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class NativeLibraryTest {

@Test
void getInstanceFromClassPath() {
System.setProperty("com.marcnuri.jkube-helm.skipRemoteJar", "true");
try {
NativeLibrary nativeLibrary = NativeLibrary.getInstance();
assertThat(nativeLibrary).isNotNull();
} finally {
System.clearProperty("com.marcnuri.jkube-helm.skipRemoteJar");
}
}

@Test
void getSnapshotInstanceFromRemoteJar() {
final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
// Use a PUBLISHED snapshot version
System.setProperty("com.marcnuri.jkube-helm.version", "0.0-SNAPSHOT");
System.setProperty("com.marcnuri.jkube-helm.forceUpdate", "true");
try {
Thread.currentThread().setContextClassLoader(new URLClassLoader(new java.net.URL[0], null));
assertThat(NativeLibrary.serviceProviderLibrary(null)).isNull();
NativeLibrary nativeLibrary = NativeLibrary.getInstance();
assertThat(nativeLibrary).isNotNull();
} finally {
Thread.currentThread().setContextClassLoader(currentClassLoader);
System.clearProperty("com.marcnuri.jkube-helm.version");
}
}

@Test
@Disabled("Can only be enabled once we do an initial stable release") // TODO
void getReleaseInstanceFromRemoteJar() {
final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
// Use a PUBLISHED release version
System.setProperty("com.marcnuri.jkube-helm.version", "0.0.0");
System.setProperty("com.marcnuri.jkube-helm.forceUpdate", "true");
try {
Thread.currentThread().setContextClassLoader(new URLClassLoader(new java.net.URL[0], null));
assertThat(NativeLibrary.serviceProviderLibrary(null)).isNull();
NativeLibrary nativeLibrary = NativeLibrary.getInstance();
assertThat(nativeLibrary).isNotNull();
} finally {
Thread.currentThread().setContextClassLoader(currentClassLoader);
System.clearProperty("com.marcnuri.jkube-helm.version");
}
}

@Test
void getInstanceNotAvailable() {
final ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
System.setProperty("com.marcnuri.jkube-helm.skipRemoteJar", "true");
try {
Thread.currentThread().setContextClassLoader(new URLClassLoader(new java.net.URL[0], null));
assertThatThrownBy(NativeLibrary::getInstance)
.isInstanceOf(IllegalStateException.class)
.hasMessage("No NativeLibrary implementation found, please add one of the supported dependencies to your project");
} finally {
System.clearProperty("com.marcnuri.jkube-helm.skipRemoteJar");
Thread.currentThread().setContextClassLoader(currentClassLoader);
}
}
}
13 changes: 13 additions & 0 deletions lib/api/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,17 @@
<artifactId>jna-platform-jpms</artifactId>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
<resource>
<directory>src/main/resources-filtered</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
22 changes: 21 additions & 1 deletion lib/api/src/main/java/com/marcnuri/helm/jni/NativeLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,34 @@
import java.util.Objects;
import java.util.ServiceLoader;

import static com.marcnuri.helm.jni.RemoteJarLoader.remoteJar;

public interface NativeLibrary {

static NativeLibrary getInstance() {
for (NativeLibrary nativeLibrary : ServiceLoader.load(NativeLibrary.class)) {
NativeLibrary nativeLibrary;
ClassLoader remoteJar;
if (
// Load from ClassPath (should work on Maven)
(nativeLibrary = serviceProviderLibrary(null)) != null ||
// Load from remote JAR (should work on Gradle if not air-gapped)
((remoteJar = remoteJar()) != null && (nativeLibrary = serviceProviderLibrary(remoteJar)) != null)
) {
return nativeLibrary;
}
// Load remote JAR (fallback for Gradle)
throw new IllegalStateException("No NativeLibrary implementation found, please add one of the supported dependencies to your project");
}

static NativeLibrary serviceProviderLibrary(ClassLoader classLoader) {
for (NativeLibrary nativeLibrary : ServiceLoader.load(
NativeLibrary.class, classLoader == null ? Thread.currentThread().getContextClassLoader() : classLoader)
) {
return nativeLibrary;
}
return null;
}

String getBinaryName();

default HelmLib load() {
Expand All @@ -45,4 +64,5 @@ default Path createTempDirectory() {
throw new IllegalStateException("Unable to create temporary directory", exception);
}
}

}
127 changes: 127 additions & 0 deletions lib/api/src/main/java/com/marcnuri/helm/jni/RemoteJarLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.marcnuri.helm.jni;

import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Properties;

class RemoteJarLoader {

private static final String TEMP_DIR = "java.io.tmpdir";
private static final String VERSION = "com.marcnuri.jkube-helm.version";
private static final String GROUP_ID = "com.marcnuri.jkube-helm.groupId";
private static final String SNAPSHOTS = "com.marcnuri.jkube-helm.repository.snapshots";
private static final String RELEASES = "com.marcnuri.jkube-helm.repository.releases";
// For testing
private static final String FORCE_UPDATE = "com.marcnuri.jkube-helm.forceUpdate";
private static final String SKIP = "com.marcnuri.jkube-helm.skipRemoteJar";

private RemoteJarLoader() {
}

static ClassLoader remoteJar() {
// For testing
if (System.getProperty(SKIP) != null) {
return null;
}
final Properties nativeLibraries = new Properties();
try (final InputStream stream = NativeLibrary.class.getResourceAsStream("/META-INF/native-libraries.properties")) {
nativeLibraries.load(stream);
final String version;
// Version can be overridden (for testing) using a system property
if (System.getProperty(VERSION) != null) {
version = System.getProperty(VERSION);
} else {
version = nativeLibraries.getProperty(VERSION);
}
final String groupId = nativeLibraries.getProperty(GROUP_ID);
final boolean isSnapshot = version.endsWith("-SNAPSHOT");
final String repository = isSnapshot ?
nativeLibraries.getProperty(SNAPSHOTS) :
nativeLibraries.getProperty(RELEASES);
final String osName = osName();
final String archName = archName();
if (osName != null && archName != null) {
final String groupUrl = repository + "/" + groupId.replace('.', '/') + "/" + osName + "-" + archName + "/" + version;
final String jarName;
if (isSnapshot) {
jarName = latestSnapshot(version, osName, archName, groupUrl);
} else {
jarName = osName + "-" + archName + "-" + version + ".jar";
}
final URL jarUrl = new URL(groupUrl + "/" + jarName);
final Path jarFile = Paths.get(System.getProperty(TEMP_DIR), jarName);
return new URLClassLoader(new URL[]{cache(jarUrl, jarFile)});
}
} catch (IOException | ParserConfigurationException | SAXException | XPathExpressionException exception) {
// NO OP
}
return null;
}

private static String latestSnapshot(String version, String osName, String archName, String groupUrl)
throws IOException, ParserConfigurationException, SAXException, XPathExpressionException {
final String metadataXml = groupUrl + "/maven-metadata.xml";
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
final Document xml = factory.newDocumentBuilder().parse(metadataXml);
final XPath xPath = XPathFactory.newInstance().newXPath();
final String versionFragment = version.replace("-SNAPSHOT", "");
final String timestamp = xPath.evaluate("//metadata/versioning/snapshot/timestamp", xml);
final String buildNumber = xPath.evaluate("//metadata/versioning/snapshot/buildNumber", xml);
return osName + "-" + archName + "-" + versionFragment + "-" + timestamp + "-" + buildNumber + ".jar";
}

private static URL cache(URL jarUrl, Path jarFile) throws IOException {
if (System.getProperty(FORCE_UPDATE) != null || !jarFile.toFile().exists() || !jarFile.toFile().isFile()) {
Files.deleteIfExists(jarFile);
try (
ReadableByteChannel inChannel = Channels.newChannel(jarUrl.openStream());
FileOutputStream fos = new FileOutputStream(jarFile.toFile());
FileChannel outChannel = fos.getChannel()
) {
outChannel.transferFrom(inChannel, 0, Long.MAX_VALUE);
}
}
return jarFile.toUri().toURL();
}

private static String osName() {
final String osName = System.getProperty("os.name").toLowerCase();
if (osName.contains("win")) {
return "windows";
} else if (osName.contains("mac")) {
return "darwin";
} else if (osName.contains("nux") || osName.contains("nix") || osName.contains("aix")) {
return "linux";
}
return null;
}

private static String archName() {
final String arch = System.getProperty("os.arch");
if (arch.equals("amd64") || arch.equals("x86_64")) {
return "amd64";
} else if (arch.equals("arch64")) {
return "arm64";
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
com.marcnuri.jkube-helm.groupId=${project.groupId}
com.marcnuri.jkube-helm.version=${project.version}
com.marcnuri.jkube-helm.repository.snapshots=${com.marcnuri.jkube-helm.repository.snapshots}
com.marcnuri.jkube-helm.repository.releases=${com.marcnuri.jkube-helm.repository.snapshots}
4 changes: 3 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
</snapshotRepository>
<repository>
<id>ossrh</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
</repository>
</distributionManagement>

Expand All @@ -51,6 +51,8 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<com.marcnuri.jkube-helm.repository.snapshots>https://oss.sonatype.org/content/repositories/snapshots</com.marcnuri.jkube-helm.repository.snapshots>
<com.marcnuri.jkube-helm.repository.stable>https://repo1.maven.org/maven2</com.marcnuri.jkube-helm.repository.stable>
<version.assertj>3.25.1</version.assertj>
<version.jna>5.14.0</version.jna>
<version.junit>5.10.1</version.junit>
Expand Down

0 comments on commit 782ceaa

Please sign in to comment.