From 64ca4940c66f43cf9e24b0a3b47717f2b71a0b75 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Sat, 30 Oct 2021 19:29:37 +0200 Subject: [PATCH] added osgi-app plugin --- Jenkinsfile | 3 +- build.gradle | 16 +- executable-jar/build.gradle | 50 --- .../executable-jar-common/build.gradle | 14 - .../net/woggioni/executable/jar/Common.java | 117 ------ .../woggioni/executable/jar/Constants.java | 31 -- .../executable-jar-launcher/build.gradle | 70 ---- .../net/woggioni/executable/jar/Launcher.java | 92 ----- .../executable/jar/MainClassLoader.java | 13 - .../executable/jar/PathClassLoader.java | 190 --------- .../src/main/java9/module-info.java | 6 - .../executable/jar/MainClassLoader.java | 39 -- .../net/woggioni/executable/jar/FooTest.java | 149 ------- .../executable/jar/ExecutableJarPlugin.java | 18 - .../executable/jar/ExecutableJarTask.java | 280 ------------- .../executable/jar/LauncherResource.java | 42 -- gradle.properties | 13 +- osgi-app/build.gradle | 66 ++++ .../osgi-simple-bootstrapper-api/build.gradle | 11 + .../simple/bootstrapper/api/Application.java | 5 + .../bootstrapper/api/FrameworkService.java | 7 + .../simple/bootstrapper/api/package-info.java | 4 + .../build.gradle | 28 ++ .../application/ApplicationRunner.java | 46 +++ .../application/package-info.java | 1 + .../osgi-simple-bootstrapper/build.gradle | 36 ++ .../simple/bootstrapper/Bootstrapper.java | 370 ++++++++++++++++++ .../bootstrapper}/JavaAgentLauncher.java | 23 +- .../gradle/osgi/app/BootstrapperResource.java | 69 ++++ .../gradle/osgi/app/BundleFileTask.java | 65 +++ .../osgi/app/FrameworkPropertyFileTask.java | 43 ++ .../osgi/app/FrameworkRuntimeCheck.java | 42 ++ .../woggioni/gradle/osgi/app/JavaAgent.java | 9 + .../gradle/osgi/app/JavaAgentFileTask.java | 46 +++ .../woggioni/gradle/osgi/app/MapBuilder.java | 31 ++ .../gradle/osgi/app/OsgiAppExtension.java | 72 ++++ .../gradle/osgi/app/OsgiAppPlugin.java | 289 ++++++++++++++ .../gradle/osgi/app/OsgiAppUtils.java | 22 ++ .../gradle/osgi/app/PropertyFileTask.java | 48 +++ .../osgi/app/SystemPackageExtraFileTask.java | 99 +++++ .../osgi/app/SystemPropertyFileTask.java | 42 ++ settings.gradle | 19 +- 42 files changed, 1498 insertions(+), 1138 deletions(-) delete mode 100644 executable-jar/build.gradle delete mode 100644 executable-jar/executable-jar-common/build.gradle delete mode 100644 executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Common.java delete mode 100644 executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java delete mode 100644 executable-jar/executable-jar-launcher/build.gradle delete mode 100644 executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java delete mode 100644 executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java delete mode 100644 executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java delete mode 100644 executable-jar/executable-jar-launcher/src/main/java9/module-info.java delete mode 100644 executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java delete mode 100644 executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java delete mode 100644 executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarPlugin.java delete mode 100644 executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java delete mode 100644 executable-jar/src/main/java/net/woggioni/gradle/executable/jar/LauncherResource.java create mode 100644 osgi-app/build.gradle create mode 100644 osgi-app/osgi-simple-bootstrapper-api/build.gradle create mode 100644 osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/Application.java create mode 100644 osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/FrameworkService.java create mode 100644 osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/package-info.java create mode 100644 osgi-app/osgi-simple-bootstrapper-application/build.gradle create mode 100644 osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/ApplicationRunner.java create mode 100644 osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/package-info.java create mode 100644 osgi-app/osgi-simple-bootstrapper/build.gradle create mode 100644 osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/Bootstrapper.java rename {executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar => osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper}/JavaAgentLauncher.java (68%) create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BootstrapperResource.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BundleFileTask.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkPropertyFileTask.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkRuntimeCheck.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgent.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgentFileTask.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/MapBuilder.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppExtension.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppPlugin.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppUtils.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/PropertyFileTask.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPackageExtraFileTask.java create mode 100644 osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPropertyFileTask.java diff --git a/Jenkinsfile b/Jenkinsfile index 48772e9..5815eeb 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -7,7 +7,7 @@ pipeline { stage("Build") { steps { sh "./gradlew clean build" - archiveArtifacts artifacts: '*/build/libs/*.jar', + archiveArtifacts artifacts: '*/build/libs/*.jar,osgi-app/*/build/libs/*.jar', allowEmptyArchive: true, fingerprint: true, onlyIfSuccessful: true @@ -20,4 +20,3 @@ pipeline { } } } - diff --git a/build.gradle b/build.gradle index 06125ea..cd28be1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,12 @@ subprojects { apply plugin: 'java-library' - apply plugin: 'maven-publish' + repositories { maven { url = woggioniMavenRepositoryUrl } mavenCentral() } - group = "net.woggioni.gradle" dependencies { ['compileOnly', 'annotationProcessor', 'testCompileOnly', 'testAnnotationProcessor'].each { conf -> @@ -23,19 +22,18 @@ subprojects { } } -childProjects.forEach { name, project -> - project.with { +childProjects.forEach { name, child -> + child.with { + apply plugin: 'maven-publish' + + group = "net.woggioni.gradle" + publishing { repositories { maven { url = woggioniMavenRepositoryUrl } } - publications { - maven(MavenPublication) { - from(components["java"]) - } - } } } } diff --git a/executable-jar/build.gradle b/executable-jar/build.gradle deleted file mode 100644 index b58967f..0000000 --- a/executable-jar/build.gradle +++ /dev/null @@ -1,50 +0,0 @@ -plugins { - id 'java-gradle-plugin' -} - -version = "0.1" - -configurations { - embedded - compileOnly.extendsFrom(embedded) -} - -dependencies { - embedded project(path: "executable-jar-common", configuration: "archives") -} - -Provider copyLauncher = tasks.register("copyLauncher", Copy) { - from(project("executable-jar-launcher").tasks.named("tar").map {it.outputs }) - into(new File(project.buildDir, "resources/main/META-INF")) -} - -tasks.named("processResources") { - inputs.files(copyLauncher) -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -jar { - manifest { - attributes "version" : archiveVersion.get() - } - from { - configurations.named('embedded').map { - it.collect { - it.isDirectory() ? it : zipTree(it) - } - } - } -} - -gradlePlugin { - plugins { - create("ExecutableJarPlugin") { - id = "net.woggioni.gradle.executable-jar" - implementationClass = "net.woggioni.gradle.executable.jar.ExecutableJarPlugin" - } - } -} diff --git a/executable-jar/executable-jar-common/build.gradle b/executable-jar/executable-jar-common/build.gradle deleted file mode 100644 index 4270b4d..0000000 --- a/executable-jar/executable-jar-common/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -plugins { - id 'java-library' -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -jar { - manifest { - attributes "Automatic-Module-Name" : "net.woggioni.executable.jar" - } -} \ No newline at end of file diff --git a/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Common.java b/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Common.java deleted file mode 100644 index e72ad8f..0000000 --- a/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Common.java +++ /dev/null @@ -1,117 +0,0 @@ -package net.woggioni.executable.jar; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.DigestInputStream; -import java.security.MessageDigest; -import java.util.AbstractMap; -import java.util.Map; -import java.util.Optional; -import java.util.function.Supplier; -import java.util.zip.CRC32; -import java.util.zip.ZipEntry; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; -import java.io.IOException; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class Common { - final private static char[] hexArray = "0123456789ABCDEF".toCharArray(); - - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - @SneakyThrows - public static byte[] computeSHA256Digest(Supplier streamSupplier) { - byte[] buffer = new byte[Constants.BUFFER_SIZE]; - MessageDigest md = MessageDigest.getInstance("SHA-256"); - return computeDigest(streamSupplier, md, buffer); - } - - @SneakyThrows - public static byte[] computeDigest(Supplier streamSupplier, MessageDigest md, byte[] buffer) { - try(InputStream stream = new DigestInputStream(streamSupplier.get(), md)) { - while(stream.read(buffer) >= 0) {} - } - return md.digest(); - } - - @SneakyThrows - public static void computeSizeAndCrc32( - ZipEntry zipEntry, - InputStream inputStream, - byte[] buffer) { - CRC32 crc32 = new CRC32(); - long sz = 0L; - while (true) { - int read = inputStream.read(buffer); - if (read < 0) break; - sz += read; - crc32.update(buffer, 0, read); - } - zipEntry.setSize(sz); - zipEntry.setCompressedSize(sz); - zipEntry.setCrc(crc32.getValue()); - } - - @SneakyThrows - public static void write2Stream(InputStream inputStream, OutputStream os, - byte[] buffer) { - while (true) { - int read = inputStream.read(buffer); - if (read < 0) break; - os.write(buffer, 0, read); - } - } - - public static void write2Stream(InputStream inputStream, OutputStream os) { - write2Stream(inputStream, os, new byte[Constants.BUFFER_SIZE]); - } - - public static Optional> splitExtension(String fileName) { - int index = fileName.lastIndexOf('.'); - if (index == -1) { - return Optional.empty(); - } else { - return Optional.of( - new AbstractMap.SimpleEntry<>(fileName.substring(0, index), fileName.substring(index))); - } - } - - /** - * Helper method to create an {@link InputStream} from a file without having to catch the possibly - * thrown {@link IOException}, use {@link FileInputStream#FileInputStream(File)} if you need to catch it. - * @param file the {@link File} to be opened - * @return an open {@link InputStream} instance reading from the file - */ - @SneakyThrows - public static InputStream read(File file, boolean buffered) { - InputStream result = new FileInputStream(file); - return buffered ? new BufferedInputStream(result) : result; - } - - /** - * Helper method to create an {@link OutputStream} from a file without having to catch the possibly - * thrown {@link IOException}, use {@link FileOutputStream#FileOutputStream(File)} if you need to catch it. - * @param file the {@link File} to be opened - * @return an open {@link OutputStream} instance writing to the file - */ - @SneakyThrows - public static OutputStream write(File file, boolean buffered) { - OutputStream result = new FileOutputStream(file); - return buffered ? new BufferedOutputStream(result) : result; - } -} diff --git a/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java b/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java deleted file mode 100644 index e770c8d..0000000 --- a/executable-jar/executable-jar-common/src/main/java/net/woggioni/executable/jar/Constants.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.woggioni.executable.jar; - -import java.util.Calendar; -import java.util.GregorianCalendar; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class Constants { - public static final String LIBRARIES_FOLDER = "LIB-INF"; - public static final String METADATA_FOLDER = "META-INF"; - public static final int BUFFER_SIZE = 0x10000; - public static final String DEFAULT_LAUNCHER = "net.woggioni.executable.jar.Launcher"; - public static final String AGENT_LAUNCHER = "net.woggioni.executable.jar.JavaAgentLauncher"; - public static final String JAVA_AGENTS_FILE = METADATA_FOLDER + "/javaAgents.properties"; - - public static class ManifestAttributes { - public static final String MAIN_MODULE = "Executable-Jar-Main-Module"; - public static final String MAIN_CLASS = "Executable-Jar-Main-Class"; - public static final String ENTRY_HASH = "SHA-256-Digest"; - } - - /** - * This value is used as a default file timestamp for all the zip entries when - * AbstractArchiveTask.isPreserveFileTimestamps - * is true; its value is taken from Gradle's ZipCopyAction - * for the reasons outlined there. - */ - public static final long ZIP_ENTRIES_DEFAULT_TIMESTAMP = - new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0).getTimeInMillis(); -} diff --git a/executable-jar/executable-jar-launcher/build.gradle b/executable-jar/executable-jar-launcher/build.gradle deleted file mode 100644 index 3ac9433..0000000 --- a/executable-jar/executable-jar-launcher/build.gradle +++ /dev/null @@ -1,70 +0,0 @@ -import java.util.jar.Attributes - -buildscript { - dependencies { - classpath project(":multi-release-jar") - } -} - -plugins { - id 'java-library' -} - -ext.setProperty("jpms.module.name", "net.woggioni.executable.jar") - -apply plugin: 'net.woggioni.gradle.multi-release-jar' - -configurations { - embedded - compileOnly.extendsFrom(embedded) -} - -dependencies { - embedded project(path: ":executable-jar:executable-jar-common", configuration: 'archives') - embedded group: "net.woggioni", name: "xclassloader", version: getProperty("version.xclassloader") -} - -java { - modularity.inferModulePath = true -} - -tasks.withType(JavaCompile).configureEach { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(16) - } - options.forkOptions.jvmArgs << "--illegal-access=permit" -} - -jar { - manifest { - attributes([ - (Attributes.Name.SPECIFICATION_TITLE) : "executable-jar-launcher", - (Attributes.Name.SEALED) : true - ].collectEntries { - [it.key.toString(), it.value.toString()] - }) - } -} - -tasks.register("tar", Tar) { - archiveFileName = "${project.name}.tar" - from(project.tasks.named(JavaPlugin.JAR_TASK_NAME) - .flatMap(Jar.&getArchiveFile) - .map(RegularFile.&getAsFile) - .map(project.&zipTree)) - from(configurations.named('embedded').map { - it.collect { - it.isDirectory() ? it : zipTree(it) - } - }) { - exclude("**/module-info.class") - exclude("META-INF/MANIFEST.MF") - } -} - -tasks.named(JavaPlugin.COMPILE_JAVA_TASK_NAME, JavaCompile) { - doFirst { - String path = project(":executable-jar:executable-jar-common").extensions.getByType(JavaPluginExtension).sourceSets.named("main").get().output.asPath - options.compilerArgs.addAll(["--patch-module", "net.woggioni.executable.jar=$path"]) - } -} diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java deleted file mode 100644 index b77616c..0000000 --- a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/Launcher.java +++ /dev/null @@ -1,92 +0,0 @@ -package net.woggioni.executable.jar; - -import java.io.InputStream; -import java.lang.reflect.Method; -import java.net.URI; -import java.net.URL; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Objects; -import java.util.function.Function; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import lombok.SneakyThrows; -import lombok.extern.java.Log; - - -@Log -public class Launcher { - - @SneakyThrows - private static URI findCurrentJar() { - String launcherClassName = Launcher.class.getName(); - URL url = Launcher.class.getClassLoader().getResource(launcherClassName.replace('.', '/') + ".class"); - if (url == null || !"jar".equals(url.getProtocol())) - throw new IllegalStateException(String.format("The class %s must be used inside a JAR file", launcherClassName)); - String path = url.getPath(); - return new URI(path.substring(0, path.indexOf('!'))); - } - - @SneakyThrows - public static void main(String[] args) { - URI currentJar = findCurrentJar(); - URL manifestResource = Launcher.class.getResource("/" + JarFile.MANIFEST_NAME); - if(Objects.isNull(manifestResource)) { - throw new RuntimeException("Launcher manifest not found"); - } - Manifest mf = new Manifest(); - try(InputStream is = manifestResource.openStream()) { - mf.read(is); - } - try(FileSystem fs = FileSystems.newFileSystem(Paths.get(currentJar), null)) { - Attributes mainAttributes = mf.getMainAttributes(); - - Collector, List> immutableListCollector = Collector.of( - ArrayList::new, - List::add, - (l1, l2) -> { l1.addAll(l2); return l1; }, - Collections::unmodifiableList); - List jarList = StreamSupport.stream(fs.getRootDirectories().spliterator(), false).flatMap(new Function>() { - @Override - @SneakyThrows - public Stream apply(Path path) { - return Files.list(path.resolve(Constants.LIBRARIES_FOLDER)) - .filter(Files::isRegularFile) - .filter(p -> p.getFileName().toString().endsWith(".jar")); - } - }).flatMap(new Function>() { - @Override - @SneakyThrows - public Stream apply(Path path) { - return StreamSupport.stream(FileSystems.newFileSystem(path, null).getRootDirectories().spliterator(), false); - } - }).collect(immutableListCollector); - - String mainClassName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_CLASS); - String mainModuleName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_MODULE); - Class mainClass = MainClassLoader.loadMainClass(jarList, mainModuleName, mainClassName); - try { - Method mainMethod = mainClass.getMethod("main", String[].class); - Class returnType = mainMethod.getReturnType(); - if (mainMethod.getReturnType() != Void.TYPE) { - throw new IllegalArgumentException(String.format("Main method in class '%s' " + - "has wrong return type, expected '%s', found '%s' instead", mainClass, Void.class.getName(), returnType)); - } - mainMethod.invoke(null, (Object) args); - } catch (NoSuchMethodException nsme) { - throw new IllegalArgumentException(String.format("No valid main method found in class '%s'", mainClass), nsme); - } - } - } -} diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java deleted file mode 100644 index 215bc56..0000000 --- a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/MainClassLoader.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.woggioni.executable.jar; - -import java.nio.file.Path; -import lombok.SneakyThrows; - - -class MainClassLoader { - @SneakyThrows - static Class loadMainClass(Iterable roots, String mainModuleName, String mainClassName) { - ClassLoader pathClassLoader = new net.woggioni.xclassloader.PathClassLoader(roots); - return pathClassLoader.loadClass(mainClassName); - } -} diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java b/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java deleted file mode 100644 index 61282fc..0000000 --- a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/PathClassLoader.java +++ /dev/null @@ -1,190 +0,0 @@ -package net.woggioni.executable.jar; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.FileVisitResult; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.List; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; - -/** - * A classloader that loads classes from a {@link Path} instance - */ -public final class PathClassLoader extends ClassLoader { - - private final Iterable paths; - - static { - registerAsParallelCapable(); - } - - public PathClassLoader(Path ...path) { - this(Arrays.asList(path), null); - } - - public PathClassLoader(Iterable paths) { - this(paths, null); - } - - public PathClassLoader(Iterable paths, ClassLoader parent) { - super(parent); - this.paths = paths; - } - - @Override - @SneakyThrows - protected Class findClass(String name) { - String resource = name.replace('.', '/').concat(".class"); - for(Path path : paths) { - Path classPath = path.resolve(resource); - if (Files.exists(classPath)) { - byte[] byteCode = Files.readAllBytes(classPath); - return defineClass(name, byteCode, 0, byteCode.length); - } - } - throw new ClassNotFoundException(name); - } - - @Override - @SneakyThrows - protected URL findResource(String name) { - for(Path path : paths) { - Path resolved = path.resolve(name); - if (Files.exists(resolved)) { - return toURL(resolved); - } - } - return null; - } - - @Override - protected Enumeration findResources(final String name) throws IOException { - final List resources = new ArrayList<>(1); - for(Path path : paths) { - Files.walkFileTree(path, new SimpleFileVisitor() { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - if (!name.isEmpty()) { - this.addIfMatches(resources, file); - } - return super.visitFile(file, attrs); - } - - @Override - public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { - if (!name.isEmpty() || path.equals(dir)) { - this.addIfMatches(resources, dir); - } - return super.preVisitDirectory(dir, attrs); - } - - void addIfMatches(List resources, Path file) throws IOException { - if (path.relativize(file).toString().equals(name)) { - resources.add(toURL(file)); - } - } - }); - } - return Collections.enumeration(resources); - } - - private static URL toURL(Path path) throws IOException { - return new URL(null, path.toUri().toString(), PathURLStreamHandler.INSTANCE); - } - - private static final class PathURLConnection extends URLConnection { - - private final Path path; - - PathURLConnection(URL url, Path path) { - super(url); - this.path = path; - } - - @Override - public void connect() {} - - @Override - public long getContentLengthLong() { - try { - return Files.size(this.path); - } catch (IOException e) { - throw new RuntimeException("could not get size of: " + this.path, e); - } - } - - @Override - public InputStream getInputStream() throws IOException { - return Files.newInputStream(this.path); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return Files.newOutputStream(this.path); - } - - @Override - @SneakyThrows - public String getContentType() { - return Files.probeContentType(this.path); - } - - @Override - @SneakyThrows - public long getLastModified() { - BasicFileAttributes attributes = Files.readAttributes(this.path, BasicFileAttributes.class); - return attributes.lastModifiedTime().toMillis(); - } - } - - @NoArgsConstructor(access = AccessLevel.PRIVATE) - private static final class PathURLStreamHandler extends URLStreamHandler { - - static final URLStreamHandler INSTANCE = new PathURLStreamHandler(); - - @Override - @SneakyThrows - protected URLConnection openConnection(URL url) { - List stack = new ArrayList<>(); - URL currentURL = url; - while(true) { - String file = currentURL.getFile(); - int exclamationMark = file.lastIndexOf('!'); - if(exclamationMark != -1) { - stack.add(file.substring(exclamationMark + 1)); - currentURL = new URL(file.substring(0, exclamationMark)); - } else { - stack.add(file); - break; - } - } - - Path path; - FileSystem fs = FileSystems.getDefault(); - while(true) { - String pathString = stack.remove(stack.size() - 1); - path = fs.getPath(pathString); - if(stack.isEmpty()) break; - else { - fs = FileSystems.newFileSystem(path, null); - } - } - return new PathURLConnection(url, path); - } - } -} \ No newline at end of file diff --git a/executable-jar/executable-jar-launcher/src/main/java9/module-info.java b/executable-jar/executable-jar-launcher/src/main/java9/module-info.java deleted file mode 100644 index e39a33f..0000000 --- a/executable-jar/executable-jar-launcher/src/main/java9/module-info.java +++ /dev/null @@ -1,6 +0,0 @@ -module net.woggioni.executable.jar { - requires java.logging; - requires static lombok; - requires net.woggioni.xclassloader; - requires java.instrument; -} \ No newline at end of file diff --git a/executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java b/executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java deleted file mode 100644 index 6b889b9..0000000 --- a/executable-jar/executable-jar-launcher/src/main/java9/net/woggioni/executable/jar/MainClassLoader.java +++ /dev/null @@ -1,39 +0,0 @@ -package net.woggioni.executable.jar; - -import java.lang.module.Configuration; -import java.lang.module.ModuleFinder; -import java.nio.file.Path; -import java.util.Collections; -import java.util.Optional; - -import lombok.SneakyThrows; - -import net.woggioni.xclassloader.PathClassLoader; -import net.woggioni.xclassloader.PathModuleFinder; - -class MainClassLoader { - @SneakyThrows - static Class loadMainClass(Iterable roots, String mainModuleName, String mainClassName) { - if (mainModuleName == null) { - ClassLoader pathClassLoader = new net.woggioni.xclassloader.PathClassLoader(roots); - return pathClassLoader.loadClass(mainClassName); - } else { - ModuleLayer bootLayer = ModuleLayer.boot(); - Configuration bootConfiguration = bootLayer.configuration(); - Configuration cfg = bootConfiguration.resolve(new PathModuleFinder(roots), ModuleFinder.of(), Collections.singletonList(mainModuleName)); - ClassLoader pathClassLoader = new PathClassLoader(roots, cfg, null); - ModuleLayer.Controller controller = - ModuleLayer.defineModules(cfg, Collections.singletonList(ModuleLayer.boot()), moduleName -> pathClassLoader); - ModuleLayer layer = controller.layer(); - for(Module module : layer.modules()) { - controller.addReads(module, pathClassLoader.getUnnamedModule()); - } - Module mainModule = layer.findModule(mainModuleName).orElseThrow( - () -> new IllegalStateException(String.format("Main module '%s' not found", mainModuleName))); - return Optional.ofNullable(mainClassName) - .or(() -> mainModule.getDescriptor().mainClass()) - .map(className -> Class.forName(mainModule, className)) - .orElseThrow(() -> new IllegalStateException(String.format("Unable to determine main class name for module '%s'", mainModule.getName()))); - } - } -} diff --git a/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java b/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java deleted file mode 100644 index 9b1a4ed..0000000 --- a/executable-jar/executable-jar-launcher/src/test/java/net/woggioni/executable/jar/FooTest.java +++ /dev/null @@ -1,149 +0,0 @@ -package net.woggioni.executable.jar; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; -import java.net.URLStreamHandler; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.attribute.BasicFileAttributes; -import java.util.HashMap; -import java.util.List; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import java.util.stream.StreamSupport; -import lombok.AccessLevel; -import lombok.NoArgsConstructor; -import lombok.SneakyThrows; -import org.gradle.internal.impldep.org.junit.Ignore; -import org.junit.jupiter.api.Test; - -public class FooTest { - @Test - @SneakyThrows - void foo() { - Path.of(new URI("jar:file:///home/woggioni/code/wson/benchmark/build/libs/benchmark-executable-1.0.jar")); - } - - @Test - @SneakyThrows - void foo2() { - Path p = Path.of(new URI("file:///home/woggioni/code/wson/benchmark/build/libs/benchmark-executable-1.0.jar")); - FileSystem fs = FileSystems.newFileSystem(p, null); - StreamSupport.stream(fs.getRootDirectories().spliterator(), false).flatMap(new Function>() { - @Override - @SneakyThrows - public Stream apply(Path path) { - return Files.list(path); - } - }).forEach(r -> { - System.out.println(r); - }); - } - - @Test - @SneakyThrows - void test() { - Path fatJar = Path.of("/home/woggioni/code/wson/benchmark/build/libs/benchmark-executable-1.0.jar"); - List jars = StreamSupport.stream(FileSystems.newFileSystem(fatJar, null).getRootDirectories().spliterator(), false) - .flatMap(new Function>() { - @Override - @SneakyThrows - public Stream apply(Path root) { - Path libDir = root.resolve("/LIB-INF"); - if (Files.exists(libDir) && Files.isDirectory(libDir)) { - return Files.list(libDir); - } else { - return Stream.empty(); - } - } - }).flatMap(new Function>() { - @Override - @SneakyThrows - public Stream apply(Path path) { - return StreamSupport.stream(FileSystems.newFileSystem(path, null).getRootDirectories().spliterator(), false); - } - }).collect(Collectors.toList()); - PathClassLoader p = new PathClassLoader(jars.toArray(new Path[jars.size()])); - Class cl = p.loadClass("net.woggioni.wson.serialization.binary.JBONParser"); - System.out.println(cl); - URL resource = p.findResource("citylots.json.xz"); - resource.openStream(); - } - - @Test - @Ignore - @SneakyThrows - void test2() { - FileSystem fs = FileSystems.newFileSystem(new URI("jar:file:/home/woggioni/code/wson/benchmark/build/libs/benchmark-executable-1.0.jar"), new HashMap<>()); - String s = "jar:jar:file:///home/woggioni/code/wson/benchmark/build/libs/benchmark-executable-1.0.jar!/LIB-INF/wson-test-utils-1.0.jar!/citylots.json.xz"; - URI uri = new URI(s); - Files.list(Path.of(uri)).forEach(System.out::println); - } - - private static final class PathURLConnection extends URLConnection { - - private final Path path; - - PathURLConnection(URL url, Path path) { - super(url); - this.path = path; - } - - @Override - public void connect() {} - - @Override - public long getContentLengthLong() { - try { - return Files.size(this.path); - } catch (IOException e) { - throw new RuntimeException("could not get size of: " + this.path, e); - } - } - - @Override - public InputStream getInputStream() throws IOException { - return Files.newInputStream(this.path); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return Files.newOutputStream(this.path); - } - - @Override - @SneakyThrows - public String getContentType() { - return Files.probeContentType(this.path); - } - - @Override - @SneakyThrows - public long getLastModified() { - BasicFileAttributes attributes = Files.readAttributes(this.path, BasicFileAttributes.class); - return attributes.lastModifiedTime().toMillis(); - } - } - - @NoArgsConstructor(access = AccessLevel.PRIVATE) - private static final class PathURLStreamHandler extends URLStreamHandler { - - static final URLStreamHandler INSTANCE = new PathURLStreamHandler(); - - @Override - @SneakyThrows - protected URLConnection openConnection(URL url) { - URI uri = url.toURI(); - Path path = Paths.get(uri); - return new PathURLConnection(url, path); - } - } -} diff --git a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarPlugin.java b/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarPlugin.java deleted file mode 100644 index 8caeb06..0000000 --- a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarPlugin.java +++ /dev/null @@ -1,18 +0,0 @@ -package net.woggioni.gradle.executable.jar; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.plugins.BasePluginExtension; -import org.gradle.api.plugins.JavaPlugin; -import org.gradle.api.tasks.bundling.Jar; - -public class ExecutableJarPlugin implements Plugin { - @Override - public void apply(Project project) { - BasePluginExtension basePluginExtension = project.getExtensions().getByType(BasePluginExtension.class); - project.getTasks().register("executable-jar", ExecutableJarTask.class, t -> { - t.includeLibraries(project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); - t.includeLibraries(project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class)); - }); - } -} diff --git a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java b/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java deleted file mode 100644 index 9f3d048..0000000 --- a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/ExecutableJarTask.java +++ /dev/null @@ -1,280 +0,0 @@ -package net.woggioni.gradle.executable.jar; - -import java.io.File; -import java.io.InputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.security.MessageDigest; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Properties; -import java.util.Set; -import java.util.function.Supplier; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; -import java.util.zip.ZipOutputStream; -import javax.annotation.Nonnull; -import javax.inject.Inject; -import lombok.Getter; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import net.woggioni.executable.jar.Common; -import net.woggioni.executable.jar.Constants; -import org.gradle.api.GradleException; -import org.gradle.api.internal.file.CopyActionProcessingStreamAction; -import org.gradle.api.internal.file.copy.CopyAction; -import org.gradle.api.internal.file.copy.CopyActionProcessingStream; -import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.plugins.BasePluginExtension; -import org.gradle.api.plugins.JavaApplication; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Optional; -import org.gradle.api.tasks.WorkResult; -import org.gradle.api.tasks.bundling.AbstractArchiveTask; -import org.gradle.util.GradleVersion; - - -import static java.util.zip.Deflater.BEST_COMPRESSION; -import static java.util.zip.Deflater.NO_COMPRESSION; -import static net.woggioni.executable.jar.Constants.*; - -@SuppressWarnings({"unused" }) -public class ExecutableJarTask extends AbstractArchiveTask { - - private static final String MINIMUM_GRADLE_VERSION = "6.0"; - - static { - if (GradleVersion.current().compareTo(GradleVersion.version(MINIMUM_GRADLE_VERSION)) < 0) { - throw new GradleException(ExecutableJarTask.class.getName() + - " requires Gradle " + MINIMUM_GRADLE_VERSION + " or newer."); - } - } - - @Getter(onMethod_ = {@Input}) - private final Property mainClass; - - @Getter(onMethod_ = {@Input, @Optional}) - private final Property mainModule; - - private final Properties javaAgents = new Properties(); - - @Input - public Set> getJavaAgents() { - return Collections.unmodifiableSet(javaAgents.entrySet()); - } - - public void javaAgent(String className, String args) { - javaAgents.put(className, args); - } - - public void includeLibraries(Object... files) { - into(LIBRARIES_FOLDER, (copySpec) -> copySpec.from(files)); - } - - - @Inject - public ExecutableJarTask(ObjectFactory objects) { - setGroup("build"); - setDescription("Creates an executable jar file, embedding all of its runtime dependencies"); - BasePluginExtension basePluginExtension = getProject().getExtensions().getByType(BasePluginExtension.class); - getDestinationDirectory().set(basePluginExtension.getDistsDirectory()); - getArchiveBaseName().convention(getProject().getName()); - getArchiveExtension().convention("jar"); - getArchiveVersion().convention(getProject().getVersion().toString()); - getArchiveAppendix().convention("executable"); - exclude("**/module-info.class"); - - mainClass = objects.property(String.class); - mainModule = objects.property(String.class); - JavaApplication javaApplication = getProject().getExtensions().findByType(JavaApplication.class); - if(!Objects.isNull(javaApplication)) { - mainClass.convention(javaApplication.getMainClass()); - mainModule.convention(javaApplication.getMainModule()); - } - from(getProject().tarTree(LauncherResource.instance), copySpec -> exclude(JarFile.MANIFEST_NAME)); - } - - @Input - public String getLauncherArchiveHash() { - return Common.bytesToHex(Common.computeSHA256Digest(LauncherResource.instance::read)); - } - - @RequiredArgsConstructor - private static class StreamAction implements CopyActionProcessingStreamAction { - - private final ZipOutputStream zoos; - private final Manifest manifest; - private final MessageDigest md; - private final ZipEntryFactory zipEntryFactory; - private final byte[] buffer; - - @Override - @SneakyThrows - public void processFile(FileCopyDetailsInternal fileCopyDetails) { - String entryName = fileCopyDetails.getRelativePath().toString(); - if (!fileCopyDetails.isDirectory() && entryName.startsWith(LIBRARIES_FOLDER)) { - Supplier streamSupplier = () -> Common.read(fileCopyDetails.getFile(), false); - Attributes attr = manifest.getEntries().computeIfAbsent(entryName, it -> new Attributes()); - md.reset(); - attr.putValue(Constants.ManifestAttributes.ENTRY_HASH, - Base64.getEncoder().encodeToString(Common.computeDigest(streamSupplier, md, buffer))); - } - if (METADATA_FOLDER.equals(entryName)) return; - if (fileCopyDetails.isDirectory()) { - ZipEntry zipEntry = zipEntryFactory.createDirectoryEntry(entryName, fileCopyDetails.getLastModified()); - zoos.putNextEntry(zipEntry); - } else { - ZipEntry zipEntry = zipEntryFactory.createZipEntry(entryName, fileCopyDetails.getLastModified()); - boolean compressed = Common.splitExtension(fileCopyDetails.getSourceName()) - .map(entry -> ".jar".equals(entry.getValue())) - .orElse(false); - if (!compressed) { - zipEntry.setMethod(ZipEntry.DEFLATED); - } else { - try (InputStream is = Common.read(fileCopyDetails.getFile(), false)) { - Common.computeSizeAndCrc32(zipEntry, is, buffer); - } - zipEntry.setMethod(ZipEntry.STORED); - } - zoos.putNextEntry(zipEntry); - try (InputStream is = Common.read(fileCopyDetails.getFile(), false)) { - Common.write2Stream(is, zoos, buffer); - } - } - } - } - - @SuppressWarnings("SameParameterValue") - @RequiredArgsConstructor - private static final class ZipEntryFactory { - - private final boolean isPreserveFileTimestamps; - private final long defaultLastModifiedTime; - - @Nonnull - ZipEntry createZipEntry(String entryName, long lastModifiedTime) { - ZipEntry zipEntry = new ZipEntry(entryName); - zipEntry.setTime(isPreserveFileTimestamps ? lastModifiedTime : ZIP_ENTRIES_DEFAULT_TIMESTAMP); - return zipEntry; - } - - @Nonnull - ZipEntry createZipEntry(String entryName) { - return createZipEntry(entryName, defaultLastModifiedTime); - } - - @Nonnull - ZipEntry createDirectoryEntry(@Nonnull String entryName, long lastModifiedTime) { - ZipEntry zipEntry = createZipEntry(entryName.endsWith("/") ? entryName : entryName + '/', lastModifiedTime); - zipEntry.setMethod(ZipEntry.STORED); - zipEntry.setCompressedSize(0); - zipEntry.setSize(0); - zipEntry.setCrc(0); - return zipEntry; - } - - @Nonnull - ZipEntry createDirectoryEntry(@Nonnull String entryName) { - return createDirectoryEntry(entryName, defaultLastModifiedTime); - } - - @Nonnull - ZipEntry copyOf(@Nonnull ZipEntry zipEntry) { - if (zipEntry.getMethod() == ZipEntry.STORED) { - return new ZipEntry(zipEntry); - } else { - ZipEntry newEntry = new ZipEntry(zipEntry.getName()); - newEntry.setMethod(ZipEntry.DEFLATED); - newEntry.setTime(zipEntry.getTime()); - newEntry.setExtra(zipEntry.getExtra()); - newEntry.setComment(zipEntry.getComment()); - return newEntry; - } - } - } - - @Override - @Nonnull - protected CopyAction createCopyAction() { - File destination = getArchiveFile().get().getAsFile(); - return new CopyAction() { - - private final ZipEntryFactory zipEntryFactory = new ZipEntryFactory(isPreserveFileTimestamps(), System.currentTimeMillis()); - - @Override - @Nonnull - @SneakyThrows - public WorkResult execute(@Nonnull CopyActionProcessingStream copyActionProcessingStream) { - Manifest manifest = new Manifest(); - Attributes mainAttributes = manifest.getMainAttributes(); - mainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); - mainAttributes.put(Attributes.Name.MAIN_CLASS, DEFAULT_LAUNCHER); - mainAttributes.put(Attributes.Name.MULTI_RELEASE, "true"); - mainAttributes.put(new Attributes.Name("Launcher-Agent-Class"), AGENT_LAUNCHER); - mainAttributes.put(new Attributes.Name("Can-Redefine-Classes"), "true"); - mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true"); - mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, mainClass.get()); - if(mainModule.isPresent()) { - mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, mainModule.get()); - } - - MessageDigest md = MessageDigest.getInstance("SHA-256"); - byte[] buffer = new byte[Constants.BUFFER_SIZE]; - - /** - * The manifest has to be the first zip entry in a jar archive, as an example, - * {@link java.util.jar.JarInputStream} assumes the manifest is the first (or second at most) - * entry in the jar and simply returns a null manifest if that is not the case. - * In this case the manifest has to contain the hash of all the jar entries, so it cannot - * be computed in advance, we write all the entries to a temporary zip archive while computing the manifest, - * then we write the manifest to the final zip file as the first entry and, finally, - * we copy all the other entries from the temporary archive. - * - * The {@link org.gradle.api.Task#getTemporaryDir} directory is guaranteed - * to be unique per instance of this task. - */ - File temporaryJar = new File(getTemporaryDir(), "premature.zip"); - try (ZipOutputStream zipOutputStream = new ZipOutputStream(Common.write(temporaryJar, true))) { - zipOutputStream.setLevel(NO_COMPRESSION); - StreamAction streamAction = new StreamAction(zipOutputStream, manifest, md, zipEntryFactory, buffer); - copyActionProcessingStream.process(streamAction); - } - - try (ZipOutputStream zipOutputStream = new ZipOutputStream(Common.write(destination, true)); - ZipInputStream zipInputStream = new ZipInputStream(Common.read(temporaryJar, true))) { - zipOutputStream.setLevel(BEST_COMPRESSION); - ZipEntry zipEntry = zipEntryFactory.createDirectoryEntry(METADATA_FOLDER); - zipOutputStream.putNextEntry(zipEntry); - zipEntry = zipEntryFactory.createZipEntry(JarFile.MANIFEST_NAME); - zipEntry.setMethod(ZipEntry.DEFLATED); - zipOutputStream.putNextEntry(zipEntry); - manifest.write(zipOutputStream); - zipEntry = zipEntryFactory.createZipEntry(JAVA_AGENTS_FILE); - zipEntry.setMethod(ZipEntry.DEFLATED); - zipOutputStream.putNextEntry(zipEntry); - javaAgents.store(zipOutputStream, null); - - while (true) { - zipEntry = zipInputStream.getNextEntry(); - if (zipEntry == null) break; - // Create a new ZipEntry explicitly, without relying on - // subtle (undocumented?) behaviour of ZipInputStream. - zipOutputStream.putNextEntry(zipEntryFactory.copyOf(zipEntry)); - Common.write2Stream(zipInputStream, zipOutputStream, buffer); - } - return () -> true; - } - } - }; - } -} \ No newline at end of file diff --git a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/LauncherResource.java b/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/LauncherResource.java deleted file mode 100644 index 178bb9b..0000000 --- a/executable-jar/src/main/java/net/woggioni/gradle/executable/jar/LauncherResource.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.woggioni.gradle.executable.jar; - -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import javax.annotation.Nonnull; -import lombok.SneakyThrows; -import org.gradle.api.resources.ReadableResource; -import org.gradle.api.resources.ResourceException; - -final class LauncherResource implements ReadableResource { - static final ReadableResource instance = new LauncherResource(); - - private final URL url; - - private LauncherResource() { - url = getClass().getResource(String.format("/META-INF/%s", getDisplayName())); - } - - @Override - @Nonnull - @SneakyThrows - public InputStream read() throws ResourceException { - return url.openStream(); - } - - @Override - public String getDisplayName() { - return getBaseName() + ".tar"; - } - - @Override - @SneakyThrows - public URI getURI() { - return url.toURI(); - } - - @Override - public String getBaseName() { - return "executable-jar-launcher"; - } -} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 7dd5cb9..ce05809 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,17 @@ woggioniMavenRepositoryUrl=https://mvn.woggioni.net/ +version.myGraglePlugins=2021.10 version.gradle=7.2 version.lombok=1.18.16 version.junitJupiter=5.7.2 -version.junitPlatform=1.7.0 \ No newline at end of file +version.junitPlatform=1.7.0 +version.bnd=5.3.0 +version.felix.config.admin=1.9.20 +version.felix=7.0.1 +version.felix.scr=2.1.30 +version.felix.security=2.8.2 +version.osgi=7.0.0 +version.osgi.cm=1.6.0 +version.osgi.service.component=1.4.0 +version.osgi.function=1.1.0 +version.osgi.promise=1.1.1 diff --git a/osgi-app/build.gradle b/osgi-app/build.gradle new file mode 100644 index 0000000..9488134 --- /dev/null +++ b/osgi-app/build.gradle @@ -0,0 +1,66 @@ +plugins { + id "java-gradle-plugin" +} + +version = "0.1" + +childProjects.forEach {name, child -> + child.with { + apply plugin: 'maven-publish' + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + publishing { + repositories { + maven { + url = woggioniMavenRepositoryUrl + } + } + publications { + maven(MavenPublication) { + from(components["java"]) + } + } + } + } +} + +evaluationDependsOnChildren() + +configurations { + embedded { + transitive = false + visible = false + canBeConsumed = false + } +} + +dependencies { + embedded project(path: "osgi-simple-bootstrapper", configuration: 'tar') + embedded project(path: "osgi-simple-bootstrapper-api") + embedded project(path: "osgi-simple-bootstrapper-application") + + implementation group: 'biz.aQute.bnd', name: 'biz.aQute.bnd.gradle', version: getProperty('version.bnd') + implementation group: 'biz.aQute.bnd', name: 'biz.aQute.bndlib', version: getProperty('version.bnd') + + ['annotationProcessor', 'testCompileOnly', 'testAnnotationProcessor'].each { conf -> + add(conf, [group: "org.projectlombok", name: "lombok", version: getProperty('version.lombok')]) + } +} + +jar { + into("META-INF") { + from(configurations.embedded) + } +} + +gradlePlugin { + plugins { + osgiAppPlugin { + id = 'net.woggioni.gradle.osgi-app' + implementationClass = 'net.woggioni.gradle.osgi.app.OsgiAppPlugin' + } + } +} diff --git a/osgi-app/osgi-simple-bootstrapper-api/build.gradle b/osgi-app/osgi-simple-bootstrapper-api/build.gradle new file mode 100644 index 0000000..15ece8a --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-api/build.gradle @@ -0,0 +1,11 @@ +plugins { + id 'java-library' +} + +group = "net.woggioni.osgi" +version = "0.1" + +dependencies { + compileOnly group: 'org.osgi', name: 'osgi.annotation', version: getProperty('version.osgi') + compileOnly group: 'org.osgi', name: 'osgi.core', version: getProperty('version.osgi') +} \ No newline at end of file diff --git a/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/Application.java b/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/Application.java new file mode 100644 index 0000000..5afaaab --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/Application.java @@ -0,0 +1,5 @@ +package net.woggioni.osgi.simple.bootstrapper.api; + +public interface Application { + int run(String[] args); +} diff --git a/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/FrameworkService.java b/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/FrameworkService.java new file mode 100644 index 0000000..da2cf65 --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/FrameworkService.java @@ -0,0 +1,7 @@ +package net.woggioni.osgi.simple.bootstrapper.api; + +public interface FrameworkService { + String getMainApplicationComponentName(); + String[] getArgs(); + void setExitCode(int exitCode); +} \ No newline at end of file diff --git a/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/package-info.java b/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/package-info.java new file mode 100644 index 0000000..124f3bd --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-api/src/main/java/net/woggioni/osgi/simple/bootstrapper/api/package-info.java @@ -0,0 +1,4 @@ +@Export +package net.woggioni.osgi.simple.bootstrapper.api; + +import org.osgi.annotation.bundle.Export; \ No newline at end of file diff --git a/osgi-app/osgi-simple-bootstrapper-application/build.gradle b/osgi-app/osgi-simple-bootstrapper-application/build.gradle new file mode 100644 index 0000000..05c7a8a --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-application/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java-library' + id 'biz.aQute.bnd.builder' +} + +group = "net.woggioni.osgi" +version = "0.1" + +dependencies { + compileOnly group: 'org.osgi', name: 'osgi.annotation', version: getProperty('version.osgi') + compileOnly group: 'org.osgi', name: 'osgi.core', version: getProperty('version.osgi') + compileOnly group: 'org.osgi', name: 'osgi.cmpn', version: getProperty('version.osgi') + compileOnly group: 'org.osgi', + name: 'org.osgi.service.component.annotations', + version: getProperty('version.osgi.service.component') + + compileOnly project(":osgi-app:osgi-simple-bootstrapper-api") + + runtimeOnly group: 'org.apache.felix', name: 'org.apache.felix.scr', version: getProperty('version.felix.scr') + runtimeOnly group: 'org.osgi', name: 'org.osgi.util.function', version: getProperty('version.osgi.function') + runtimeOnly group: 'org.osgi', name: 'org.osgi.util.promise', version: getProperty('version.osgi.promise') +} + +jar { + bnd '''\ + Import-Package: !lombok, * + ''' +} \ No newline at end of file diff --git a/osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/ApplicationRunner.java b/osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/ApplicationRunner.java new file mode 100644 index 0000000..d27932d --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/ApplicationRunner.java @@ -0,0 +1,46 @@ +package net.woggioni.osgi.simple.bootstrapper.application; + +import lombok.SneakyThrows; +import lombok.extern.java.Log; +import net.woggioni.osgi.simple.bootstrapper.api.Application; +import net.woggioni.osgi.simple.bootstrapper.api.FrameworkService; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.service.component.ComponentContext; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ServiceScope; + +import java.util.Objects; +import java.util.logging.Level; + +@Log +@Component(scope = ServiceScope.SINGLETON) +public class ApplicationRunner { + + private final Application application; + + @Activate + @SneakyThrows + public ApplicationRunner(@Reference ServiceReference ref, BundleContext bundleContext, ComponentContext componentContext) { + application = bundleContext.getService(ref); + String componentName = (String) ref.getProperty("component.name"); + ServiceReference frameworkServiceReference = bundleContext.getServiceReference(FrameworkService.class); + FrameworkService frameworkService = bundleContext.getService(frameworkServiceReference); + String mainApplicationComponentName = frameworkService.getMainApplicationComponentName(); + if(mainApplicationComponentName == null || Objects.equals(mainApplicationComponentName, componentName)) { + Application application = bundleContext.getService(ref); + try { + frameworkService.setExitCode(application.run(frameworkService.getArgs())); + } catch(Exception ex) { + log.log(Level.SEVERE, ex, ex::getMessage); + frameworkService.setExitCode(1); + } finally { + bundleContext.getBundle(0).stop(); + bundleContext.ungetService(ref); + } + } + } +} + diff --git a/osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/package-info.java b/osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/package-info.java new file mode 100644 index 0000000..02a72a2 --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper-application/src/main/java/net/woggioni/osgi/simple/bootstrapper/application/package-info.java @@ -0,0 +1 @@ +package net.woggioni.osgi.simple.bootstrapper.application; diff --git a/osgi-app/osgi-simple-bootstrapper/build.gradle b/osgi-app/osgi-simple-bootstrapper/build.gradle new file mode 100644 index 0000000..0feb7cc --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper/build.gradle @@ -0,0 +1,36 @@ +plugins { + id 'java-library' +} + +group = "net.woggioni.osgi" +version = "0.1" + +configurations { + tar { + visible = true + canBeConsumed = true + transitive = false + } +} + +dependencies { + compileOnly group: 'org.osgi', name: 'osgi.annotation', version: getProperty('version.osgi') + compileOnly group: 'org.osgi', name: 'osgi.core', version: getProperty('version.osgi') + compileOnly group: 'org.osgi', + name: 'org.osgi.service.component.annotations', + version: getProperty('version.osgi.service.component') + + compileOnly project(":osgi-app:osgi-simple-bootstrapper-api") +} + +Provider tarTaskProvider = tasks.register("tar", Tar) { + archiveFileName = "${project.name}.tar" + from(project.tasks.named(JavaPlugin.JAR_TASK_NAME) + .flatMap { it.archiveFile } + .map { it.getAsFile() } + .map(project.&zipTree)) +} + +artifacts { + tar tarTaskProvider +} diff --git a/osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/Bootstrapper.java b/osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/Bootstrapper.java new file mode 100644 index 0000000..a561d80 --- /dev/null +++ b/osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/Bootstrapper.java @@ -0,0 +1,370 @@ +package net.woggioni.osgi.simple.bootstrapper; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import net.woggioni.osgi.simple.bootstrapper.api.FrameworkService; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AllPermission; +import java.security.CodeSource; +import java.security.Permission; +import java.security.PermissionCollection; +import java.security.Policy; +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.function.Consumer; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RequiredArgsConstructor +enum BundleState { + STARTING(Bundle.STARTING, "starting"), + INSTALLED(Bundle.INSTALLED, "installed"), + STOPPING(Bundle.STOPPING, "stopping"), + ACTIVE(Bundle.ACTIVE, "active"), + RESOLVED(Bundle.RESOLVED, "resolved"), + UNINSTALLED(Bundle.UNINSTALLED, "uninstalled"); + + @Getter + private final int code; + + @Getter + private final String description; + + public static BundleState fromCode(int code) { + return Arrays.stream(values()) + .filter(it -> it.code == code) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Unknown bundle state with code $code")); + } +} + +@RequiredArgsConstructor +class FrameworkListener implements org.osgi.framework.FrameworkListener { + private static final Logger log = Logger.getLogger(FrameworkListener.class.getName()); + private final Framework framework; + + private static String eventMessage(FrameworkEvent evt) { + StringBuilder sb = new StringBuilder(); + Bundle bundle =evt.getBundle(); + sb.append("Bundle "); + sb.append(bundle.getSymbolicName()); + sb.append("-"); + sb.append(bundle.getVersion()); + sb.append(':'); + Optional.ofNullable(evt.getThrowable()) + .map(Throwable::getMessage) + .ifPresent(sb::append); + return sb.toString(); + } + + @Override + public void frameworkEvent(FrameworkEvent evt) { + switch (evt.getType()) { + case FrameworkEvent.ERROR: + log.log(Level.SEVERE, evt.getThrowable(), + () -> eventMessage(evt)); + break; + case FrameworkEvent.WARNING: + log.log(Level.WARNING, evt.getThrowable(), () -> eventMessage(evt)); + break; + case FrameworkEvent.INFO: + log.log(Level.INFO, evt.getThrowable(), () -> eventMessage(evt)); + break; + case FrameworkEvent.STARTED: + log.log(Level.INFO, () -> String.format("OSGI framework '%s' started", + framework.getClass().getName())); + break; + case FrameworkEvent.WAIT_TIMEDOUT: + log.log(Level.WARNING, () -> String.format("OSGI framework '%s' did not stop", + framework.getClass().getName())); + break; + case FrameworkEvent.STOPPED: + log.log(Level.INFO, () -> String.format("OSGI framework '%s' stopped", + framework.getClass().getName())); + break; + } + } +} + +final class BundleListener implements org.osgi.framework.BundleListener { + private static final Logger log = Logger.getLogger(BundleListener.class.getName()); + + @Override + public void bundleChanged(BundleEvent evt) { + Bundle bundle = evt.getBundle(); + log.fine(() -> String.format("Bundle-Location: %s, " + + "Bundle ID: %s, Bundle-SymbolicName: %s, Bundle-Version: %s, State: %s", + bundle.getLocation(), + bundle.getBundleId(), + bundle.getSymbolicName(), + bundle.getVersion(), + BundleState.fromCode(bundle.getState()).getDescription())); + } +} + +class AllPolicy extends Policy { + private static final Logger log = Logger.getLogger(BundleListener.class.getName()); + private static final PermissionCollection all = new PermissionCollection() { + private final List PERMISSIONS = Collections.singletonList(new AllPermission()); + + { + setReadOnly(); + } + + @Override + public void add(Permission permission) {} + + @Override + public Enumeration elements() { + return Collections.enumeration(PERMISSIONS); + } + + @Override + public boolean implies(Permission permission) { + return true; + } + }; + + @Override + public PermissionCollection getPermissions(CodeSource codesource) { + if (codesource == null) + log.finest("Granting AllPermission to a bundle without codesource!"); + else + log.finest(String.format("Granting AllPermission to %s", codesource.getLocation())); + return all; + } + + @Override + public void refresh() { + log.finest("Policy refresh"); + } +} + +public class Bootstrapper implements AutoCloseable { + private static final String BUNDLE_LIST_FILE = "META-INF/bundles"; + private static final String SYSTEM_PACKAGES_FILE = "META-INF/system_packages"; + private static final String SYSTEM_PROPERTIES_FILE = "META-INF/system.properties"; + private static final String FRAMEWORK_PROPERTIES_FILE = "META-INF/framework.properties"; + private static final String MAIN_APPLICATION_COMPONENT_ATTRIBUTE = "Main-Application-Component"; + + private static final Logger log = Logger.getLogger(Bootstrapper.class.getName()); + + @SneakyThrows + private FrameworkFactory getFrameWorkFactory() { + ServiceLoader serviceLoader = ServiceLoader.load(FrameworkFactory.class); + for (FrameworkFactory frameworkFactory : serviceLoader) { + return frameworkFactory; + } + throw new IllegalStateException(String.format( + "No provider found for service '%s'", FrameworkFactory.class)); + } + + @SneakyThrows + private static String loadSystemPackages() { + URL resourceUrl = Bootstrapper.class.getClassLoader().getResource(SYSTEM_PACKAGES_FILE); + if(resourceUrl != null) { + try(BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream()))) { + return reader.lines().collect(Collectors.joining(",")); + } + } else { + throw new IOException(String.format("'%s' not found", SYSTEM_PACKAGES_FILE)); + } + } + + + private final String[] cliArgs; + private final Path storageDir; + private final Framework framework; + private final String mainApplicationComponentName; + + @Getter(AccessLevel.PACKAGE) + private int exitCode = 0; + + @SneakyThrows + private Bootstrapper(String[] cliArgs) { + this.cliArgs = cliArgs; + this.storageDir = Files.createTempDirectory("osgi-cache"); + + InputStream is = getClass().getClassLoader().getResourceAsStream(SYSTEM_PROPERTIES_FILE); + if(is != null) { + Properties props = new Properties(); + try(Reader reader = new InputStreamReader(is)) { + props.load(reader); + } + props.forEach((key, value) -> System.getProperties().computeIfAbsent(key, k -> value)); + } + + Stream> entryStream = Stream.of( + new AbstractMap.SimpleEntry<>(Constants.FRAMEWORK_STORAGE, storageDir.toString()), + new AbstractMap.SimpleEntry<>(Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT), + new AbstractMap.SimpleEntry<>(Constants.FRAMEWORK_SYSTEMPACKAGES_EXTRA, loadSystemPackages()) + ); + + + is = getClass().getClassLoader().getResourceAsStream(FRAMEWORK_PROPERTIES_FILE); + if(is != null) { + Properties props = new Properties(); + try(Reader reader = new InputStreamReader(is)) { + props.load(reader); + } + entryStream = Stream.concat(entryStream, + props.entrySet().stream() + .map(it -> new AbstractMap.SimpleEntry<>((String) it.getKey(), (String) it.getValue()))); + } + entryStream = Stream.concat(entryStream, + System.getProperties().entrySet().stream() + .map(it -> new AbstractMap.SimpleEntry<>((String) it.getKey(), (String) it.getValue()))); + Map frameworkPropertyMap = entryStream.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + framework = getFrameWorkFactory().newFramework(frameworkPropertyMap); + Manifest mf = new Manifest(); + URL manifestURL = getClass().getClassLoader().getResource(JarFile.MANIFEST_NAME); + if(manifestURL != null) { + mf.read(manifestURL.openStream()); + } + mainApplicationComponentName = mf.getMainAttributes().getValue(MAIN_APPLICATION_COMPONENT_ATTRIBUTE); + Policy.setPolicy(new AllPolicy()); + } + + @SneakyThrows + private void start() { + log.fine(() -> String.format("Starting OSGi framework %s %s", + framework.getClass().getName(), framework.getVersion())); + framework.start(); + framework.getBundleContext().addFrameworkListener(new FrameworkListener(framework)); + framework.getBundleContext().addBundleListener(new BundleListener()); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(BUNDLE_LIST_FILE); + BundleContext ctx = framework.getBundleContext(); + try(BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + reader.lines().forEach(new Consumer() { + @Override + @SneakyThrows + public void accept(String line) { + Enumeration it = getClass().getClassLoader().getResources(line); + while (it.hasMoreElements()) { + URL url = it.nextElement(); + try (InputStream bundleInputStream = url.openStream()) { + ctx.installBundle(url.toString(), bundleInputStream); + } + } + } + }); + } + ctx.registerService(FrameworkService.class, new FrameworkService() { + @Override + public String[] getArgs() { + return cliArgs; + } + + @Override + public void setExitCode(int exitCode) { + Bootstrapper.this.exitCode = exitCode; + } + + @Override + public String getMainApplicationComponentName() { + return mainApplicationComponentName; + } + }, null); + } + + @SneakyThrows + private void activate(long ...bundleId) { + BundleContext ctx = framework.getBundleContext(); + + if(bundleId.length == 0) { + for(Bundle bundle : ctx.getBundles()) { + if(bundle.getHeaders().get(Constants.FRAGMENT_HOST) == null && + (bundle.getState() == BundleState.INSTALLED.getCode() || bundle.getState() == BundleState.RESOLVED.getCode())) { + bundle.start(); + } + } + } else { + for(long id : bundleId) { + ctx.getBundle(id).start(); + } + } + } + + @Override + @SneakyThrows + public void close() { + if(framework.getState() == BundleState.ACTIVE.getCode() || framework.getState() == BundleState.STARTING.getCode()) { + framework.stop(); + waitForStop(); + Files.walk(storageDir) + .sorted(Comparator.reverseOrder()) + .forEach(new Consumer() { + @Override + @SneakyThrows + public void accept(Path path) { + Files.delete(path); + } + }); + } + } + + private void waitForStop() { + waitForStop(5000L); + } + + @SneakyThrows + private void waitForStop(long timeout) { + FrameworkEvent evt = framework.waitForStop(timeout); + switch (evt.getType()) { + case FrameworkEvent.ERROR: + log.log(Level.SEVERE, evt.getThrowable().getMessage(), evt.getThrowable()); + throw evt.getThrowable(); + case FrameworkEvent.WAIT_TIMEDOUT: + log.warning("OSGi framework shutdown timed out"); + break; + case FrameworkEvent.STOPPED: + break; + default: + throw new IllegalStateException(String.format("Unknown event type %d", evt.getType())); + } + } + + public static void main(String[] args) { + int exitCode; + Bootstrapper cnt = new Bootstrapper(args); + Runtime.getRuntime().addShutdownHook(new Thread(cnt::close)); + try { + cnt.start(); + cnt.activate(); + cnt.waitForStop(0L); + } finally { + cnt.close(); + } + exitCode = cnt.getExitCode(); + System.exit(exitCode); + } +} diff --git a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java b/osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/JavaAgentLauncher.java similarity index 68% rename from executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java rename to osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/JavaAgentLauncher.java index 7460868..3b57f21 100644 --- a/executable-jar/executable-jar-launcher/src/main/java/net/woggioni/executable/jar/JavaAgentLauncher.java +++ b/osgi-app/osgi-simple-bootstrapper/src/main/java/net/woggioni/osgi/simple/bootstrapper/JavaAgentLauncher.java @@ -1,4 +1,8 @@ -package net.woggioni.executable.jar; +package net.woggioni.osgi.simple.bootstrapper; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; import java.io.InputStream; import java.lang.instrument.Instrumentation; @@ -7,21 +11,22 @@ import java.net.URL; import java.util.Enumeration; import java.util.Map; import java.util.Properties; -import lombok.SneakyThrows; -class JavaAgentLauncher { +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class JavaAgentLauncher { @SneakyThrows static void premain(String agentArguments, Instrumentation instrumentation) { ClassLoader cl = JavaAgentLauncher.class.getClassLoader(); - Enumeration it = cl.getResources(Constants.JAVA_AGENTS_FILE); - while (it.hasMoreElements()) { + Enumeration it = cl.getResources("META-INF/javaAgents.properties"); + while(it.hasMoreElements()) { URL url = it.nextElement(); Properties properties = new Properties(); - try (InputStream is = url.openStream()) { - properties.load(is); + try(InputStream inputStream = url.openStream()) { + properties.load(inputStream); } - for (Map.Entry entry : properties.entrySet()) { + + for(Map.Entry entry : properties.entrySet()) { String agentClassName = (String) entry.getKey(); String agentArgs = (String) entry.getValue(); Class agentClass = cl.loadClass(agentClassName); @@ -34,4 +39,4 @@ class JavaAgentLauncher { static void agentmain(String agentArguments, Instrumentation instrumentation) { premain(agentArguments, instrumentation); } -} \ No newline at end of file +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BootstrapperResource.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BootstrapperResource.java new file mode 100644 index 0000000..2a43d30 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BootstrapperResource.java @@ -0,0 +1,69 @@ +package net.woggioni.gradle.osgi.app; + +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import javax.annotation.Nonnull; +import lombok.SneakyThrows; +import org.gradle.api.resources.ReadableResource; +import org.gradle.api.resources.ResourceException; + +abstract class EmbeddedResource implements ReadableResource { + private final URL url; + + private final String baseName; + private final String extension; + + protected EmbeddedResource(String baseName, String extension) { + this.baseName = baseName; + this.extension = extension; + url = getClass().getResource(String.format("/META-INF/%s.%s", baseName, extension)); + } + + @Override + @Nonnull + @SneakyThrows + public InputStream read() throws ResourceException { + return url.openStream(); + } + + @Override + public String getDisplayName() { + return getBaseName() + "." + extension; + } + + @Override + @SneakyThrows + public URI getURI() { + return url.toURI(); + } + + @Override + public String getBaseName() { + return baseName; + } +} + +final class BootstrapperResource extends EmbeddedResource { + static final ReadableResource instance = new BootstrapperResource(); + + private BootstrapperResource() { + super("osgi-simple-bootstrapper", "tar"); + } +} + +final class BootstrapperApiResource extends EmbeddedResource { + static final ReadableResource instance = new BootstrapperApiResource(); + + private BootstrapperApiResource() { + super("osgi-simple-bootstrapper-api", "jar"); + } +} + +final class BootstrapperApplicationResource extends EmbeddedResource { + static final ReadableResource instance = new BootstrapperApplicationResource(); + + private BootstrapperApplicationResource() { + super("osgi-simple-bootstrapper-application", "jar"); + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BundleFileTask.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BundleFileTask.java new file mode 100644 index 0000000..69f648c --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/BundleFileTask.java @@ -0,0 +1,65 @@ +package net.woggioni.gradle.osgi.app; + +import aQute.bnd.osgi.Constants; +import groovy.lang.Tuple2; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; +import org.osgi.framework.Version; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.jar.Attributes; +import java.util.jar.JarFile; + +public class BundleFileTask extends DefaultTask { + + private final File systemBundleFile; + private final List bundles; + + public BundleFileTask() { + systemBundleFile = new File(getTemporaryDir(), "bundles"); + bundles = new ArrayList<>(); + } + + @OutputFile + public File getOutputFile() { + return systemBundleFile; + } + + public void bundle(File file) { + bundles.add(file); + } + + @TaskAction + @SneakyThrows + public void run() { + Map, String> map = new TreeMap<>(); + for(File bundleFile : bundles) { + try(JarFile jarFile = new JarFile(bundleFile)) { + Attributes mainAttributes = jarFile.getManifest().getMainAttributes(); + String bundleSymbolicName = mainAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME); + String bundleVersion = mainAttributes.getValue(Constants.BUNDLE_VERSION); + if(bundleSymbolicName != null && bundleVersion != null) { + Tuple2 key = new Tuple2<>( + bundleSymbolicName, + Version.parseVersion(bundleVersion)); + map.put(key, bundleFile.getName()); + } + } + } + try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(systemBundleFile)))) { + for(Map.Entry, String> entry : map.entrySet()) { + writer.write("bundles/" + entry.getValue()); + writer.newLine(); + } + } + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkPropertyFileTask.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkPropertyFileTask.java new file mode 100644 index 0000000..899d0bb --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkPropertyFileTask.java @@ -0,0 +1,43 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Getter; +import lombok.Setter; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.File; +import java.io.Writer; +import java.nio.file.Files; +import java.util.Properties; + +public class FrameworkPropertyFileTask extends DefaultTask { + + @OutputFile + File getOutputFile() { + return new File(getTemporaryDir(), "framework.properties"); + } + + @Getter(onMethod_ = @Input) + final MapProperty frameworkProperties; + + @Inject + public FrameworkPropertyFileTask(ObjectFactory objects) { + frameworkProperties = objects.mapProperty(String.class, String.class); + } + + @TaskAction + @SneakyThrows + void run() { + try(Writer writer = Files.newBufferedWriter(getOutputFile().toPath())) { + Properties properties = new Properties(); + properties.putAll(frameworkProperties.get()); + properties.store(writer, null); + } + } +} \ No newline at end of file diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkRuntimeCheck.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkRuntimeCheck.java new file mode 100644 index 0000000..b44028d --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/FrameworkRuntimeCheck.java @@ -0,0 +1,42 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Set; + +@RequiredArgsConstructor(onConstructor_ = @Inject) +public class FrameworkRuntimeCheck extends DefaultTask { + + @Getter(onMethod_ = @InputFiles) + private final Provider confProvider; + + @TaskAction + @SneakyThrows + void run() { + Configuration conf = confProvider.get(); + Set files = conf.getFiles(); + URL[] urls = new URL[files.size()]; + int i = 0; + for(File file : files) { + urls[i++] = file.toURI().toURL(); + } + try { + new URLClassLoader(urls).loadClass("org.osgi.framework.launch.Framework"); + } catch (ClassNotFoundException cnfe) { + throw new GradleException( + String.format("No OSGi framework runtime found in '%s' configuration", conf.getName())); + } + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgent.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgent.java new file mode 100644 index 0000000..09f0cdb --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgent.java @@ -0,0 +1,9 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Data; + +@Data +public class JavaAgent { + private final String className; + private final String args; +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgentFileTask.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgentFileTask.java new file mode 100644 index 0000000..5df73b4 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/JavaAgentFileTask.java @@ -0,0 +1,46 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Getter; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.File; +import java.io.Writer; +import java.nio.file.Files; +import java.util.Collections; +import java.util.Properties; + +public class JavaAgentFileTask extends DefaultTask { + + @OutputFile + public File getOutputFile() { + return new File(getTemporaryDir(), "javaAgents.properties"); + } + + @Getter(onMethod_ = @Input) + private final ListProperty javaAgents; + + @Inject + public JavaAgentFileTask(ObjectFactory objects) { + javaAgents = objects.listProperty(JavaAgent.class) + .convention(getProject().provider(Collections::emptyList)); + } + + @TaskAction + @SneakyThrows + public void run() { + try(Writer writer = Files.newBufferedWriter(getOutputFile().toPath())) { + Properties props = new Properties(); + for(JavaAgent javaAgent : javaAgents.get()) { + props.setProperty(javaAgent.getClassName(), javaAgent.getArgs()); + } + props.store(writer, null); + } + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/MapBuilder.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/MapBuilder.java new file mode 100644 index 0000000..d95a717 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/MapBuilder.java @@ -0,0 +1,31 @@ +package net.woggioni.gradle.osgi.app; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Supplier; + +public class MapBuilder> { + + public static > MapBuilder getInstance(Supplier mapConstructor) { + return new MapBuilder<>(mapConstructor); + } + + private final M map; + + private MapBuilder(Supplier mapConstructor) { + map = mapConstructor.get(); + } + + public MapBuilder of(K key, V value) { + map.put(key, value); + return this; + } + + public M build() { + return map; + } + + public Map buildImmutable() { + return Collections.unmodifiableMap(map); + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppExtension.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppExtension.java new file mode 100644 index 0000000..cbcd308 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppExtension.java @@ -0,0 +1,72 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Getter; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +class OsgiAppExtension { + + public static final String BOOTSTRAPPER_GROUP = "net.woggioni.osgi"; + public static final String BOOTSTRAPPER_NAME = "osgi-simple-bootstrapper"; + public static final String BOOTSTRAPPER_API_NAME = "osgi-simple-bootstrapper-api"; + public static final String BOOTSTRAPPER_APPLICATION_NAME = "osgi-simple-bootstrapper-application"; + + public static final String BOOTSTRAP_CLASSPATH_CONFIGURATION_NAME = "bootstrapClasspath"; + public static final String BUNDLES_CONFIGURATION_NAME = "bundles"; + public static final String SYSTEM_PACKAGES_CONFIGURATION_NAME = "systemPackages"; + + + final List javaAgents = new ArrayList<>(); + + @Getter + private final MapProperty frameworkProperties; + + @Getter + private final MapProperty systemProperties; + + @Getter + private final Property bootstrapperVersion; + + @Getter + private final ListProperty systemPackages; + + @Getter + private final Property mainApplicationComponent; + + @Getter + private final Property frameworkFactoryClass; + + @Inject + public OsgiAppExtension(ObjectFactory objects) { + frameworkFactoryClass = objects.property(String.class) + .convention("org.apache.felix.framework.FrameworkFactory"); + frameworkProperties = objects.mapProperty(String.class, String.class).convention(new HashMap<>()); + systemProperties = objects.mapProperty(String.class, String.class).convention(new HashMap<>()); + bootstrapperVersion = objects.property(String.class); + systemPackages = objects.listProperty(String.class).convention( + Stream.of( + "sun.net.www.protocol.jar", + "sun.nio.ch", + "sun.security.x509", + "sun.security.ssl", + "javax.servlet", + "javax.transaction.xa;version=1.1.0", + "javax.xml.stream;version=1.0", + "javax.xml.stream.events;version=1.0", + "javax.xml.stream.util;version=1.0").collect(Collectors.toList())); + mainApplicationComponent = objects.property(String.class); + } + + public void agent(String className, String args) { + javaAgents.add(new JavaAgent(className, args)); + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppPlugin.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppPlugin.java new file mode 100644 index 0000000..cc0dcd8 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppPlugin.java @@ -0,0 +1,289 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.gradle.api.Action; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ExternalDependency; +import org.gradle.api.artifacts.ModuleVersionIdentifier; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.file.DuplicatesStrategy; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileCopyDetails; +import org.gradle.api.plugins.BasePluginExtension; +import org.gradle.api.plugins.ExtraPropertiesExtension; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.provider.Provider; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.JavaExec; +import org.gradle.api.tasks.bundling.Jar; + +import java.io.File; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.jar.JarFile; +import java.util.stream.Collectors; + +public class OsgiAppPlugin implements Plugin { + + private static void applyDependencySubstitution(Configuration conf) { + conf.getResolutionStrategy().dependencySubstitution(dependencySubstitutions -> { + //Replace Kotlin stdlib + dependencySubstitutions.substitute(dependencySubstitutions.module("org.jetbrains.kotlin:kotlin-stdlib-common")) + .using(dependencySubstitutions.module("org.jetbrains.kotlin:kotlin-osgi-bundle:$kotlinVersion")); + dependencySubstitutions.substitute(dependencySubstitutions.module("org.jetbrains.kotlin:kotlin-stdlib")) + .using(dependencySubstitutions.module("org.jetbrains.kotlin:kotlin-osgi-bundle:$kotlinVersion")); + dependencySubstitutions.substitute(dependencySubstitutions.module("org.jetbrains.kotlin:kotlin-reflect")) + .using(dependencySubstitutions.module("org.jetbrains.kotlin:kotlin-osgi-bundle:$kotlinVersion")); + }); + } + + private static Map createDependencyNotation(String group, String name, String version) { + Map m = new TreeMap<>(); + m.put("group", group); + m.put("name", name); + m.put("version", version); + return Collections.unmodifiableMap(m); + } + + private static Dependency createProjectDependency(DependencyHandler handler, String path) { + return createProjectDependency(handler, path, null); + } + + private static Dependency createProjectDependency(DependencyHandler handler, String path, String configuration) { + Map m = new TreeMap<>(); + m.put("path", path); + if(configuration != null) { + m.put("configuration", configuration); + } + return handler.project(m); + } + + @Getter + @EqualsAndHashCode + @RequiredArgsConstructor + private static final class NameAndGroup { + private final String group; + private final String name; + } + + private static void alignVersionsTo(ConfigurationContainer cc, String sourceConfigurationName, Configuration target) { + target.withDependencies(dependencies -> { + Map modules = new HashMap<>(); + cc.getByName(sourceConfigurationName).getIncoming().getResolutionResult().getAllComponents().forEach(resolvedComponentResult -> { + ModuleVersionIdentifier moduleVersionIdentifier = resolvedComponentResult.getModuleVersion(); + modules.put(new NameAndGroup(moduleVersionIdentifier.getGroup(), moduleVersionIdentifier.getName()), moduleVersionIdentifier.getVersion()); + }); + dependencies.configureEach(dependency -> { + if(dependency instanceof ExternalDependency) { + NameAndGroup needle = new NameAndGroup(dependency.getGroup(), dependency.getName()); + String constrainedVersion = modules.get(needle); + if(constrainedVersion != null) { + ((ExternalDependency) dependency).version(versionConstraint -> { + versionConstraint.require(constrainedVersion); + }); + } + } + }); + }); + } + + + @Override + @SneakyThrows + public void apply(Project project) { + project.getPlugins().apply("java-library"); + project.getPlugins().apply("biz.aQute.bnd.builder"); + + OsgiAppExtension osgiAppExtension = project.getExtensions().create("osgiApp", OsgiAppExtension.class); + + ExtraPropertiesExtension ext = project.getExtensions().getExtraProperties(); + + ConfigurationContainer cc = project.getConfigurations(); + + Provider runtimeClasspathConfiguration = cc.named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME, conf -> { + conf.getAttributes() + .attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, + project.getObjects().named(LibraryElements.class, LibraryElements.JAR)); + }); + + Configuration systemPackagesConf = cc.create(OsgiAppExtension.SYSTEM_PACKAGES_CONFIGURATION_NAME); + systemPackagesConf.setCanBeConsumed(false); + systemPackagesConf.setTransitive(false); + applyDependencySubstitution(systemPackagesConf); + + cc.named(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, conf -> conf.extendsFrom(systemPackagesConf)); + + Provider bootstrapClasspathConf = cc.register(OsgiAppExtension.BOOTSTRAP_CLASSPATH_CONFIGURATION_NAME, conf -> { + conf.setCanBeConsumed(false); + conf.setTransitive(true); + conf.extendsFrom(systemPackagesConf); + applyDependencySubstitution(conf); + alignVersionsTo(project.getConfigurations(), JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME, conf); + }); + + Provider bundlesConf = cc.register(OsgiAppExtension.BUNDLES_CONFIGURATION_NAME, conf -> { + conf.setTransitive(true); + conf.setCanBeConsumed(false); + conf.extendsFrom(cc.getAt(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); + conf.getAttributes().attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, project.getObjects().named(LibraryElements.class, LibraryElements.JAR)); + applyDependencySubstitution(conf); + }); + + cc.named(JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME, conf -> conf.extendsFrom(systemPackagesConf)); + + DependencyHandler dependencyHandler = project.getDependencies(); + + Provider> bootstrapperDependencyNotationProvider = project.provider(() -> + createDependencyNotation( + OsgiAppExtension.BOOTSTRAPPER_GROUP, + OsgiAppExtension.BOOTSTRAPPER_NAME, + osgiAppExtension.getBootstrapperVersion().get())); + Provider> bootstrapperApiDependencyNotationProvider = project.provider(() -> + createDependencyNotation( + OsgiAppExtension.BOOTSTRAPPER_GROUP, + OsgiAppExtension.BOOTSTRAPPER_API_NAME, + osgiAppExtension.getBootstrapperVersion().get())); + Provider> bootstrapperApplicationDependencyNotationProvider = project.provider(() -> + createDependencyNotation( + OsgiAppExtension.BOOTSTRAPPER_GROUP, + OsgiAppExtension.BOOTSTRAPPER_APPLICATION_NAME, + osgiAppExtension.getBootstrapperVersion().get())); + + project.getDependencies().addProvider(OsgiAppExtension.BOOTSTRAP_CLASSPATH_CONFIGURATION_NAME, + bootstrapperDependencyNotationProvider, + it -> {}); + project.getDependencies().addProvider(OsgiAppExtension.BUNDLES_CONFIGURATION_NAME, + bootstrapperApplicationDependencyNotationProvider, + it -> {}); + project.getDependencies().addProvider(OsgiAppExtension.SYSTEM_PACKAGES_CONFIGURATION_NAME, + bootstrapperApiDependencyNotationProvider, + it -> {}); + + Provider frameworkPropertyFileTaskProvider = project.getTasks().register("frameworkPropertyFile", PropertyFileTask.class, task -> { + task.getFileName().set("framework.properties"); + task.getProperties().set(osgiAppExtension.getFrameworkProperties()); + }); + + Provider systemPropertyFileTaskProvider = project.getTasks().register("systemPropertyFile", PropertyFileTask.class, task -> { + task.getFileName().set("system.properties"); + task.getProperties().set(osgiAppExtension.getSystemProperties()); + }); + + Provider javaAgentFileTask = project.getTasks().register("javaAgentFile", JavaAgentFileTask.class, task -> { + task.getJavaAgents().set(osgiAppExtension.javaAgents); + }); + + Provider jarFileTask = project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class); + Supplier bundlesSupplier = () -> { + return bundlesConf.get() + .minus(systemPackagesConf).plus(project.files(jarFileTask.get().getArchiveFile())) + .filter(new Spec() { + @Override + @SneakyThrows + public boolean isSatisfiedBy(File file) { + return OsgiAppUtils.isJar(file.getName()); + } + }); + }; + + Provider systemPackageExtraFileTask = + project.getTasks().register("systemPackageExtraFile", SystemPackageExtraFileTask.class, t -> { + t.getExtraSystemPackages().set(osgiAppExtension.getSystemPackages()); + }); + + Provider bundleFileTask = project.getTasks() + .register("bundleFile", BundleFileTask.class, task -> { + task.getInputs().files(jarFileTask); + FileCollection bundles = bundlesSupplier.get(); + task.getInputs().files(bundlesSupplier.get()); + bundles.forEach(task::bundle); + }); + + Provider osgiJar = project.getTasks().register("osgiJar", Jar.class, (Jar task) -> { + BasePluginExtension basePluginExtension = project.getExtensions() + .findByType(BasePluginExtension.class); + task.getDestinationDirectory().set(basePluginExtension.getDistsDirectory()); + task.getArchiveClassifier().set("osgi"); + + FileCollection bundles = bundlesSupplier.get(); + Provider jarTaskProvider = project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class); + task.getInputs().files(bootstrapClasspathConf); + task.getInputs().files(bundles); + task.getInputs().files(systemPackagesConf); +// task.getInputs().file(jarTaskProvider); + + task.exclude("META-INF/MANIFEST.MF"); + task.exclude("META-INF/*.SF"); + task.exclude("META-INF/*.DSA"); + task.exclude("META-INF/*.RSA"); + task.exclude("META-INF/*.EC"); + task.exclude("META-INF/DEPENDENCIES"); + task.exclude("META-INF/LICENSE"); + task.exclude("META-INF/NOTICE"); + task.exclude("module-info.class"); + task.exclude("META-INF/versions/*/module-info.class"); + task.setDuplicatesStrategy(DuplicatesStrategy.WARN); + MapBuilder> mapBuilder = MapBuilder.getInstance(TreeMap::new) + .of("Main-Class", "net.woggioni.osgi.simple.bootstrapper.Bootstrapper") + .of("Launcher-Agent-Class", "net.woggioni.osgi.simple.bootstrapper.JavaAgentLauncher") + .of("Can-Redefine-Classes", Boolean.toString(true)) + .of("Can-Retransform-Classes", Boolean.toString(true)); + if(osgiAppExtension.getMainApplicationComponent().isPresent()) { + mapBuilder.of("Main-Application-Component", osgiAppExtension.getMainApplicationComponent().get()); + } + task.getManifest().attributes(mapBuilder.buildImmutable()); + + + task.into("META-INF/", copySpec -> { + copySpec.from(javaAgentFileTask); + copySpec.from(bundleFileTask); + copySpec.from(frameworkPropertyFileTaskProvider); + copySpec.from(systemPropertyFileTaskProvider); + copySpec.from(systemPackageExtraFileTask); + }); + task.from(bootstrapClasspathConf.get().getFiles().stream().map(it -> { + if(it.isDirectory()) { + return it; + } else { + return project.zipTree(it); + } + }).collect(Collectors.toList())); + task.into("bundles", copySpec -> { + copySpec.from(bundlesSupplier.get(), copySpec2 -> + copySpec2.eachFile(new Action() { + @Override + @SneakyThrows + public void execute(FileCopyDetails fcd) { + try (JarFile jarFile = new JarFile(fcd.getFile())) { + if (!OsgiAppUtils.isBundle(jarFile)) { + fcd.exclude(); + } + } + } + }) + ); + }); + }); + + project.getTasks().register("osgiRun", JavaExec.class, javaExec -> { + javaExec.setClasspath(project.files(osgiJar)); + }); + + Provider frameworkRuntimeCheckTaskProvider = + project.getTasks().register("frameworkRuntimeCheck", + FrameworkRuntimeCheck.class, bootstrapClasspathConf); + project.getTasks().named(JavaPlugin.CLASSES_TASK_NAME, + t -> t.dependsOn(frameworkRuntimeCheckTaskProvider)); + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppUtils.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppUtils.java new file mode 100644 index 0000000..22b11d2 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/OsgiAppUtils.java @@ -0,0 +1,22 @@ +package net.woggioni.gradle.osgi.app; + +import aQute.bnd.osgi.Constants; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; + +import java.util.jar.JarFile; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class OsgiAppUtils { + + @SneakyThrows + static boolean isBundle(JarFile jarFile) { + java.util.jar.Attributes mainAttributes = jarFile.getManifest().getMainAttributes(); + return mainAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME) != null && mainAttributes.getValue(Constants.BUNDLE_VERSION) != null; + } + + static boolean isJar(String fileName) { + return fileName.endsWith(".jar"); + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/PropertyFileTask.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/PropertyFileTask.java new file mode 100644 index 0000000..8fd6906 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/PropertyFileTask.java @@ -0,0 +1,48 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Getter; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.File; +import java.io.Writer; +import java.nio.file.Files; +import java.util.Properties; + +public class PropertyFileTask extends DefaultTask { + + @Getter(onMethod_ = @Input) + private final Property fileName; + + @OutputFile + Provider getOutputFile() { + return fileName.map(name -> new File(getTemporaryDir(), name)); + } + + @Getter(onMethod_ = @Input) + final MapProperty properties; + + @Inject + public PropertyFileTask(ObjectFactory objects) { + fileName = objects.property(String.class); + properties = objects.mapProperty(String.class, String.class); + } + + @TaskAction + @SneakyThrows + void run() { + try(Writer writer = Files.newBufferedWriter(getOutputFile().get().toPath())) { + Properties properties = new Properties(); + properties.putAll(this.properties.get()); + properties.store(writer, null); + } + } +} \ No newline at end of file diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPackageExtraFileTask.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPackageExtraFileTask.java new file mode 100644 index 0000000..8fefc79 --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPackageExtraFileTask.java @@ -0,0 +1,99 @@ +package net.woggioni.gradle.osgi.app; + +import aQute.bnd.header.Attrs; +import aQute.bnd.header.OSGiHeader; +import lombok.Getter; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.artifacts.ArtifactCollection; +import org.gradle.api.artifacts.ResolvableDependencies; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; +import org.gradle.api.file.FileCollection; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStreamWriter; +import java.util.Map; +import java.util.NavigableSet; +import java.util.TreeSet; +import java.util.jar.JarFile; + +import static net.woggioni.gradle.osgi.app.OsgiAppUtils.isBundle; +import static net.woggioni.gradle.osgi.app.OsgiAppUtils.isJar; + +public class SystemPackageExtraFileTask extends DefaultTask { + + @Getter(onMethod_ = @Input) + private final ListProperty extraSystemPackages; + + final File systemPackagesExtraFile; + + @Inject + public SystemPackageExtraFileTask(ObjectFactory objects) { + extraSystemPackages = objects.listProperty(String.class); + systemPackagesExtraFile = new File(getTemporaryDir(), "system_packages"); + } + + @InputFiles + FileCollection getInputFiles() { + return getProject().getConfigurations().getByName("systemPackages"); + } + + @OutputFile + File getOutputFile() { + return systemPackagesExtraFile; + } + + @SneakyThrows + private NavigableSet buildPackagesExtra(ArtifactCollection artifacts) { + TreeSet result = new TreeSet<>(); + for(ResolvedArtifactResult resolvedArtifactResult : artifacts.getArtifacts()) { + if(isJar(resolvedArtifactResult.getFile().getName())) { + try(JarFile jarFile = new JarFile(resolvedArtifactResult.getFile(), true, JarFile.OPEN_READ, JarFile.runtimeVersion())) { + if (isBundle(jarFile)) { + String exportPackages = jarFile.getManifest().getMainAttributes().getValue(org.osgi.framework.Constants.EXPORT_PACKAGE); + if(exportPackages != null) { + for (Map.Entry entry : OSGiHeader.parseHeader(exportPackages).entrySet()) { + String exportString = entry.getKey() + ";" + entry.getValue().toString(); + result.add(exportString); + } + } + } else { + jarFile.versionedStream() + .filter(jarEntry -> jarEntry.getName().endsWith(".class")) + .forEach(jarEntry -> { + String entryName = jarEntry.getName(); + int end = entryName.lastIndexOf('/'); + if (end > 0) { + String export = entryName.substring(0, end).replace('/', '.'); + result.add(export); + } + }); + } + } + } + } + result.addAll(extraSystemPackages.get()); + return result; + } + + @TaskAction + @SneakyThrows + public void run() { + ResolvableDependencies resolvableDependencies = getProject().getConfigurations().getByName(OsgiAppExtension.SYSTEM_PACKAGES_CONFIGURATION_NAME).getIncoming(); + try(BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(systemPackagesExtraFile)))) { + for(String export : buildPackagesExtra(resolvableDependencies.getArtifacts())) { + writer.write(export); + writer.newLine(); + } + } + } +} diff --git a/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPropertyFileTask.java b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPropertyFileTask.java new file mode 100644 index 0000000..2fe0f0c --- /dev/null +++ b/osgi-app/src/main/java/net/woggioni/gradle/osgi/app/SystemPropertyFileTask.java @@ -0,0 +1,42 @@ +package net.woggioni.gradle.osgi.app; + +import lombok.Getter; +import lombok.SneakyThrows; +import org.gradle.api.DefaultTask; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; + +import javax.inject.Inject; +import java.io.File; +import java.io.Writer; +import java.nio.file.Files; +import java.util.Properties; + +public class SystemPropertyFileTask extends DefaultTask { + + @OutputFile + File getOutputFile() { + return new File(getTemporaryDir(), "system.properties"); + } + + @Getter(onMethod_ = @Input) + final MapProperty system; + + @Inject + public SystemPropertyFileTask(ObjectFactory objects) { + system = objects.mapProperty(String.class, String.class); + } + + @TaskAction + @SneakyThrows + void run() { + try(Writer writer = Files.newBufferedWriter(getOutputFile().toPath())) { + Properties properties = new Properties(); + properties.putAll(system.get()); + properties.store(writer, null); + } + } +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 81c264a..6314b68 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,13 +2,20 @@ pluginManagement { repositories { gradlePluginPortal() } + plugins { + id 'biz.aQute.bnd.builder' version getProperty('version.bnd') + } } rootProject.name = "my-gradle-plugins" -include("dependency-export") -include("jpms-check") -include("multi-release-jar") -include("wildfly") -include("lombok") -include("jlink") +include 'dependency-export' +include 'jpms-check' +include 'lombok' +include 'multi-release-jar' +include "osgi-app" +include 'osgi-app:osgi-simple-bootstrapper' +include 'osgi-app:osgi-simple-bootstrapper-api' +include 'osgi-app:osgi-simple-bootstrapper-application' +include 'wildfly' +include 'jlink'