blob: 70768c4509ea2e29fb62eb6620ae7fef1ffc72f8 [file] [log] [blame]
/**
* Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.winegrower;
import org.apache.winegrower.deployer.OSGiBundleLifecycle;
import org.apache.winegrower.scanner.StandaloneScanner;
import org.apache.winegrower.scanner.manifest.HeaderManifestContributor;
import org.apache.winegrower.scanner.manifest.KarafCommandManifestContributor;
import org.apache.winegrower.scanner.manifest.ManifestContributor;
import org.apache.winegrower.scanner.manifest.OSGIInfContributor;
import org.apache.winegrower.scanner.manifest.OSGiCDIManifestContributor;
import org.apache.winegrower.scanner.manifest.RequirementManifestContributor;
import org.apache.winegrower.service.BundleRegistry;
import org.apache.winegrower.service.DefaultConfigurationAdmin;
import org.apache.winegrower.service.DefaultEventAdmin;
import org.apache.winegrower.service.OSGiServices;
import org.apache.winegrower.service.Slf4jOSGiLoggerFactory;
import org.osgi.framework.ServiceReference;
import org.osgi.service.cm.ConfigurationAdmin;
import org.osgi.service.cm.ConfigurationListener;
import org.osgi.service.event.EventAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
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.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.Locale.ROOT;
import static java.util.Optional.ofNullable;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
public interface Ripener extends AutoCloseable {
Configuration getConfiguration();
long getStartTime();
Ripener start();
void stop();
OSGiServices getServices();
BundleRegistry getRegistry();
ConfigurationAdmin getConfigurationAdmin();
EventAdmin getEventAdmin();
@Override
void close();
class Configuration {
private static final Collection<String> DEFAULT_EXCLUSIONS = asList(
"slf4j-",
"xbean-",
"org.osgi.",
"opentest4j-",
"junit-platform-",
"junit-jupiter-",
"debugger-agent",
"asm-"
);
private File workDir = new File(System.getProperty("java.io.tmpdir"), "karaf-boot_" + UUID.randomUUID().toString());
private Predicate<String> jarFilter = it -> DEFAULT_EXCLUSIONS.stream().anyMatch(it::startsWith);
private Collection<String> scanningIncludes;
private Collection<String> scanningExcludes;
private Collection<String> ignoredBundles = emptyList();
private Collection<ManifestContributor> manifestContributors = Stream.concat(
// built-in
Stream.of(
new HeaderManifestContributor(), new RequirementManifestContributor(),
new OSGIInfContributor(),
new KarafCommandManifestContributor(),
new OSGiCDIManifestContributor()),
// extensions
StreamSupport.stream(ServiceLoader.load(ManifestContributor.class).spliterator(), false)
).collect(toList());
// known bundles
private List<String> prioritizedBundles = asList(
"org.apache.aries.blueprint.core",
"org.apache.aries.blueprint.cm",
"pax-web-extender-whiteboard",
"org.apache.aries.jax.rs.whiteboard",
"pax-web-runtime",
"org.apache.aries.cdi");
public Collection<String> getIgnoredBundles() {
return ignoredBundles;
}
public void setIgnoredBundles(final Collection<String> ignoredBundles) {
this.ignoredBundles = ignoredBundles;
}
public List<String> getPrioritizedBundles() {
return prioritizedBundles;
}
public void setPrioritizedBundles(final List<String> prioritizedBundles) {
this.prioritizedBundles = prioritizedBundles;
}
public Collection<ManifestContributor> getManifestContributors() {
return manifestContributors;
}
public void setManifestContributors(final Collection<ManifestContributor> manifestContributors) {
this.manifestContributors = manifestContributors;
}
public Collection<String> getScanningIncludes() {
return scanningIncludes;
}
public void setScanningIncludes(final Collection<String> scanningIncludes) {
this.scanningIncludes = scanningIncludes;
}
public Collection<String> getScanningExcludes() {
return scanningExcludes;
}
public void setScanningExcludes(final Collection<String> scanningExcludes) {
this.scanningExcludes = scanningExcludes;
}
public File getWorkDir() {
return workDir;
}
public void setWorkDir(final File workDir) {
this.workDir = workDir;
}
public void setJarFilter(final Predicate<String> jarFilter) {
this.jarFilter = jarFilter;
}
public Predicate<String> getJarFilter() {
return jarFilter;
}
}
class Impl implements Ripener {
private static final Logger LOGGER = LoggerFactory.getLogger(Ripener.class);
private final ConfigurationAdmin configurationAdmin;
private final EventAdmin eventAdmin;
private final OSGiServices services;
private final BundleRegistry registry;
private final Configuration configuration;
private long startTime = -1;
public Impl(final Configuration configuration) {
this.configuration = configuration;
final Collection<ConfigurationListener> configurationListeners = new ArrayList<>();
final Collection<DefaultEventAdmin.EventHandlerInstance> eventListeners = new ArrayList<>();
this.services = new OSGiServices(this, configurationListeners, eventListeners);
this.registry = new BundleRegistry(services, configuration);
this.configurationAdmin = loadConfigurationAdmin(configurationListeners);
this.eventAdmin = loadEventAdmin(eventListeners);
registerBuiltInService(ConfigurationAdmin.class, this.configurationAdmin, new Hashtable<>());
registerBuiltInService(EventAdmin.class, this.eventAdmin, new Hashtable<>());
registerBuiltInService(org.osgi.service.log.LoggerFactory.class, loadLoggerFactory(), new Hashtable<>());
try (final InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream("winegrower.properties")) {
loadConfiguration(stream);
} catch (final IOException e) {
LOGGER.warn(e.getMessage());
}
}
public <T> void registerBuiltInService(final Class<T> type, final T impl, final Dictionary<String, Object> props) {
if (Boolean.getBoolean("winegrower.builtin.services." + type.getName() + ".skip")) {
return;
}
this.services.registerService(new String[]{type.getName()}, impl, props, this.registry.getBundles().get(0L).getBundle());
}
private ConfigurationAdmin loadConfigurationAdmin(final Collection<ConfigurationListener> configurationListeners) {
final Iterator<ConfigurationAdmin> configurationAdminIterator = ServiceLoader.load(ConfigurationAdmin.class).iterator();
if (configurationAdminIterator.hasNext()) {
return configurationAdminIterator.next();
}
return new DefaultConfigurationAdmin(new HashMap<>(), configurationListeners) {
@Override
protected ServiceReference<ConfigurationAdmin> getSelfReference() {
return (ServiceReference<ConfigurationAdmin>) services.getServices().iterator().next().getReference();
}
};
}
private org.osgi.service.log.LoggerFactory loadLoggerFactory() {
final Iterator<org.osgi.service.log.LoggerFactory> eventAdminIterator = ServiceLoader.load(org.osgi.service.log.LoggerFactory.class).iterator();
if (eventAdminIterator.hasNext()) {
return eventAdminIterator.next();
}
return new Slf4jOSGiLoggerFactory();
}
private EventAdmin loadEventAdmin(final Collection<DefaultEventAdmin.EventHandlerInstance> listeners) {
final Iterator<EventAdmin> eventAdminIterator = ServiceLoader.load(EventAdmin.class).iterator();
if (eventAdminIterator.hasNext()) {
return eventAdminIterator.next();
}
return new DefaultEventAdmin(
listeners,
Integer.getInteger("winegrower.builtin.services." + EventAdmin.class.getName() + ".pool.core", Math.max(Runtime.getRuntime().availableProcessors(), 2)));
}
public void loadConfiguration(final InputStream stream) throws IOException {
final Properties embedConfig = new Properties();
if (stream != null) {
embedConfig.load(stream);
if (!embedConfig.isEmpty()) {
loadConfiguration(embedConfig);
}
}
}
// case insensitive
public void loadConfiguration(final Properties embedConfig) {
final Map<Object, Method> setters = Stream.of(this.configuration.getClass().getMethods())
.filter(it -> it.getName().startsWith("set") && it.getParameterCount() == 1)
.collect(toMap(it ->
(Character.toLowerCase(it.getName().charAt(3)) + it.getName().substring(4)).toLowerCase(ROOT),
identity()));
final Collection<String> matched = new ArrayList<>();
embedConfig.stringPropertyNames().stream().filter(it -> setters.containsKey(it.toLowerCase(ROOT))).forEach(key -> {
final String value = embedConfig.getProperty(key);
if (value == null) {
return;
}
final String keyLowerCase = key.toLowerCase(ROOT);
final Method setter = setters.get(keyLowerCase);
matched.add(keyLowerCase);
final Class<?> type = setter.getParameters()[0].getType();
try {
if (type == String.class) {
setter.invoke(this.configuration, value);
return;
} else if (type == File.class) {
setter.invoke(this.configuration, new File(value));
return;
}
// from here all parameters are lists
final Collection<String> asList = Stream.of(value.split(","))
.map(String::trim)
.filter(it -> !it.isEmpty())
.collect(toList());
if (type == Predicate.class) { // Predicate<String> + startsWith logic
final Predicate<String> predicate =
val -> val != null && asList.stream().anyMatch(val::startsWith);
setter.invoke(this.configuration, predicate);
} else if (type == List.class) {
setter.invoke(this.configuration, asList);
} else if (type == Collection.class
&& ManifestContributor.class == ParameterizedType.class.cast(
setter.getParameters()[0].getParameterizedType()).getActualTypeArguments()[0]) {
final ClassLoader loader = Thread.currentThread().getContextClassLoader();
setter.invoke(this.configuration, asList.stream()
.map(it -> {
try {
return loader.loadClass(it);
} catch (final ClassNotFoundException e) {
throw new IllegalArgumentException(e);
}
})
.collect(toList()));
} else if (type == Collection.class) { // Collection<String>
setter.invoke(this.configuration, asList);
} else {
throw new IllegalArgumentException("Unsupported: " + setter);
}
} catch (final IllegalAccessException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException e) {
throw new IllegalArgumentException(e.getTargetException());
}
});
if (DefaultConfigurationAdmin.class.isInstance(configurationAdmin)) {
final DefaultConfigurationAdmin dca = DefaultConfigurationAdmin.class.cast(configurationAdmin);
embedConfig.stringPropertyNames()
.stream()
.filter(it -> it.startsWith("winegrower.service."))
.peek(matched::add)
.forEach(key -> dca.getProvidedConfiguration().put(key, embedConfig.getProperty(key)));
}
embedConfig.stringPropertyNames().stream().filter(it -> !matched.contains(it.toLowerCase(ROOT)))
.forEach(it -> LOGGER.warn("Didn't match configuration {}, did you mispell it?", it));
}
@Override
public Configuration getConfiguration() {
return configuration;
}
@Override
public long getStartTime() {
return startTime;
}
@Override
public synchronized Ripener start() {
startTime = System.currentTimeMillis();
LOGGER.info("Starting Apache Winegrower application on {}",
LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZoneId.systemDefault()));
final StandaloneScanner scanner = new StandaloneScanner(configuration, registry.getFramework());
final AtomicLong bundleIdGenerator = new AtomicLong(1);
Stream.concat(Stream.concat(
scanner.findOSGiBundles().stream(),
scanner.findPotentialOSGiBundles().stream()),
scanner.findEmbeddedClasses().stream())
.sorted(this::compareBundles)
.map(it -> new OSGiBundleLifecycle(
it.getManifest(), it.getJar(),
services, registry, configuration,
bundleIdGenerator.getAndIncrement(),
it.getFiles()))
.peek(OSGiBundleLifecycle::start)
.peek(it -> registry.getBundles().put(it.getBundle().getBundleId(), it))
.forEach(bundle -> LOGGER.debug("Bundle {}", bundle));
return this;
}
@Override
public synchronized void stop() {
LOGGER.info("Stopping Apache Winegrower application on {}", LocalDateTime.now());
final Map<Long, OSGiBundleLifecycle> bundles = registry.getBundles();
bundles.values().stream()
.sorted((o1, o2) -> (int) (o2.getBundle().getBundleId() - o1.getBundle().getBundleId()))
.forEach(OSGiBundleLifecycle::stop);
bundles.clear();
if (configuration.getWorkDir().exists()) {
try {
Files.walkFileTree(configuration.getWorkDir().toPath(), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(final Path dir, final IOException exc) throws IOException {
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
} catch (final IOException e) {
LOGGER.warn("Can't delete work directory", e);
}
}
if (DefaultEventAdmin.class.isInstance(eventAdmin)) {
DefaultEventAdmin.class.cast(eventAdmin).close();
}
}
@Override
public OSGiServices getServices() {
return services;
}
@Override
public BundleRegistry getRegistry() {
return registry;
}
@Override
public ConfigurationAdmin getConfigurationAdmin() {
return configurationAdmin;
}
@Override
public EventAdmin getEventAdmin() {
return eventAdmin;
}
@Override // for try with resource syntax
public void close() {
stop();
}
private int compareBundles(final StandaloneScanner.BundleDefinition bundle1, final StandaloneScanner.BundleDefinition bundle2) {
final String id1 = getBundleId(bundle1);
final String id2 = getBundleId(bundle2);
final int index1 = matchPriorities(id1);
final int index2 = matchPriorities(id2);
if (index1 == index2) {
return id1.compareTo(id2);
}
if (index1 == -1) {
return 1;
}
if (index2 == -1) {
return -1;
}
return index1 - index2;
}
private String getBundleId(final StandaloneScanner.BundleDefinition bundle) {
return ofNullable(bundle.getJar()).map(File::getName)
.orElseGet(() -> Stream.of("Bundle-SymbolicName", "Bundle-Name")
.map(k -> bundle.getManifest().getMainAttributes().getValue(k))
.findFirst()
.orElseGet(() -> Long.toString(System.identityHashCode(bundle))));
}
private int matchPriorities(final String name) {
return configuration.getPrioritizedBundles().stream()
.filter(name::startsWith)
.findFirst()
.map(it -> configuration.getPrioritizedBundles().indexOf(it))
.orElse(-1);
}
}
static Ripener create(final Configuration configuration) {
// we can plug a SPI later on here if needed
return new Impl(configuration);
}
static void main(final String[] args) {
final CountDownLatch latch = new CountDownLatch(1);
final Configuration configuration = new Configuration();
ofNullable(System.getProperty("winegrower.ripener.configuration.workdir"))
.map(String::valueOf)
.map(File::new)
.ifPresent(configuration::setWorkDir);
ofNullable(System.getProperty("winegrower.ripener.configuration.prioritizedBundles"))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(configuration::setPrioritizedBundles);
ofNullable(System.getProperty("winegrower.ripener.configuration.ignoredBundles"))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(configuration::setIgnoredBundles);
ofNullable(System.getProperty("winegrower.ripener.configuration.scanningIncludes"))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(configuration::setScanningIncludes);
ofNullable(System.getProperty("winegrower.ripener.configuration.scanningExcludes"))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(configuration::setScanningExcludes);
ofNullable(System.getProperty("winegrower.ripener.configuration.manifestContributors"))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.map(it -> asList(it.split(",")))
.ifPresent(contributors -> {
configuration.setManifestContributors(contributors.stream().map(clazz -> {
try {
return Thread.currentThread().getContextClassLoader().loadClass(clazz).getConstructor().newInstance();
} catch (final InstantiationException | NoSuchMethodException | IllegalAccessException
| ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException e) {
throw new IllegalArgumentException(e.getTargetException());
}
}).map(ManifestContributor.class::cast).collect(toList()));
});
ofNullable(System.getProperty("winegrower.ripener.configuration.jarFilter"))
.map(String::valueOf)
.filter(it -> !it.isEmpty())
.ifPresent(filter -> {
try {
configuration.setJarFilter((Predicate<String>) Thread.currentThread().getContextClassLoader().loadClass(filter)
.getConstructor().newInstance());
} catch (final InstantiationException | NoSuchMethodException | IllegalAccessException | ClassNotFoundException e) {
throw new IllegalArgumentException(e);
} catch (final InvocationTargetException e) {
throw new IllegalArgumentException(e.getTargetException());
}
});
final Ripener main = new Impl(configuration).start();
Runtime.getRuntime().addShutdownHook(new Thread() {
{
setName(getClass().getName() + "-shutdown-hook");
}
@Override
public void run() {
main.stop();
latch.countDown();
}
});
try {
latch.await();
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}