blob: 3b9f49537a66dd1df4f164d99dc8afe6375d27e9 [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
*
* 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.felix.atomos;
import static org.apache.felix.atomos.impl.base.AtomosBase.NO_OP_HEADER_PROVIDER;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.function.BiFunction;
import org.apache.felix.atomos.AtomosLayer.LoaderType;
import org.apache.felix.atomos.impl.base.AtomosBase;
import org.osgi.annotation.versioning.ConsumerType;
import org.osgi.annotation.versioning.ProviderType;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.connect.ConnectContent;
import org.osgi.framework.connect.ConnectFrameworkFactory;
import org.osgi.framework.connect.ModuleConnector;
import org.osgi.framework.launch.Framework;
/**
* Atomos can be used for creating new OSGi {@link Framework} instances
* with bundles loaded from the module or class
* path. The Framework instance will have a set of contents discovered and
* installed as connected bundles automatically when the Framework is
* {@link Framework#init() initialized}.
* <p>
* When loading Atomos from the module path the contents will be loaded from the
* Java modules included on the module path and will use the class loader
* provided by the Java module system. This implies that the normal protection
* provided by the OSGi module layer will not be available to the classes loaded
* out of the module path bundles. Classes in one bundle will be able to load
* classes from other bundles even when the package is not exported. But the
* Java module system will provide protection against modules trying to execute
* code from packages that are not exported. This means you may find cases where
* a bundle can load a class from another bundle but then get runtime exceptions
* when trying to execute methods on the class. Other limitations are also
* imposed because a single class loader is used to load all contents from connected
* bundles. For example, multiple versions of a package cannot be supported because the class
* loader can only define a single package of a given name.
* <p>
* When loading from the module path Atomos contents are discovered using the
* module layer which loads the Atomos framework. This is typically the boot
* layer. Atomos contents are discovered by searching the current module layer
* for the available modules as well as the modules available in the parent
* layers until the {@link ModuleLayer#empty() empty} layer is reached.
* <p>
* When loading Atomos from the class path (i.e as an unnamed module) the
* contents will be loaded from JARs included on the class path along side the
* Atomos JARs and will use the application class loader provided by the JVM.
* This also implies that the normal protection provided by the OSGi module
* layer will not be available to the classes loaded out of the class path
* bundles. But unlike when loading bundles from the module path, the Java
* module system will not provide any protection against executing code from
* other bundles from private packages. This mode also suffers from the typical
* issues that arise if you have multiple JARs on the class path that provide
* the same package. When in module path mode the JVM will fail to launch and
* inform you that it detected duplicate packages from the modules on the module
* path. In class path mode the JVM does no such check, this leaves you open
* to have unexpected shadowing if you have multiple versions of the same package
* on the class path.
* <p>
* When loading from the class path Atomos contents are discovered by searching
* the class path for bundle manifests (META-INF/MANIFEST.MF) resources and
* discovering any that contain OSGI meta-data (e.g. Bundle-SymbolicName). The
* boot layer is also searched and loads all boot modules as bundles similar to
* when loading from the module path.
* <p>
* The Framework can be created using the {@link ConnectFrameworkFactory} APIs.
* The following will launch using the standard {@link ConnectFrameworkFactory}:
*
* <pre>
* {@code Map<String, String>} config = getConfiguration();
* config.put(Constants.FRAMEWORK_SYSTEMPACKAGES, "");
* ModuleConnector atomosConnector = Atomos.newAtomos().newModuleConnector();
* Framework framework = ServiceLoader.load(ConnectFrameworkFactory.class).iterator().next().newFramework(config, atomosConnector);
* framework.start();
* </pre>
*
* Note that the {@link Constants#FRAMEWORK_SYSTEMPACKAGES} must be set to an
* empty value to prevent discovery of packages from the
* {@link ModuleLayer#boot() boot} layer. Without doing this the OSGi R7
* framework will discover all modules from the boot layer and assume they all
* should be exported by the system bundle. That is normally desired so the
* packages available from the boot layer (typically the modules provided by the
* Java platform itself) can be available for import from bundles installed into
* the framework. But with Atomos you may load all your bundles within the boot
* layer itself. In that case you obviously do not want all the packages from
* the bundles in the boot layer also be exported by the system bundle. Setting
* {@link Constants#FRAMEWORK_SYSTEMPACKAGES} to the empty value will prevent
* this from happening. Atomos still needs to make the packages exported by the
* Java platform modules available for import. In order to do this Atomos will
* create an {@link AtomosContent Atomos content} for each module contained in
* the {@link ModuleLayer#boot() boot} layer.
* <p>
* The following code uses the Atomos to first create a child layer which
* is then used to load more modules in addition to the ones included on the
* module path.
*
* <pre>
* Atomos atomos = Atomos.newAtomos();
*
* // Add a new layer using a Path that contains a set of modules to load
* Path modulesDir = getModulesDir();
* atomos.getBootLayer().addLayer("child", LoaderType.OSGI, modulesDir);
*
* Framework framework = atomos.newFramework(frameworkConfig);
* framework.start();
* </pre>
*
* When using the {@link Atomos#newFramework(Map)} to create a
* new Framework the {@link Constants#FRAMEWORK_SYSTEMPACKAGES}
* is set to the empty value automatically.
*
* By default all Atomos contents will be installed and started in the OSGi
* framework when the framework is started. If {@link #ATOMOS_CONTENT_INSTALL
* atomos.content.install} is set to <code>false</code> in the framework
* configuration then the boot contents will not be installed by default. In that
* case the {@link AtomosContent#install(String)} method can be used to
* selectively install Atomos contents. If {@link #ATOMOS_CONTENT_START
* atomos.content.start} is set to <code>false</code> in the framework
* configuration then the Atomos bundles will not be started by default. The
* system.bundle of the initialized framework will also have an Atomos
* service registered with its bundle context.
*/
@org.osgi.annotation.bundle.Header(name = "Main-Class", value = "org.apache.felix.atomos.Atomos")
@ProviderType
public interface Atomos
{
/**
* Framework launching property specifying if the Atomos contents
* will not be automatically installed as bundles. Default is true, which
* will install all discovered Atomos content as bundles.
*/
String ATOMOS_CONTENT_INSTALL = AtomosBase.ATOMOS_PROP_PREFIX
+ "content.install";
/**
* Framework launching property specifying if the Atomos contents installed
* as connected bundles will not be marked for start. Default is true, which
* will start all discovered Atomos content that are installed as bundles.
*/
String ATOMOS_CONTENT_START = AtomosBase.ATOMOS_PROP_PREFIX + "content.start";
/**
* Returns the Atomos content that is connected with the specified bundle location.
* The Atomos content returned is used by the connected bundle installed
* in the framework with the specified bundle location.
*
* @param bundleLocation the bundle location.
* @return the Atomos content with the specified location or {@code null} if no
* Atomos content is installed at the location.
*/
AtomosContent getConnectedContent(String bundleLocation);
/**
* The initial Atomos boot layer. Depending on the mode Atomos is running
* this may be the backed by {@link ModuleLayer#boot()} or by the
* class path.
*
* @return the boot Atomos layer
*/
AtomosLayer getBootLayer();
/**
* Returns the module connector for this runtime instance.
* The module connector can be used to create a new framework
* by using a {@link ConnectFrameworkFactory} directly by calling
* the {@link ConnectFrameworkFactory#newFramework(Map, ModuleConnector)}
* method.
* @return the module connector for this runtime
*/
ModuleConnector getModuleConnector();
/**
* Adds a layer as a child of the specified parents and loads modules from the specified
* module paths
*
* @param parents the parent layers. Must include at least one parent layer
* @param name the name of the new layer
* @param loaderType the type of class loader to use
* @param modulePaths the paths to load modules for the new layer
* @return a newly created layer
* @throws UnsupportedOperationException if the {@link #getBootLayer() boot} layer
* {@link AtomosLayer#isAddLayerSupported()} returns false.
*/
AtomosLayer addLayer(List<AtomosLayer> parents, String name, LoaderType loaderType,
Path... modulePaths);
/**
* Creates a new {@link Framework} instance that uses this Atomos instance. The
* {@link ServiceLoader} is used to load an implementation of a {@link ConnectFrameworkFactory}
* which is used to create a new {@link Framework} instance with the specified Atomos runtime.
* The supplied framework configuration is used to create the new {@code Framework} instance.
* Additional configuration options maybe configured automatically in order to correctly configure
* the system packages for the {@code Framework} instance.
* @param frameworkConfig The framework configuration options, or {@code null} if the defaults should be used
* @return The new uninitialized Framework instance which uses this Atomos instance
*/
Framework newFramework(Map<String, String> frameworkConfig);
/**
* A main method that can be used by executable jars to initialize and start
* Atomos with an available OSGi {@link Framework} implementation.
* Each string in the arguments array may contain a key=value
* pair that will be used for the framework configuration.
*
* @param args the args will be converted into a {@code Map<String, String>} to
* use as configuration parameters for the OSGi Framework.
* @throws BundleException when an error occurs
* @see #newAtomos(Map)
* @see #newFramework(Map)
*/
static void main(String... args) throws BundleException
{
Map<String, String> config = getConfiguration(args);
// default to reporting resolution issues from main
config.putIfAbsent(AtomosBase.ATOMOS_REPORT_RESOLUTION_PROP, "true");
Atomos atomos = Atomos.newAtomos(config);
Framework framework = atomos.newFramework(config);
framework.start();
}
/**
* Converts a string array into a {@code Map<String,String>}
*
* @param args the arguments where each element has key=string value, the key
* cannot contain an '=' (equals) character.
* @return a map of the configuration specified by the args
*/
static Map<String, String> getConfiguration(String... args)
{
Map<String, String> config = new HashMap<>();
if (args != null)
{
for (String arg : args)
{
int equals = arg.indexOf('=');
if (equals != -1)
{
String key = arg.substring(0, equals);
String value = arg.substring(equals + 1);
config.put(key, value);
}
}
}
return config;
}
/**
* Creates a new Atomos that can be used to create a new OSGi framework
* instance. Same as calling {@code newAtomos(Map)} with an empty
* configuration.
*
* @return a new Atomos.
*/
static Atomos newAtomos()
{
return newAtomos(NO_OP_HEADER_PROVIDER);
}
/**
* Creates a new Atomos that can be used to create a new OSGi framework
* instance. Same as calling {@code newAtomos(Map,HeaderProvider)} with an empty
* configuration.
*
* @param headerProvider the header provider function
* @return a new Atomos.
*/
static Atomos newAtomos(HeaderProvider headerProvider)
{
return newAtomos(Collections.emptyMap(), headerProvider);
}
/**
* Creates a new Atomos that can be used to create a new OSGi framework
* instance. Same as calling {@code newAtomos(Map,HeaderProvider)} with a
* no-op {@code headerProvider} function.
*
* @param configuration the properties to configure the new Atomos
* @return a new Atomos.
*/
static Atomos newAtomos(Map<String, String> configuration)
{
return newAtomos(configuration, NO_OP_HEADER_PROVIDER);
}
/**
* Creates a new Atomos that can be used to create a new OSGi framework
* instance. If Atomos is running as a Java Module then this Atomos can
* be used to create additional layers by using the
* {@link AtomosLayer#addLayer(String, AtomosLayer.LoaderType, Path...)} method. If the additional layers are added
* before {@link ConnectFrameworkFactory#newFramework(Map, ModuleConnector)} creating} and {@link Framework#init()
* initializing} the framework then the Atomos contents found in the added layers
* will be automatically installed and started according to the
* {@link #ATOMOS_CONTENT_INSTALL} and {@link #ATOMOS_CONTENT_START} options.
* <p>
* Note that this {@code Atomos} must be used for creating a new framework instance
* with the method {@link ConnectFrameworkFactory#newFramework(Map, ModuleConnector)} to use
* the layers added to this {@code Atomos} or the {@link #newFramework(Map)} method can
* be called on this {@code Atomos}.
* <p>
* The given headerProvider function maps each Atomos content
* {@link AtomosContent#getAtomosLocation() location}
* and existing headers of the content to a new optional map of headers.
* The resulting map will be used as the headers for the {@link ConnectContent#getHeaders()}.
* If the function returns an empty optional then the existing
* headers will be used.
*
* @param configuration the properties to configure the new Atomos
* @param headerProvider a function that will be called with the location and the existing headers for each Atomos content.
* @return a new Atomos.
*/
static Atomos newAtomos(Map<String, String> configuration,
HeaderProvider headerProvider)
{
return AtomosBase.newAtomos(configuration, headerProvider);
}
/**
* A function that maps each {@code AtomosContent} {@link AtomosContent#getAtomosLocation() location}
* and its existing headers to a new optional map of headers to be used for the
* {@link ConnectContent#getHeaders() headers} of the {@link AtomosContent#getConnectContent() ConnectContent}.
*/
@FunctionalInterface
@ConsumerType
interface HeaderProvider extends BiFunction<String, Map<String, String>, Optional<Map<String, String>>>
{
/**
* Applies this header provider function to the specified
* {@code AtomosContent} {@link AtomosContent#getAtomosLocation() location} and
* map of existing headers. The returned {@code Optional} map of headers will
* be used by the {@link ConnectContent#getHeaders()} method for the
* {@code ConnectContent} {@link AtomosContent#getConnectContent() associated}
* with the {@code AtomosContent} that has the specified
* {@link AtomosContent#getAtomosLocation() location}.
* <p>
* This method allows a header provider to augment existing bundle manifest
* headers or add completely new bundle manifest headers that are not present
* in the existing headers.
* <p>
* This function may be applied before the instance of the {@code AtomosContent}
* instance is created which may result in the symbolic name and or version
* of the {@code AtomosContent} to be influenced by this function.
* @param location The {@code AtomosContent} {@link AtomosContent#getAtomosLocation() location}
* @param existingHeaders The existing headers found for the {@code AtomosContent}
* @return the {@code Optional} map of headers to use instead of the {@code existingHeaders}. If
* the existing headers should be used then an empty {@code Optional} may be returned.
*/
@Override
Optional<Map<String, String>> apply(String location,
Map<String, String> existingHeaders);
}
}