blob: 3c945e07b3e7a5dbb04cfd24dbe1d5879e01a3f0 [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.runtime.base;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.felix.atomos.impl.runtime.base.AtomosRuntimeBase.AtomosLayerBase.AtomosContentBase;
import org.apache.felix.atomos.impl.runtime.base.AtomosRuntimeBase.AtomosLayerBase.AtomosContentIndexed;
import org.apache.felix.atomos.impl.runtime.content.ConnectContentCloseableJar;
import org.apache.felix.atomos.impl.runtime.content.ConnectContentFile;
import org.apache.felix.atomos.impl.runtime.content.ConnectContentIndexed;
import org.apache.felix.atomos.impl.runtime.content.ConnectContentJar;
import org.apache.felix.atomos.runtime.AtomosContent;
import org.apache.felix.atomos.runtime.AtomosLayer;
import org.apache.felix.atomos.runtime.AtomosLayer.LoaderType;
import org.apache.felix.atomos.runtime.AtomosRuntime;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.SynchronousBundleListener;
import org.osgi.framework.Version;
import org.osgi.framework.connect.ConnectContent;
import org.osgi.framework.connect.ConnectFrameworkFactory;
import org.osgi.framework.connect.FrameworkUtilHelper;
import org.osgi.framework.connect.ModuleConnector;
import org.osgi.framework.hooks.bundle.CollisionHook;
import org.osgi.framework.hooks.resolver.ResolverHookFactory;
import org.osgi.framework.namespace.IdentityNamespace;
import org.osgi.framework.namespace.PackageNamespace;
import org.osgi.framework.wiring.BundleCapability;
import org.osgi.framework.wiring.BundleRevision;
import org.osgi.framework.wiring.FrameworkWiring;
import sun.misc.Signal;
public abstract class AtomosRuntimeBase implements AtomosRuntime, SynchronousBundleListener, FrameworkUtilHelper, FrameworkListener
{
static final String JAR_PROTOCOL = "jar";
static final String FILE_PROTOCOL = "file";
public static final String ATOMOS_PROP_PREFIX = "atomos.";
public static final String ATOMOS_REPORT_RESOLUTION_PROP = ATOMOS_PROP_PREFIX
+ "enable.resolution.errors";
public static final String ATOMOS_DEBUG_PROP = ATOMOS_PROP_PREFIX + "enable.debug";
public static final String ATOMOS_INDEX_PATH_PROP = ATOMOS_PROP_PREFIX + "index.path";
public static final String ATOMOS_IGNORE_INDEX = "IGNORE";
public static final String ATOMOS_BUNDLES_INDEX_DEFAULT = "/atomos/bundles.index";
public static final String ATOMOS_BUNDLE = "ATOMOS_BUNDLE";
public static final String ATOMOS_LIB_DIR_PROP = ATOMOS_PROP_PREFIX + "lib.dir";
public static final String ATOMOS_RUNTIME_CLASS_PROP = ATOMOS_PROP_PREFIX
+ "runtime.class";
public static final String ATOMOS_RUNTIME_MODULES_CLASS = "org.apache.felix.atomos.impl.runtime.modules.AtomosRuntimeModules";
public static final String ATOMOS_LIB_DIR = "atomos_lib";
public static final String GRAAL_NATIVE_IMAGE_KIND = "org.graalvm.nativeimage.kind";
private final boolean DEBUG;
private final boolean REPORT_RESOLUTION_ERRORS;
private final String indexPath;
private final AtomicReference<BundleContext> context = new AtomicReference<>();
private final AtomicReference<File> storeRoot = new AtomicReference<>();
private ServiceRegistration<?> atomosCommandsReg = null;
private ServiceRegistration<?> atomosRuntimeReg = null;
protected final Map<String, String> config = new ConcurrentHashMap<String, String>();
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// The following are all protected by the read/write lock
// A map of Atomos contents that have a connect location; the key is the connect location
private final Map<String, AtomosContentBase> connectLocationToAtomosContent = new HashMap<>();
// A map of all Atomos contents discovered (may not be installed as OSGi bundles); the key is the Atomos location
private final Map<String, AtomosContentBase> atomosLocationToAtomosContent = new HashMap<>();
// A map of connect locations for Atomos contents; key is the AtomosContentBase.getKey()
// Used to lookup an OSGi bundle location for a Class<?> in getBundleLocation(Class<?>)
protected final Map<Object, String> atomosKeyToConnectLocation = new HashMap<>();
// A map of Layers keyed by layer ID
final Map<Long, AtomosLayerBase> idToLayer = new HashMap<>();
// A map of connect locations for Atomos contents; key is Atomos content
private final Map<AtomosContent, String> atomosContentToConnectLocation = new HashMap<>();
// A set of connect locations that the framework has connected using the AtomosModuleConnector
private final Map<String, AtomosContentBase> connectedLocations = new HashMap<>();
// A map of indexed content; key is package name
private final Map<String, AtomosContentIndexed> packageToAtomosContent = new ConcurrentHashMap<>();
protected final AtomicLong nextLayerId = new AtomicLong(0);
public static enum Index
{
IGNORE, FIRST
}
public static AtomosRuntime newAtomosRuntime(Map<String, String> config)
{
String runtimeClass = config.get(ATOMOS_RUNTIME_CLASS_PROP);
if (runtimeClass != null)
{
return loadRuntime(runtimeClass, config);
}
if (config.get(
ATOMOS_LIB_DIR_PROP) != null
|| System.getProperty(GRAAL_NATIVE_IMAGE_KIND) != null)
{
return new AtomosRuntimeClassPath(config);
}
try
{
Class.forName("java.lang.Module");
return loadRuntime(ATOMOS_RUNTIME_MODULES_CLASS, config);
}
catch (ClassNotFoundException e)
{
// ignore
}
// default to classpath
return new AtomosRuntimeClassPath(config);
}
private static AtomosRuntime loadRuntime(String runtimeClass,
Map<String, String> config)
{
try
{
return (AtomosRuntimeBase) Class.forName(
runtimeClass).getConstructor(Map.class).newInstance(config);
}
catch (Exception e)
{
throw e instanceof RuntimeException ? (RuntimeException) e
: new RuntimeException(e);
}
}
public File findAtomosLibDir()
{
String libDirProp = config.get(ATOMOS_LIB_DIR_PROP);
return new File(libDirProp, ATOMOS_LIB_DIR);
}
protected AtomosRuntimeBase(Map<String, String> config)
{
saveConfig(config);
DEBUG = Boolean.parseBoolean(this.config.get(ATOMOS_DEBUG_PROP));
REPORT_RESOLUTION_ERRORS = Boolean.parseBoolean(
this.config.get(ATOMOS_REPORT_RESOLUTION_PROP));
indexPath = getIndexPath(this.config.get(ATOMOS_INDEX_PATH_PROP));
try
{
// substrate native image does not run shutdown hooks on ctrl-c
// this works around it by using our own signal handler
Signal.handle(new Signal("INT"), sig -> System.exit(0));
}
catch (Throwable t)
{
// do nothing if Signal isn't available
}
}
private String getIndexPath(String indexPath)
{
if (indexPath == null)
{
indexPath = ATOMOS_BUNDLES_INDEX_DEFAULT;
}
else if (!ATOMOS_IGNORE_INDEX.equals(indexPath) && !indexPath.startsWith("/"))
{
indexPath = "/" + indexPath;
}
return indexPath;
}
protected final void lockWrite()
{
lock.writeLock().lock();
}
protected final void unlockWrite()
{
lock.writeLock().unlock();
}
protected final void lockRead()
{
lock.readLock().lock();
}
protected final void unlockRead()
{
lock.readLock().unlock();
}
protected final AtomosContentBase getByConnectLocation(String location, boolean isManaged)
{
lockRead();
try
{
AtomosContentBase result = null;
if (isManaged && !Constants.SYSTEM_BUNDLE_LOCATION.equals(location))
{
result = connectedLocations.get(location);
}
else
{
result = connectLocationToAtomosContent.get(location);
}
debug("Found content %s for location: %s %s", result, location, isManaged);
return result;
}
finally
{
unlockRead();
}
}
final void connectAtomosContent(
String connectLocation,
AtomosContentBase atomosContent)
{
debug("Connecting content: %s %s", atomosContent, connectLocation);
if (connectLocation == null)
{
throw new IllegalArgumentException("A null connect loation is not allowed.");
}
lockWrite();
try
{
AtomosContent existing = connectLocationToAtomosContent.get(connectLocation);
if (existing != null && !atomosContent.equals(existing))
{
throw new IllegalStateException(
"The bundle location is already used by the AtomosContent "
+ existing);
}
String computeLocation = atomosContentToConnectLocation.compute(atomosContent,
(c, l) -> l == null ? connectLocation : l);
if (!Objects.equals(connectLocation, computeLocation))
{
throw new IllegalStateException(
"Atomos content location is already set: " + computeLocation);
}
connectLocationToAtomosContent.put(connectLocation, atomosContent);
atomosKeyToConnectLocation.put(atomosContent.getKey(), connectLocation);
}
finally
{
unlockWrite();
}
}
void disconnectAtomosContent(AtomosContentBase atomosContent)
{
debug("Disconnecting connent: %s", atomosContent);
lockWrite();
try
{
if (Constants.SYSTEM_BUNDLE_LOCATION.equals(
atomosContent.getAtomosLocation()))
{
throw new UnsupportedOperationException(
"Cannot disconnect the system bundle content");
}
String removedLocation = atomosContentToConnectLocation.remove(atomosContent);
if (removedLocation != null)
{
debug("Disconnecting location: %s %s", removedLocation, atomosContent);
connectLocationToAtomosContent.remove(removedLocation);
atomosKeyToConnectLocation.remove(atomosContent.getKey());
connectedLocations.remove(removedLocation);
}
else
{
debug("No connected location found for content: %s", atomosContent);
}
}
finally
{
unlockWrite();
}
}
final AtomosContentBase getByAtomosLocation(String location)
{
lockRead();
try
{
return atomosLocationToAtomosContent.get(location);
}
finally
{
unlockRead();
}
}
protected final AtomosLayerBase getById(long id)
{
lockRead();
try
{
return idToLayer.get(id);
}
finally
{
unlockRead();
}
}
final String getByAtomosContent(AtomosContent atomosContent)
{
lockRead();
try
{
return atomosContentToConnectLocation.get(atomosContent);
}
finally
{
unlockRead();
}
}
@Override
public final AtomosContent getConnectedContent(String bundleLocation)
{
return getByConnectLocation(bundleLocation, true);
}
final Bundle getBundle(AtomosContent atomosContent)
{
String location = getByAtomosContent(atomosContent);
if (location != null)
{
if (atomosContent == getByConnectLocation(location, true))
{
BundleContext bc = context.get();
if (bc != null)
{
return bc.getBundle(location);
}
}
}
return null;
}
protected AtomosLayer addLayer(List<AtomosLayer> parents, String name, long id,
LoaderType loaderType, Path... paths)
{
throw new UnsupportedOperationException(
"Cannot add module layers when Atomos is not loaded as module.");
}
@Override
public final AtomosLayer addLayer(List<AtomosLayer> parents, String name,
LoaderType loaderType, Path... modulePaths)
{
return addLayer(parents, name, -1, loaderType, modulePaths);
}
@Override
public ModuleConnector getModuleConnector()
{
return new AtomosModuleConnector(this);
}
abstract public ConnectFrameworkFactory findFrameworkFactory();
protected final BundleContext getBundleContext()
{
return context.get();
}
final ThreadLocal<Deque<AtomosContent>> managingConnected = ThreadLocal.withInitial(ArrayDeque::new);
final Bundle installAtomosContent(String prefix,
AtomosContentBase atomosContent)
throws BundleException
{
if (prefix == null)
{
prefix = "atomos";
}
if (prefix.indexOf(':') != -1)
{
throw new IllegalArgumentException("The prefix cannot contain ':'");
}
prefix = prefix + ':';
debug("Installing atomos content: %s%s", prefix,
atomosContent.getAtomosLocation());
BundleContext bc = context.get();
if (bc == null)
{
throw new IllegalStateException("Framework has not been initialized.");
}
String location = atomosContent.getAtomosLocation();
if (!Constants.SYSTEM_BUNDLE_LOCATION.equals(location))
{
location = prefix + location;
}
AtomosLayerBase atomosLayer = (AtomosLayerBase) atomosContent.getAtomosLayer();
if (atomosLayer.isNotValid())
{
throw new BundleException("Atomos layer has been uninstalled.",
BundleException.INVALID_OPERATION);
}
String existingLoc = getByAtomosContent(atomosContent);
if (existingLoc != null)
{
Bundle existing = bc.getBundle(existingLoc);
if (existing != null)
{
if (Constants.SYSTEM_BUNDLE_LOCATION.equals(existingLoc)
|| (existingLoc.equals(location)
&& atomosContent.getBundle() == existing))
{
return existing;
}
throw new BundleException(
"Atomos content is already connected with bundle: "
+ existing,
BundleException.DUPLICATE_BUNDLE_ERROR);
}
}
atomosContent.disconnect();
atomosContent.connect(location);
Bundle result = null;
try
{
result = bc.installBundle(location);
}
finally
{
// check if the layer is still valid
if (atomosLayer.isNotValid())
{
// The atomosLayer became invalid while installing
if (result != null)
{
result.uninstall();
result = null;
}
}
}
return result;
}
final AtomosContent currentlyManagingConnected()
{
return managingConnected.get().peekLast();
}
final void addManagingConnected(AtomosContentBase atomosBundle, String location)
{
lockWrite();
try
{
connectedLocations.compute(location, (l, a) -> {
if (a == null || a == atomosBundle)
{
return atomosBundle;
}
throw new IllegalStateException(
"Atomos connect location is already managed by: " + a);
});
}
finally
{
unlockWrite();
}
if (context.get() != null)
{
managingConnected.get().addLast(atomosBundle);
}
}
@Override
public final void bundleChanged(BundleEvent event)
{
boolean connectionManaged = true;
String location = event.getBundle().getLocation();
switch (event.getType())
{
case BundleEvent.INSTALLED:
case BundleEvent.UPDATED :
addPackages(event.getBundle());
AtomosContent content = getByConnectLocation(location, true);
if (content != null)
{
debug("Bundle successfully connected %s", content);
connectionManaged = managingConnected.get().removeLastOccurrence(
content);
}
else
{
connectionManaged = false;
}
break;
case BundleEvent.UNINSTALLED :
connectionManaged = false;
break;
default:
break;
}
if (!connectionManaged)
{
lockWrite();
try
{
debug("Removing location %s as a connected location.", location);
connectedLocations.remove(location);
}
finally
{
unlockWrite();
}
}
}
@Override
public void frameworkEvent(FrameworkEvent event)
{
if (REPORT_RESOLUTION_ERRORS && event.getType() == FrameworkEvent.ERROR)
{
if (event.getThrowable() instanceof BundleException
&& ((BundleException) event.getThrowable()).getType() == BundleException.RESOLVE_ERROR)
{
Bundle b = event.getBundle();
BundleRevision rev = b == null ? null : b.adapt(BundleRevision.class);
if (rev != null)
{
rev.getCapabilities(IdentityNamespace.IDENTITY_NAMESPACE).forEach(
i -> {
@SuppressWarnings("unchecked")
List<String> tags = (List<String>) i.getAttributes().get(
IdentityNamespace.CAPABILITY_TAGS_ATTRIBUTE);
if (tags != null
&& tags.contains(ConnectContent.TAG_OSGI_CONNECT))
{
System.out.println("Unable to resolve connected bundle: "
+ event.getThrowable().getMessage());
}
});
}
}
}
}
protected final void addAtomosLayer(AtomosLayerBase atomosLayer)
{
addingLayer(atomosLayer);
if (idToLayer.putIfAbsent(atomosLayer.getId(), atomosLayer) != null)
{
throw new IllegalStateException(
"AtomosLayer already exists for id: " + atomosLayer.getId());
}
for (AtomosContent atomosContent : atomosLayer.getAtomosContents())
{
if (atomosLocationToAtomosContent.putIfAbsent(
atomosContent.getAtomosLocation(), (AtomosContentBase) atomosContent) != null)
{
throw new IllegalStateException("Atomos content location already exists: "
+ atomosContent.getAtomosLocation());
}
if (Constants.SYSTEM_BUNDLE_LOCATION.equals(atomosContent.getAtomosLocation()))
{
// system bundle location is always marked as connected
connectAtomosContent(Constants.SYSTEM_BUNDLE_LOCATION,
(AtomosContentBase) atomosContent);
}
}
for (AtomosLayer parent : atomosLayer.getParents())
{
((AtomosLayerBase) parent).addChild(atomosLayer);
}
}
abstract protected void addingLayer(AtomosLayerBase atomosLayer);
abstract protected void removedLayer(AtomosLayerBase atomosLayer);
abstract public class AtomosLayerBase implements AtomosLayer
{
private final long id;
private final String name;
private final LoaderType loaderType;
private final List<AtomosLayer> parents;
private final Set<AtomosLayer> children = new HashSet<>();
private final List<Path> paths;
private volatile boolean valid = true;
private volatile Map<String, AtomosContent> nameToBundle;
public AtomosLayerBase(List<AtomosLayer> parents, long id, String name, LoaderType loaderType, Path... paths)
{
this.id = id;
this.name = name == null ? "" : name;
this.paths = Arrays.asList(paths);
this.parents = parents;
this.loaderType = loaderType;
}
@Override
public AtomosLayer addLayer(String name, LoaderType loaderType,
Path... modulePaths)
{
return AtomosRuntimeBase.this.addLayer(Collections.singletonList(this), name,
-1, loaderType, modulePaths);
}
@Override
public AtomosLayer addModules(String name, Path path)
{
throw new UnsupportedOperationException(
"Cannot add module layers when Atomos is not loaded as module.");
}
@Override
public boolean isAddLayerSupported()
{
return false;
}
protected final void addChild(AtomosLayerBase child)
{
children.add(child);
}
protected final void removeChild(AtomosLayerBase child)
{
children.remove(child);
}
protected final Set<AtomosContentBase> findAtomosContents()
{
Set<AtomosContentBase> bootBundles = new LinkedHashSet<>();
// first get the modules from the boot ModuleLayer (Java 9+ JPMS)
findBootModuleLayerAtomosContents(bootBundles);
// for class path mode we look for manifests on the classpath
findAtomosContentsByClassLoaderManifests(bootBundles);
// finally find by Atomos index, note that Atomos index content will override duplicates
findAtomosIndexedContents(bootBundles);
return Collections.unmodifiableSet(bootBundles);
}
protected abstract void findBootModuleLayerAtomosContents(
Set<AtomosContentBase> result);
void findAtomosContentsByClassLoaderManifests(Set<AtomosContentBase> result)
{
try
{
ClassLoader cl = getClass().getClassLoader();
Set<URL> parentManifests = new HashSet<>();
if (cl.getParent() != null)
{
Enumeration<URL> eParentManifests = cl.getParent().getResources(
JarFile.MANIFEST_NAME);
while (eParentManifests.hasMoreElements())
{
parentManifests.add(eParentManifests.nextElement());
}
}
Enumeration<URL> classpathManifests = cl.getResources(
JarFile.MANIFEST_NAME);
while (classpathManifests.hasMoreElements())
{
URL manifest = classpathManifests.nextElement();
if (parentManifests.contains(manifest))
{
// ignore parent manifests
continue;
}
Attributes headers = new Manifest(
manifest.openStream()).getMainAttributes();
String symbolicName = headers.getValue(Constants.BUNDLE_SYMBOLICNAME);
if (symbolicName != null)
{
int semiColon = symbolicName.indexOf(';');
if (semiColon != -1)
{
symbolicName = symbolicName.substring(0, semiColon);
}
symbolicName = symbolicName.trim();
Object content = getBundleContent(manifest);
if (content != null)
{
ConnectContent connectContent;
URL url;
if (content instanceof File)
{
connectContent = new ConnectContentFile((File) content);
url = ((File) content).toURI().toURL();
}
else
{
connectContent = new ConnectContentJar(
() -> ((JarFile) content), null);
url = new File(
((JarFile) content).getName()).toURI().toURL();
}
String location;
if (connectContent.getEntry(
"META-INF/services/org.osgi.framework.launch.FrameworkFactory").isPresent())
{
location = Constants.SYSTEM_BUNDLE_LOCATION;
}
else
{
location = content instanceof File
? ((File) content).getPath()
: ((JarFile) content).getName();
if (!getName().isEmpty())
{
location = getName() + ":" + location;
}
}
Version version = Version.parseVersion(
headers.getValue(Constants.BUNDLE_VERSION));
result.add(new AtomosContentClassPath(location, symbolicName,
version, connectContent, url));
}
}
}
}
catch (IOException e)
{
throw new IllegalStateException("Error finding class path bundles.", e);
}
}
private void findAtomosIndexedContents(Set<AtomosContentBase> bootBundles)
{
URL index = ATOMOS_IGNORE_INDEX.equals(indexPath)
? null
: getClass().getResource(indexPath);
debug("Atomos index url: %s", index);
if (index != null)
{
findAtomosIndexedContent(index, bootBundles);
}
else
{
File atomosLibDir = findAtomosLibDir();
if (atomosLibDir.isDirectory())
{
findAtomosLibIndexedContent(bootBundles, atomosLibDir);
}
}
}
private void findAtomosLibIndexedContent(Set<AtomosContentBase> bootBundles,
File atomosLibDir)
{
for (File f : atomosLibDir.listFiles())
{
if (f.isFile())
{
try (JarFile jar = new JarFile(f))
{
Attributes headers = jar.getManifest().getMainAttributes();
String symbolicName = headers.getValue(
Constants.BUNDLE_SYMBOLICNAME);
if (symbolicName != null)
{
int semiColon = symbolicName.indexOf(';');
if (semiColon != -1)
{
symbolicName = symbolicName.substring(0, semiColon);
}
symbolicName = symbolicName.trim();
ConnectContent connectContent = new ConnectContentCloseableJar(
f.getName(), () -> atomosLibDir);
connectContent.open();
String location;
if (connectContent.getEntry(
"META-INF/services/org.osgi.framework.launch.FrameworkFactory").isPresent())
{
location = Constants.SYSTEM_BUNDLE_LOCATION;
}
else
{
location = f.getName();
if (!getName().isEmpty())
{
location = getName() + ":" + location;
}
}
Version version = Version.parseVersion(
headers.getValue(Constants.BUNDLE_VERSION));
AtomosContentBase bundle = new AtomosContentIndexed(location,
symbolicName, version, connectContent);
bootBundles.add(bundle);
}
}
catch (IOException e)
{
// ignore and continue
}
}
}
}
private AtomosContentIndexed createIndexedContent(String indexRoot,
String currentIndex,
String currentBSN, Version currentVersion, List<String> currentPaths)
{
String bundleIndexPath = indexRoot + currentIndex;
ConnectContentIndexed content = new ConnectContentIndexed(bundleIndexPath,
currentPaths);
debug("Found indexed content: %s %s %s %s", currentIndex, currentBSN,
currentVersion, currentPaths);
return new AtomosContentIndexed(getIndexedLocation(content, currentBSN),
currentBSN, currentVersion, content);
}
private void findAtomosIndexedContent(URL index,
Set<AtomosContentBase> bootBundles)
{
final String indexRoot = indexPath.substring(0,
indexPath.lastIndexOf('/') + 1);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(index.openStream())))
{
String currentIndex = null;
String currentBSN = null;
Version currentVersion = null;
List<String> currentPaths = null;
String line = reader.readLine();
while (ATOMOS_BUNDLE.equals(line))
{
if (currentIndex != null)
{
bootBundles.add(createIndexedContent(indexRoot, currentIndex,
currentBSN, currentVersion, currentPaths));
}
currentIndex = null;
currentBSN = null;
currentVersion = null;
currentPaths = new ArrayList<>();
while ((line = reader.readLine()) != null
&& !ATOMOS_BUNDLE.equals(line))
{
if (currentIndex == null)
{
currentIndex = line;
}
else if (currentBSN == null)
{
currentBSN = line;
}
else if (currentVersion == null)
{
currentVersion = Version.valueOf(line);
}
else
{
currentPaths.add(line);
}
}
}
if (currentIndex != null)
{
bootBundles.add(createIndexedContent(indexRoot, currentIndex,
currentBSN, currentVersion, currentPaths));
}
}
catch (IOException e)
{
throw new RuntimeException(e);
}
}
private static final String FWK_FACTORY_SERVICE = "META-INF/services/org.osgi.framework.launch.FrameworkFactory";
private String getIndexedLocation(ConnectContent content, String location)
{
return content.getEntry(FWK_FACTORY_SERVICE).map(
e -> Constants.SYSTEM_BUNDLE_LOCATION).orElseGet(() -> {
String qualified = location;
;
if (!getName().isEmpty())
{
qualified = getName() + ":" + location;
}
return qualified;
});
}
/**
* Returns the bundle content that contains the specified manifest URL.
* The return type will be a JarFile or a File for an exploded bundle.
* @param manifest the manifest URL to get the bundle content for
* @return a JarFile or File
*/
private Object getBundleContent(URL manifest)
{
if (JAR_PROTOCOL.equals(manifest.getProtocol()))
{
// Use a connection to get the JarFile this avoids having to parse the jar: URL
// For spring loader they support nested jars with additional !/
// For example:
// jar:file:/path/to/out.jar!/path/to/inner.jar!/META-INF/MANIFEST.MF
// Instead of dealing with that just get the JarFile directly that supports this
// embedded jar stuff
try
{
URLConnection conn = manifest.openConnection();
if (conn instanceof JarURLConnection)
{
return ((JarURLConnection) conn).getJarFile();
}
}
catch (IOException e)
{
// TODO log?
}
// TODO either log or add tracing to help debug issues
}
else if (FILE_PROTOCOL.equals(manifest.getProtocol()))
{
try
{
File f = new File(manifest.toURI());
// return two parents up from the manifest file
return f.getParentFile().getParentFile();
}
catch (URISyntaxException e)
{
throw new RuntimeException(e);
}
}
return null;
}
@Override
public final String getName()
{
return name;
}
@Override
public final Set<AtomosLayer> getChildren()
{
lockRead();
try
{
return new HashSet<>(children);
}
finally
{
unlockRead();
}
}
@Override
public final List<AtomosLayer> getParents()
{
return parents;
}
final List<Path> getPaths()
{
return paths;
}
@Override
public final long getId()
{
return id;
}
@Override
public final LoaderType getLoaderType()
{
return loaderType;
}
@Override
public final void uninstall() throws BundleException
{
List<Bundle> uninstalledBundles = new ArrayList<>();
BundleContext bc = getBundleContext();
if (bc != null)
{
uninstallLayer(uninstalledBundles);
}
lockWrite();
try
{
// now remove the layer from the runtime
removeLayerFromRuntime();
}
finally
{
unlockWrite();
}
if (bc != null)
{
// now refresh any uninstalled bundles
bc.getBundle(Constants.SYSTEM_BUNDLE_LOCATION).adapt(
FrameworkWiring.class).refreshBundles(uninstalledBundles);
}
}
private void removeLayerFromRuntime()
{
for (AtomosLayer parent : getParents())
{
((AtomosLayerBase) parent).removeChild(this);
}
for (AtomosLayer child : getChildren())
{
((AtomosLayerBase) child).removeLayerFromRuntime();
}
getAtomosContents().forEach(AtomosContent::disconnect);
idToLayer.remove(getId());
removedLayer(this);
}
final void uninstallLayer(List<Bundle> uninstalledBundles)
throws BundleException
{
// mark as invalid first to prevent installs
valid = false;
if (getBootLayer().equals(this))
{
throw new UnsupportedOperationException(
"Cannot uninstall the boot layer.");
}
// first uninstall all children
for (AtomosLayer child : getChildren())
{
((AtomosLayerBase) child).uninstallLayer(uninstalledBundles);
}
uninstallBundles(uninstalledBundles);
}
final boolean isNotValid()
{
return !valid;
}
private void uninstallBundles(List<Bundle> uninstalled)
throws BundleException
{
for (AtomosContent content : getAtomosContents())
{
Bundle b = content.getBundle();
if (b != null)
{
uninstalled.add(b);
b.uninstall();
}
}
}
@Override
public final String toString()
{
StringBuilder result = new StringBuilder();
result.append('[').append(getId()).append(']');
result.append(' ').append(getName());
result.append(' ').append(getLoaderType());
List<AtomosLayer> parents = getParents();
if (!parents.isEmpty())
{
result.append(" PARENTS: {");
for (AtomosLayer parent : parents)
{
result.append("[").append(parent.getId()).append(']');
result.append(' ').append(parent.getName()).append(", ");
}
result.delete(result.length() - 2, result.length());
result.append("}");
}
Set<AtomosLayer> children = getChildren();
if (!children.isEmpty())
{
result.append(" CHILDREN: {");
for (AtomosLayer child : getChildren())
{
result.append("[").append(child.getId()).append(']');
result.append(' ').append(child.getName()).append(", ");
}
result.delete(result.length() - 2, result.length());
result.append("}");
}
return result.toString();
}
@Override
public <T> Optional<T> adapt(Class<T> type)
{
// do nothing by default
return Optional.empty();
}
@Override
public Optional<AtomosContent> findAtomosContent(String symbolicName)
{
Map<String, AtomosContent> nameToBundle = this.nameToBundle;
if (nameToBundle == null)
{
nameToBundle = new HashMap<>();
final Map<String, AtomosContent> populate = nameToBundle;
getAtomosContents().forEach(
(a) -> populate.putIfAbsent(a.getSymbolicName(), a));
Set<AtomosLayer> visited = new HashSet<>();
Deque<AtomosLayer> stack = new ArrayDeque<>();
visited.add(this);
stack.push(this);
while (!stack.isEmpty())
{
AtomosLayer layer = stack.pop();
layer.getAtomosContents().forEach(
(a) -> populate.putIfAbsent(a.getSymbolicName(), a));
List<AtomosLayer> parents = layer.getParents();
for (int i = parents.size() - 1; i >= 0; i--)
{
AtomosLayer parent = parents.get(i);
if (!visited.contains(parent))
{
visited.add(parent);
stack.push(parent);
}
}
}
this.nameToBundle = populate;
}
return Optional.ofNullable(nameToBundle.get(symbolicName));
}
/**
* The base Atomos content implementation that all AtomosContent implementations extend
*/
public abstract class AtomosContentBase implements AtomosContent, Comparable<AtomosContent>
{
/**
* The bundle location used to install the bundle with.
*/
private final String location;
/**
* Bundle symbolic name
*/
private final String symbolicName;
/**
* Bundle version
*/
private final Version version;
/**
* Connect content
*/
private final ConnectContent content;
public AtomosContentBase(String location, String symbolicName, Version version, ConnectContent content)
{
this.location = location;
this.symbolicName = symbolicName;
this.version = version;
this.content = content;
}
@Override
public final String getAtomosLocation()
{
return location;
}
@Override
public final String getSymbolicName()
{
return symbolicName;
}
@Override
public final Version getVersion()
{
return version;
}
@Override
public <T> Optional<T> adapt(Class<T> type)
{
return Optional.empty();
}
@Override
public final AtomosLayer getAtomosLayer()
{
return AtomosLayerBase.this;
}
@Override
public final boolean equals(Object o)
{
if (!(o instanceof AtomosContentBase))
{
return false;
}
AtomosContentBase info = (AtomosContentBase) o;
return getSymbolicName().equals(info.getSymbolicName())
&& getVersion().equals(info.getVersion())
&& getAtomosLayer() == info.getAtomosLayer();
}
@Override
public final int hashCode()
{
return getSymbolicName().hashCode() ^ getVersion().hashCode();
}
@Override
public final int compareTo(AtomosContent o)
{
int bsnCompare = getSymbolicName().compareTo(o.getSymbolicName());
if (bsnCompare != 0)
{
return bsnCompare;
}
int vCompare = -(getVersion().compareTo(o.getVersion()));
if (vCompare != 0)
{
return vCompare;
}
return getAtomosLocation().compareTo(o.getAtomosLocation());
}
protected abstract Object getKey();
@Override
public ConnectContent getConnectContent()
{
debug("Getting connect content for %s", this);
return content;
}
public final String toString()
{
return symbolicName;
}
@Override
public final Bundle install(String prefix) throws BundleException
{
return installAtomosContent(prefix, this);
}
@Override
public Bundle getBundle()
{
return AtomosRuntimeBase.this.getBundle(this);
}
@Override
public String getConnectLocation()
{
return getByAtomosContent(this);
}
@Override
public void connect(String bundleLocation)
{
connectAtomosContent(bundleLocation, this);
}
@Override
public void disconnect()
{
disconnectAtomosContent(this);
}
}
/**
* Atomos content discovered on the class path. The key is the a file URL
* to the file on disk which is on the class path.
*
*/
public class AtomosContentClassPath extends AtomosContentBase
{
private final URL contentURL;
public AtomosContentClassPath(String location, String symbolicName, Version version, ConnectContent connectContent, URL url)
{
super(location, symbolicName, version, connectContent);
this.contentURL = url;
}
@Override
protected final Object getKey()
{
return contentURL;
}
}
/**
* Atomos content discovered in an Atomos index. The key is this content itself
* which is used to lookup the content based on package name.
*/
public class AtomosContentIndexed extends AtomosContentBase
{
public AtomosContentIndexed(String location, String symbolicName, Version version, ConnectContent content)
{
super(location, symbolicName, version, content);
}
@Override
protected final Object getKey()
{
// TODO a bit hokey to use ourselves as a key
return this;
}
}
}
@Override
public final Optional<Bundle> getBundle(Class<?> classFromBundle)
{
String location = getConnectLocation(classFromBundle);
if (location != null)
{
BundleContext bc = context.get();
if (bc != null)
{
return Optional.ofNullable(bc.getBundle(location));
}
}
return Optional.empty();
}
protected final String getConnectLocation(Class<?> classFromBundle)
{
lockRead();
try
{
return atomosKeyToConnectLocation.get(getAtomosKey(classFromBundle));
}
finally
{
unlockRead();
}
}
protected Object getAtomosKey(Class<?> classFromBundle)
{
Package pkg = classFromBundle.getPackage();
if (pkg != null)
{
AtomosContentIndexed indexed = packageToAtomosContent.get(pkg.getName());
if (indexed != null)
{
return indexed;
}
}
return classFromBundle.getProtectionDomain().getCodeSource().getLocation();
}
protected abstract void filterBasedOnReadEdges(
AtomosContent atomosContent,
Collection<BundleCapability> candidates);
protected final void filterNotVisible(
AtomosContent atomosContent,
Collection<BundleCapability> candidates)
{
if (atomosContent != null)
{
candidates.removeIf(candidate -> !isVisible(atomosContent, candidate));
}
}
private boolean isVisible(
AtomosContent atomosContent,
BundleCapability candidate)
{
AtomosContent candidateAtomos = getByConnectLocation(
candidate.getRevision().getBundle().getLocation(), true);
if (candidateAtomos == null)
{
// atomos connected content cannot see normal bundles
return false;
}
else
{
AtomosLayer thisLayer = atomosContent.getAtomosLayer();
return isInLayerHierarchy(thisLayer, candidateAtomos.getAtomosLayer());
}
}
final boolean isInLayerHierarchy(AtomosLayer thisLayer, AtomosLayer candLayer)
{
if (thisLayer.equals(candLayer))
{
return true;
}
for (AtomosLayer parent : thisLayer.getParents())
{
if (isInLayerHierarchy(parent, candLayer))
{
return true;
}
}
return false;
}
@SuppressWarnings("unchecked")
public static <T> Set<T> asSet(Set<? extends T> l)
{
return (Set<T>) l;
}
Thread saveOnVMExit = new Thread(() -> {
try
{
new AtomosStorage(this).saveLayers(storeRoot.get());
}
catch (IOException e)
{
throw new RuntimeException("Failed to save atomos runtime.", e);
}
});
protected void start(BundleContext bc) throws BundleException
{
debug("Activating Atomos runtime");
this.context.set(bc);
Runtime.getRuntime().addShutdownHook(saveOnVMExit);
AtomosFrameworkUtilHelper.addHelper(this);
bc.addBundleListener(this);
bc.addFrameworkListener(this);
for (Bundle b : bc.getBundles())
{
addPackages(b);
}
AtomosFrameworkHooks hooks = new AtomosFrameworkHooks(this);
bc.registerService(ResolverHookFactory.class, hooks, null);
bc.registerService(CollisionHook.class, hooks, null);
boolean installBundles = Boolean.parseBoolean(
getProperty(bc, AtomosRuntime.ATOMOS_CONTENT_INSTALL, "true"));
boolean startBundles = Boolean.parseBoolean(
getProperty(bc, AtomosRuntime.ATOMOS_CONTENT_START, "true"));
installAtomosContents(getBootLayer(), installBundles, startBundles);
atomosRuntimeReg = bc.registerService(AtomosRuntime.class, this, null);
atomosCommandsReg = new AtomosCommands(this).register(bc);
}
void addPackages(Bundle b)
{
AtomosContentBase atomosContent = (AtomosContentBase) getConnectedContent(
b.getLocation());
if (atomosContent instanceof AtomosContentIndexed)
{
BundleRevision r = b.adapt(BundleRevision.class);
r.getDeclaredCapabilities(PackageNamespace.PACKAGE_NAMESPACE).forEach(
(p) -> packageToAtomosContent.putIfAbsent(
(String) p.getAttributes().get(PackageNamespace.PACKAGE_NAMESPACE),
(AtomosContentIndexed) atomosContent));
String privatePackages = b.getHeaders("").get("Private-Package");
if (privatePackages != null)
{
for (String pkgName : privatePackages.split(","))
{
pkgName = pkgName.trim();
packageToAtomosContent.put(pkgName,
(AtomosContentIndexed) atomosContent);
}
}
else
{
// ensure content is open
b.getEntry("does.not.exist");
ConnectContent content = atomosContent.getConnectContent();
try
{
content.getEntries().forEach((s) -> {
if (s.length() > 1 && s.endsWith("/") && s.indexOf('-') < 0)
{
String pkg = s.substring(0, s.length() - 1).replace('/', '.');
packageToAtomosContent.put(pkg,
(AtomosContentIndexed) atomosContent);
}
});
}
catch (IOException e)
{
// ignore
debug("IOException getting entries: %s", e.getMessage());
}
}
}
}
protected void stop(BundleContext bc) throws BundleException
{
debug("Stopping Atomos runtime");
this.context.compareAndSet(bc, null);
try
{
Runtime.getRuntime().removeShutdownHook(saveOnVMExit);
new AtomosStorage(this).saveLayers(storeRoot.get());
}
catch (IllegalStateException e)
{
// ignore this; happens if the JVM already is in the process of running shutdown hooks
// in that case we can skip saveLayers call
}
catch (IOException e)
{
throw new BundleException("Failed to save atomos runtime.", e);
}
bc.removeBundleListener(this);
bc.removeFrameworkListener(this);
AtomosFrameworkUtilHelper.removeHelper(this);
atomosCommandsReg.unregister();
atomosRuntimeReg.unregister();
}
private String getProperty(BundleContext bc, String key, String defaultValue)
{
String result = bc.getProperty(key);
return result == null ? defaultValue : result;
}
private void installAtomosContents(AtomosLayer atomosLayer,
boolean installBundles,
boolean startBundles)
{
if (installBundles)
{
debug("Installing Atomos content.");
List<Bundle> bundles = new ArrayList<>();
atomosLayer.getAtomosContents().stream().sorted().forEach((atomosContent) -> //
{
if (getBundle(atomosContent) == null)
{
debug("Installing AtomosContent: %s", atomosContent);
try
{
Bundle b = atomosContent.install("atomos");
if (b != null && b.getBundleId() != 0)
{
bundles.add(b);
}
}
catch (BundleException e)
{
debug("Failed to install to install %s: %s", atomosContent,
e.getMessage());
}
}
});
if (startBundles)
{
for (Bundle b : bundles)
{
debug("Starting connected bundle: %s", b);
BundleRevision rev = b.adapt(BundleRevision.class);
if ((rev.getTypes() & BundleRevision.TYPE_FRAGMENT) == 0)
{
try
{
b.start();
}
catch (BundleException e)
{
debug("Failed to install to install %s: %s", e,
b,
e.getMessage());
}
}
}
for (AtomosLayer child : atomosLayer.getChildren())
{
installAtomosContents(child, installBundles, startBundles);
}
}
}
}
public void initialize(File storage, Map<String, String> configuration)
{
saveConfig(configuration);
if (!storeRoot.compareAndSet(null, storage))
{
throw new IllegalStateException(
"This AtomosRuntime is already being used by store: " + storeRoot.get());
}
try
{
new AtomosStorage(this).loadLayers(storage);
}
catch (IOException e)
{
throw new UncheckedIOException(e);
}
}
private void saveConfig(Map<String, String> configuration) {
configuration.forEach((k,v) -> {
if (k != null && k.startsWith(ATOMOS_PROP_PREFIX) && v != null)
{
config.put(k, v);
}
});
}
public void debug(String message, BundleException e, Object... args)
{
debug(message, args);
if (DEBUG)
{
e.printStackTrace();
}
}
public void debug(String message, Object... args)
{
if (DEBUG)
{
try
{
System.out.println("ATOMOS DEBUG: " + String.format(message, args));
}
catch (Throwable t)
{
t.printStackTrace();
}
}
}
@SuppressWarnings("unchecked")
public static <E extends Throwable> void sneakyThrow(Throwable e) throws E
{
throw (E) e;
}
public void populateConfig(Map<String, String> frameworkConfig)
{
// do nothing by default
}
}