JdkModules.java
/*-
* #%L
* io.earcam.instrumental.archive.jpms
* %%
* Copyright (C) 2018 earcam
* %%
* SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
*
* You <b>must</b> choose to accept, in full - any individual or combination of
* the following licenses:
* <ul>
* <li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
* <li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
* <li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
* <li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
* </ul>
* #L%
*/
package io.earcam.instrumental.archive.jpms.auto;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Objects;
import io.earcam.instrumental.module.jpms.ModuleInfo;
import io.earcam.unexceptional.Closing;
/**
* <p>
* JdkModules class.
* </p>
*
*/
public final class JdkModules extends AbstractPackageModuleMapper {
private static final String META_INF = "META-INF";
public static final String CACHE_FILENAME = "jpms.cache";
public static final String PROPERTY_JDK_HOME = "instrumental.jdk.home";
static final Path DEFAULT_DIRECTORY = Paths.get("target", "classes", META_INF);
static final String JDK_HOME = "/usr/lib/jvm/java-10-oracle/";
private static volatile List<ModuleInfo> modules;
/**
* Note: JDK 9+ home may be set via system property, see {@link #PROPERTY_JDK_HOME}
*
* @param args single optional argument; the output directory
* @throws java.io.IOException
* @throws java.lang.InterruptedException
*/
public static void main(String[] args) throws IOException, InterruptedException
{
Path outputFile = outputFile(args);
Path jdk9Home = jdk9Home();
String script = runnableScript(outputFile);
execute(jdk9Home, script);
}
private static Path outputFile(String[] args)
{
Path output = outputDirectory(args);
output.toFile().mkdirs();
return output.resolve(CACHE_FILENAME);
}
private static Path outputDirectory(String[] args)
{
return (args.length == 0) ? DEFAULT_DIRECTORY : Paths.get(args[0]);
}
private static Path jdk9Home()
{
return Paths.get(System.getProperty(PROPERTY_JDK_HOME, JDK_HOME));
}
private static String runnableScript(Path outputFile)
{
return script(outputFile).replace('\n', ' ') + '\n';
}
@SuppressWarnings("squid:S1192")
private static String script(Path output)
{
return "import static java.util.stream.Collectors.toSet; \n" +
"\n" +
"import java.io.File; \n" +
"import java.io.FileOutputStream; \n" +
"import java.io.IOException; \n" +
"import java.io.ObjectOutputStream; \n" +
"import java.io.UncheckedIOException; \n" +
"import java.lang.module.ModuleFinder; \n" +
"import java.util.List; \n" +
"import java.util.Set; \n" +
"import java.util.TreeSet; \n" +
"\n" +
"import io.earcam.instrumental.module.jpms.ExportModifier; \n" +
"import io.earcam.instrumental.module.jpms.ModuleInfo; \n" +
"import io.earcam.instrumental.module.jpms.ModuleInfoBuilder; \n" +
"import io.earcam.instrumental.module.jpms.ModuleModifier; \n" +
"import io.earcam.instrumental.module.jpms.RequireModifier; \n" +
"\n" +
"\n" +
" List<ModuleInfo> l = ModuleFinder.ofSystem().findAll().stream() \n" +
" .map(java.lang.module.ModuleReference::descriptor) \n" +
" .map(d -> { \n" +
" ModuleInfoBuilder builder = ModuleInfo.moduleInfo() \n" +
" .named(d.name()) \n" +
" .withAccess(d.modifiers().stream() \n" +
" .map(Enum::name) \n" +
" .map(e -> Enum.valueOf(ModuleModifier.class, e)) \n" +
" .collect(toSet())) \n" +
" .packaging(new TreeSet<>(d.packages())) \n" +
" .using(new TreeSet<>(d.uses())); \n" +
"\n" +
" d.rawVersion().ifPresent(builder::versioned); \n" +
" d.mainClass().ifPresent(builder::launching); \n" +
"\n" +
" d.requires().forEach(r -> { \n" +
" Set<RequireModifier> modifiers = r.modifiers().stream() \n" +
" .map(Enum::name) \n" +
" .map(e -> Enum.valueOf(RequireModifier.class, e)) \n" +
" .collect(toSet()); \n" +
" builder.requiring(r.name(), modifiers, r.rawCompiledVersion().orElse(null)); \n" +
" }); \n" +
"\n" +
" d.exports().forEach(e -> { \n" +
" Set<ExportModifier> modifiers = e.modifiers().stream() \n" +
" .map(Enum::name) \n" +
" .map(m -> Enum.valueOf(ExportModifier.class, m)) \n" +
" .collect(toSet()); \n" +
" builder.exporting(e.source(), modifiers, new TreeSet<>(e.targets())); \n" +
" }); \n" +
"\n" +
" d.opens().forEach(e -> { \n" +
" Set<ExportModifier> modifiers = e.modifiers().stream() \n" +
" .map(Enum::name) \n" +
" .map(m -> Enum.valueOf(ExportModifier.class, m)) \n" +
" .collect(toSet()); \n" +
" builder.opening(e.source(), modifiers, new TreeSet<>(e.targets())); \n" +
" }); \n" +
"\n" +
" d.provides().forEach(p -> builder.providing(p.service(), new TreeSet<>(p.providers()))); \n" +
"\n" +
" return builder.construct(); \n" +
" }) \n" +
" .collect(java.util.stream.Collectors.toList()); \n" +
"\n" +
" try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(new File(\"" + output.toAbsolutePath() + "\")))) { \n" +
" oos.writeObject(l); \n" +
" } catch(IOException ioe) { \n" +
" throw new UncheckedIOException(ioe); \n" +
" } \n" +
// printed as individual chars concatenated, in case script error causes dumping
// of script source (and we're checking output for "DONE" aside from exit code)
" System.out.println(\"D\" + \"O\" + \"N\" + \"E\"); \n" +
"";
}
private static void execute(Path jdk9Home, String script) throws IOException, InterruptedException
{
Process process = launchJShell(jdk9Home);
executeScript(script, process);
processOutput(process);
}
private static Process launchJShell(Path jdk9Home) throws IOException
{
Path jshell = jdk9Home.resolve(Paths.get("bin", "jshell"));
String classpath = System.getProperty("java.class.path");
return new ProcessBuilder(jshell.toString(), "--class-path", classpath).start();
}
private static void executeScript(String script, Process process) throws IOException
{
try(OutputStream os = process.getOutputStream()) {
os.write(script.getBytes(StandardCharsets.UTF_8));
}
}
static void processOutput(Process process) throws IOException, InterruptedException
{
process.waitFor();
String output = new String(inputStreamToBytes(process.getInputStream()), UTF_8);
if(process.exitValue() != 0 || !output.contains("DONE")) {
String error = new String(inputStreamToBytes(process.getErrorStream()), UTF_8);
throw new IOException("output: " + output + "\nerror: " + error);
}
}
private static byte[] inputStreamToBytes(InputStream input) throws IOException
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int read = input.read();
while(read != -1) {
baos.write(read);
read = input.read();
}
return baos.toByteArray();
}
@Override
protected List<ModuleInfo> modules()
{
loadCache();
return modules;
}
private static synchronized void loadCache()
{
if(modules == null) {
String resource = META_INF + '/' + CACHE_FILENAME;
InputStream serial = JdkModules.class.getClassLoader().getResourceAsStream(resource);
Objects.requireNonNull(serial, "Unable to load " + resource);
modules = deserialize(serial);
}
}
@SuppressWarnings("unchecked")
static List<ModuleInfo> deserialize(InputStream in)
{
return List.class.cast(Closing.closeAfterApplying(ObjectInputStream::new, in, ObjectInputStream::readObject));
}
}