/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.stanbol.commons.installer.provider.bundle.impl;
import static org.apache.stanbol.commons.installer.provider.bundle.BundleInstallerConstants.BUNDLE_INSTALLER_HEADER;
import static org.apache.stanbol.commons.installer.provider.bundle.BundleInstallerConstants.PROVIDER_SCHEME;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.sling.installer.api.InstallableResource;
import org.apache.sling.installer.api.OsgiInstaller;
import org.apache.sling.installer.api.tasks.ResourceTransformer;
import org.apache.stanbol.commons.installer.provider.bundle.BundleInstallerConstants;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Installs resources within bundles by using the Apache Sling Installer
* framework.
* <p>
* NOTE that currently installed resources are not removed if the bundle is
* deactivated because it is not clear if this is a good thing to do. maybe one
* should use {@link Bundle#UNINSTALLED} for that. However this needs some
* additional testing.
* <p>
* The OSGi extender pattern (as described at [1]) is used. The value of the
* {@link BundleInstallerConstants#BUNDLE_INSTALLER_HEADER}
* ({@value BundleInstallerConstants#BUNDLE_INSTALLER_HEADER}) is used as relative path within
* the bundle to search for installable resources. Also files in sub folders
* are considered as installable resources.<p>
* The files are installed in the order as returned by
* {@link Bundle#findEntries(String, String, boolean)}. Directories are
* ignored.<p>
* All resources installed by this provider do use
* {@link BundleInstallerConstants#PROVIDER_SCHEME} ({@value BundleInstallerConstants#PROVIDER_SCHEME}) as
* scheme and the path additional to the value of
* {@link BundleInstallerConstants#BUNDLE_INSTALLER_HEADER}.<p>
* To give an example:<p>
* If the Bundle header notes<br>
* <pre><code>
* {@value BundleInstallerConstants#BUNDLE_INSTALLER_HEADER}=resources
* </code></pre><br>
* and the bundle contains the resources: <br>
* <pre><code>
* resources/bundles/10/myBundle.jar
* resources/config/myComponent.cfg
* resoruces/data/myIndex.solrondex.zip
* </code></pre><br>
* then the following resources will be installed:
* <pre><code>
* {@value BundleInstallerConstants#PROVIDER_SCHEME}:bundles/10/myBundle.jar
* {@value BundleInstallerConstants#PROVIDER_SCHEME}:config/myComponent.cfg
* {@value BundleInstallerConstants#PROVIDER_SCHEME}:data/myIndex.solrondex.zip
* </code></pre>
* <p>
* This means that {@link ResourceTransformer}s can both use the original name
* of the resource and the path relative to the install folder.
* <p>
* [1] <a href="http://www.aqute.biz/Snippets/Extender"> The OSGi extender pattern </a>
* <p>
*
* @author Rupert Westenthaler
*/
public class BundleInstaller implements BundleListener {
private static final Logger log = LoggerFactory.getLogger(BundleInstaller.class);
/**
* The scheme we use to register our resources.
*/
private final OsgiInstaller installer;
private final BundleContext context;
/**
* The directory used to keep the IDs of registered resources
*/
File configDir;
/**
* contains all active bundles as key and the path to the config directory
* as value. A <code>null</code> value indicates that this bundle needs not
* to be processed.
*/
private final Map<Bundle, String> activated = new HashMap<Bundle, String>();
public BundleInstaller(OsgiInstaller installer, BundleContext context) {
if (installer == null) {
throw new IllegalArgumentException("The OsgiInstaller service MUST NOT be NULL");
}
if (context == null) {
throw new IllegalArgumentException("The BundleContext MUST NOT be NULL");
}
this.installer = installer;
this.context = context;
this.configDir = context.getDataFile(".config");
if(configDir.exists()) {
if(!configDir.isDirectory()){
throw new IllegalStateException("The config directory '"
+ configDir.getAbsolutePath()+"' exists but is NOT a directory!");
}
} else {
if(!configDir.mkdirs()){
throw new IllegalStateException("Unable to create the config directory '"
+ configDir.getAbsolutePath()+"'!");
}
}
//start with the assumption that the framework is active
this.context.addBundleListener(this);
//register the already active bundles
registerActive(this.context);
}
/**
* Checks if the state is not {@link BundleEvent#STOPPED},
* {@link BundleEvent#STOPPING} or {@link BundleEvent#UNINSTALLED}
* @return the state
*/
private boolean isFrameworkActive(){
return (context.getBundle(0).getState() &
(BundleEvent.STOPPED | BundleEvent.STOPPING | BundleEvent.UNINSTALLED))
== 0;
}
/**
* Uses the parsed bundle context to register the already active (and currently
* starting) bundles.
*/
private void registerActive(BundleContext context) {
for (Bundle bundle : context.getBundles()) {
if ((bundle.getState() & (Bundle.STARTING | Bundle.ACTIVE)) != 0) {
register(bundle);
}
}
}
@Override
public void bundleChanged(BundleEvent event) {
log.debug("bundleChanged(bundle:{}|state:{})",event.getBundle().getSymbolicName(),event.getType());
Bundle source = event.getBundle();
//if(event instanceof Framework)
switch (event.getType()) {
case BundleEvent.STARTED:
register(source);
break;
//use uninstalled instead of stopped so that unregister is not called
//when the OSGI environment closes
case BundleEvent.STOPPED:
unregister(source);
break;
case BundleEvent.UPDATED:
unregister(source);
register(source);
}
}
/**
* Registers the bundle to the {@link #activated} map.
*
* @param bundle the bundle to register
*/
@SuppressWarnings("unchecked")
private void register(Bundle bundle) {
log.debug("register request for Bundle {}",bundle.getSymbolicName());
synchronized (activated) {
if(!isFrameworkActive()){
log.debug("ignore because Framework is shutting down!");
return;
}
if (activated.containsKey(bundle)) {
log.debug(" .. already registered ");
return;
}
}
log.debug(" ... registering");
Dictionary<String, String> headers = (Dictionary<String, String>) bundle.getHeaders();
// log.info("With Headers:");
// for(Enumeration<String> keys = headers.keys();keys.hasMoreElements();){
// String key = keys.nextElement();
// log.info(" > "+key+"="+headers.get(key));
// }
String path = (String) headers.get(BUNDLE_INSTALLER_HEADER);
activated.put(bundle, path);
if (path != null) {
log.info(" ... process configuration within path {} for bundle {}",path,bundle.getSymbolicName());
Enumeration<URL> resources = (Enumeration<URL>) bundle.findEntries(path, null, true);
if(resources != null){
ArrayList<InstallableResource> updated = new ArrayList<InstallableResource>();
while (resources.hasMoreElements()) {
URL url = resources.nextElement();
if(url != null){
log.debug(" > installable RDFTerm {}",url);
InstallableResource resource = createInstallableResource(bundle, path, url);
if (resource != null) {
updated.add(resource);
}
}
}
try {
storeConfig(bundle, updated);
} catch (IOException e) {
throw new IllegalStateException("Unablt to save the IDs of"
+ "the resources installed for bundle '"
+ bundle.getSymbolicName() + "'!",e);
}
installer.updateResources(PROVIDER_SCHEME, updated.toArray(new InstallableResource[updated.size()]), new String[]{});
} else {
log.warn(" ... no Entries found in path '{}' configured for Bundle '{}' with Manifest header field '{}'!",
new Object[]{path,bundle.getSymbolicName(),BUNDLE_INSTALLER_HEADER});
}
} else {
log.debug(" ... no Configuration to process");
}
}
/**
* Used to stores/overrides the ids of installed resource for a bundle.
* @param bundle
* @param resources
* @throws IOException
*/
private void storeConfig(Bundle bundle,Collection<InstallableResource> resources) throws IOException{
synchronized (configDir) {
File config = new File(configDir,bundle.getBundleId()+".resources");
if(config.exists()){
config.delete();
}
FileOutputStream out = new FileOutputStream(config);
List<String> ids = new ArrayList<String>(resources.size());
for(InstallableResource resource : resources){
ids.add(resource.getId());
}
try {
IOUtils.writeLines(ids, null, out, "UTF-8");
} finally {
IOUtils.closeQuietly(out);
}
}
}
/**
* Reads the installed resources for the parsed bundle and deletes the
* configuration afterwards.
* @param bundle the bundle
* @return the list of resources
* @throws IOException if the config file was not found or on any other
* exception while reading the file
*/
private Collection<String> consumeConfig(Bundle bundle) throws IOException{
synchronized (configDir) {
File config = new File(configDir,bundle.getBundleId()+".resources");
if(!config.exists()){
throw new IOException("Configuration File '"+ config.getAbsolutePath()
+ "' not found!");
}
FileInputStream in = new FileInputStream(config);
try {
return IOUtils.readLines(in, "UTF-8");
} finally {
IOUtils.closeQuietly(in);
config.delete();
}
}
}
/**
* Creates an {@link InstallableResource} for {@link URL}s of files within
* the parsed bundle.
*
* @param bundle the bundle containing the parsed resource
* @param bundleResource a resource within the bundle that need to be installed
*
* @return the installable resource or <code>null</code> in case of an error
*/
private InstallableResource createInstallableResource(Bundle bundle, String path, URL bundleResource) {
//define the id
String relPath = getInstallableResourceId(path, bundleResource);
String name = FilenameUtils.getName(relPath);
if (name == null || name.isEmpty()) {
return null; //ignore directories!
}
InstallableResource resource;
try {
/*
* Notes:
* - use <relativepath> as id
* - parse null as type to enable autodetection for configs as
* implemented by InternalReseouce.create(..)
* - we use the symbolic name and the modification date of the bundle as digest
* - the Dictionary will be ignored if an input stream is present
* so it is best to parse null
* - No idea how the priority is used by the Sling Installer. For
* now parse null than the default priority is used.
*/
resource = new InstallableResource(relPath,
bundleResource.openStream(), null,
String.valueOf(bundle.getSymbolicName()+bundle.getLastModified()), null, null);
log.info(" ... found installable resource " + bundleResource);
} catch (IOException e) {
log.error(String.format("Unable to process configuration File %s from Bundle %s",
bundleResource, bundle.getSymbolicName()), e);
return null;
}
return resource;
}
/**
* @param path
* @param bundleResource
* @return
*/
private String getInstallableResourceId(String path, URL bundleResource) {
String id = bundleResource.toString();
String namespace; //do not search the path within the file name!!
int nsIndex = Math.max(id.lastIndexOf(':'), id.lastIndexOf(File.separatorChar));
if(nsIndex > 0){
namespace = id.substring(0,Math.min(nsIndex+1,id.length()));
} else {
namespace = id;
}
String relPath = id.substring(namespace.lastIndexOf(path) + path.length(), id.length());
return relPath;
}
private void unregister(Bundle bundle) {
log.debug("unregister request for Bundle {}",bundle.getSymbolicName());
String path;
synchronized (activated) {
if(!isFrameworkActive()){
log.debug("ignore because Framework is shutting down!");
return;
}
if (!activated.containsKey(bundle)) {
log.debug(" .. not registered ");
return;
}
path = activated.remove(bundle);
}
if (path != null) {
log.info(" ... remove configurations for Bundle {}", bundle.getSymbolicName());
Collection<String> removedResources;
try {
removedResources = consumeConfig(bundle);
installer.updateResources(PROVIDER_SCHEME, null, removedResources.toArray(new String[removedResources.size()]));
} catch (IOException e) {
log.warn("Unable to remove installed Resources for Bundle '" +
bundle.getSymbolicName()+ "' because an Exeption while" +
"reading the installed ID from the config file",e);
}
} else {
log.debug(" ... no Configuration to process");
}
}
/**
* removes the bundle listener
*/
public void close() {
context.removeBundleListener(this);
}
}