DefaultAsJpmsModule.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;
import static io.earcam.instrumental.archive.ArchiveResourceSource.ResourceSourceLifecycle.PRE_MANIFEST;
import static io.earcam.instrumental.module.auto.Reader.reader;
import static io.earcam.instrumental.module.jpms.RequireModifier.MANDATED;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singleton;
import static java.util.stream.Collectors.toSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import io.earcam.instrumental.archive.AbstractAsJarBuilder;
import io.earcam.instrumental.archive.ArchiveRegistrar;
import io.earcam.instrumental.archive.ArchiveResource;
import io.earcam.instrumental.archive.ArchiveResourceListener;
import io.earcam.instrumental.archive.jpms.auto.ClasspathModules;
import io.earcam.instrumental.archive.jpms.auto.JdkModules;
import io.earcam.instrumental.module.auto.Reader;
import io.earcam.instrumental.module.jpms.ModuleInfo;
import io.earcam.instrumental.module.jpms.ModuleInfoBuilder;
/**
* <p>
* AsJpmsModule class.
* </p>
*
*/
class DefaultAsJpmsModule extends AbstractAsJarBuilder<AsJpmsModule> implements AsJpmsModule, ArchiveResourceListener {
private final ModuleInfoBuilder builder = ModuleInfo.moduleInfo();
class ExportMatcher {
final Predicate<String> matcher;
final String[] to;
final BiConsumer<String, String[]> method;
ExportMatcher(BiConsumer<String, String[]> method, Predicate<String> matcher, String... to)
{
this.method = method;
this.matcher = matcher;
this.to = to;
}
public void test(ArchiveResource resource)
{
String pkg = resource.pkg();
if(matcher.test(pkg)) {
method.accept(pkg, to);
}
}
}
class AutoRequiring {
final Map<String, Set<String>> imports = new HashMap<>();
final Reader reader = reader()
.addImportListener(imports::put)
.ignoreAnnotations()
.setImportedTypeReducer(Reader::typeToPackageReducer)
.setImportingTypeReducer(Reader::typeToPackageReducer);
void process(ArchiveResource resource)
{
reader.processClass(resource.bytes());
}
Set<String> imported()
{
Predicate<? super String> ownPackages = imports.keySet()::contains;
Stream<Set<String>> importsRequired = Stream.concat(
imports.values().stream(),
Stream.of(singleton("java.lang")));
return importsRequired
.flatMap(Set::stream)
.filter(ownPackages.negate())
.collect(toSet());
}
}
private final List<ExportMatcher> exportMatchers = new ArrayList<>();
private boolean listingPackages;
private AutoRequiring autoRequiring;
private List<PackageModuleMapper> packageModuleMappers = new ArrayList<>();
private boolean providingFromMetaInfServices;
DefaultAsJpmsModule()
{}
@Override
protected AsJpmsModule self()
{
return this;
}
@Override
public void added(ArchiveResource resource)
{
super.added(resource);
if(listingPackages) {
builder.packaging(resource.pkg());
}
if(resource.isQualifiedClass()) {
if(autoRequiring != null) {
autoRequiring.process(resource);
}
for(ExportMatcher matcher : exportMatchers) {
matcher.test(resource);
}
} else if(providingFromMetaInfServices && resource.name().startsWith(SPI_ROOT_PATH)) { // config optional
String service = resource.name().substring(SPI_ROOT_PATH.length());
String[] implementations = new String(resource.bytes(), UTF_8).split("\r?\n");
builder.providing(service, implementations);
}
}
@Override
public Stream<ArchiveResource> drain(ResourceSourceLifecycle stage)
{
Stream<ArchiveResource> drained = super.drain(stage);
if(stage == PRE_MANIFEST) {
resolveAutoImports();
ArchiveResource moduleInfo = new ArchiveResource("module-info.class", builder.construct().toBytecode());
drained = Stream.concat(drained, Stream.of(moduleInfo));
}
return drained;
}
private void resolveAutoImports()
{
if(autoRequiring == null) {
return;
}
Set<String> imports = autoRequiring.imported();
String name = ((ModuleInfo) builder).name();
for(PackageModuleMapper mapper : packageModuleMappers) {
mapper.moduleRequiredFor(name, imports.iterator())
.forEach(builder::requiring);
}
if(validate() && !imports.isEmpty()) {
throw new IllegalStateException("For module '" + name + "' unresolved imports remain: " + imports);
}
}
@Override
public void attach(ArchiveRegistrar core)
{
super.attach(core);
core.registerResourceListener(this);
}
@Override
public DefaultAsJpmsModule launching(Class<?> mainClass)
{
super.launching(mainClass);
builder.launching(cn(mainClass));
return this;
}
@Override
public AsJpmsModule launching(String mainClass)
{
super.launching(mainClass);
return this;
}
private static String cn(Class<?> type)
{
return type.getCanonicalName();
}
@Override
public AsJpmsModule providing(Class<?> service, List<Class<?>> implementations)
{
super.providing(service, implementations);
Set<String> imps = implementations.stream()
.map(DefaultAsJpmsModule::cn)
.collect(toSet());
return providing(cn(service), imps);
}
@Override
public AsJpmsModule providing(String contract, Set<String> concretes)
{
super.providing(contract, concretes);
builder.providing(contract, concretes);
return this;
}
/**
* <p>
* named.
* </p>
*
* @param moduleName a {@link java.lang.String} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule named(String moduleName)
{
builder.named(moduleName);
return this;
}
public AsJpmsModule versioned(String moduleVersion)
{
builder.versioned(moduleVersion);
return this;
}
/**
* <p>
* exporting... Composition of predicates is more powerful than regex or globs.
* Exports classes and resources
* </p>
*
* @param predicate a {@link java.util.function.Predicate} object.
* @param onlyToModules a {@link java.lang.String} object.
* @see java.util.regex.Pattern#asPredicate()
* @see Predicate#and(Predicate)
* @see Predicate#or(Predicate)
* @see Predicate#negate()
* @see Predicate#and(Predicate)
* @see Predicate#or(Predicate)
* @see Predicate#negate()
* @see Predicate#and(Predicate)
* @see Predicate#or(Predicate)
* @see Predicate#negate()
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule exporting(Predicate<String> predicate, String... onlyToModules)
{
exportMatchers.add(new ExportMatcher(builder::exporting, predicate, onlyToModules));
return this;
}
/**
* <p>
* opening.
* </p>
*
* @param predicate a {@link java.util.function.Predicate} object.
* @param onlyToModules a {@link java.lang.String} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule opening(Predicate<String> predicate, String... onlyToModules)
{
exportMatchers.add(new ExportMatcher(builder::opening, predicate, onlyToModules));
return this;
}
/**
* <p>
* requiring.
* </p>
*
* @param moduleName a {@link java.lang.String} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule requiring(String moduleName)
{
return requiring(moduleName, null);
}
/**
* <p>
* requiring.
* </p>
*
* @param moduleName a {@link java.lang.String} object.
* @param version a {@link java.lang.String} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule requiring(String moduleName, String version)
{
builder.requiring(moduleName, EnumSet.of(MANDATED), version);
return this;
}
/**
* <p>
* using.
* </p>
*
* @param service a {@link java.lang.Class} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule using(Class<?> service)
{
return using(service.getCanonicalName());
}
/**
* <p>
* using.
* </p>
*
* @param service a {@link java.lang.String} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule using(String service)
{
builder.using(service);
return this;
}
/**
* <p>
* listingPackages.
* </p>
*
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*/
public AsJpmsModule listingPackages()
{
listingPackages = true;
return this;
}
public AsJpmsModule autoRequiringClasspath()
{
return autoRequiring(new ClasspathModules());
}
public AsJpmsModule autoRequiringJdkModules()
{
return autoRequiring(new JdkModules());
}
/**
* <p>
* Note: if you also want the default {@link PackageModuleMapper} ({@link JdkModules} and {@link ClasspathModules})
* then you must also invoke {@link #autoRequiring()} or add them manually here
* </p>
*
* @param mappers a {@link io.earcam.instrumental.archive.jpms.PackageModuleMapper} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*
* @see #autoRequiring()
*/
public AsJpmsModule autoRequiring(PackageModuleMapper... mappers)
{
return autoRequiring(Arrays.asList(mappers));
}
/**
* <p>
* Note: if you also want the default {@link PackageModuleMapper} ({@link JdkModules} and {@link ClasspathModules})
* then you must also invoke {@link #autoRequiring()} or add them manually here
* </p>
*
* @param mappers a {@link java.lang.Iterable} object.
* @return a {@link io.earcam.instrumental.archive.jpms.DefaultAsJpmsModule} object.
*
* @see #autoRequiring()
*/
public AsJpmsModule autoRequiring(Iterable<PackageModuleMapper> mappers)
{
autoRequiring = new AutoRequiring();
mappers.forEach(packageModuleMappers::add);
return this;
}
public AsJpmsModule providingFromMetaInfServices(boolean enable)
{
this.providingFromMetaInfServices = enable;
return this;
}
}