AbstractAsJarBuilder.java
/*-
* #%L
* io.earcam.instrumental.archive
* %%
* 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;
import static java.lang.reflect.Modifier.PUBLIC;
import static java.lang.reflect.Modifier.STATIC;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.jar.Attributes.Name.SEALED;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toSet;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Predicate;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import io.earcam.instrumental.reflect.Methods;
import io.earcam.instrumental.reflect.Types;
/**
* AsJar, configures an {@link Archive} as a JAR.
*/
public abstract class AbstractAsJarBuilder<T extends AsJarBuilder<T>>
implements AsJarBuilder<T>, ArchiveConfigurationPlugin, ManifestProcessor, ArchiveResourceListener, ArchiveResourceSource {
public static final String MANIFEST_VERSION = Name.MANIFEST_VERSION.toString();
public static final String MAIN_CLASS = Name.MAIN_CLASS.toString();
public static final String MANIFEST_PATH = "META-INF/MANIFEST.MF";
public static final String SPI_ROOT_PATH = "META-INF/services/";
public static final String CREATED_BY = "Created-By";
public static final String V1_0 = "1.0";
protected final BasicArchiveResourceSource source = new BasicArchiveResourceSource();
protected final Manifest manifest = new Manifest();
private final Map<String, Set<String>> spi = new HashMap<>();
private Predicate<String> packageMatcher = p -> false;
private Set<String> sealedPackages = new HashSet<>();
private boolean validate = true;
/**
* <p>
* Constructor for AsJar.
* </p>
*/
protected AbstractAsJarBuilder()
{
manifest.getMainAttributes().putValue(MANIFEST_VERSION, V1_0);
}
protected abstract T self();
@Override
public T disableValidation()
{
validate = false;
return self();
}
protected boolean validate()
{
return validate;
}
@OverridingMethodsMustInvokeSuper
@Override
public void added(ArchiveResource resource)
{
if(packageMatcher.test(resource.pkg())) {
sealedPackages.add(resource.pkg());
}
}
@OverridingMethodsMustInvokeSuper
@Override
public void process(Manifest manifest)
{
merge(this.manifest, manifest);
Map<String, Attributes> entries = manifest.getEntries();
for(String sealed : sealedPackages) {
entries.computeIfAbsent(sealed, k -> new Attributes())
.put(SEALED, Boolean.TRUE.toString());
}
}
@OverridingMethodsMustInvokeSuper
@Override
public void attach(ArchiveRegistrar core)
{
core.registerResourceListener(this);
core.registerManifestProcessor(this);
core.registerResourceSource(this);
}
@OverridingMethodsMustInvokeSuper
@Override
public Stream<ArchiveResource> drain(ResourceSourceLifecycle stage)
{
Stream<ArchiveResource> drained = source.drain(stage);
if(stage == ResourceSourceLifecycle.PRE_MANIFEST) {
List<ArchiveResource> spiSources = new ArrayList<>();
for(Map.Entry<String, Set<String>> e : spi.entrySet()) {
byte[] bytes = spiFileContent(e.getValue());
spiSources.add(new ArchiveResource(SPI_ROOT_PATH + e.getKey(), bytes));
}
drained = Stream.concat(drained, spiSources.stream());
}
return drained;
}
static byte[] spiFileContent(Collection<String> implementors)
{
return implementors.stream().collect(joining("\r\n")).getBytes(UTF_8);
}
/**
* AsJar API **
*
* @param mainClass a {@link java.lang.Class} object.
* @return a {@link io.earcam.instrumental.archive.AbstractAsJarBuilder} object.
*/
@Override
public T launching(Class<?> mainClass)
{
if(validate) {
AbstractAsJarBuilder.requireMainMethod(mainClass);
}
source.with(mainClass);
return launching(mainClass.getCanonicalName());
}
@Override
public T launching(String mainClass)
{
manifest.getMainAttributes().putValue(MAIN_CLASS, mainClass);
return self();
}
protected static void requireMainMethod(Class<?> type)
{
Method main = Methods.getMethod(type, "main", String[].class)
.orElseThrow(IllegalArgumentException::new);
if(!isPublicStaticVoid(main)) {
throw new IllegalArgumentException("'public static void main(String[] args)' method not found on " + type);
}
}
/**
* <p>
* isPublicStaticVoid.
* </p>
*
* @param method a {@link java.lang.reflect.Method} object.
* @return a boolean.
*/
protected static boolean isPublicStaticVoid(Method method)
{
return isPublicStatic(method) && method.getReturnType().equals(void.class);
}
private static boolean isPublicStatic(Method method)
{
return (method.getModifiers() & (PUBLIC | STATIC)) == (PUBLIC | STATIC);
}
/**
* <p>
* sealing.
* </p>
*
* @param packageMatcher a {@link java.util.function.Predicate} object.
* @return this builder.
*/
@Override
public T sealing(Predicate<String> packageMatcher)
{
this.packageMatcher = this.packageMatcher.or(packageMatcher);
return self();
}
/**
* <p>
* providing.
* </p>
*
* @param service a {@link java.lang.Class} object.
* @param implementations a {@link java.util.List} object.
* @return this builder.
*/
@Override
public T providing(Class<?> service, List<Class<?>> implementations)
{
if(validate) {
implementations.forEach(i -> requireImplements(i, service));
}
implementations.forEach(source::with);
Set<String> imps = implementations.stream()
.map(Class::getCanonicalName)
.collect(toSet());
return providing(service.getCanonicalName(), imps);
}
private static void requireImplements(Class<?> implementor, Class<?> implementee)
{
if(!Types.implementsAll(implementor, implementee)) {
throw new IllegalArgumentException(implementor + " does not implement " + implementee);
}
}
@Override
public T providing(String service, Set<String> implementations)
{
Set<String> imps = spi.computeIfAbsent(service, x -> new TreeSet<>());
imps.addAll(implementations);
return self();
}
/**
* @param manifest the manifest to merge from
* @return this builder.
*/
@Override
public T mergingManifest(Manifest manifest)
{
merge(manifest, this.manifest);
return self();
}
protected static void merge(Manifest sauce, Manifest sink)
{
sink.getMainAttributes().putAll(sauce.getMainAttributes());
sauce.getEntries().forEach((k, v) -> sink.getEntries().merge(k, v, (v1, v2) -> {
v1.putAll(v2);
return v1;
}));
}
@Override
public T withManifestHeader(String key, String value)
{
manifest.getMainAttributes().putValue(key, value);
return self();
}
}