/**
* OrbisGIS is a java GIS application dedicated to research in GIScience.
* OrbisGIS is developed by the GIS group of the DECIDE team of the
* Lab-STICC CNRS laboratory, see <http://www.lab-sticc.fr/>.
*
* The GIS group of the DECIDE team is located at :
*
* Laboratoire Lab-STICC – CNRS UMR 6285
* Equipe DECIDE
* UNIVERSITÉ DE BRETAGNE-SUD
* Institut Universitaire de Technologie de Vannes
* 8, Rue Montaigne - BP 561 56017 Vannes Cedex
*
* OrbisGIS is distributed under GPL 3 license.
*
* Copyright (C) 2007-2014 CNRS (IRSTV FR CNRS 2488)
* Copyright (C) 2015-2017 CNRS (Lab-STICC UMR CNRS 6285)
*
* This file is part of OrbisGIS.
*
* OrbisGIS is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* OrbisGIS is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* OrbisGIS. If not, see <http://www.gnu.org/licenses/>.
*
* For more information, please consult: <http://www.orbisgis.org/>
* or contact directly:
* info_at_ orbisgis.org
*/
package org.orbisgis.framework;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.felix.framework.Logger;
import org.apache.felix.framework.util.manifestparser.ManifestParser;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
import org.osgi.framework.Version;
import org.osgi.framework.wiring.BundleCapability;
import org.xnap.commons.i18n.I18n;
import org.xnap.commons.i18n.I18nFactory;
/**
* Functions used by Bundle Host. As long as OrbisGIS is not entirely converted into OSGi bundles,
* the host need to export packages using this class.
* @author Nicolas Fortin
*/
public class BundleTools {
private final static I18n I18N = I18nFactory.getI18n(BundleTools.class);
public final static String MANIFEST_FILENAME = "MANIFEST.MF";
private final static String PACKAGE_NAMESPACE = "osgi.wiring.package";
public final static String BUNDLE_DIRECTORY = "bundle";
private final Logger LOGGER;
private static final int DELETE_TRY_COUNT = 3;
private static final long WAIT_RETRY_DELETE = 250;
public BundleTools(Logger LOGGER) {
this.LOGGER = LOGGER;
}
/**
* Find if the bundle already exists and retrieve it.
* @param hostBundle
* @param symbolicName
* @param version
* @return
*/
private static Bundle getBundle(BundleContext hostBundle, String symbolicName, Version version) {
Bundle[] bundles = hostBundle.getBundles();
for (int i = 0; (bundles != null) && (i < bundles.length); i++)
{
String sym = bundles[i].getSymbolicName();
Version ver = bundles[i].getVersion();
if ((symbolicName != null)
&& (sym != null)
&& symbolicName.equals(sym)
&& version.equals(ver))
{
return bundles[i];
}
}
return null;
}
/**
* String version of bundle state index
* @param i Bundle state like {@link Bundle#ACTIVE}
* @return Bundle state string version
*/
public static String getStateString(int i) {
switch (i) {
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 "Unknown ";
}
}
/**
* Register in the host bundle the provided list of bundle reference
* @param hostBundle Host BundleContext
* @param bundleToInstall Bundle Reference array
*/
/*
public static void installBundles(BundleContext hostBundle,BundleReference[] bundleToInstall) {
for(BundleReference bundleRef : bundleToInstall) {
if(bundleRef.getBundleJarContent()==null) {
LOGGER.warn(I18N.tr("OrbisGIS package does not contain the {0} bundle plugin",bundleRef.getArtifactId()));
} else {
try {
Bundle builtInBundle = hostBundle.installBundle(bundleRef.getBundleUri(),
bundleRef.getBundleJarContent());
//Do not start if bundle is a fragment bundle (no Activator in fragment)
if(bundleRef.isAutoStart() && ((
builtInBundle.adapt(BundleRevision.class).getTypes() &
BundleRevision.TYPE_FRAGMENT) == 0)) {
LOGGER.debug("Starting "+bundleRef.getArtifactId()+"..");
builtInBundle.start();
LOGGER.debug("Started");
}
} catch(BundleException ex) {
LOGGER.error(ex.getLocalizedMessage(), ex);
}
}
}
}
*/
/**
* @param bundle The bundle instance
* @return True if the bundle has no Activator and cannot be started
*/
private static boolean isFragment(Bundle bundle) {
return bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null;
}
/**
* @param bundle The bundle instance
* @return True if the bundle has no Activator and cannot be started
*/
private static String getFragmentHost(Bundle bundle) {
return bundle.getHeaders().get(Constants.FRAGMENT_HOST);
}
/**
* Delete OSGi fragment bundles that are both in OSGi cache and in bundle sub-dir
* @param bundleCache OSGi bundle cache ex: ~/.Orbisgis/4.X/cache/
*/
public void deleteFragmentInCache(File bundleCache) {
if(bundleCache.exists()) {
// List bundles in the /bundle subdirectory
File bundleFolder = new File(BUNDLE_DIRECTORY);
if(!bundleFolder.exists()) {
return;
}
File[] files = bundleFolder.listFiles();
if (files != null) {
List<String> fragmentBundlesArtifacts = new ArrayList<>(files.length);
// Search for Fragment in /bundle/ subdir
for(File file : files) {
if(FilenameUtils.isExtension(file.getName(),"jar")) {
// Read Manifest
try(JarFile jar = new JarFile(file)) {
Manifest manifest = jar.getManifest();
if(manifest!=null && manifest.getMainAttributes()!=null) {
String artifact = manifest.getMainAttributes().getValue(Constants.FRAGMENT_HOST);
if(artifact != null) {
fragmentBundlesArtifacts.add(parseManifest(manifest, null).getArtifactId());
}
}
} catch (IOException ex) {
LOGGER.log(Logger.LOG_ERROR, "Error while reading Jar manifest:\n"+file.getPath());
}
}
}
// Remove folders in bundle cache that contain a fragment cache
File[] cacheFolders = bundleCache.listFiles((FileFilter)DirectoryFileFilter.DIRECTORY);
if(cacheFolders != null) {
for (File folder : cacheFolders) {
try {
// Get the first folder, may contain only one ex:"version0.0"
File[] cacheBundleFolder = folder.listFiles((FileFilter) DirectoryFileFilter.DIRECTORY);
if (cacheBundleFolder != null && cacheBundleFolder.length > 0) {
// Read Jar manifest
File jarBundle = new File(cacheBundleFolder[0], "bundle.jar");
if (jarBundle.exists()) {
// Read artifact
String artifact = parseJarManifest(jarBundle, null).getArtifactId();
if (fragmentBundlesArtifacts.contains(artifact)) {
// Delete the cache folder
int tryCount = 0;
while (tryCount < DELETE_TRY_COUNT) {
try {
if(jarBundle.delete()) {
FileUtils.deleteDirectory(folder);
} else {
throw new IOException("Could not delete jar file");
}
break;
} catch (IOException ex) {
tryCount++;
try {
Thread.sleep(WAIT_RETRY_DELETE);
} catch (InterruptedException iex) {
break;
}
}
}
if(folder.exists()) {
LOGGER.log(Logger.LOG_ERROR, "Cannot delete a bundle cache folder, library may not be up to" +
" date, please delete the following folder and restart OrbisGIS:" +
"\n"+folder.getPath());
} else {
LOGGER.log(Logger.LOG_INFO, I18N.tr("Delete fragment bundle {0} in " +
"cache directory", artifact));
}
}
}
}
} catch (IOException ex) {
LOGGER.log(Logger.LOG_ERROR, "Error while reading Jar manifest:\n" + folder.getPath(), ex);
}
}
}
}
}
}
/**
* Register in the host bundle the provided list of bundle reference
* @param hostBundle Host BundleContext
* @param nonDefaultBundleDeploying Bundle Reference array to deploy bundles in a non default way (install&start)
*/
public void installBundles(BundleContext hostBundle,BundleReference[] nonDefaultBundleDeploying) {
//Create a Map of nonDefaultBundleDeploying by their artifactId
Map<String,BundleReference> customDeployBundles = new HashMap<String, BundleReference>(nonDefaultBundleDeploying.length);
for(BundleReference ref : nonDefaultBundleDeploying) {
customDeployBundles.put(ref.getArtifactId(),ref);
}
// List bundles in the /bundle subdirectory
File bundleFolder = new File(BUNDLE_DIRECTORY);
if(!bundleFolder.exists()) {
return;
}
File[] files = bundleFolder.listFiles();
List<File> jarList = new ArrayList<File>();
if (files != null) {
for(File file : files) {
if(FilenameUtils.isExtension(file.getName(),"jar")) {
jarList.add(file);
}
}
}
if (!jarList.isEmpty()) {
Map<String,Bundle> installedBundleMap = new HashMap<String,Bundle>();
Set<String> fragmentHosts = new HashSet<>();
// Keep a reference to bundles in the framework cache
for (Bundle bundle : hostBundle.getBundles()) {
String key = bundle.getSymbolicName();
installedBundleMap.put(key, bundle);
String fragmentHost = getFragmentHost(bundle);
if(fragmentHost != null) {
fragmentHosts.add(fragmentHost);
}
}
//
final List<Bundle> installedBundleList = new LinkedList<Bundle>();
for (File jarFile : jarList) {
// Extract version and symbolic name of the bundle
String key="";
BundleReference jarRef;
try {
List<PackageDeclaration> packageDeclarations = new ArrayList<PackageDeclaration>();
jarRef = parseJarManifest(jarFile, packageDeclarations);
key = jarRef.getArtifactId();
} catch (IOException ex) {
LOGGER.log(Logger.LOG_ERROR, ex.getLocalizedMessage(), ex);
// Do not install this jar
continue;
}
// Retrieve from the framework cache the bundle at this location
Bundle installedBundle = installedBundleMap.remove(key);
// Read Jar manifest without installing it
BundleReference reference = new BundleReference(""); // Default deploy
try(JarFile jar = new JarFile(jarFile)) {
Manifest manifest = jar.getManifest();
if(manifest!=null && manifest.getMainAttributes()!=null) {
String artifact = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
BundleReference customRef = customDeployBundles.get(artifact);
if(customRef!=null) {
reference = customRef;
}
}
} catch (Exception ex) {
LOGGER.log(Logger.LOG_ERROR, I18N.tr("Could not read bundle manifest"), ex);
}
try {
if(installedBundle != null) {
if(getFragmentHost(installedBundle) != null) {
// Fragment cannot be reinstalled
continue;
} else {
String installedBundleLocation = installedBundle.getLocation();
int verDiff = -1;
if(installedBundle.getVersion() != null && jarRef.getVersion()!=null) {
verDiff = installedBundle.getVersion().compareTo(jarRef.getVersion());
}
if (verDiff == 0) {
// If the same version or SNAPSHOT that is not used by fragments
if (!fragmentHosts.contains(installedBundle.getSymbolicName()) && (!installedBundleLocation.equals(jarFile.toURI().toString()) ||
(installedBundle.getVersion() != null && "SNAPSHOT".equals(installedBundle.getVersion().getQualifier())))) {
//if the location is not the same reinstall it
LOGGER.log(Logger.LOG_INFO, "Uninstall bundle " + installedBundle
.getSymbolicName());
installedBundle.uninstall();
installedBundle = null;
}
} else if (verDiff < 0) {
// Installed version is older than the bundle version
LOGGER.log(Logger.LOG_INFO, "Uninstall bundle " + installedBundle.getLocation());
installedBundle.uninstall();
installedBundle = null;
} else {
// Installed version is more recent than the bundle version
// Do not install this jar
continue;
}
}
}
// If the bundle is not in the framework cache install it
if ((installedBundle == null) && reference.isAutoInstall()) {
installedBundle = hostBundle.installBundle(jarFile.toURI().toString());
LOGGER.log(Logger.LOG_INFO, "Install bundle " + installedBundle.getSymbolicName());
if (!isFragment(installedBundle) && reference.isAutoStart()) {
installedBundleList.add(installedBundle);
}
}
}
catch (BundleException ex) {
LOGGER.log(Logger.LOG_ERROR, "Error while installing bundle in bundle directory", ex);
}
}
// Start new bundles
for (Bundle bundle :installedBundleList) {
try {
bundle.start();
} catch (BundleException ex) {
LOGGER.log(Logger.LOG_ERROR, "Error while starting bundle in bundle directory", ex);
}
}
}
}
/**
* Read the class path, open all Jars and folders, retrieve the package list.
* This kind of package list does not contain versions and are useful for non-OSGi packages only.
* @return List of package name
*/
public List<String> getAvailablePackages() {
List<String> packages = new ArrayList<String>(getAllPackages());
Collections.sort(packages);
return packages;
}
private List<String> getClassPath() {
List<String> classPath = new LinkedList<String>();
// Read declared class in the manifest
try {
Enumeration<URL> resources = BundleTools.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
try {
Manifest manifest = new Manifest(url.openStream());
String value = manifest.getMainAttributes().getValue(Attributes.Name.CLASS_PATH);
if(value!=null) {
String[] pathElements = value.split(" ");
if(pathElements==null) {
pathElements = new String[] {value};
}
classPath.addAll(Arrays.asList(pathElements));
}
} catch (IOException ex) {
LOGGER.log(Logger.LOG_WARNING, "Unable to retrieve Jar MANIFEST " + url, ex);
}
}
} catch (IOException ex) {
LOGGER.log(Logger.LOG_WARNING, "Unable to retrieve Jar MANIFEST", ex);
}
// Read packages in the class path
String javaClasspath = System.getProperty("java.class.path");
String[] pathElements = javaClasspath.split(":");
if(pathElements==null) {
pathElements = new String[] {javaClasspath};
}
classPath.addAll(Arrays.asList(pathElements));
return classPath;
}
/**
* Read the class path, search for OSGi manifest declaration.
* Reading MANIFEST is useful to read package versions of OSGi compliant Jars.
* If a package version is not properly exported then an OSGi bundle that depends on this package with a specified
* version could not be Resolved.
* @return
*/
public Collection<PackageDeclaration> fetchManifests() {
List<PackageDeclaration> packages = new LinkedList<PackageDeclaration>();
List<String> pathElements = getClassPath();
// Fetch
for (String element : pathElements) {
File filePath = new File(element);
if (FilenameUtils.getExtension(element).equals("jar") && filePath.exists()) {
try {
parseJarManifest(filePath, packages);
} catch (IOException ex) {
LOGGER.log(Logger.LOG_DEBUG, "Unable to fetch packages in " + filePath.getAbsolutePath(), ex);
}
} else if (filePath.isDirectory()) {
try {
parseDirectoryManifest(filePath, filePath, packages);
} catch (SecurityException ex) {
LOGGER.log(Logger.LOG_DEBUG, "Unable to fetch the folder " + filePath.getAbsolutePath(), ex);
}
}
}
return packages;
}
/**
* Parse a Manifest in order to extract Exported Package
* @param manifest Jar Manifest
* @param packages package array or null
* @throws IOException
*/
public static BundleReference parseManifest(Manifest manifest, List<PackageDeclaration> packages) throws IOException {
Attributes attributes = manifest.getMainAttributes();
String exports = attributes.getValue(Constants.EXPORT_PACKAGE);
String versionProperty = attributes.getValue(Constants.BUNDLE_VERSION_ATTRIBUTE);
Version version=null;
if(versionProperty!=null) {
version = new Version(versionProperty);
}
String symbolicName = attributes.getValue(Constants.BUNDLE_SYMBOLICNAME);
if(packages != null) {
org.apache.felix.framework.Logger logger = new org.apache.felix.framework.Logger();
// Use Apache Felix to parse the Manifest Export header
List<BundleCapability> exportsCapability = ManifestParser.parseExportHeader(logger, null, exports, "0",
new Version(0, 0, 0));
for (BundleCapability bc : exportsCapability) {
Map<String, Object> attr = bc.getAttributes();
// If the package contain a package name and a package version
if (attr.containsKey(PACKAGE_NAMESPACE)) {
Version packageVersion = new Version(0, 0, 0);
if (attr.containsKey(Constants.VERSION_ATTRIBUTE)) {
packageVersion = (Version) attr.get(Constants.VERSION_ATTRIBUTE);
}
if (packageVersion.getMajor() != 0 || packageVersion.getMinor() != 0 || packageVersion.getMicro() != 0) {
packages.add(new PackageDeclaration((String) attr.get(PACKAGE_NAMESPACE),
packageVersion));
} else {
// No version, take the bundle version
packages.add(new PackageDeclaration((String) attr.get(PACKAGE_NAMESPACE),
version));
}
}
}
}
return new BundleReference(symbolicName,version);
}
private void parseDirectoryManifest(File rootPath, File path, List<PackageDeclaration> packages) throws SecurityException {
File[] files = path.listFiles();
for (File file : files) {
// TODO Java7 check for non-symlink,
// without this check it might generate an infinite loop
// @link http://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html#isSymbolicLink(java.nio.file.Path)
if (!file.isDirectory()) {
if (file.getName().equals(MANIFEST_FILENAME)) {
try {
Manifest manifest = new Manifest(new FileInputStream(file));
parseManifest(manifest, packages);
} catch (Exception ex) {
LOGGER.log(Logger.LOG_WARNING, "Unable to read manifest in " + file.getAbsolutePath(), ex);
}
} if (FilenameUtils.getExtension(file.getName()).equals("jar") && file.exists()) {
try {
parseJarManifest(file, packages);
} catch (IOException ex) {
LOGGER.log(Logger.LOG_WARNING, "Unable to fetch packages in " + file.getAbsolutePath(), ex);
}
}
} else {
parseDirectoryManifest(rootPath, file, packages);
}
}
}
/**
* Parse a Manifest in order to extract Exported Package
* @param jarFilePath Jar file
* @param packages package array or null (out)
* @throws IOException
*/
public static BundleReference parseJarManifest(File jarFilePath, List<PackageDeclaration> packages) throws IOException {
try(JarFile jar = new JarFile(jarFilePath)) {
return parseManifest(jar.getManifest(), packages);
}
}
private Set<String> getAllPackages() {
Set<String> packages = new HashSet<String>();
List<String> pathElements = getClassPath();
for (String element : pathElements) {
File filePath = new File(element);
if (element.endsWith("jar")) {
try {
parseJar(filePath, packages);
} catch (IOException ex) {
LOGGER.log(Logger.LOG_DEBUG, "Unable to fetch packages in " + filePath.getAbsolutePath());
}
} else if (filePath.isDirectory()) {
try {
parseDirectory(filePath,filePath, packages);
} catch (SecurityException ex) {
LOGGER.log(Logger.LOG_DEBUG, "Unable to fetch the folder " + filePath.getAbsolutePath());
}
}
}
return packages;
}
private static void parseDirectory(File rootPath, File path, Set<String> packages) throws SecurityException {
File[] files = path.listFiles();
for (File file : files) {
if (!file.isDirectory()) {
if (file.getName().endsWith(".class")
&& file.getParent().length()>rootPath.getAbsolutePath().length()) {
String parentPath = file.getParent().substring(rootPath.getAbsolutePath().length()+1);
packages.add(parentPath.replace(File.separator, "."));
}
} else {
if(!Files.isSymbolicLink(file.toPath())) {
parseDirectory(rootPath, file, packages);
}
}
}
}
private static void parseJar(File jarFilePath, Set<String> packages) throws IOException {
try(JarFile jar = new JarFile(jarFilePath)) {
Enumeration<? extends JarEntry> entryEnum = jar.entries();
while (entryEnum.hasMoreElements()) {
JarEntry entry = entryEnum.nextElement();
if (!entry.isDirectory()) {
final String path = entry.getName();
if (path.endsWith(".class")) {
// Extract folder
String parentPath = (new File(path)).getParent();
if (parentPath != null) {
packages.add(parentPath.replace(File.separator, "."));
}
}
}
}
}
}
}