blob: 602f2eff0e367e23d06f665bb2f326226afdd5e0 [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.impl.modules;
import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.module.Configuration;
import java.lang.module.ModuleDescriptor;
import java.lang.module.ModuleDescriptor.Requires;
import java.lang.module.ModuleFinder;
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.felix.atomos.Atomos;
import org.apache.felix.atomos.AtomosContent;
import org.apache.felix.atomos.AtomosLayer;
import org.apache.felix.atomos.AtomosLayer.LoaderType;
import org.apache.felix.atomos.impl.base.AtomosBase;
import org.apache.felix.atomos.impl.base.JavaServiceNamespace;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.connect.ConnectContent;
import org.osgi.framework.connect.ConnectFrameworkFactory;
import org.osgi.framework.launch.FrameworkFactory;
import org.osgi.framework.namespace.BundleNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.resource.Namespace;
public class AtomosModules extends AtomosBase
{
private final static String ATOMOS_GENERATED = "Atomos-GeneratedManifest";
private final static String ATOMOS_TEMPORARY_GENERATED_REQUIRES = "Atomos-TemporaryGeneratedRequires";
private final Module thisModule = AtomosModules.class.getModule();
private final Configuration thisConfig = thisModule.getLayer() == null ? null
: thisModule.getLayer().configuration();
private final Map<Configuration, AtomosLayerBase> byConfig = new HashMap<>();
private final AtomosLayer bootLayer = createBootLayer();
public AtomosModules(Map<String, String> config, HeaderProvider headerProvider)
{
super(config, headerProvider);
}
private AtomosLayer createBootLayer()
{
return createAtomosLayer(thisConfig, "boot", -1, LoaderType.SINGLE);
}
AtomosLayerBase getByConfig(Configuration config)
{
lockRead();
try
{
return byConfig.get(config);
}
finally
{
unlockRead();
}
}
@Override
protected AtomosLayer addLayer(List<AtomosLayer> parents, String name, long id,
LoaderType loaderType, Path... paths)
{
if (parents.isEmpty())
{
throw new IllegalArgumentException("Must specify at least one parent layer.");
}
if (bootLayer.adapt(ModuleLayer.class).isEmpty())
{
throw new UnsupportedOperationException(
"Cannot add module layers when Atomos is not loaded as module.");
}
List<Configuration> parentConfigs = parents.stream().map(
(l) -> l.adapt(ModuleLayer.class).get().configuration()).collect(
Collectors.toList());
ModuleFinder finder = ModuleFinder.of(paths);
List<String> roots = finder.findAll().stream().map(
(m) -> m.descriptor().name()).collect(Collectors.toList());
Configuration config = Configuration.resolve(ModuleFinder.of(), parentConfigs,
ModuleFinder.of(paths), roots);
return createAtomosLayer(config, name, id, loaderType, paths);
}
@Override
public ConnectFrameworkFactory findFrameworkFactory()
{
ServiceLoader<ConnectFrameworkFactory> loader;
if (Atomos.class.getModule().getLayer() == null)
{
loader = ServiceLoader.load(ConnectFrameworkFactory.class,
getClass().getClassLoader());
}
else
{
loader = ServiceLoader.load( //
getClass().getModule().getLayer(), //
ConnectFrameworkFactory.class);
}
return loader.findFirst() //
.orElseThrow(
() -> new RuntimeException("No Framework implementation found."));
}
AtomosLayerBase createAtomosLayer(Configuration config, String name, long id,
LoaderType loaderType, Path... paths)
{
AtomosLayerBase existing = getByConfig(config);
if (existing != null)
{
return existing;
}
existing = getById(id);
if (existing != null)
{
throw new IllegalArgumentException("The a layer already exists with the id: "
+ id + " " + existing.getName());
}
lockWrite();
try
{
List<AtomosLayer> parents = findParents(config, name);
id = id < 0 ? nextLayerId.getAndIncrement() : id;
if (Configuration.empty().equals(config))
{
name = "empty";
}
AtomosLayerModules result = new AtomosLayerModules(config, parents, id, name,
loaderType, paths);
addAtomosLayer(result);
return result;
}
finally
{
unlockWrite();
}
}
private List<AtomosLayer> findParents(Configuration config, String name)
{
if (config == null || config.parents().isEmpty())
{
return List.of();
}
List<AtomosLayer> found = new ArrayList<>(config.parents().size());
for (Configuration parentConfig : config.parents())
{
AtomosLayerBase existingParent = getByConfig(parentConfig);
if (existingParent != null)
{
found.add(existingParent);
}
else
{
// If it didn't exist already we really don't know what type of loader it is;
// just use SINGLE for now
// We also don't know what paths it could be using
found.add(createAtomosLayer(parentConfig, name, -1, LoaderType.SINGLE));
}
}
return Collections.unmodifiableList(found);
}
@Override
protected void addingLayer(AtomosLayerBase atomosLayer)
{
Configuration config = atomosLayer.adapt(ModuleLayer.class).map(
ModuleLayer::configuration).orElse(null);
if (byConfig.putIfAbsent(config, atomosLayer) != null)
{
throw new IllegalStateException(
"AtomosLayer already exists for configuration.");
}
}
@Override
protected void removedLayer(AtomosLayerBase atomosLayer)
{
byConfig.remove(
atomosLayer.adapt(ModuleLayer.class).map(ModuleLayer::configuration).orElse(
null));
}
ModuleLayer findModuleLayer(Configuration config, List<AtomosLayer> parents,
LoaderType loaderType)
{
if (config == null)
{
return null;
}
if (config.equals(thisConfig))
{
return thisModule.getLayer();
}
if (Configuration.empty().equals(config))
{
return ModuleLayer.empty();
}
List<ModuleLayer> parentLayers = parents.stream().sequential().map(
(a) -> a.adapt(ModuleLayer.class).get()).collect(Collectors.toList());
ModuleLayer.Controller controller;
switch (loaderType)
{
case SINGLE:
return ModuleLayer.defineModulesWithOneLoader(config, parentLayers,
null).layer();
case OSGI:
ConcurrentHashMap<String, ModuleConnectLoader> classLoaders = new ConcurrentHashMap<>();
Function<String, ClassLoader> clf = (moduleName) -> {
ResolvedModule m = config.findModule(moduleName).orElse(null);
if (m == null || m.configuration() != config)
{
return null;
}
return classLoaders.computeIfAbsent(moduleName, (mn) -> {
try
{
return new ModuleConnectLoader(m, this);
}
catch (IOException e)
{
throw new UncheckedIOException(e);
}
});
};
controller = ModuleLayer.defineModules(config, parentLayers, clf);
controller.layer().modules().forEach((m) -> {
ModuleConnectLoader loader = (ModuleConnectLoader) m.getClassLoader();
loader.initEdges(m, config, classLoaders);
});
return controller.layer();
case MANY:
return ModuleLayer.defineModulesWithManyLoaders(config, parentLayers,
null).layer();
default:
throw new UnsupportedOperationException(loaderType.toString());
}
}
@Override
public AtomosLayer getBootLayer()
{
return bootLayer;
}
@Override
protected Object getAtomosKey(Class<?> classFromBundle)
{
Module m = classFromBundle.getModule();
if (m != null && m.isNamed())
{
return m;
}
return super.getAtomosKey(classFromBundle);
}
@Override
protected void filterBasedOnReadEdges(AtomosContent atomosContent,
Collection<BundleCapability> candidates)
{
if (atomosContent == null)
{
// only do this for atomos contents
return;
}
Module m = atomosContent.adapt(Module.class).orElse(null);
if (m == null)
{
filterNotVisible(atomosContent, candidates);
}
else
{
for (Iterator<BundleCapability> iCands = candidates.iterator(); iCands.hasNext();)
{
BundleCapability candidate = iCands.next();
AtomosContent candidateAtomos = getByConnectLocation(
candidate.getRevision().getBundle().getLocation(), true);
if (candidateAtomos == null
|| candidateAtomos.adapt(Module.class).isEmpty())
{
iCands.remove();
}
else
{
if (!m.canRead(candidateAtomos.adapt(Module.class).get()))
{
iCands.remove();
}
}
}
}
}
public class AtomosLayerModules extends AtomosLayerBase
{
private final ModuleLayer moduleLayer;
private final Set<AtomosContentBase> atomosBundles;
private final Map<Module, AtomosContentBase> atomosModules;
AtomosLayerModules(Configuration config, List<AtomosLayer> parents, long id, String name, LoaderType loaderType, Path... paths)
{
super(parents, id, name, loaderType, paths);
moduleLayer = findModuleLayer(config, parents, loaderType);
atomosBundles = findAtomosLayerContent();
atomosModules = atomosBundles.stream().filter(
a -> a.adapt(Module.class).isPresent()).collect(
Collectors.toUnmodifiableMap((k) -> k.adapt(Module.class).get(),
(v) -> v));
for (AtomosContentBase content : atomosBundles)
{
final Module m = content.adapt(Module.class).orElse(null);
if (m == null)
{
continue;
}
content.getConnectContent().getHeaders().ifPresent(headers -> {
if (Boolean.parseBoolean(headers.get(ATOMOS_GENERATED)) &&
Boolean.parseBoolean(headers.get(ATOMOS_TEMPORARY_GENERATED_REQUIRES)))
{
calculateRequires(headers, m, (requires) -> {
return m.getLayer().findModule(requires).map(
requiredModule -> {
return getAtomosContent(
requiredModule).getSymbolicName();
}).orElse(requires);
});
headers.remove(ATOMOS_TEMPORARY_GENERATED_REQUIRES);
}
});
}
}
@Override
public boolean isAddLayerSupported()
{
return thisConfig != null;
}
@Override
public AtomosLayer addModules(String name, Path path)
{
if (isAddLayerSupported())
{
if (path == null)
{
ResolvedModule resolved = thisConfig.findModule(
Atomos.class.getModule().getName()).get();
URI location = resolved.reference().location().get();
if (location.getScheme().equals("file"))
{
// Use the module location as the relative base to locate the modules folder
File thisModuleFile = new File(location);
File candidate = new File(thisModuleFile.getParent(), name);
path = candidate.isDirectory() ? candidate.toPath() : null;
}
}
if (path != null)
{
return addLayer(name, LoaderType.OSGI, path);
}
else
{
return null;
}
}
else
{
return super.addModules(name, path);
}
}
private Set<AtomosContentBase> findAtomosLayerContent()
{
return moduleLayer == null ? findAtomosContents()
: findModuleLayerAtomosBundles(moduleLayer);
}
private Set<AtomosContentBase> findModuleLayerAtomosBundles(
ModuleLayer searchLayer)
{
Set<AtomosContentBase> found = new LinkedHashSet<>();
Map<ModuleDescriptor, Module> descriptorMap = searchLayer.modules().stream().collect(
Collectors.toMap(Module::getDescriptor, m -> (m)));
for (ResolvedModule resolved : searchLayer.configuration().modules())
{
// include only if it is not excluded
Module m = descriptorMap.get(resolved.reference().descriptor());
if (m == null)
{
continue;
}
String location;
if (m.getDescriptor().provides().stream().anyMatch(
(p) -> FrameworkFactory.class.getName().equals(p.service())))
{
// we assume a module that provides the FrameworkFactory is the system bundle
location = Constants.SYSTEM_BUNDLE_LOCATION;
}
else
{
location = resolved.reference().location().map((u) -> {
StringBuilder sb = new StringBuilder();
if (!getName().isEmpty())
{
sb.append(getName()).append(':');
}
sb.append(u.toString());
return sb.toString();
}).orElse(null);
}
if (location == null)
{
continue;
}
ManifestHolder holder = new ManifestHolder();
ConnectContent content = new ConnectContentModule(m, resolved.reference(), AtomosLayerModules.this, holder::getHeaders);
Map<String, String> headers;
try
{
content.open();
try
{
headers = getRawHeaders(content);
}
finally
{
content.close();
}
}
catch (IOException e)
{
throw new UncheckedIOException("Error reading connect manifest.", e);
}
generateHeaders(headers, m);
headers = applyHeaderProvider(holder, location, headers);
String symbolicName = headers.get(Constants.BUNDLE_SYMBOLICNAME);
if (symbolicName != null)
{
int semiColon = symbolicName.indexOf(';');
if (semiColon != -1)
{
symbolicName = symbolicName.substring(0, semiColon);
}
symbolicName = symbolicName.trim();
Version version = Version.parseVersion(
headers.get(Constants.BUNDLE_VERSION));
found.add(new AtomosContentModule(m, location,
symbolicName, version, content));
}
}
return Collections.unmodifiableSet(found);
}
private void generateHeaders(Map<String, String> headers, Module m)
{
ModuleDescriptor desc = m.getDescriptor();
StringBuilder capabilities = new StringBuilder();
StringBuilder requirements = new StringBuilder();
String bsn = headers.get(Constants.BUNDLE_SYMBOLICNAME);
if (bsn == null)
{
// NOTE that we depend on the framework connect implementation to allow connect bundles
// to export java.* packages
headers.put(Constants.BUNDLE_MANIFESTVERSION, "2");
// set the symbolic name for the module; don't allow fragments to attach
headers.put(Constants.BUNDLE_SYMBOLICNAME,
desc.name() + "; " + Constants.FRAGMENT_ATTACHMENT_DIRECTIVE + ":="
+ Constants.FRAGMENT_ATTACHMENT_NEVER);
// set the version
Version v;
try
{
v = Version.parseVersion(desc.version().map(
java.lang.module.ModuleDescriptor.Version::toString).orElse("0"));
}
catch (IllegalArgumentException e)
{
v = Version.emptyVersion;
}
headers.put(Constants.BUNDLE_VERSION, v.toString());
// only do exports for non bundle modules
// real OSGi bundles already have good export capabilities
StringBuilder exportPackageHeader = new StringBuilder();
// ModuleDescriptor.exports() is empty for an automatic module, which is different from
// JPMS at runtime where all packages in the module are exported for an automatic module
if (desc.isAutomatic()) {
desc.packages().stream().sorted().forEach((packages) -> {
if (exportPackageHeader.length() > 0)
{
exportPackageHeader.append(", ");
}
exportPackageHeader.append(packages);
});
}
else {
desc.exports().stream().sorted().forEach((exports) -> {
if (exportPackageHeader.length() > 0)
{
exportPackageHeader.append(", ");
}
exportPackageHeader.append(exports.source());
// TODO map targets to x-friends directive?
});
}
if (exportPackageHeader.length() > 0)
{
headers.put(Constants.EXPORT_PACKAGE, exportPackageHeader.toString());
}
// Note that for generated manifests based of module descriptor
// will have their requires calculated later.
// Place a header indicating this is generated
headers.put(ATOMOS_GENERATED, Boolean.TRUE.toString());
if (calculateRequires(headers, m, Function.identity()))
{
// Set a temporary header if we need to recalculate later
headers.put(ATOMOS_TEMPORARY_GENERATED_REQUIRES, Boolean.TRUE.toString());
}
}
else
{
String origCaps = headers.get(Constants.PROVIDE_CAPABILITY);
if (origCaps != null)
{
capabilities.append(origCaps);
}
String origReqs = headers.get(Constants.REQUIRE_CAPABILITY);
if (origReqs != null)
{
requirements.append(origReqs);
}
}
// map provides to a made up namespace only to give proper resolution errors
// (although JPMS will likely complain first
for (ModuleDescriptor.Provides provides : desc.provides())
{
if (capabilities.length() > 0)
{
capabilities.append(", ");
}
capabilities.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append(
"; ");
capabilities.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append(
"=").append(provides.service()).append("; ");
capabilities.append(
JavaServiceNamespace.CAPABILITY_PROVIDES_WITH_ATTRIBUTE).append(
"=\"").append(String.join(",", provides.providers())).append(
"\"");
}
// map uses to a made up namespace only to give proper resolution errors
// (although JPMS will likely complain first)
for (String uses : desc.uses())
{
if (requirements.length() > 0)
{
requirements.append(", ");
}
requirements.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append(
"; ");
requirements.append(Constants.RESOLUTION_DIRECTIVE).append(":=").append(
Constants.RESOLUTION_OPTIONAL).append("; ");
requirements.append(Constants.FILTER_DIRECTIVE).append(":=").append(
"\"(").append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append(
"=").append(uses).append(")\"");
}
if (capabilities.length() > 0)
{
headers.put(Constants.PROVIDE_CAPABILITY, capabilities.toString());
}
if (requirements.length() > 0)
{
headers.put(Constants.REQUIRE_CAPABILITY, requirements.toString());
}
}
private boolean calculateRequires(Map<String, String> headers, Module m,
Function<String, String> mapper)
{
// only do requires for non bundle modules
// map requires to require bundle
StringBuilder requireBundleHeader = new StringBuilder();
for (Requires requires : m.getDescriptor().requires())
{
if (requireBundleHeader.length() > 0)
{
requireBundleHeader.append(", ");
}
// before requiring based on the name make sure the required
// module has a BSN that is the same
String mapping = mapper.apply(requires.name());
requireBundleHeader.append(mapping).append("; ");
// determine the resolution value based on the STATIC modifier
String resolution = requires.modifiers().contains(
Requires.Modifier.STATIC) ? Namespace.RESOLUTION_OPTIONAL
: Namespace.RESOLUTION_MANDATORY;
requireBundleHeader.append(Constants.RESOLUTION_DIRECTIVE).append(
":=").append(resolution).append("; ");
// determine the visibility value based on the TRANSITIVE modifier
String visibility = requires.modifiers().contains(
Requires.Modifier.TRANSITIVE) ? BundleNamespace.VISIBILITY_REEXPORT
: BundleNamespace.VISIBILITY_PRIVATE;
requireBundleHeader.append(Constants.VISIBILITY_DIRECTIVE).append(
":=").append(visibility);
}
if (requireBundleHeader.length() > 0)
{
headers.put(Constants.REQUIRE_BUNDLE, requireBundleHeader.toString());
return true;
}
else
{
return false;
}
}
@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> adapt(Class<T> type)
{
if (ModuleLayer.class.equals(type))
{
return Optional.ofNullable((T) moduleLayer);
}
return super.adapt(type);
}
/**
* Atomos content discovered on the module path. The key is the module
* of this Atomos content.
*
*/
public class AtomosContentModule extends AtomosContentBase
{
/**
* The module for this atomos content.
*/
private final Module module;
public AtomosContentModule(Module module, String location, String symbolicName, Version version, ConnectContent content)
{
super(location, symbolicName, version, content);
this.module = module;
}
@Override
protected final Object getKey()
{
return module;
}
@SuppressWarnings("unchecked")
@Override
public <T> Optional<T> adapt(Class<T> type)
{
if (Module.class.equals(type))
{
return Optional.ofNullable((T) module);
}
return super.adapt(type);
}
}
@Override
public Set<AtomosContent> getAtomosContents()
{
return asSet(atomosBundles);
}
@Override
protected void findBootModuleLayerAtomosContents(Set<AtomosContentBase> result)
{
result.addAll(findModuleLayerAtomosBundles(ModuleLayer.boot()));
}
AtomosContentBase getAtomosContent(Module m)
{
AtomosContentBase result = atomosModules.get(m);
if (result != null)
{
return result;
}
for (AtomosLayer parent : getParents())
{
if (parent instanceof AtomosLayerModules)
{
result = ((AtomosLayerModules) parent).getAtomosContent(m);
}
if (result != null)
{
return result;
}
}
return null;
}
}
public Bundle getBundle(Module module)
{
if (module == null)
{
return null;
}
String location;
lockRead();
try
{
location = atomosKeyToConnectLocation.get(module);
if (location == null)
{
return null;
}
}
finally
{
unlockRead();
}
BundleContext bc = getBundleContext();
if (bc == null)
{
return null;
}
return bc.getBundle(location);
}
@Override
public void populateConfig(Map<String, String> config)
{
super.populateConfig(config);
if (config.get(Constants.FRAMEWORK_SYSTEMPACKAGES) == null)
{
// this is to prevent the framework from exporting all the packages provided
// by the module path.
config.put(Constants.FRAMEWORK_SYSTEMPACKAGES, "");
}
}
}