diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 13dc139794..50e04c8728 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,38 @@ This page describes the noteworthy improvements provided by each release of Ecli ## 3.0.0 (under development) +### new sisu-osgi-connect + +The new sisu-osgi-connect provides an implementation for plexus according to the [Connect Specification](http://docs.osgi.org/specification/osgi.core/8.0.0/framework.connect.html#framework.connect) that allows to run an embedded OSGi Framework from the classpath of a maven-plugin. +As both, the maven plugin and the embedded framework, share the same classlaoder you can use the best of both worlds and interact seamless with them. + +This can be used in the following way: + +``` +@Component(role = MyPlexusComponent.class) +public class MyPlexusComponent { + @Requirement(hint = "connect") + private EquinoxServiceFactory serviceFactory; + + public void helloConnect() { + serviceFactory.getService(HelloWorldService.class).sayHello(); + } +} +``` + +For the setup you need to do the following: + +1. include any bundle you like to make up your plexus-osgi-connect framework as a dependency of your maven plugin +2. include a file `META-INF/sisu-connect.bundles` that list all your bundles you like to have installed in the format `bsn[,true]`, where `bsn` is the symbolid name and optionally you can control if your bundle has to be started or not +3. include the following additional dependency +``` + + org.eclipse.tycho + sisu-osgi-connect + ${tycho-version} + +``` + ### Deprecated Features The `tycho-compiler:compile` and `tycho-compiler:testCompile` option `requireJREPackageImports` is deprecated now and will produce a warning when used, bundles currently rely on this option should migrate to proper importing packages from the non java.* namespace. diff --git a/p2-maven-plugin/pom.xml b/p2-maven-plugin/pom.xml index 7c2518e33f..a97f9a23c6 100644 --- a/p2-maven-plugin/pom.xml +++ b/p2-maven-plugin/pom.xml @@ -72,9 +72,19 @@ 1.4.100 - org.eclipse.platform - org.eclipse.equinox.frameworkadmin - 2.2.0 + org.eclipse.platform + org.eclipse.equinox.frameworkadmin + 2.2.0 + + + org.eclipse.platform + org.eclipse.equinox.preferences + 3.10.1 + + + org.eclipse.platform + org.eclipse.equinox.security + 1.3.800 org.apache.maven @@ -102,11 +112,110 @@ org.eclipse.equinox.p2.updatesite 1.2.300 + org.eclipse.tycho tycho-embedder-api ${project.version} + + org.eclipse.tycho + sisu-osgi-api + ${project.version} + + + org.eclipse.tycho + sisu-osgi-connect + ${project.version} + + + + org.eclipse.platform + org.eclipse.equinox.p2.jarprocessor + 1.2.300 + + + + org.eclipse.platform + org.eclipse.equinox.concurrent + 1.2.100 + + + org.eclipse.platform + org.eclipse.equinox.simpleconfigurator + 1.4.0 + + + org.eclipse.platform + org.eclipse.equinox.app + 1.6.100 + + + org.eclipse.platform + org.eclipse.equinox.frameworkadmin.equinox + 1.2.200 + + + org.eclipse.platform + org.eclipse.equinox.simpleconfigurator.manipulator + 2.2.0 + + + + org.eclipse.platform + org.eclipse.equinox.p2.transport.ecf + 1.3.300 + + + org.eclipse.ecf + org.eclipse.ecf.identity + 3.9.402 + + + org.eclipse.ecf + org.eclipse.ecf.provider.filetransfer + 3.2.800 + + + org.eclipse.ecf + org.eclipse.ecf.filetransfer + 5.1.102 + + + org.eclipse.ecf + org.eclipse.ecf + 3.10.0 + + + + + org.apache.felix + org.apache.felix.scr + 2.2.2 + + + + org.osgi + org.osgi.service.component + 1.5.0 + + + + org.bouncycastle + bcpg-jdk15on + 1.70 + + + org.bouncycastle + bcprov-jdk15on + 1.70 + + + commons-io + commons-io + 2.11.0 + + diff --git a/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/DefaultProvisioningAgent.java b/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/DefaultProvisioningAgent.java index e850a3ded6..57df2e5d4e 100644 --- a/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/DefaultProvisioningAgent.java +++ b/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/DefaultProvisioningAgent.java @@ -12,15 +12,11 @@ *******************************************************************************/ package org.eclipse.tycho.p2maven; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import org.codehaus.plexus.component.annotations.Component; import org.codehaus.plexus.component.annotations.Requirement; import org.codehaus.plexus.logging.Logger; import org.eclipse.equinox.p2.core.IProvisioningAgent; -import org.eclipse.equinox.p2.core.spi.IAgentServiceFactory; -import org.osgi.framework.BundleContext; +import org.eclipse.sisu.equinox.EquinoxServiceFactory; @Component(role = IProvisioningAgent.class) public class DefaultProvisioningAgent implements IProvisioningAgent { @@ -28,38 +24,27 @@ public class DefaultProvisioningAgent implements IProvisioningAgent { @Requirement private Logger log; - @Requirement(hint = "plexus") - private BundleContext bundleContext; - - @Requirement(role = IAgentServiceFactory.class) - private Map factoryMap; - - private Map services = new ConcurrentHashMap(); + @Requirement(hint = "connect") + private EquinoxServiceFactory serviceFactory; @Override public Object getService(String serviceName) { - return services.computeIfAbsent(serviceName, role -> { - IAgentServiceFactory serviceFactory = factoryMap.get(role); - if (serviceFactory != null) { - return serviceFactory.createService(DefaultProvisioningAgent.this); - } - return null; - }); + return serviceFactory.getService(IProvisioningAgent.class).getService(serviceName); } @Override public void registerService(String serviceName, Object service) { - throw new UnsupportedOperationException(); + serviceFactory.getService(IProvisioningAgent.class).registerService(serviceName, service); } @Override public void stop() { - + throw new UnsupportedOperationException(); } @Override public void unregisterService(String serviceName, Object service) { - throw new UnsupportedOperationException(); + serviceFactory.getService(IProvisioningAgent.class).unregisterService(serviceName, service); } } diff --git a/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/InstallableUnitGenerator.java b/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/InstallableUnitGenerator.java index c50c93ffac..f3b5ea5262 100644 --- a/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/InstallableUnitGenerator.java +++ b/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/InstallableUnitGenerator.java @@ -41,10 +41,12 @@ import org.eclipse.equinox.internal.p2.publisher.eclipse.IProductDescriptor; import org.eclipse.equinox.internal.p2.updatesite.CategoryParser; import org.eclipse.equinox.internal.p2.updatesite.SiteModel; +import org.eclipse.equinox.p2.core.IProvisioningAgent; import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.equinox.p2.publisher.IPublisherAction; import org.eclipse.equinox.p2.publisher.eclipse.BundlesAction; import org.eclipse.equinox.p2.publisher.eclipse.Feature; +import org.eclipse.equinox.p2.repository.artifact.IArtifactRepositoryManager; import org.eclipse.tycho.PackagingType; import org.eclipse.tycho.p2maven.actions.AuthoredIUAction; import org.eclipse.tycho.p2maven.actions.CategoryDependenciesAction; @@ -53,7 +55,6 @@ import org.eclipse.tycho.p2maven.actions.ProductFile2; import org.eclipse.tycho.p2maven.helper.PluginRealmHelper; import org.eclipse.tycho.p2maven.io.MetadataIO; -import org.osgi.framework.BundleContext; import org.xml.sax.SAXException; /** @@ -71,9 +72,8 @@ public class InstallableUnitGenerator { private static final String KEY_UNITS = "InstallableUnitGenerator.units"; - // this requirement is here to bootstrap P2 service access - @Requirement(hint = "plexus") - private BundleContext bundleContext; + @Requirement + private IProvisioningAgent provisioningAgent; @Requirement(role = InstallableUnitProvider.class) private Map additionalUnitProviders; @@ -97,6 +97,7 @@ public class InstallableUnitGenerator { public Map> getInstallableUnits(Collection projects, MavenSession session) throws CoreException { + init(); Objects.requireNonNull(session); List errors = new CopyOnWriteArrayList(); Map> result = new ConcurrentHashMap>(); @@ -120,6 +121,13 @@ public Map> getInstallableUnits(Colle throw new CoreException(multiStatus); } + private void init() { + // this requirement is here to bootstrap P2 service access + // see https://github.com/eclipse-equinox/p2/issues/100 + // then this would not be required anymore + provisioningAgent.getService(IArtifactRepositoryManager.class); + } + /** * Computes the {@link IInstallableUnit}s for the given project, the computation * is cached unless forceUpdate is true meaning data is always @@ -136,6 +144,7 @@ public Map> getInstallableUnits(Colle public Collection getInstallableUnits(MavenProject project, MavenSession session, boolean forceUpdate) throws CoreException { + init(); Objects.requireNonNull(session); log.debug("Computing installable units for " + project + ", force update = " + forceUpdate); synchronized (project) { diff --git a/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/PlexusBundleContext.java b/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/PlexusBundleContext.java deleted file mode 100644 index 8990a13d85..0000000000 --- a/p2-maven-plugin/src/main/java/org/eclipse/tycho/p2maven/PlexusBundleContext.java +++ /dev/null @@ -1,219 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2022 Christoph Läubrich and others. - * This program and the accompanying materials - * are made available under the terms of the Eclipse Public License 2.0 - * which accompanies this distribution, and is available at - * https://www.eclipse.org/legal/epl-2.0/ - * - * SPDX-License-Identifier: EPL-2.0 - * - * Contributors: - * Christoph Läubrich - initial API and implementation - *******************************************************************************/ -package org.eclipse.tycho.p2maven; - -import java.io.File; -import java.io.InputStream; -import java.util.Collection; -import java.util.Collections; -import java.util.Dictionary; -import java.util.List; - -import org.codehaus.plexus.component.annotations.Component; -import org.codehaus.plexus.component.annotations.Requirement; -import org.codehaus.plexus.logging.Logger; -import org.codehaus.plexus.personality.plexus.lifecycle.phase.Disposable; -import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; -import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; -import org.eclipse.osgi.internal.framework.FilterImpl; -import org.osgi.framework.Bundle; -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleException; -import org.osgi.framework.BundleListener; -import org.osgi.framework.Filter; -import org.osgi.framework.FrameworkListener; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceListener; -import org.osgi.framework.ServiceObjects; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; - -//FIXME this should not be necessary at all see https://bugs.eclipse.org/bugs/show_bug.cgi?id=578387 -@Component(role = BundleContext.class, hint = "plexus") -public class PlexusBundleContext implements BundleContext, Initializable, Disposable { - - @Requirement - private Logger log; - - private List legacyActivators = List.of( - // see https://github.com/eclipse-equinox/p2/issues/100 - new org.eclipse.pde.internal.publishing.Activator() // - ); - - @Override - public String getProperty(String key) { - return System.getProperty(key); - } - - @Override - public Bundle getBundle() { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public Bundle installBundle(String location, InputStream input) throws BundleException { - throw new BundleException("this context does not support installations"); - } - - @Override - public Bundle installBundle(String location) throws BundleException { - throw new BundleException("this context does not support installations"); - } - - @Override - public Bundle getBundle(long id) { - return getBundle(); - } - - @Override - public Bundle[] getBundles() { - return new Bundle[0]; - } - - @Override - public void addServiceListener(ServiceListener listener, String filter) throws InvalidSyntaxException { - - } - - @Override - public void addServiceListener(ServiceListener listener) { - - } - - @Override - public void removeServiceListener(ServiceListener listener) { - - } - - @Override - public void addBundleListener(BundleListener listener) { - - } - - @Override - public void removeBundleListener(BundleListener listener) { - - } - - @Override - public void addFrameworkListener(FrameworkListener listener) { - - } - - @Override - public void removeFrameworkListener(FrameworkListener listener) { - - } - - @Override - public ServiceRegistration registerService(String[] clazzes, Object service, Dictionary properties) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public ServiceRegistration registerService(String clazz, Object service, Dictionary properties) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public ServiceRegistration registerService(Class clazz, S service, Dictionary properties) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public ServiceRegistration registerService(Class clazz, ServiceFactory factory, - Dictionary properties) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public ServiceReference[] getServiceReferences(String clazz, String filter) throws InvalidSyntaxException { - return new ServiceReference[0]; - } - - @Override - public ServiceReference[] getAllServiceReferences(String clazz, String filter) throws InvalidSyntaxException { - return new ServiceReference[0]; - } - - @Override - public ServiceReference getServiceReference(String clazz) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public ServiceReference getServiceReference(Class clazz) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public Collection> getServiceReferences(Class clazz, String filter) - throws InvalidSyntaxException { - return Collections.emptyList(); - } - - @Override - public S getService(ServiceReference reference) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public boolean ungetService(ServiceReference reference) { - return true; - } - - @Override - public ServiceObjects getServiceObjects(ServiceReference reference) { - throw new IllegalStateException("this is not OSGi!"); - } - - @Override - public File getDataFile(String filename) { - return null; - } - - @Override - public Filter createFilter(String filter) throws InvalidSyntaxException { - return FilterImpl.newInstance(filter); - } - - @Override - public Bundle getBundle(String location) { - return getBundle(); - } - - @Override - public void initialize() throws InitializationException { - for (BundleActivator bundleActivator : legacyActivators) { - try { - bundleActivator.start(this); - } catch (Exception e) { - log.warn("Can't init " + bundleActivator.getClass() + "! (" + e + ")"); - } - } - } - - @Override - public void dispose() { - for (BundleActivator bundleActivator : legacyActivators) { - try { - bundleActivator.stop(this); - } catch (Exception e) { - log.warn("Can't init " + bundleActivator.getClass() + "! (" + e + ")"); - } - } - } - -} diff --git a/p2-maven-plugin/src/main/resources/META-INF/sisu-connect.bundles b/p2-maven-plugin/src/main/resources/META-INF/sisu-connect.bundles new file mode 100644 index 0000000000..98af23c4d3 --- /dev/null +++ b/p2-maven-plugin/src/main/resources/META-INF/sisu-connect.bundles @@ -0,0 +1,46 @@ +################################################# +# This contains a list of bundles that should # +# be included in the connect osgi framework # +# and if these should be started # +# Bundles are started in the given order # +################################################# + +org.apache.felix.scr,true +org.eclipse.equinox.common,true +org.eclipse.ecf,true +org.eclipse.ecf.filetransfer,true +org.eclipse.ecf.identity,true +org.eclipse.ecf.provider.filetransfer,true +org.eclipse.equinox.p2.core,true +org.eclipse.equinox.p2.artifact.repository,true +org.eclipse.equinox.p2.director,true +org.eclipse.equinox.p2.engine,true +org.eclipse.equinox.p2.jarprocessor,true +org.eclipse.equinox.p2.metadata,true +org.eclipse.equinox.p2.metadata.repository,true +org.eclipse.equinox.p2.publisher,true +org.eclipse.equinox.p2.publisher.eclipse,true +org.eclipse.equinox.p2.repository,true +org.eclipse.equinox.p2.transport.ecf,true +org.eclipse.equinox.p2.updatesite,true +org.eclipse.osgi.compatibility.state +org.eclipse.core.jobs +org.eclipse.equinox.app +org.eclipse.equinox.concurrent +org.eclipse.equinox.frameworkadmin +org.eclipse.equinox.frameworkadmin.equinox +org.eclipse.equinox.preferences +org.eclipse.equinox.registry,true +org.eclipse.equinox.security +org.eclipse.equinox.simpleconfigurator +org.eclipse.equinox.simpleconfigurator.manipulator +org.osgi.namespace.extender +org.osgi.service.component +org.osgi.service.prefs +org.osgi.util.function +org.osgi.util.promise +org.sat4j.core +org.sat4j.pb +org.tukaani.xz +bcpg +bcprov \ No newline at end of file diff --git a/sisu-osgi/pom.xml b/sisu-osgi/pom.xml index 41e54d7b91..d55d3fdc25 100644 --- a/sisu-osgi/pom.xml +++ b/sisu-osgi/pom.xml @@ -29,6 +29,7 @@ sisu-osgi-api sisu-equinox-embedder sisu-equinox-launching + sisu-osgi-connect diff --git a/sisu-osgi/sisu-osgi-connect/pom.xml b/sisu-osgi/sisu-osgi-connect/pom.xml new file mode 100644 index 0000000000..fc45c757f2 --- /dev/null +++ b/sisu-osgi/sisu-osgi-connect/pom.xml @@ -0,0 +1,56 @@ + + 4.0.0 + + org.eclipse.tycho + sisu-osgi + 3.0.0-SNAPSHOT + + sisu-osgi-connect + Sisu Implementation of the OSGi R8 Framework Connect Specification + + + + org.eclipse.tycho + sisu-osgi-api + ${project.version} + + + org.eclipse.platform + org.eclipse.osgi + + + org.osgi + org.osgi.service.component + 1.5.0 + + + org.eclipse.sisu + org.eclipse.sisu.plexus + + + org.codehaus.plexus + plexus-component-annotations + + + commons-io + commons-io + 2.11.0 + + + + + + + org.codehaus.plexus + plexus-component-metadata + + + + generate-metadata + + + + + + + \ No newline at end of file diff --git a/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusConnectContent.java b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusConnectContent.java new file mode 100644 index 0000000000..4a968bebcf --- /dev/null +++ b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusConnectContent.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2022 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.sisu.osgi.connect; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; + +import org.osgi.framework.connect.ConnectContent; +import org.osgi.framework.connect.ConnectModule; + +/** + * Implements the {@link ConnectContent} on top of a jar file and a classloader + * + */ +class PlexusConnectContent implements ConnectContent, ConnectModule { + private ClassLoader classLoader; + private Optional> header; + private JarFile jarFile; + private String location; + + public PlexusConnectContent(JarFile jarFile, ClassLoader classLoader) throws IOException { + this.jarFile = jarFile; + this.location = jarFile.getName(); + this.classLoader = classLoader; + Attributes attributes = jarFile.getManifest().getMainAttributes(); + Map headers = new LinkedHashMap(); + for (Entry entry : attributes.entrySet()) { + headers.put(entry.getKey().toString(), entry.getValue().toString()); + } + this.header = Optional.of(Collections.unmodifiableMap(headers)); + } + + @Override + public Optional getClassLoader() { + return Optional.of(classLoader); + } + + @Override + public Iterable getEntries() throws IOException { + return jarFile.stream().map(JarEntry::getName).collect(Collectors.toList()); + } + + @Override + public Optional getEntry(String path) { + final ZipEntry entry = jarFile.getEntry(path); + if (entry == null) { + return Optional.empty(); + } + return Optional.of(new ZipConnectEntry(jarFile, entry)); + } + + @Override + public Optional> getHeaders() { + return header; + } + + @Override + public void open() throws IOException { + if (jarFile == null) { + jarFile = new JarFile(location); + } + } + + @Override + public void close() throws IOException { + if (jarFile != null) { + try { + jarFile.close(); + } finally { + jarFile = null; + } + } + } + + private static final class ZipConnectEntry implements ConnectEntry { + + private ZipEntry entry; + private JarFile jarFile; + + public ZipConnectEntry(JarFile jarFile, ZipEntry entry) { + this.jarFile = jarFile; + this.entry = entry; + } + + @Override + public String getName() { + return entry.getName(); + } + + @Override + public long getContentLength() { + return entry.getSize(); + } + + @Override + public long getLastModified() { + return entry.getTime(); + } + + @Override + public InputStream getInputStream() throws IOException { + return jarFile.getInputStream(entry); + } + + } + + @Override + public ConnectContent getContent() throws IOException { + return this; + } + +} \ No newline at end of file diff --git a/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusFrameworkConnectServiceFactory.java b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusFrameworkConnectServiceFactory.java new file mode 100644 index 0000000000..4670139022 --- /dev/null +++ b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusFrameworkConnectServiceFactory.java @@ -0,0 +1,317 @@ +/******************************************************************************* + * Copyright (c) 2022 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.sisu.osgi.connect; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.Comparator; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ServiceLoader; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import org.apache.commons.io.FileUtils; +import org.codehaus.plexus.component.annotations.Component; +import org.codehaus.plexus.component.annotations.Requirement; +import org.codehaus.plexus.logging.Logger; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Disposable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.Initializable; +import org.codehaus.plexus.personality.plexus.lifecycle.phase.InitializationException; +import org.eclipse.sisu.equinox.EquinoxServiceFactory; +import org.eclipse.sisu.equinox.embedder.EmbeddedEquinox; +import org.eclipse.sisu.equinox.embedder.EquinoxLifecycleListener; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.framework.connect.ConnectFrameworkFactory; +import org.osgi.framework.launch.Framework; +import org.osgi.service.component.runtime.ServiceComponentRuntime; +import org.osgi.service.component.runtime.dto.ComponentConfigurationDTO; +import org.osgi.service.component.runtime.dto.ComponentDescriptionDTO; +import org.osgi.util.tracker.ServiceTracker; + +/** + * The {@link PlexusFrameworkConnectServiceFactory} provides a + * {@link EquinoxServiceFactory} using the Connect + * Specification that allows to connect the plexus-world with the maven + * world. + */ +@Component(role = EquinoxServiceFactory.class, hint = "connect") +public class PlexusFrameworkConnectServiceFactory implements Initializable, Disposable, EquinoxServiceFactory { + + private static final StackWalker WALKER = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE); + + static PlexusFrameworkConnectServiceFactory instance; + + @Requirement + private Logger log; + + final Map frameworkMap = new HashMap(); + + @Requirement(role = EquinoxLifecycleListener.class) + private Map lifecycleListeners; + + private Map, ServiceTracker> trackerMap = new ConcurrentHashMap, ServiceTracker>(); + + /** + * + * + * @param classloader the classloader to use for discovering bundles to add to + * the framework + * @return get (or creates) the Framework that is made of the given classloader + * @throws BundleException if creation of the framework failed + */ + synchronized Framework getFramework(ClassLoader classloader) throws BundleException { + Framework framework = frameworkMap.get(classloader); + if (framework != null) { + return framework; + } + Map bundleStartMap = readBundles(classloader); + Map p = new HashMap<>(); + p.put(Constants.FRAMEWORK_STORAGE, + System.getProperty("java.io.tmpdir") + File.separator + "plexus.osgi." + UUID.randomUUID()); + p.put(Constants.FRAMEWORK_BEGINNING_STARTLEVEL, "6"); + ServiceLoader sl = ServiceLoader.load(ConnectFrameworkFactory.class, classloader); + ConnectFrameworkFactory factory = sl.iterator().next(); + PlexusModuleConnector connector = new PlexusModuleConnector(classloader, log); + Framework osgiFramework = factory.newFramework(p, connector); + osgiFramework.init(new FrameworkListener() { + + @Override + public void frameworkEvent(FrameworkEvent event) { + log.info(event.toString()); + } + }); + frameworkMap.put(classloader, osgiFramework); + connector.installBundles(osgiFramework.getBundleContext(), bsn -> bundleStartMap.containsKey(bsn)); + osgiFramework.start(); + Map> bundles = Arrays.stream(osgiFramework.getBundleContext().getBundles()) + .collect(Collectors.groupingBy(Bundle::getSymbolicName)); + for (Entry entry : bundleStartMap.entrySet()) { + List list = bundles.get(entry.getKey()); + if (list == null) { + log.warn("Bundle " + entry.getKey() + " was not found in the framework!"); + } else if (entry.getValue()) { + for (Bundle bundle : list) { + try { + bundle.start(); + } catch (BundleException e) { + log.warn("Can't start bundle " + bundle.getSymbolicName() + " " + bundle.getVersion() + ": ", + e); + } + } + } + } + EmbeddedEquinox embeddedEquinox = new EmbeddedEquinox() { + + @Override + public void registerService(Class clazz, T service, Dictionary properties) { + // TODO better return the service reference here! + osgiFramework.getBundleContext().registerService(clazz, service, properties); + } + + @Override + public void registerService(Class clazz, T service) { + registerService(clazz, service, null); + } + + @Override + public EquinoxServiceFactory getServiceFactory() { + return PlexusFrameworkConnectServiceFactory.this; + } + }; + for (EquinoxLifecycleListener listener : lifecycleListeners.values()) { + listener.afterFrameworkStarted(embeddedEquinox); + } + if (log.isDebugEnabled()) { + printFrameworkState(osgiFramework); + } + return osgiFramework; + } + + private Map readBundles(ClassLoader classloader) { + Enumeration resources; + try { + resources = classloader.getResources("META-INF/sisu-connect.bundles"); + } catch (IOException e1) { + return Map.of(); + } + LinkedHashMap map = new LinkedHashMap<>(); + while (resources.hasMoreElements()) { + URL url = resources.nextElement(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) { + reader.lines().forEachOrdered(line -> { + if (line.startsWith("#") || line.isBlank()) { + return; + } + String[] split = line.split(",", 2); + boolean start; + if (split.length == 2) { + start = Boolean.parseBoolean(split[1]); + } else { + start = false; + } + map.put(split[0], start); + }); + } catch (IOException e) { + log.warn("Can't read bundle infos from url " + url); + } + } + return map; + } + + private void printFrameworkState(Framework framework) { + Bundle[] bundles = framework.getBundleContext().getBundles(); + log.info("============ Framework Bundles =================="); + Comparator bySymbolicName = Comparator.comparing(Bundle::getSymbolicName, + String.CASE_INSENSITIVE_ORDER); + Comparator byState = Comparator.comparingInt(Bundle::getState); + Arrays.stream(bundles).sorted(byState.thenComparing(bySymbolicName)).forEachOrdered(bundle -> { + log.info(toBundleState(bundle.getState()) + " | " + bundle.getSymbolicName()); + }); + ServiceTracker st = new ServiceTracker( + framework.getBundleContext(), ServiceComponentRuntime.class, null); + st.open(); + try { + ServiceComponentRuntime componentRuntime = st.getService(); + if (componentRuntime != null) { + log.info("============ Framework Components =================="); + Collection descriptionDTOs = componentRuntime.getComponentDescriptionDTOs(); + Comparator byComponentName = Comparator + .comparing(dto -> dto.description.name, String.CASE_INSENSITIVE_ORDER); + Comparator byComponentState = Comparator.comparingInt(dto -> dto.state); + descriptionDTOs.stream().flatMap(dto -> componentRuntime.getComponentConfigurationDTOs(dto).stream()) + .sorted(byComponentState.thenComparing(byComponentName)).forEachOrdered(dto -> { + if (dto.state == ComponentConfigurationDTO.FAILED_ACTIVATION) { + log.info(toComponentState(dto.state) + " | " + dto.description.name + " | " + + dto.failure); + } else { + log.info(toComponentState(dto.state) + " | " + dto.description.name); + } + }); + } + } finally { + st.close(); + } + } + + private String toComponentState(int state) { + switch (state) { + case ComponentConfigurationDTO.ACTIVE: + return "ACTIVE "; + case ComponentConfigurationDTO.FAILED_ACTIVATION: + return "FAILED "; + case ComponentConfigurationDTO.SATISFIED: + return "SATISFIED "; + case ComponentConfigurationDTO.UNSATISFIED_CONFIGURATION: + case ComponentConfigurationDTO.UNSATISFIED_REFERENCE: + return "UNSATISFIED"; + default: + return String.valueOf(state); + } + } + + private String toBundleState(int state) { + switch (state) { + case Bundle.ACTIVE: + return "ACTIVE "; + case Bundle.INSTALLED: + return "INSTALLED"; + case Bundle.RESOLVED: + return "RESOLVED "; + case Bundle.STARTING: + return "STARTING "; + case Bundle.STOPPING: + return "STOPPING "; + default: + return String.valueOf(state); + } + } + + @Override + public void dispose() { + frameworkMap.values().forEach(fw -> { + String storage = fw.getBundleContext().getProperty(Constants.FRAMEWORK_STORAGE); + try { + fw.stop(); + } catch (BundleException e) { + } + try { + fw.waitForStop(TimeUnit.SECONDS.toMillis(10)); + } catch (InterruptedException e) { + } + if (storage != null) { + FileUtils.deleteQuietly(new File(storage)); + } + }); + frameworkMap.clear(); + instance = null; + } + + @Override + public void initialize() throws InitializationException { + instance = this; + } + + @Override + public T getService(Class clazz) { + return getService(clazz, null); + } + + @Override + public T getService(Class clazz, String filter) { + Class callerClass = WALKER.getCallerClass(); + try { + Framework framework = getFramework(callerClass.getClassLoader()); + try { + ServiceTracker serviceTracker = trackerMap.computeIfAbsent(clazz, cls -> { + ServiceTracker tracker = new ServiceTracker<>(framework.getBundleContext(), cls, null); + tracker.open(); + return tracker; + }); + if (filter == null) { + return clazz.cast(serviceTracker.getService()); + } + Filter f = framework.getBundleContext().createFilter(filter); + for (var entry : serviceTracker.getTracked().entrySet()) { + if (f.match(entry.getKey())) { + return clazz.cast(entry.getValue()); + } + } + return null; + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } catch (BundleException e) { + throw new RuntimeException("can't acquire the framework!", e); + } + } +} diff --git a/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusFrameworkUtilHelper.java b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusFrameworkUtilHelper.java new file mode 100644 index 0000000000..644f6b174f --- /dev/null +++ b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusFrameworkUtilHelper.java @@ -0,0 +1,49 @@ +/******************************************************************************* + * Copyright (c) 2022 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.sisu.osgi.connect; + +import java.util.Optional; + +import org.osgi.framework.Bundle; +import org.osgi.framework.connect.FrameworkUtilHelper; +import org.osgi.framework.launch.Framework; + +/** + * Implementation of the {@link FrameworkUtilHelper} to connect classes to + * bundles + */ +public class PlexusFrameworkUtilHelper implements FrameworkUtilHelper { + + @Override + public Optional getBundle(Class classFromBundle) { + PlexusFrameworkConnectServiceFactory factory = PlexusFrameworkConnectServiceFactory.instance; + if (factory != null) { + Framework framework = factory.frameworkMap.get(classFromBundle.getClassLoader()); + if (framework != null) { + String location = classFromBundle.getProtectionDomain().getCodeSource().getLocation().toString(); + for (Bundle bundle : framework.getBundleContext().getBundles()) { + String bundleLocation = bundle.getLocation(); + if (locationsMatch(location, bundleLocation)) { + return Optional.of(bundle); + } + } + } + } + return Optional.empty(); + } + + private static boolean locationsMatch(String classLocation, String bundleLocation) { + return classLocation.endsWith(bundleLocation); + } + +} diff --git a/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusModuleConnector.java b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusModuleConnector.java new file mode 100644 index 0000000000..5311c0fcaf --- /dev/null +++ b/sisu-osgi/sisu-osgi-connect/src/main/java/org/eclipse/sisu/osgi/connect/PlexusModuleConnector.java @@ -0,0 +1,132 @@ +/******************************************************************************* + * Copyright (c) 2022 Christoph Läubrich and others. + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Christoph Läubrich - initial API and implementation + *******************************************************************************/ +package org.eclipse.sisu.osgi.connect; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.codehaus.plexus.logging.Logger; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleException; +import org.osgi.framework.Constants; +import org.osgi.framework.connect.ConnectContent; +import org.osgi.framework.connect.ConnectModule; +import org.osgi.framework.connect.ModuleConnector; + +/** + * The PlexusModuleConnector scans a linear classpath for bundles and install + * them as {@link ConnectContent} into the given {@link BundleContext} + */ +final class PlexusModuleConnector implements ModuleConnector { + + private static final String JAR_FILE_PREFIX = "jar:file:"; + + private ClassLoader classloader; + + private Map modulesMap = new HashMap<>(); + + private Logger logger; + + private File storage; + + public PlexusModuleConnector(ClassLoader classloader, Logger logger) { + this.classloader = classloader; + this.logger = logger; + } + + public void installBundles(BundleContext bundleContext, Predicate filter) { + Enumeration resources; + try { + resources = classloader.getResources(JarFile.MANIFEST_NAME); + } catch (IOException e) { + logger.error("Can't load resources for classloader " + classloader); + return; + } + while (resources.hasMoreElements()) { + String location = resources.nextElement().toExternalForm(); + logger.debug("Scan " + location + " for bundle data..."); + if (location.startsWith(JAR_FILE_PREFIX)) { + String name = location.substring(JAR_FILE_PREFIX.length()).split("!")[0]; + try { + JarFile jarFile = new JarFile(name); + try { + Manifest manifest = jarFile.getManifest(); + Attributes mainAttributes = manifest.getMainAttributes(); + if (mainAttributes == null) { + jarFile.close(); + continue; + } + String bundleSymbolicName = getBsn(mainAttributes.getValue(Constants.BUNDLE_SYMBOLICNAME)); + if (bundleSymbolicName == null || !filter.test(bundleSymbolicName)) { + if (bundleSymbolicName != null) { + logger.debug("Ignore bundle " + bundleSymbolicName + + " as it is not included in the bundle list."); + } + jarFile.close(); + continue; + } + logger.debug("Discovered bundle " + bundleSymbolicName + " @ " + name); + modulesMap.put(name, new PlexusConnectContent(jarFile, classloader)); + try { + bundleContext.installBundle(name); + } catch (BundleException e) { + logger.warn("Can't install bundle at " + name, e); + jarFile.close(); + modulesMap.remove(name); + } + } catch (IOException e) { + jarFile.close(); + } + } catch (IOException e) { + logger.warn("Can't open jar at " + name, e); + } + } + } + } + + private String getBsn(String value) { + if (value != null) { + return value.split(";")[0].trim(); + } + return null; + } + + @Override + public Optional connect(String location) throws BundleException { + return Optional.ofNullable(modulesMap.get(location)); + } + + @Override + public void initialize(File storage, Map configuration) { + this.storage = storage; + } + + @Override + public Optional newBundleActivator() { + return Optional.empty(); + } + + public File getStorage() { + return storage; + } +} \ No newline at end of file diff --git a/sisu-osgi/sisu-osgi-connect/src/main/resources/META-INF/services/org.osgi.framework.connect.FrameworkUtilHelper b/sisu-osgi/sisu-osgi-connect/src/main/resources/META-INF/services/org.osgi.framework.connect.FrameworkUtilHelper new file mode 100644 index 0000000000..9a4502c7cd --- /dev/null +++ b/sisu-osgi/sisu-osgi-connect/src/main/resources/META-INF/services/org.osgi.framework.connect.FrameworkUtilHelper @@ -0,0 +1 @@ +org.eclipse.sisu.osgi.connect.PlexusFrameworkUtilHelper \ No newline at end of file diff --git a/tycho-build/src/main/java/org/eclipse/tycho/build/TychoGraphBuilder.java b/tycho-build/src/main/java/org/eclipse/tycho/build/TychoGraphBuilder.java index 09059edeeb..7c4fc54f01 100644 --- a/tycho-build/src/main/java/org/eclipse/tycho/build/TychoGraphBuilder.java +++ b/tycho-build/src/main/java/org/eclipse/tycho/build/TychoGraphBuilder.java @@ -49,11 +49,9 @@ import org.eclipse.equinox.p2.metadata.IInstallableUnit; import org.eclipse.tycho.PackagingType; import org.eclipse.tycho.core.shared.MavenLogger; -import org.eclipse.tycho.p2maven.InstallableUnitGenerator; import org.eclipse.tycho.p2maven.MavenProjectDependencyProcessor; import org.eclipse.tycho.p2maven.MavenProjectDependencyProcessor.ProjectDependencyClosure; import org.eclipse.tycho.pomless.AbstractTychoMapping; -import org.osgi.framework.BundleContext; import org.sonatype.maven.polyglot.mapping.Mapping; @Component(role = GraphBuilder.class, hint = GraphBuilder.HINT) @@ -62,15 +60,9 @@ public class TychoGraphBuilder extends DefaultGraphBuilder { @Requirement private Logger log; - @Requirement(hint = "plexus") - private BundleContext bundleContext; - @Requirement(role = Mapping.class) private Map polyglotMappings; - @Requirement - private InstallableUnitGenerator generator; - @Requirement private MavenProjectDependencyProcessor dependencyProcessor; diff --git a/tycho-core/src/main/java/org/eclipse/tycho/osgi/configuration/OSGiProxyConfigurator.java b/tycho-core/src/main/java/org/eclipse/tycho/osgi/configuration/OSGiProxyConfigurator.java index eddc07816a..85f0b138d7 100644 --- a/tycho-core/src/main/java/org/eclipse/tycho/osgi/configuration/OSGiProxyConfigurator.java +++ b/tycho-core/src/main/java/org/eclipse/tycho/osgi/configuration/OSGiProxyConfigurator.java @@ -39,6 +39,9 @@ public void afterFrameworkStarted(EmbeddedEquinox framework) { MavenSession session = context.getSession(); ProxyServiceFacade proxyService = framework.getServiceFactory().getService(ProxyServiceFacade.class); + if (proxyService == null) { + return; + } // make sure there is no old state from previous aborted builds clearProxyConfiguration(proxyService);