From 784ebd9f4860afdad42d2e6f494d9105f456b79b Mon Sep 17 00:00:00 2001 From: jjongsma Date: Thu, 6 Feb 2014 13:56:11 -0600 Subject: [PATCH] New feature deployer --- .../deployer/features/BundleEventType.java | 60 ++ .../features/FeatureDeploymentListener.java | 883 ++++++++++++------ .../karaf/deployer/features/PropBean.java | 154 +++ 3 files changed, 834 insertions(+), 263 deletions(-) create mode 100644 deployer/features/src/main/java/org/apache/karaf/deployer/features/BundleEventType.java create mode 100644 deployer/features/src/main/java/org/apache/karaf/deployer/features/PropBean.java diff --git a/deployer/features/src/main/java/org/apache/karaf/deployer/features/BundleEventType.java b/deployer/features/src/main/java/org/apache/karaf/deployer/features/BundleEventType.java new file mode 100644 index 00000000000..72d8afb43df --- /dev/null +++ b/deployer/features/src/main/java/org/apache/karaf/deployer/features/BundleEventType.java @@ -0,0 +1,60 @@ +/* + * 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.karaf.deployer.features; + +import org.osgi.framework.BundleEvent; + +/** + * Convenience OSGI event representation. + */ +public enum BundleEventType { + + INSTALLED(BundleEvent.INSTALLED), // + RESOLVED(BundleEvent.RESOLVED), // + LAZY_ACTIVATION(BundleEvent.LAZY_ACTIVATION), // + STARTING(BundleEvent.STARTING), // + STARTED(BundleEvent.STARTED), // + STOPPING(BundleEvent.STOPPING), // + STOPPED(BundleEvent.STOPPED), // + UPDATED(BundleEvent.UPDATED), // + UNRESOLVED(BundleEvent.UNRESOLVED), // + UNINSTALLED(BundleEvent.UNINSTALLED), // + + UNKNOWN(0), // + + ; + + public final int code; + + BundleEventType(final int code) { + this.code = code; + } + + public static BundleEventType from(final BundleEvent event) { + return from(event.getType()); + } + + public static BundleEventType from(final int code) { + for (final BundleEventType known : BundleEventType.values()) { + if (known.code == code) { + return known; + } + } + return UNKNOWN; + } + +} diff --git a/deployer/features/src/main/java/org/apache/karaf/deployer/features/FeatureDeploymentListener.java b/deployer/features/src/main/java/org/apache/karaf/deployer/features/FeatureDeploymentListener.java index 1ef86c359d3..5d1dd402796 100644 --- a/deployer/features/src/main/java/org/apache/karaf/deployer/features/FeatureDeploymentListener.java +++ b/deployer/features/src/main/java/org/apache/karaf/deployer/features/FeatureDeploymentListener.java @@ -17,39 +17,27 @@ package org.apache.karaf.deployer.features; import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; import java.util.EnumSet; import java.util.Enumeration; -import java.util.HashSet; import java.util.List; -import java.util.Properties; -import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.w3c.dom.Document; - import org.apache.felix.fileinstall.ArtifactUrlTransformer; +import org.apache.karaf.features.Dependency; import org.apache.karaf.features.Feature; import org.apache.karaf.features.FeaturesNamespaces; import org.apache.karaf.features.FeaturesService; +import org.apache.karaf.features.FeaturesService.Option; import org.apache.karaf.features.Repository; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleEvent; -import org.osgi.framework.BundleListener; +import org.osgi.framework.SynchronousBundleListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; @@ -58,253 +46,622 @@ import org.xml.sax.SAXParseException; /** - * A deployment listener able to hot deploy a feature descriptor + * A deployment listener able to hot deploy (install/uninstall) a repository + * descriptor as well as auto-install features. + *

+ * Assumptions and Conventions: + *

  • feature.xml file must have external file-name based on artifact-id + *
  • feature.xml file must have internal root-name based on artifact-id + *
  • feature.xml file must have file-extension managed by this component + *
  • external file-name and internal root-name must be the same + *
  • dependency features must be resolvable from available repositories + *
  • dependency features must have a version */ -public class FeatureDeploymentListener implements ArtifactUrlTransformer, BundleListener { - - public static final String FEATURE_PATH = "org.apache.karaf.shell.features"; - - private final Logger logger = LoggerFactory.getLogger(FeatureDeploymentListener.class); - - private DocumentBuilderFactory dbf; - private FeaturesService featuresService; - private BundleContext bundleContext; - private Properties properties = new Properties(); - - public void setFeaturesService(FeaturesService featuresService) { - this.featuresService = featuresService; - } - - public FeaturesService getFeaturesService() { - return featuresService; - } - - public BundleContext getBundleContext() { - return bundleContext; - } - - public void setBundleContext(BundleContext bundleContext) { - this.bundleContext = bundleContext; - } - - public void init() throws Exception { - bundleContext.addBundleListener(this); - loadProperties(); - // Scan bundles - for (Bundle bundle : bundleContext.getBundles()) { - if (bundle.getState() == Bundle.RESOLVED || bundle.getState() == Bundle.STARTING - || bundle.getState() == Bundle.ACTIVE) - bundleChanged(new BundleEvent(BundleEvent.RESOLVED, bundle)); - } - } - - public void destroy() throws Exception { - bundleContext.removeBundleListener(this); - } - - private boolean isKnownFeaturesURI(String uri){ - if(uri == null){ - return true; - } - if(FeaturesNamespaces.URI_0_0_0.equalsIgnoreCase(uri)){ - return true; - } - if(FeaturesNamespaces.URI_1_0_0.equalsIgnoreCase(uri)){ - return true; - } - if(FeaturesNamespaces.URI_1_1_0.equalsIgnoreCase(uri)){ - return true; - } - if(FeaturesNamespaces.URI_1_2_0.equalsIgnoreCase(uri)){ - return true; - } - if(FeaturesNamespaces.URI_CURRENT.equalsIgnoreCase(uri)){ - return true; - } - return false; - } - - private void loadProperties() throws IOException { - // Load properties - File file = getPropertiesFile(); - if (file != null) { - if (file.exists()) { - InputStream input = new FileInputStream(file); - try { - properties.load(input); - } finally { - input.close(); - } - } - } - } - - private void saveProperties() throws IOException { - File file = getPropertiesFile(); - if (file != null) { - OutputStream output = new FileOutputStream(file); - try { - properties.store(output, null); - } finally { - output.close(); - } - } - } - - private File getPropertiesFile() { - return bundleContext.getDataFile("FeatureDeploymentListener.cfg"); - } - - public boolean canHandle(File artifact) { - try { - if (artifact.isFile() && artifact.getName().endsWith(".xml")) { - Document doc = parse(artifact); - String name = doc.getDocumentElement().getLocalName(); - String uri = doc.getDocumentElement().getNamespaceURI(); - if ("features".equals(name) ) { - if(isKnownFeaturesURI(uri)){ - return true; - } else { - logger.error("unknown features uri", new Exception("" + uri)); - } - } - } - } catch (Exception e) { - logger.error("Unable to parse deployed file " + artifact.getAbsolutePath(), e); - } - return false; - } - - public URL transform(URL artifact) { - // We can't really install the feature right now and just return nothing. - // We would not be aware of the fact that the bundle has been uninstalled - // and therefore require the feature to be uninstalled. - // So instead, create a fake bundle with the file inside, which will be listened by - // this deployer: installation / uninstallation of the feature will be done - // while the bundle is installed / uninstalled. - try { - return new URL("feature", null, artifact.toString()); - } catch (Exception e) { - logger.error("Unable to build feature bundle", e); - return null; - } - } - - public void bundleChanged(BundleEvent bundleEvent) { - Bundle bundle = bundleEvent.getBundle(); - if (bundleEvent.getType() == BundleEvent.RESOLVED) { - try { - List urls = new ArrayList(); - Enumeration featuresUrlEnumeration = bundle.findEntries("/META-INF/" + FEATURE_PATH + "/", "*.xml", false); - while (featuresUrlEnumeration != null && featuresUrlEnumeration.hasMoreElements()) { - URL url = (URL) featuresUrlEnumeration.nextElement(); - try { - featuresService.addRepository(url.toURI()); - URI needRemovedRepo = null; - for (Repository repo : featuresService.listRepositories()) { - if (repo.getURI().equals(url.toURI())) { - Set features = new HashSet(Arrays.asList(repo.getFeatures())); - Set autoInstallFeatures = new HashSet(); - for(Feature feature:features) { - if(feature.getInstall() != null && feature.getInstall().equals(Feature.DEFAULT_INSTALL_MODE)){ - autoInstallFeatures.add(feature); - } - } - featuresService.installFeatures(autoInstallFeatures, EnumSet.noneOf(FeaturesService.Option.class)); - } else { - //remove older out-of-data feature repo - if (repo.getURI().toString().contains(FEATURE_PATH)) { - String featureFileName = repo.getURI().toString(); - featureFileName = featureFileName.substring(featureFileName.lastIndexOf('/') + 1); - String newFeatureFileName = url.toURI().toString(); - newFeatureFileName = newFeatureFileName.substring(newFeatureFileName.lastIndexOf('/') + 1); - if (featureFileName.equals(newFeatureFileName)) { - needRemovedRepo = repo.getURI(); - } - } - } - - } - urls.add(url); - if (needRemovedRepo != null) { - featuresService.removeRepository(needRemovedRepo); - } - } catch (Exception e) { - logger.error("Unable to install features", e); - } - } - synchronized (this) { - String prefix = bundle.getSymbolicName() + "-" + bundle.getVersion(); - String old = (String) properties.get(prefix + ".count"); - if (old != null && urls.isEmpty()) { - properties.remove(prefix + ".count"); - saveProperties(); - } else if (!urls.isEmpty()) { - properties.put(prefix + ".count", Integer.toString(urls.size())); - for (int i = 0; i < urls.size(); i++) { - properties.put(prefix + ".url." + i, urls.get(i).toExternalForm()); - } - saveProperties(); - } - } - } catch (Exception e) { - logger.error("Unable to install deployed features for bundle: " + bundle.getSymbolicName() + " - " + bundle.getVersion(), e); - } - } else if (bundleEvent.getType() == BundleEvent.UNINSTALLED) { - try { - synchronized (this) { - String prefix = bundle.getSymbolicName() + "-" + bundle.getVersion(); - String countStr = (String) properties.remove(prefix + ".count"); - if (countStr != null) { - int count = Integer.parseInt(countStr); - for (int i = 0; i < count; i++) { - URL url = new URL((String) properties.remove(prefix + ".url." + i)); - for (Repository repo : featuresService.listRepositories()) { - try { - if (repo.getURI().equals(url.toURI())) { - for (Feature f : repo.getFeatures()) { - try { - featuresService.uninstallFeature(f.getName(), f.getVersion()); - } catch (Exception e) { - logger.error("Unable to uninstall feature: " + f.getName(), e); - } - } - } - } catch (Exception e) { - logger.error("Unable to uninstall features: " + url, e); - } - } - try { - featuresService.removeRepository(url.toURI()); - } catch (URISyntaxException e) { - logger.error("Unable to remove repository: " + url, e); - } - } - saveProperties(); - } - } - } catch (Exception e) { - logger.error("Unable to uninstall deployed features for bundle: " + bundle.getSymbolicName() + " - " + bundle.getVersion(), e); - } - } - } - - protected Document parse(File artifact) throws Exception { - if (dbf == null) { - dbf = DocumentBuilderFactory.newInstance(); - dbf.setNamespaceAware(true); - } - DocumentBuilder db = dbf.newDocumentBuilder(); - db.setErrorHandler(new ErrorHandler() { - public void warning(SAXParseException exception) throws SAXException { - } - public void error(SAXParseException exception) throws SAXException { - } - public void fatalError(SAXParseException exception) throws SAXException { - throw exception; - } - }); - return db.parse(artifact); - } +public class FeatureDeploymentListener implements ArtifactUrlTransformer, + SynchronousBundleListener { + + /** Repository feature.xml file extension managed by this component. */ + static final String EXTENSION = "xml"; + + /** Features folder inside the wrapper bundle. */ + static final String FEATURE_PATH = "org.apache.karaf.shell.features"; + + /** Features path inside the wrapper bundle jar. */ + static final String META_PATH = "/META-INF/" + FEATURE_PATH + "/"; + + /** Deployer state properties file name. */ + static final String PROP_FILE = FeatureDeploymentListener.class.getName() + + "@repository.properties"; + + /** Feature deployer protocol, used by default feature deployer. */ + static final String PROTOCOL = "feature"; + + /** Root node in feature.xml */ + static final String ROOT_NODE = "features"; + + private volatile BundleContext bundleContext; + + private volatile DocumentBuilderFactory dbf; + + private volatile FeaturesService featuresService; + + private final Logger logger = LoggerFactory + .getLogger(FeatureDeploymentListener.class); + + /** + * Ensure all feature dependencies are installed into feature service. + */ + void assertDependencyInstalled(final Feature feature) throws Exception { + final List depencencyList = feature.getDependencies(); + for (final Dependency depencency : depencencyList) { + if (isInstalled(depencency)) { + continue; + } else { + logger.error( + "Expected feature dependency must be already installed: {} -> {}", + feature, depencency); + throw new IllegalStateException("Missing feature dependency."); + } + } + } + + /** + * Ensure all feature dependencies are registered with feature service. + */ + void assertDependencyRegistered(final Feature feature) throws Exception { + final List depencencyList = feature.getDependencies(); + for (final Dependency depencency : depencencyList) { + if (isRegistered(depencency)) { + continue; + } else { + logger.error( + "Expected feature dependency must be already registered: {} -> {}", + feature, depencency); + throw new IllegalStateException("Missing feature dependency."); + } + } + } + + @Override + public void bundleChanged(final BundleEvent event) { + + final Bundle bundle = event.getBundle(); + + if (!hasRepoDescriptor(bundle)) { + return; + } + + final BundleEventType type = BundleEventType.from(event); + + synchronized (Runnable.class) { + try { + switch (type) { + default: + return; + case INSTALLED: + logBundleEvent(event); + repoCreate(bundle); + break; + case UNINSTALLED: + logBundleEvent(event); + repoDelete(bundle); + break; + case UPDATED: + logBundleEvent(event); + repoDelete(bundle); + repoCreate(bundle); + } + logger.info("Success: " + type + " " + bundle); + } catch (final Throwable e) { + logger.error("Failure: " + type + " " + bundle, e); + } + } + + } + + @Override + public boolean canHandle(final File file) { + try { + if (file.isFile() && file.getName().endsWith("." + EXTENSION)) { + final Document doc = parse(file); + final String name = doc.getDocumentElement().getLocalName(); + final String uri = doc.getDocumentElement().getNamespaceURI(); + if (ROOT_NODE.equals(name)) { + if (isKnownFeaturesURI(uri)) { + return true; + } else { + logger.error("Unknown features uri", new Exception("" + + uri)); + } + } + } + } catch (final Exception e) { + logger.error( + "Unable to parse deployed file " + file.getAbsolutePath(), + e); + } + return false; + } + + /** + * Add all dependencies of a feature. + */ + void dependencyAdd(final Repository repo, final Feature feature) + throws Exception { + + assertDependencyRegistered(feature); + + final List depencencyList = feature.getDependencies(); + for (final Dependency depencency : depencencyList) { + featureAdd(repo, featureRegistered(depencency)); + } + + } + + /** + * Remove all dependencies of a feature. + */ + void dependencyRemove(final Repository repo, final Feature feature) + throws Exception { + + assertDependencyInstalled(feature); + + final List depencencyList = feature.getDependencies(); + for (final Dependency depencency : depencencyList) { + featureRemove(repo, featureInstalled(depencency)); + } + + } + + /** + * Component deactivate. + */ + public void destroy() throws Exception { + bundleContext.removeBundleListener(this); + logger.info("Deployer deactivate."); + } + + /** + * Identity of feature/dependency based on name and version. + */ + boolean equals(final Feature feature, final Dependency depencency) { + final boolean sameName = feature.getName().equals(depencency.getName()); + final boolean sameVersion = feature.getVersion().equals( + depencency.getVersion()); + return sameName && sameVersion; + } + + /** + * Activate auto-install features in a repository. + */ + void featureAdd(final Repository repo) throws Exception { + final Feature[] featureArray = repo.getFeatures(); + for (final Feature feature : featureArray) { + if (isAutoInstall(feature)) { + featureAdd(repo, feature); + } + } + } + + /** + * Increment counts, install feature when due. + */ + void featureAdd(final Repository repo, final Feature feature) + throws Exception { + + final PropBean propBean = propBean(); + final boolean isMissing = isMissing(feature); + + if (propBean.checkIncrement(repo, feature)) { + final int totalCount = propBean.countValue(null, feature); + if (isMissing) { + if (totalCount > 1) { + logger.error( + "Feature count error.", + new IllegalStateException( + "Feature is missing when should be present.")); + } + dependencyAdd(repo, feature); + featureInstall(feature); + } + logger.info("Feature added: {} @ {} {} {}", totalCount, + repo.getName(), feature.getName(), feature.getVersion()); + } else { + logger.error("Feature count error.", new IllegalStateException( + "Trying to install feature already added.")); + } + } + + /** + * Install feature. + */ + void featureInstall(final Feature feature) throws Exception { + + assertDependencyInstalled(feature); + + final String name = feature.getName(); + final String version = feature.getVersion(); + getFeaturesService().installFeature(name, version, options()); + + logger.info("Feature installed: {} {}", name, version); + + } + + /** + * Find installed feature based on dependency identity. + */ + Feature featureInstalled(final Dependency depencency) throws Exception { + final Feature[] featureArray = getFeaturesService() + .listInstalledFeatures(); + for (final Feature feature : featureArray) { + if (equals(feature, depencency)) { + return feature; + } + } + return null; + } + + /** + * Find registered feature based on dependency identity. + */ + Feature featureRegistered(final Dependency depencency) throws Exception { + final Feature[] featureArray = getFeaturesService().listFeatures(); + for (final Feature feature : featureArray) { + if (equals(feature, depencency)) { + return feature; + } + } + return null; + } + + /** + * Deactivate auto-install features in a repository. + */ + void featureRemove(final Repository repo) throws Exception { + final Feature[] featureArray = repo.getFeatures(); + for (final Feature feature : featureArray) { + if (isAutoInstall(feature)) { + featureRemove(repo, feature); + } + } + } + + /** + * Decrement counts, uninstall feature when due. + */ + void featureRemove(final Repository repo, final Feature feature) + throws Exception { + + final PropBean propBean = propBean(); + final boolean isPresent = isPresent(feature); + + if (propBean.checkDecrement(repo, feature)) { + final int totalCount = propBean.countValue(null, feature); + if (totalCount == 0) { + if (isPresent) { + featureUninstall(feature); + dependencyRemove(repo, feature); + } else { + logger.error( + "Feature count error.", + new IllegalStateException( + "Feature is missing when should be present.")); + } + } + logger.info("Feature removed: {} @ {} {} {}", totalCount, + repo.getName(), feature.getName(), feature.getVersion()); + } else { + logger.error("Feature count error.", new IllegalStateException( + "Trying to uninstall feature already removed.")); + } + } + + /** + * Uninstall feature. + */ + void featureUninstall(final Feature feature) throws Exception { + + final String name = feature.getName(); + final String version = feature.getVersion(); + getFeaturesService().uninstallFeature(name, version); + + logger.info("Feature uninstalled: {} {}", name, version); + + } + + public BundleContext getBundleContext() { + return bundleContext; + } + + public FeaturesService getFeaturesService() { + return featuresService; + } + + /** + * Bundle contains stored feature.xml + */ + boolean hasRepoDescriptor(final Bundle bundle) { + return repoUrl(bundle) != null; + } + + /** + * Feature service contains named repository. + */ + boolean hasRepoRegistered(final String repoId) { + return repo(repoId) != null; + } + + /** + * Component activate. + */ + public void init() throws Exception { + logger.info("Deployer activate."); + bundleContext.addBundleListener(this); + } + + /** + * Feature auto-install mode. + */ + boolean isAutoInstall(final Feature feature) { + return Feature.DEFAULT_INSTALL_MODE.equals(feature.getInstall()); + } + + /** + * Verify if feature dependency is currently installed. + */ + boolean isInstalled(final Dependency depencency) throws Exception { + return featureInstalled(depencency) != null; + } + + /** + * Feature name space check. + */ + boolean isKnownFeaturesURI(final String uri) { + if (uri == null) { + return true; + } + if (FeaturesNamespaces.URI_0_0_0.equalsIgnoreCase(uri)) { + return true; + } + if (FeaturesNamespaces.URI_1_0_0.equalsIgnoreCase(uri)) { + return true; + } + if (FeaturesNamespaces.URI_1_1_0.equalsIgnoreCase(uri)) { + return true; + } + if (FeaturesNamespaces.URI_1_2_0.equalsIgnoreCase(uri)) { + return true; + } + if (FeaturesNamespaces.URI_CURRENT.equalsIgnoreCase(uri)) { + return true; + } + return false; + } + + /** + * Feature not installed. + */ + boolean isMissing(final Feature feature) { + return !isPresent(feature); + } + + /** + * Feature is installed. + */ + boolean isPresent(final Feature feature) { + return getFeaturesService().isInstalled(feature); + } + + /** + * Verify if feature dependency is present in any repository. + */ + boolean isRegistered(final Dependency depencency) throws Exception { + return featureRegistered(depencency) != null; + } + + void logBundleEvent(final BundleEvent event) { + + final Bundle bundle = event.getBundle(); + + final BundleEventType type = BundleEventType.from(event); + + logger.info("Event: {} {}", type, bundle); + + } + + /** + * Default feature install options. + */ + EnumSet