| /** |
| * 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.felix.http.jetty", |
| "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(); |
| } |
| } |
| } |