blob: bb4c957278d1026db49e657149f074a61b5389fa [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.arthur.knight.winegrower;
import static java.util.Collections.list;
import static java.util.Objects.requireNonNull;
import static java.util.Optional.ofNullable;
import static org.apache.xbean.finder.ClassLoaders.findUrls;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.geronimo.arthur.spi.ArthurExtension;
import org.apache.geronimo.arthur.spi.model.ClassReflectionModel;
import org.apache.geronimo.arthur.spi.model.ResourceModel;
import org.apache.winegrower.extension.build.common.MetadataBuilder;
import org.apache.xbean.finder.UrlSet;
import org.apache.xbean.finder.util.Files;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;
import lombok.extern.slf4j.Slf4j;
@Slf4j // todo: handle manifest.mf generation (replace bundle extension?)
public class WinegrowerExtension implements ArthurExtension {
private DocumentBuilderFactory documentBuilderFactory;
private XPath xpath;
@Override
public void execute(final Context context) {
final Predicate<String> userFiler = context.createIncludesExcludes("extension.winegrower.", PredicateType.STARTS_WITH);
final boolean generateMetadata = Boolean.parseBoolean(ofNullable(context.getProperty("extension.winegrower.metadata.generate")).orElse("true"));
final boolean metadataAutoFiltering = Boolean.parseBoolean(ofNullable(context.getProperty("extension.winegrower.metadata.autoFiltering")).orElse("true"));
final String metadataDefaultJarName = context.getProperty("extension.winegrower.metadata.defaultJarName");
final Predicate<String> filter = name -> userFiler.test(name) &&
!name.startsWith("plexus-") &&
!name.startsWith("animal-sniffer") &&
!name.startsWith("winegrower-build") &&
!name.startsWith("winegrower-core") &&
!name.startsWith("winegrower-knight") &&
!name.startsWith("commons-") &&
!name.startsWith("xbean-") &&
!name.startsWith("osgi.");
final MetadataBuilder metadata = !generateMetadata ? null : new MetadataBuilder(metadataAutoFiltering);
try {
final Collection<Class<?>> classes = visitClasspath(context, filter, metadata, metadataDefaultJarName);
registerClasses(context, classes);
if (metadata != null && !metadata.getMetadata().isEmpty()) {
final Path workDir = Paths.get(requireNonNull(context.getProperty("workingDirectory"), "workingDirectory property"));
if (metadata.getMetadata().containsKey("index")) {
context.addNativeImageOption("-H:WinegrowerIndex=" +
dump(workDir, "winegrower.index.properties", metadata.getMetadata().get("index")));
}
if (metadata.getMetadata().containsKey("manifests")) {
context.addNativeImageOption("-H:WinegrowerManifests=" +
dump(workDir, "winegrower.manifests.properties", metadata.getMetadata().get("manifests")));
}
} else if (generateMetadata) {
log.info("No winegrower metadata to dump");
}
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
private String dump(final Path workDir, final String name, final Properties index) {
if (!java.nio.file.Files.isDirectory(workDir)) {
try {
java.nio.file.Files.createDirectories(workDir);
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
final Path out = workDir.resolve(name);
try (final OutputStream outputStream = java.nio.file.Files.newOutputStream(out)) {
index.store(outputStream, name);
} catch (final IOException e) {
throw new IllegalStateException(e);
}
log.info("Created '{}'", out);
return out.toAbsolutePath().toString();
}
private void registerClasses(final Context context, final Collection<Class<?>> classes) {
final Consumer<Class<?>> logger = log.isDebugEnabled() ? c -> log.debug("Registering '{}'", c) : c -> {};
classes.stream().peek(logger).map(it -> {
final ClassReflectionModel model = new ClassReflectionModel();
model.setName(it.getName());
model.setAllPublicConstructors(true);
model.setAllPublicMethods(true);
model.setAllDeclaredFields(true);
return model;
}).forEach(context::register);
}
private Collection<Class<?>> visitClasspath(final Context context, final Predicate<String> filter,
final MetadataBuilder metadata, final String metadataDefaultJarName) throws IOException {
final Collection<Class<?>> classes = new ArrayList<>();
new UrlSet(findUrls(Thread.currentThread().getContextClassLoader()))
.excludeJvm()
.getUrls()
.stream()
.map(Files::toFile)
.filter(file -> filter.test(file.getName()))
.map(File::toPath)
.forEach(jarOrDirectory -> {
if (java.nio.file.Files.isDirectory(jarOrDirectory)) {
browseDirectory(context, jarOrDirectory, classes, metadata, metadataDefaultJarName);
} else if (jarOrDirectory.getFileName().toString().endsWith(".jar")) {
browseJar(context, jarOrDirectory, classes, metadata);
} else {
log.info("Ignoring '{}'", jarOrDirectory);
return;
}
if (metadata != null) {
metadata.afterJar();
}
});
return classes;
}
private void browseJar(final Context context, final Path jarOrDirectory,
final Collection<Class<?>> classes, final MetadataBuilder metadata) {
try (final JarFile jar = new JarFile(jarOrDirectory.toFile())) {
if (metadata == null) { // winegrower metadata
Stream.of("index", "manifests")
.map(it -> "WINEGROWER-INF/" + it + ".properties")
.map(jar::getEntry)
.filter(Objects::nonNull)
.forEach(it -> context.register(resource(it.getName())));
}
// activator if needed
final ZipEntry manifestEntry = jar.getEntry("META-INF/MANIFEST.MF");
if (manifestEntry != null) {
try (final InputStream inputStream = jar.getInputStream(manifestEntry)) {
final Manifest manifest = handleManifest(classes, inputStream, context);
if (metadata != null) {
metadata.onJar(jarOrDirectory.getFileName().toString(), manifest);
}
} catch (final IOException e) {
throw new IllegalStateException(e);
}
} else if (metadata != null) {
metadata.onJar(jarOrDirectory.getFileName().toString(), null);
}
list(jar.entries()).stream()
.peek(e -> { // register metadata
if (metadata != null) {
metadata.onFile(e.getName());
}
})
// SCR and friends
.filter(e -> e.getName().startsWith("OSGI-INF/"))
.filter(e -> isOSGiInfDescriptor(e.getName()))
.peek(e -> {
if (e.getName().endsWith(".xml")) {
try (final InputStream stream = jar.getInputStream(e)) {
registerScrComponentsIfNeeded(jar + "#" + e.getName(), stream, classes, context);
} catch (final IOException ex) {
throw new IllegalStateException(ex);
}
}
})
.forEach(it -> context.register(resource("OSGI-INF/" + it.getName())));
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
private void browseDirectory(final Context context, final Path directory,
final Collection<Class<?>> classes, final MetadataBuilder metadata,
final String metadataDefaultJarName) {
// winegrower metadata
if (metadata == null) {
final Path winegrowerInf = directory.resolve("WINEGROWER-INF");
if (java.nio.file.Files.isDirectory(winegrowerInf)) {
Stream.of("index", "manifests")
.map(it -> it + ".properties")
.filter(it -> java.nio.file.Files.exists(winegrowerInf.resolve(it)))
.forEach(it -> context.register(resource("WINEGROWER-INF/" + it)));
}
}
// activator if needed
final Path manifest = directory.resolve("META-INF/MANIFEST.MF");
if (java.nio.file.Files.exists(manifest)) {
try (final InputStream inputStream = java.nio.file.Files.newInputStream(manifest)) {
handleManifest(classes, inputStream, context);
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
// SCR and friends
final Path osgiInf = directory.resolve("OSGI-INF");
if (java.nio.file.Files.isDirectory(osgiInf)) {
try {
java.nio.file.Files.list(osgiInf)
.filter(path -> isOSGiInfDescriptor(path.getFileName().toString()))
.peek(it -> {
if (it.getFileName().toString().endsWith(".xml")) {
try (final InputStream stream = java.nio.file.Files.newInputStream(it)) {
registerScrComponentsIfNeeded(it.toString(), stream, classes, context);
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
})
.forEach(it -> context.register(resource("OSGI-INF/" + it.getFileName())));
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
// finally init the metadata if needed
if (metadata != null) {
metadata.visitFolder(ofNullable(metadataDefaultJarName).orElseGet(() -> { // bad heuristic to not get a NPE
final AtomicReference<Path> current = new AtomicReference<>(directory);
while (Stream.of("classes", "target").anyMatch(it -> it.equals(current.get().getFileName().toString())) &&
current.get().getParent() != null &&
(java.nio.file.Files.exists(current.get().getParent().resolve("pom.xml")) ||
current.get().getParent().getParent() != null &&
java.nio.file.Files.exists(current.get().getParent().getParent().resolve("pom.xml")))) {
current.set(current.get().getParent());
}
return current.get().getFileName().toString();
}), directory, new SimpleFileVisitor<Path>() {});
}
}
private boolean isOSGiInfDescriptor(final String filename) {
return filename.endsWith(".xml") || filename.endsWith(".properties");
}
private Manifest handleManifest(final Collection<Class<?>> classes, final InputStream inputStream, final Context context) throws IOException {
final Manifest mf = new Manifest(inputStream);
ofNullable(mf.getMainAttributes().getValue("Bundle-Activator")).ifPresent(activator -> {
try {
classes.add(context.loadClass(activator));
} catch (final IllegalStateException e) {
log.info("Missing class: {}", activator);
}
});
return mf;
}
private void registerScrComponentsIfNeeded(final String source, final InputStream stream, final Collection<Class<?>> classes,
final Context context) {
try {
ensureXmlIsInitialized();
final Document document = documentBuilderFactory.newDocumentBuilder().parse(stream);
xpath.reset();
final String implementation = xpath.evaluate("/*[local-name()='component']/implementation/@class", document.getDocumentElement());
if (implementation != null && !implementation.isEmpty()) {
context.findHierarchy(context.loadClass(implementation)).forEach(classes::add);
}
} catch (final XPathExpressionException | ParserConfigurationException | IOException e) {
throw new IllegalStateException(e);
} catch (final SAXException sax) {
log.warn("Can't read xml {}", source);
} catch (final IllegalStateException e) {
log.info("Missing class: {}", e.getMessage());
}
}
private void ensureXmlIsInitialized() throws ParserConfigurationException {
if (documentBuilderFactory == null) {
documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
xpath = XPathFactory.newInstance().newXPath();
}
}
private ResourceModel resource(final String name) {
final ResourceModel resource = new ResourceModel();
resource.setPattern(Pattern.quote(name));
return resource;
}
}