diff --git a/integration-test.bndrun b/integration-test.bndrun
new file mode 100644
index 0000000..f97ab76
--- /dev/null
+++ b/integration-test.bndrun
@@ -0,0 +1,56 @@
+-tester: biz.aQute.tester.junit-platform
+
+-runrequires: \
+ bnd.identity;id='${project.groupId}.${project.artifactId}-tests',\
+ bnd.identity;id='org.apache.felix.configadmin',\
+ bnd.identity;id='org.glassfish.jakarta.json'
+
+-runproperties: \
+ localRepositoryPath=${project.build.directory}/m2Repo
+
+-resolve.effective: active
+
+-runee: JavaSE-17
+
+-runfw: org.apache.felix.framework
+
+-runstartlevel: \
+ order=sortbynameversion,\
+ begin=-1
+
+-runbundles: \
+ com.fasterxml.woodstox.woodstox-core;version='[7.0.0,7.0.1)',\
+ com.kentyou.featurelauncher;version='[1.0.0,1.0.1)',\
+ com.kentyou.featurelauncher-tests;version='[1.0.0,1.0.1)',\
+ junit-jupiter-api;version='[5.11.1,5.11.2)',\
+ junit-jupiter-engine;version='[5.11.1,5.11.2)',\
+ junit-jupiter-params;version='[5.11.1,5.11.2)',\
+ junit-platform-commons;version='[1.11.1,1.11.2)',\
+ junit-platform-engine;version='[1.11.1,1.11.2)',\
+ junit-platform-launcher;version='[1.11.1,1.11.2)',\
+ org.apache.aries.spifly.dynamic.framework.extension;version='[1.3.7,1.3.8)',\
+ org.apache.felix.cm.json;version='[2.0.6,2.0.7)',\
+ org.apache.felix.configadmin;version='[1.9.26,1.9.27)',\
+ org.apache.felix.feature;version='[1.0.2,1.0.3)',\
+ org.apache.felix.scr;version='[2.2.2,2.2.3)',\
+ org.apache.maven.resolver.api;version='[2.0.1,2.0.2)',\
+ org.apache.maven.resolver.connector.basic;version='[2.0.1,2.0.2)',\
+ org.apache.maven.resolver.impl;version='[2.0.1,2.0.2)',\
+ org.apache.maven.resolver.named.locks;version='[2.0.1,2.0.2)',\
+ org.apache.maven.resolver.spi;version='[2.0.1,2.0.2)',\
+ org.apache.maven.resolver.transport.file;version='[2.0.1,2.0.2)',\
+ org.apache.maven.resolver.util;version='[2.0.1,2.0.2)',\
+ org.codehaus.plexus.interpolation;version='[1.27.0,1.27.1)',\
+ org.glassfish.jakarta.json;version='[2.0.1,2.0.2)',\
+ org.opentest4j;version='[1.3.0,1.3.1)',\
+ org.osgi.service.component;version='[1.5.1,1.5.2)',\
+ org.osgi.service.feature;version='[1.0.0,1.0.1)',\
+ org.osgi.service.featurelauncher;version='[1.0.0,1.0.1)',\
+ org.osgi.test.common;version='[1.3.0,1.3.1)',\
+ org.osgi.test.junit5;version='[1.3.0,1.3.1)',\
+ org.osgi.util.converter;version='[1.0.9,1.0.10)',\
+ org.osgi.util.function;version='[1.0.0,1.0.1)',\
+ org.osgi.util.promise;version='[1.0.0,1.0.1)',\
+ slf4j.api;version='[2.0.11,2.0.12)',\
+ slf4j.simple;version='[2.0.11,2.0.12)',\
+ stax2-api;version='[4.2.2,4.2.3)'
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 1ee1453..94772d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,170 +21,239 @@
UTF-8
+ 17
+ 17
17
+
+
+ 8.0.0
+ 1.6.1
+ 1.5.1
+ 1.0.0
+ 1.0.0-SNAPSHOT
+ 1.0.9
+ 2.0.2
+ 2.0.6
+ 1.0.2
+ 4.0.0-beta-4
+ 2.0.1
+ 2.0.11
+
+
+ 7.0.5
+ 1.9.26
+ 2.2.2
+
+
+ 1.3.0
+ 5.11.1
+ 1.11.1
+ 5.14.1
+ 1.3.7
+ 2.0.1
+
+
+ 7.0.0
+ 3.8.0
+ 1.2.1
+ 3.4.0
+ 3.3.1
+ 3.13.0
+ 3.3.0
+ 3.4.2
+ 3.1.2
+ 3.1.2
+ 3.12.1
+ 3.6.1
+
+
+ true
+ false
+
+
${project.build.directory}/m2Repo
-
-
-
- org.junit
- junit-bom
- 5.11.0
- pom
- import
-
-
-
-
org.osgi
- osgi.annotation
- 8.0.0
+ osgi.core
+ ${osgi.core.version}
provided
org.osgi
- osgi.core
- 8.0.0
+ osgi.annotation
+ ${osgi.core.version}
provided
org.osgi
- org.osgi.service.feature
- 1.0.0
- provided
+ org.osgi.service.cm
+ ${osgi.cm.version}
org.osgi
- org.osgi.service.featurelauncher
- 1.0.0-SNAPSHOT
- provided
+ org.osgi.service.component
+ ${osgi.ds.version}
- jakarta.json
- jakarta.json-api
- 2.0.2
- provided
+ org.osgi
+ org.osgi.service.component.annotations
+ ${osgi.ds.version}
- org.apache.felix
- org.apache.felix.configadmin
- 1.9.26
+ org.osgi
+ org.osgi.service.feature
+ ${osgi.feature.version}
+
+
+ org.osgi
+ org.osgi.service.featurelauncher
+ ${osgi.featurelauncher.version}
org.osgi
org.osgi.util.converter
- 1.0.9
- provided
+ ${osgi.util.converter.version}
- org.apache.felix
- org.apache.felix.cm.json
- 2.0.0
- provided
+ jakarta.json
+ jakarta.json-api
+ ${jakarta.json.version}
org.apache.felix
org.apache.felix.feature
- 1.0.2
-
-
- org.slf4j
- slf4j-api
- 2.0.9
+ ${felix.feature.version}
- org.slf4j
- slf4j-simple
- 2.0.9
+ org.apache.felix
+ org.apache.felix.cm.json
+ ${felix.cm.json.version}
org.apache.maven
maven-resolver-provider
- 4.0.0-beta-4
+ ${maven.version}
org.apache.maven.resolver
maven-resolver-supplier-mvn4
- 2.0.1
+ ${maven.resolver.version}
org.apache.maven.resolver
maven-resolver-spi
- 2.0.1
+ ${maven.resolver.version}
+
+
+ org.slf4j
+ slf4j-api
+ ${slf4j.version}
+
+
+ org.slf4j
+ slf4j-simple
+ ${slf4j.version}
- junit
- junit
- 4.13.2
+ org.junit.jupiter
+ junit-jupiter-api
+ ${junit-jupiter.version}
test
- org.mockito
- mockito-core
- 2.8.9
+ org.junit.platform
+ junit-platform-commons
+ ${junit-platform.version}
test
- org.apache.johnzon
- johnzon-core
- 1.2.19
- jakarta
+ org.junit.jupiter
+ junit-jupiter-params
+ ${junit-jupiter.version}
test
- org.junit.jupiter
- junit-jupiter-api
+ org.osgi
+ org.osgi.test.junit5
+ ${osgi.test.version}
test
-
-
- org.apache.felix
- org.apache.felix.framework
- 7.0.5
+ org.osgi
+ org.osgi.test.junit5.cm
+ ${osgi.test.version}
test
-
-
- org.apache.felix
- org.apache.felix.gogo.command
- 1.1.2
+ org.osgi
+ org.osgi.test.common
+ ${osgi.test.version}
test
- org.apache.felix
- org.apache.felix.gogo.shell
- 1.1.4
+ org.osgi
+ org.osgi.test.assertj.framework
+ ${osgi.test.version}
test
- org.apache.felix
- org.apache.felix.gogo.runtime
- 1.1.6
+ org.junit.jupiter
+ junit-jupiter-engine
+ ${junit-jupiter.version}
test
- biz.aQute
- biz.aQute.gogo.commands.provider
- 1.9.0
+ org.junit.platform
+ junit-platform-launcher
+ ${junit-platform.version}
+ test
+
+
+ org.mockito
+ mockito-junit-jupiter
+ ${mockito.version}
test
- biz.aQute.bnd
- biz.aQute.bnd.util
- 7.0.0
+ org.glassfish
+ jakarta.json
+ ${glassfish.jakarta.json.version}
test
org.apache.aries.spifly
org.apache.aries.spifly.dynamic.framework.extension
- 1.3.6
+ ${aries.spifly.dynamic.framework.extension.version}
+ test
+
+
+
+
+ org.apache.felix
+ org.apache.felix.framework
+ ${felix.framework.version}
+ test
+
+
+
+
+ org.apache.felix
+ org.apache.felix.configadmin
+ ${felix.configadmin.version}
+ test
+
+
+
+
+ org.apache.felix
+ org.apache.felix.scr
+ ${felix.scr.version}
test
@@ -197,43 +266,68 @@
https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
maven-clean-plugin
- 3.4.0
+ ${maven.clean.plugin.version}
maven-resources-plugin
- 3.3.1
+ ${maven.resources.plugin.version}
maven-compiler-plugin
- 3.13.0
+ ${maven.compiler.plugin.version}
maven-surefire-plugin
- 3.3.0
+ ${maven.surefire.plugin.version}
maven-jar-plugin
- 3.4.2
+ ${maven.jar.plugin.version}
maven-install-plugin
- 3.1.2
+ ${maven.install.plugin.version}
maven-deploy-plugin
- 3.1.2
+ ${maven.deploy.plugin.version}
maven-site-plugin
- 3.12.1
+ ${maven.site.plugin.version}
maven-project-info-reports-plugin
- 3.6.1
+ ${maven.project.info.reports.plugin.version}
+
+
+ biz.aQute.bnd
+ bnd-maven-plugin
+ ${bnd.version}
+
+
+ biz.aQute.bnd
+ bnd-resolver-maven-plugin
+ ${bnd.version}
+
+
+ biz.aQute.bnd
+ bnd-testing-maven-plugin
+ ${bnd.version}
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ ${maven.dependency.plugin.version}
+
+
+ org.codehaus.mojo
+ properties-maven-plugin
+ ${maven.properties.plugin.version}
@@ -241,20 +335,63 @@
biz.aQute.bnd
bnd-maven-plugin
- 7.0.0
bnd-process
+
="org.osgi.service.featurelauncher.runtime.FeatureRuntime";uses:="org.osgi.service.featurelauncher.runtime",\
osgi.service;objectClass:List="org.osgi.service.featurelauncher.repository.ArtifactRepositoryFactory";uses:="org.osgi.service.featurelauncher.repository"
+ Git-Descriptor: ${system-allow-fail;git describe --dirty --always --abbrev=9}
+ Git-SHA: ${system-allow-fail;git rev-list -1 --no-abbrev-commit HEAD}
+ -conditionalpackage: org.apache.maven.internal.impl.resolver,org.apache.maven.api.*,org.apache.maven.internal.impl.*,org.apache.maven.metadata.v4,org.apache.maven.internal.xml,org.apache.maven.model.v4,org.apache.maven.settings.v4,org.apache.maven.toolchain.v4,org.apache.maven.repository.internal.*,org.apache.maven.artifact.repository.metadata,org.apache.maven.building,org.codehaus.plexus.util.xml.*,org.apache.maven.utils
+ Import-Package: \
+ org.codehaus.plexus.configuration;resolution:=optional,\
+ org.codehaus.plexus.util;resolution:=optional,\
+ org.eclipse.sisu;resolution:=optional,\
+ javax.inject;resolution:=optional,\
+ org.apache.commons.codec.binary;resolution:=optional,\
+ org.apache.commons.logging;resolution:=optional,\
+ org.ietf.jgss;resolution:=optional,\
+ *
+ -includeresource: \
+ @maven-model-builder-[0-9.]*-beta-[0-9.].jar!/*,\
+ @maven-resolver-supplier-mvn4-[0-9.]*.jar!/*,\
+ @maven-resolver-transport-apache-[0-9.]*.jar!/*,\
+ @httpcore-[0-9.]*.jar!/*,\
+ @httpclient-[0-9.]*.jar!/*
+ -noimportjava: true
]]>
+
+ bnd-process-tests
+ process-test-classes
+
+ bnd-process-tests
+
+
+
+ tests.bnd
+ useTestCasesHeader
+
+
@@ -266,11 +403,25 @@
${project.build.outputDirectory}/META-INF/MANIFEST.MF
+
+
+ test-jar
+ package
+
+ test-jar
+
+
+
+
+ ${project.build.testOutputDirectory}/META-INF/MANIFEST.MF
+
+
+
+
org.apache.maven.plugins
maven-dependency-plugin
- 3.8.0
copy-dependencies
@@ -289,7 +440,6 @@
org.codehaus.mojo
properties-maven-plugin
- 1.2.1
@@ -313,8 +463,84 @@
0
+
+ **/integration/**
+
+
+ biz.aQute.bnd
+ bnd-resolver-maven-plugin
+
+
+ resolve-test
+ pre-integration-test
+
+ resolve
+
+
+ ${save.test.bndrun.changes}
+ ${verify.test.bndruns}
+ true
+
+ compile
+ runtime
+ test
+
+
+
+ ${project.build.directory}/${project.build.finalName}-tests.jar
+
+
+ *-test.bndrun
+
+
+
+
+ resolve
+
+ resolve
+
+
+ true
+ false
+ true
+
+ compile
+ runtime
+
+
+
+
+
+
+ biz.aQute.bnd
+ bnd-testing-maven-plugin
+
+ false
+ false
+ true
+
+ compile
+ runtime
+ test
+
+
+
+ ${project.build.directory}/${project.build.finalName}-tests.jar
+
+
+ *-test.bndrun
+
+
+
+
+
+ testing
+
+
+
+
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/FeatureConfigurationManager.java b/src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherConfigurationManager.java
similarity index 93%
rename from src/main/java/com/kentyou/featurelauncher/impl/FeatureConfigurationManager.java
rename to src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherConfigurationManager.java
index 92e170c..80f9814 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/FeatureConfigurationManager.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherConfigurationManager.java
@@ -13,6 +13,8 @@
*/
package com.kentyou.featurelauncher.impl;
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.*;
+
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
@@ -33,7 +35,7 @@
import org.slf4j.LoggerFactory;
/**
- * Manages feature configurations via Configuration Admin Service.
+ * Manages feature configurations via Configuration Admin Service for {@link com.kentyou.featurelauncher.impl.FeatureLauncherImpl}
*
* As defined in the following sections of the "160. Feature Launcher Service Specification":
* - 160.4.3.4
@@ -43,8 +45,8 @@
* @author Michael H. Siemaszko (mhs@into.software)
* @since Sep 25, 2024
*/
-class FeatureConfigurationManager implements ServiceTrackerCustomizer {
- private static final Logger LOG = LoggerFactory.getLogger(FeatureConfigurationManager.class);
+public class FeatureLauncherConfigurationManager implements ServiceTrackerCustomizer {
+ private static final Logger LOG = LoggerFactory.getLogger(FeatureLauncherConfigurationManager.class);
private static final String CONFIGURATION_ADMIN_CLASS_NAME = "org.osgi.service.cm.ConfigurationAdmin";
private static final String CONFIGURATION_CLASS_NAME = "org.osgi.service.cm.Configuration";
@@ -64,7 +66,7 @@ class FeatureConfigurationManager implements ServiceTrackerCustomizer featureConfigurations) {
this.bundleContext = bundleContext;
this.featureConfigurations = featureConfigurations;
@@ -162,7 +164,7 @@ private void createConfiguration(String featureConfigurationPid, FeatureConfigur
LOG.info(String.format("Creating configuration %s", featureConfigurationPid));
Object configurationObject = getConfigurationMethod.invoke(configurationAdminService,
- featureConfiguration.getPid(), "?");
+ featureConfiguration.getPid(), CONFIGURATION_DEFAULT_LOCATION);
updateConfigurationProperties(configurationObject, featureConfigurationPid, featureConfiguration);
@@ -177,7 +179,7 @@ private void createFactoryConfiguration(String featureConfigurationPid, FeatureC
LOG.info(String.format("Creating factory configuration %s", featureConfigurationPid));
Object configurationObject = getFactoryConfigurationMethod.invoke(configurationAdminService,
- featureConfiguration.getFactoryPid().get(), featureConfiguration.getPid(), "?");
+ featureConfiguration.getFactoryPid().get(), normalizePid(featureConfiguration.getPid()), CONFIGURATION_DEFAULT_LOCATION);
updateConfigurationProperties(configurationObject, featureConfigurationPid, featureConfiguration);
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherImpl.java b/src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherImpl.java
index 6ea6a9b..3918d70 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherImpl.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/FeatureLauncherImpl.java
@@ -13,6 +13,8 @@
*/
package com.kentyou.featurelauncher.impl;
+import static com.kentyou.featurelauncher.impl.FeatureLauncherImplConstants.FRAMEWORK_STORAGE_CLEAN_TESTONLY;
+
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
@@ -61,7 +63,6 @@
*/
public class FeatureLauncherImpl extends ArtifactRepositoryFactoryImpl implements FeatureLauncher {
private static final Logger LOG = LoggerFactory.getLogger(FeatureLauncherImpl.class);
- static final String FRAMEWORK_STORAGE_CLEAN_TESTONLY = "testOnly";
/*
* (non-Javadoc)
@@ -105,7 +106,7 @@ class LaunchBuilderImpl implements LaunchBuilder {
private Map frameworkProps;
private List decorators;
private Map extensionHandlers;
- private FeatureConfigurationManager featureConfigurationManager;
+ private FeatureLauncherConfigurationManager featureConfigurationManager;
LaunchBuilderImpl(Feature feature) {
Objects.requireNonNull(feature, "Feature cannot be null!");
@@ -242,20 +243,20 @@ public Framework launchFramework() {
/////////////////////////////////////////////////////////
// 160.4.3.4: Installing bundles and configurations
installBundles(framework);
-
+
createConfigurationAdminTrackerIfNeeded(framework.getBundleContext());
//////////////////////////////////////////
// 160.4.3.5: Starting the framework
startFramework(framework);
-
- waitForConfigurationAdminTrackerIfNeeded(FeatureConfigurationManager.CONFIGURATION_TIMEOUT_DEFAULT);
+
+ waitForConfigurationAdminTrackerIfNeeded(FeatureLauncherConfigurationManager.CONFIGURATION_TIMEOUT_DEFAULT);
this.isLaunched = true;
return framework;
}
-
+
private Framework createFramework(FrameworkFactory frameworkFactory, Map frameworkProperties) {
Framework framework = frameworkFactory.newFramework(frameworkProperties);
try {
@@ -269,14 +270,15 @@ private Framework createFramework(FrameworkFactory frameworkFactory, Map typeOptional) {
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeConfigurationManager.java b/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeConfigurationManager.java
new file mode 100644
index 0000000..facd984
--- /dev/null
+++ b/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeConfigurationManager.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2024 Kentyou and others.
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Kentyou - initial implementation
+ */
+package com.kentyou.featurelauncher.impl.runtime;
+
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.CONFIGURATIONS_FILTER;
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.CONFIGURATION_DEFAULT_LOCATION;
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.constructConfigurationsFilter;
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.normalizePid;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.feature.FeatureConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Manages feature configurations via Configuration Admin Service for {@link com.kentyou.featurelauncher.impl.runtime.FeatureRuntimeImpl}
+ *
+ * As defined in the following sections of the "160. Feature Launcher Service Specification":
+ * - 160.4.3.4
+ * - 160.4.3.5
+ * - 160.5.2.1.3
+ *
+ * @author Michael H. Siemaszko (mhs@into.software)
+ * @since Oct 4, 2024
+ */
+@Component(service = FeatureRuntimeConfigurationManager.class)
+public class FeatureRuntimeConfigurationManager {
+ private static final Logger LOG = LoggerFactory.getLogger(FeatureRuntimeConfigurationManager.class);
+
+ @Reference
+ ConfigurationAdmin configurationAdmin;
+
+ public void createConfigurations(List featureConfigurations) {
+ featureConfigurations.forEach(this::createConfiguration);
+ }
+
+ public void removeConfigurations(Set featuresConfigurationsPids) {
+ try {
+ Map existingConfigurations = getExistingConfigurations();
+
+ if (!existingConfigurations.isEmpty()) {
+ for (String featuresConfigurationsPid : featuresConfigurationsPids) {
+ if (existingConfigurations.containsKey(featuresConfigurationsPid)) {
+ existingConfigurations.remove(featuresConfigurationsPid).delete();
+ }
+ }
+ }
+
+ } catch (IOException | InvalidSyntaxException e) {
+ LOG.error("Error removing configurations!", e);
+ }
+ }
+
+ public List getConfigurations(String filter) {
+ try {
+ Configuration[] configurations = configurationAdmin.listConfigurations(filter);
+
+ if (configurations != null) {
+ return Stream.of(configurations).collect(Collectors.toList());
+ }
+
+ } catch (IOException | InvalidSyntaxException e) {
+ LOG.error("Error retrieving configurations!", e);
+ }
+
+ return Collections.emptyList();
+ }
+
+ private void createConfiguration(FeatureConfiguration featureConfiguration) {
+ if (featureConfiguration.getFactoryPid().isPresent()) {
+ createFactoryConfiguration(featureConfiguration);
+ return;
+ }
+
+ try {
+ LOG.info(String.format("Creating configuration %s", featureConfiguration.getPid()));
+
+ Configuration configuration = configurationAdmin.getConfiguration(featureConfiguration.getPid(),
+ CONFIGURATION_DEFAULT_LOCATION);
+
+ updateConfigurationProperties(configuration, featureConfiguration);
+
+ } catch (IllegalArgumentException | IOException e) {
+ LOG.error(String.format("Error creating configuration %s!", featureConfiguration.getPid()), e);
+ }
+ }
+
+ private void createFactoryConfiguration(FeatureConfiguration featureConfiguration) {
+ try {
+ LOG.info(String.format("Creating factory configuration %s", featureConfiguration.getPid()));
+
+ Configuration configuration = configurationAdmin.getFactoryConfiguration(
+ featureConfiguration.getFactoryPid().get(), normalizePid(featureConfiguration.getPid()),
+ CONFIGURATION_DEFAULT_LOCATION);
+
+ updateConfigurationProperties(configuration, featureConfiguration);
+
+ } catch (IllegalArgumentException | IOException e) {
+ LOG.error(String.format("Error creating configuration %s!", featureConfiguration.getPid()), e);
+ }
+ }
+
+ private void updateConfigurationProperties(Configuration configuration, FeatureConfiguration featureConfiguration) {
+ Dictionary configurationProperties = new Hashtable<>(featureConfiguration.getValues());
+
+ configurationProperties.put(CONFIGURATIONS_FILTER, Boolean.TRUE);
+
+ try {
+ configuration.updateIfDifferent(configurationProperties);
+ } catch (IOException e) {
+ LOG.error(String.format("Error updating configuration properties %s!", featureConfiguration.getPid()), e);
+ }
+ }
+
+ private Map getExistingConfigurations() throws IOException, InvalidSyntaxException {
+ // @formatter:off
+ return Optional.ofNullable(configurationAdmin.listConfigurations(constructConfigurationsFilter()))
+ .map(Arrays::stream)
+ .map(s -> s.collect(Collectors.toMap(Configuration::getPid, Function.identity())))
+ .orElse(Map.of());
+ // @formatter:on
+ }
+}
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeImpl.java b/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeImpl.java
index 328da3a..67d6f5e 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeImpl.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/runtime/FeatureRuntimeImpl.java
@@ -13,44 +13,127 @@
*/
package com.kentyou.featurelauncher.impl.runtime;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.DEFAULT_LOCAL_ARTIFACT_REPOSITORY_NAME;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.DEFAULT_REMOTE_ARTIFACT_REPOSITORY_NAME;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.LOCAL_ARTIFACT_REPOSITORY_PATH;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.REMOTE_ARTIFACT_REPOSITORY_URI;
+import static org.osgi.service.feature.FeatureExtension.Kind.MANDATORY;
+import static org.osgi.service.featurelauncher.FeatureLauncherConstants.REMOTE_ARTIFACT_REPOSITORY_NAME;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
import java.io.Reader;
-import java.net.URI;
import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.stream.Collectors;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
import org.osgi.service.feature.Feature;
+import org.osgi.service.feature.FeatureBundle;
+import org.osgi.service.feature.FeatureConfiguration;
+import org.osgi.service.feature.FeatureService;
import org.osgi.service.feature.ID;
+import org.osgi.service.featurelauncher.decorator.FeatureDecorator;
+import org.osgi.service.featurelauncher.decorator.FeatureExtensionHandler;
import org.osgi.service.featurelauncher.repository.ArtifactRepository;
import org.osgi.service.featurelauncher.runtime.FeatureRuntime;
+import org.osgi.service.featurelauncher.runtime.FeatureRuntimeException;
+import org.osgi.service.featurelauncher.runtime.InstalledBundle;
+import org.osgi.service.featurelauncher.runtime.InstalledConfiguration;
import org.osgi.service.featurelauncher.runtime.InstalledFeature;
+import org.osgi.service.featurelauncher.runtime.RuntimeBundleMerge;
+import org.osgi.service.featurelauncher.runtime.RuntimeConfigurationMerge;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryFactoryImpl;
+import com.kentyou.featurelauncher.impl.repository.FileSystemArtifactRepository;
/**
- * TODO
+ * 160.5 The Feature Runtime Service
+ *
+ * Some parts based on {@link org.eclipse.sensinact.gateway.launcher.FeatureLauncher}
*
* @author Michael H. Siemaszko (mhs@into.software)
* @since Sep 15, 2024
*/
-class FeatureRuntimeImpl implements FeatureRuntime {
+@Component
+public class FeatureRuntimeImpl extends ArtifactRepositoryFactoryImpl implements FeatureRuntime {
+ private static final Logger LOG = LoggerFactory.getLogger(FeatureRuntimeImpl.class);
- /*
- * (non-Javadoc)
- * @see org.osgi.service.featurelauncher.repository.ArtifactRepositoryFactory#createRepository(java.nio.file.Path)
- */
- @Override
- public ArtifactRepository createRepository(Path path) {
- // TODO Auto-generated method stub
- return null;
- }
+ @Reference
+ FeatureService featureService;
- /*
- * (non-Javadoc)
- * @see org.osgi.service.featurelauncher.repository.ArtifactRepositoryFactory#createRepository(java.net.URI, java.util.Map)
- */
- @Override
- public ArtifactRepository createRepository(URI uri, Map props) {
- // TODO Auto-generated method stub
- return null;
+ @Reference
+ FeatureRuntimeConfigurationManager featureRuntimeConfigurationManager;
+
+ private BundleContext bundleContext;
+
+ private final Path defaultM2RepositoryPath;
+ private final Map defaultArtifactRepositories;
+
+ // Bundles installed by this feature runtime
+ private final Map installedBundlesByIdentifier;
+
+ // Lists of artifacts for each feature installed
+ private final Map> installedFeaturesToBundles;
+
+ // List of configurations for each feature installed
+ private final Map> installedFeaturesConfigurations;
+
+ // List of installed features
+ private final List installedFeatures;
+
+ // List of symbolic names of bundles already present in running framework
+ private final List existingBundlesSymbolicNames;
+
+ @Activate
+ public FeatureRuntimeImpl(BundleContext context) {
+ this.bundleContext = context;
+
+ try {
+ this.defaultM2RepositoryPath = getDefaultM2RepositoryPath();
+
+ // set up default repositories - one local repository ( .m2 ), one remote
+ // repository ( Maven Central )
+ this.defaultArtifactRepositories = Map.of(DEFAULT_LOCAL_ARTIFACT_REPOSITORY_NAME,
+ getDefaultLocalArtifactRepository(), DEFAULT_REMOTE_ARTIFACT_REPOSITORY_NAME,
+ getDefaultRemoteArtifactRepository());
+
+ // collect symbolic names of bundles already present in running framework
+ this.existingBundlesSymbolicNames = getExistingBundlesSymbolicNames();
+
+ } catch (IOException e) {
+ throw new FeatureRuntimeException("Could not create default artifact repositories!");
+ }
+
+ this.installedBundlesByIdentifier = new HashMap<>();
+ this.installedFeaturesToBundles = new HashMap<>();
+ this.installedFeaturesConfigurations = new HashMap<>();
+ this.installedFeatures = new ArrayList<>();
+
+ LOG.info("Started FeatureRuntime!");
}
/*
@@ -59,8 +142,7 @@ public ArtifactRepository createRepository(URI uri, Map props) {
*/
@Override
public Map getDefaultRepositories() {
- // TODO Auto-generated method stub
- return null;
+ return defaultArtifactRepositories;
}
/*
@@ -69,8 +151,9 @@ public Map getDefaultRepositories() {
*/
@Override
public InstallOperationBuilder install(Feature feature) {
- // TODO Auto-generated method stub
- return null;
+ Objects.requireNonNull(feature, "Feature cannot be null!");
+
+ return new InstallOperationBuilderImpl(feature);
}
/*
@@ -79,8 +162,17 @@ public InstallOperationBuilder install(Feature feature) {
*/
@Override
public InstallOperationBuilder install(Reader jsonReader) {
- // TODO Auto-generated method stub
- return null;
+ Objects.requireNonNull(jsonReader, "Feature JSON cannot be null!");
+
+ try {
+ Feature feature = featureService.readFeature(jsonReader);
+
+ return install(feature);
+
+ } catch (IOException e) {
+ LOG.error("Error reading feature!", e);
+ throw new FeatureRuntimeException("Error reading feature!", e);
+ }
}
/*
@@ -89,8 +181,7 @@ public InstallOperationBuilder install(Reader jsonReader) {
*/
@Override
public List getInstalledFeatures() {
- // TODO Auto-generated method stub
- return null;
+ return installedFeatures;
}
/*
@@ -99,8 +190,7 @@ public List getInstalledFeatures() {
*/
@Override
public void remove(ID featureId) {
- // TODO Auto-generated method stub
-
+ removeFeature(featureId);
}
/*
@@ -109,8 +199,10 @@ public void remove(ID featureId) {
*/
@Override
public UpdateOperationBuilder update(ID featureId, Feature feature) {
- // TODO Auto-generated method stub
- return null;
+ Objects.requireNonNull(featureId, "Feature ID cannot be null!");
+ Objects.requireNonNull(feature, "Feature cannot be null!");
+
+ return new UpdateOperationBuilderImpl(feature);
}
/*
@@ -119,8 +211,525 @@ public UpdateOperationBuilder update(ID featureId, Feature feature) {
*/
@Override
public UpdateOperationBuilder update(ID featureId, Reader jsonReader) {
- // TODO Auto-generated method stub
- return null;
+ Objects.requireNonNull(featureId, "Feature ID cannot be null!");
+ Objects.requireNonNull(jsonReader, "Feature JSON cannot be null!");
+
+ try {
+ Feature feature = featureService.readFeature(jsonReader);
+
+ return update(featureId, feature);
+
+ } catch (IOException e) {
+ LOG.error("Error reading feature!", e);
+ throw new FeatureRuntimeException("Error reading feature!", e);
+ }
+ }
+
+ abstract class AbstractOperationBuilderImpl> implements OperationBuilder {
+ private final Feature feature;
+ private boolean isCompleted;
+ private boolean useDefaultRepositories;
+ private Map artifactRepositories;
+ private RuntimeBundleMerge runtimeBundleMerge;
+ private RuntimeConfigurationMerge runtimeConfigurationMerge;
+ private Map variables;
+ private List decorators;
+ private Map extensionHandlers;
+
+ public AbstractOperationBuilderImpl(Feature feature) {
+ Objects.requireNonNull(feature, "Feature cannot be null!");
+
+ this.feature = feature;
+ this.isCompleted = false;
+ this.useDefaultRepositories = true;
+ this.artifactRepositories = getDefaultRepositories();
+ this.variables = new HashMap<>();
+ this.decorators = new ArrayList<>();
+ this.extensionHandlers = new HashMap<>();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#addRepository(java.lang.String, org.osgi.service.featurelauncher.repository.ArtifactRepository)
+ */
+ @Override
+ public T addRepository(String name, ArtifactRepository repository) {
+ Objects.requireNonNull(name, "Artifact Repository name cannot be null!");
+ Objects.requireNonNull(repository, "Artifact Repository cannot be null!");
+
+ ensureNotCompletedYet();
+
+ maybeResetDefaultRepositories();
+
+ this.artifactRepositories.put(name, repository);
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#useDefaultRepositories(boolean)
+ */
+ @Override
+ public T useDefaultRepositories(boolean include) {
+ ensureNotCompletedYet();
+
+ this.useDefaultRepositories = include;
+
+ maybeResetDefaultRepositories();
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#withBundleMerge(org.osgi.service.featurelauncher.runtime.RuntimeBundleMerge)
+ */
+ @Override
+ public T withBundleMerge(RuntimeBundleMerge merge) {
+ Objects.requireNonNull(merge, "Runtime bundle merge cannot be null!");
+
+ ensureNotCompletedYet();
+
+ this.runtimeBundleMerge = merge;
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#withConfigurationMerge(org.osgi.service.featurelauncher.runtime.RuntimeConfigurationMerge)
+ */
+ @Override
+ public T withConfigurationMerge(RuntimeConfigurationMerge merge) {
+ Objects.requireNonNull(merge, "Runtime configuration merge cannot be null!");
+
+ ensureNotCompletedYet();
+
+ this.runtimeConfigurationMerge = merge;
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#withVariables(java.util.Map)
+ */
+ @Override
+ public T withVariables(Map variables) {
+ Objects.requireNonNull(variables, "Variables cannot be null!");
+
+ ensureNotCompletedYet();
+
+ this.variables = variables;
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#withDecorator(org.osgi.service.featurelauncher.decorator.FeatureDecorator)
+ */
+ @Override
+ public T withDecorator(FeatureDecorator decorator) {
+ Objects.requireNonNull(decorator, "Feature Decorator cannot be null!");
+
+ ensureNotCompletedYet();
+
+ this.decorators.add(decorator);
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#withExtensionHandler(java.lang.String, org.osgi.service.featurelauncher.decorator.FeatureExtensionHandler)
+ */
+ @Override
+ public T withExtensionHandler(String extensionName, FeatureExtensionHandler extensionHandler) {
+ Objects.requireNonNull(extensionName, "Feature extension name cannot be null!");
+ Objects.requireNonNull(extensionHandler, "Feature extension handler cannot be null!");
+
+ ensureNotCompletedYet();
+
+ this.extensionHandlers.put(extensionName, extensionHandler);
+
+ return castThis();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.OperationBuilder#complete()
+ */
+ @Override
+ public InstalledFeature complete() throws FeatureRuntimeException {
+ this.isCompleted = true;
+
+ return addOrUpdateFeature(feature);
+ }
+
+ protected InstalledFeature addOrUpdateFeature(Feature feature) {
+ ID featureId = feature.getID();
+
+ validateFeatureExtensions(feature);
+
+ // @formatter:off
+ List featureBundles = feature.getBundles().stream()
+ .map(featureBundle -> featureBundle.getID())
+ .collect(Collectors.toList());
+ // @formatter:on
+
+ // Check if feature is already installed or out of date
+ if (installedFeaturesToBundles.containsKey(featureId)) {
+ LOG.info(String.format("Updating feature %s", featureId));
+
+ if (installedFeaturesToBundles.get(featureId).equals(featureBundles)) {
+ // No work to do, already installed
+ LOG.info(String.format("The feature %s is already up to date", featureId));
+
+ return getInstalledFeatureById(featureId);
+ } else {
+ // Feature is out of date - remove and re-install
+ LOG.info(String.format("The feature %s is out of date and will be removed and re-installed",
+ featureId));
+
+ removeFeature(featureId);
+ }
+ }
+
+ // Install bundles
+ List installedBundles = installBundles(featureId, featureBundles);
+ installedFeaturesToBundles.put(featureId, featureBundles);
+
+ // Create configurations
+ featureRuntimeConfigurationManager.createConfigurations(List.copyOf(feature.getConfigurations().values()));
+
+ // Start bundles
+ startBundles(featureId, installedBundles);
+
+ boolean isInitialLaunch = false; // TODO: check if feature was installed by FeatureLauncher
+
+ InstalledFeature installedFeature = constructInstalledFeature(feature.getID(), isInitialLaunch,
+ installedBundles, constructInstalledConfigurations(feature));
+ installedFeatures.add(installedFeature);
+
+ return installedFeature;
+ }
+
+ protected List installBundles(ID featureId, List featureBundles) {
+ List installedBundles = new ArrayList<>();
+ for (FeatureBundle featureBundle : feature.getBundles()) {
+ ID bundleId = featureBundle.getID();
+
+ boolean bundleAlreadyPresentInRunningFramework = duplicatesExistingBundle(bundleId);
+ boolean bundleAlreadyInstalledByRuntime = installedBundlesByIdentifier.containsKey(bundleId);
+
+ if (!bundleAlreadyPresentInRunningFramework && !bundleAlreadyInstalledByRuntime) {
+ Bundle bundle = installBundle(bundleId);
+ if (bundle != null) {
+ installedBundlesByIdentifier.put(bundleId, bundle);
+
+ // TODO: check for and include other "owning features"
+ installedBundles.add(constructInstalledBundle(bundleId, bundle, List.of(featureId)));
+ }
+ } else {
+ if (bundleAlreadyPresentInRunningFramework) {
+ LOG.warn(String.format(
+ "Bundle %s duplicates bundle already present in running framework! Skipping..",
+ bundleId));
+ } else if (bundleAlreadyInstalledByRuntime) {
+ LOG.warn(String.format(
+ "Bundle %s duplicates bundle already installed by feature runtime! Skipping..",
+ bundleId));
+ }
+
+ // bundle was not installed after all, so we remove it from from list of feature
+ // bundles
+ featureBundles.remove(bundleId);
+ }
+ }
+
+ return installedBundles;
+ }
+
+ protected void startBundles(ID featureId, List installedBundles) {
+ for (InstalledBundle installedBundle : installedBundles) {
+ try {
+ BundleRevision rev = installedBundle.getBundle().adapt(BundleRevision.class);
+ if (rev != null && (rev.getTypes() & BundleRevision.TYPE_FRAGMENT) == 0) {
+ // Start all but fragment bundles
+ installedBundle.getBundle().start();
+ } else {
+ LOG.info(String.format("Not starting bundle %s as it is a fragment",
+ installedBundle.getBundle().getSymbolicName()));
+ }
+ } catch (Exception e) {
+ LOG.warn(String.format("An error occurred starting a bundle in feature %s", featureId));
+ }
+ }
+ }
+
+ protected InstalledFeature getInstalledFeatureById(ID featureId) {
+ // @formatter:off
+ return installedFeatures.stream()
+ .filter(f -> featureId.equals(f.getFeatureId()))
+ .findFirst()
+ .orElse(null);
+ // @formatter:on
+ }
+
+ protected InstalledFeature constructInstalledFeature(ID featureId, boolean isInitialLaunch,
+ List installedBundles, List installedConfigurations) {
+ // @formatter:off
+ return new InstalledFeatureImpl(
+ featureId,
+ isInitialLaunch,
+ installedBundles,
+ installedConfigurations);
+ // @formatter:on
+ }
+
+ protected InstalledBundle constructInstalledBundle(ID bundleId, Bundle bundle, List owningFeatures) {
+ return new InstalledBundleImpl(bundleId, Collections.emptyList(), bundle, 1, owningFeatures); // TODO:
+ // aliases
+ }
+
+ protected List constructInstalledConfigurations(Feature feature) {
+ // @formatter:off
+ return feature.getConfigurations().values().stream()
+ .map(fc -> constructInstalledConfiguration(fc, List.of(feature.getID())))
+ .toList();
+ // @formatter:on
+ }
+
+ protected InstalledConfiguration constructInstalledConfiguration(FeatureConfiguration featureConfiguration,
+ List owningFeatures) {
+ return new InstalledConfigurationImpl(featureConfiguration.getPid(), featureConfiguration.getFactoryPid(),
+ featureConfiguration.getValues(), owningFeatures);
+ }
+
+ protected Bundle installBundle(ID featureBundleID) {
+ try (InputStream featureBundleIs = getArtifact(featureBundleID)) {
+ if (featureBundleIs.available() != 0) {
+ Bundle installedBundle = bundleContext.installBundle(featureBundleID.toString(), featureBundleIs);
+
+ LOG.info(String.format("Installed bundle '%s'", installedBundle.getSymbolicName()));
+
+ return installedBundle;
+ }
+ } catch (IOException | BundleException e) {
+ throw new FeatureRuntimeException(String.format("Could not install bundle '%s'!", featureBundleID), e);
+ }
+
+ return null;
+ }
+
+ protected boolean duplicatesExistingBundle(ID featureBundleID) {
+ Path featureBundlePath = getArtifactPath(featureBundleID);
+ if (featureBundlePath != null) {
+ try {
+ JarFile featureBundleJarFile = new JarFile(featureBundlePath.toFile());
+
+ Manifest featureBundleJarMf = featureBundleJarFile.getManifest();
+
+ if ((featureBundleJarMf != null) && (featureBundleJarMf.getMainAttributes() != null)) {
+ // TODO: take into account bundle version as well, if different versions of same
+ // bundle should be supported
+ String featureBundleSymbolicName = featureBundleJarMf.getMainAttributes()
+ .getValue("Bundle-SymbolicName");
+ if (featureBundleSymbolicName != null) {
+ return existingBundlesSymbolicNames.contains(featureBundleSymbolicName);
+ }
+ }
+
+ } catch (IOException e) {
+ LOG.error(String.format("Error checking for if bundle %s duplicates existing bundles!",
+ featureBundleID), e);
+ }
+ }
+
+ return false;
+ }
+
+ protected Path getArtifactPath(ID featureBundleID) {
+ for (ArtifactRepository artifactRepository : artifactRepositories.values()) {
+ if (FileSystemArtifactRepository.class.isInstance(artifactRepository)) {
+ Path featureBundlePath = ((FileSystemArtifactRepository) artifactRepository)
+ .getArtifactPath(featureBundleID);
+ if (featureBundlePath != null) {
+ return featureBundlePath;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ protected InputStream getArtifact(ID featureBundleID) {
+ for (ArtifactRepository artifactRepository : artifactRepositories.values()) {
+ InputStream featureBundleIs = artifactRepository.getArtifact(featureBundleID);
+ if (featureBundleIs != null) {
+ return featureBundleIs;
+ }
+ }
+
+ return InputStream.nullInputStream();
+ }
+
+ protected void validateFeatureExtensions(Feature feature) {
+ List unknownMandatoryFeatureExtensions = feature.getExtensions().entrySet().stream()
+ .filter(e -> e.getValue().getKind() == MANDATORY).map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+ if (!unknownMandatoryFeatureExtensions.isEmpty()) {
+ throw new FeatureRuntimeException(
+ String.format("The feature %d has mandatory extensions for which are not understood",
+ unknownMandatoryFeatureExtensions.size()));
+ }
+ }
+
+ private void maybeResetDefaultRepositories() {
+ if (!this.useDefaultRepositories) {
+ this.artifactRepositories = new HashMap<>();
+ }
+ }
+
+ protected void ensureNotCompletedYet() {
+ if (this.isCompleted == true) {
+ throw new IllegalStateException("Operation already completed!");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T castThis() {
+ return (T) this;
+ }
+ }
+
+ public class InstallOperationBuilderImpl extends AbstractOperationBuilderImpl
+ implements InstallOperationBuilder {
+
+ public InstallOperationBuilderImpl(Feature feature) {
+ super(feature);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.InstallOperationBuilder#install()
+ */
+ @Override
+ public InstalledFeature install() {
+ return complete();
+ }
+ }
+
+ public class UpdateOperationBuilderImpl extends AbstractOperationBuilderImpl
+ implements UpdateOperationBuilder {
+
+ public UpdateOperationBuilderImpl(Feature feature) {
+ super(feature);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.osgi.service.featurelauncher.runtime.FeatureRuntime.UpdateOperationBuilder#update()
+ */
+ @Override
+ public InstalledFeature update() {
+ return complete();
+ }
+ }
+
+ private void removeFeatureConfigurations(ID featureId) {
+ if (installedFeaturesConfigurations.containsKey(featureId)) {
+ featureRuntimeConfigurationManager
+ .removeConfigurations(Set.copyOf(installedFeaturesConfigurations.remove(featureId)));
+ }
+ }
+
+ private void removeFeature(ID featureId) {
+ Deque orderedBundleIDsForRemoval = getBundleIDsForRemoval(featureId);
+ LOG.info(String.format("The following bundles %s are no longer required and will be removed.",
+ Arrays.toString(orderedBundleIDsForRemoval.toArray())));
+
+ stopBundles(orderedBundleIDsForRemoval);
+
+ uninstallBundles(orderedBundleIDsForRemoval);
+
+ removeFeatureConfigurations(featureId);
+
+ installedFeatures.removeIf(f -> featureId.equals(f.getFeatureId()));
}
+ private Deque getBundleIDsForRemoval(ID featureId) {
+ // Get all the bundles to remove in "install order", clearing the features map
+ Set bundlesToRemove = installedFeaturesToBundles.remove(featureId).stream()
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+
+ // Create a deque of bundles to remove, in the order they should be removed
+ Deque orderedBundleIDsForRemoval = new LinkedList<>();
+ for (ID bundleToRemove : bundlesToRemove) {
+ // Only remove the bundle if no remaining features reference it
+ if (installedFeaturesToBundles.values().stream().noneMatch(c -> c.contains(bundleToRemove))) {
+ // Add to the start of the deque, so that we reverse the install order
+ orderedBundleIDsForRemoval.addFirst(bundleToRemove);
+ }
+ }
+
+ return orderedBundleIDsForRemoval;
+ }
+
+ private void stopBundles(Deque bundleIDsToStop) {
+ for (ID bundleIDToStop : bundleIDsToStop) {
+ Bundle bundleForRemoval = installedBundlesByIdentifier.get(bundleIDToStop);
+ if (bundleForRemoval != null) {
+ try {
+ BundleRevision rev = bundleForRemoval.adapt(BundleRevision.class);
+ if (rev != null && (rev.getTypes() & BundleRevision.TYPE_FRAGMENT) == 0) {
+ bundleForRemoval.stop();
+ }
+ } catch (BundleException e) {
+ LOG.warn(String.format("An error occurred stopping bundle %s", bundleIDToStop), e);
+ }
+ }
+ }
+ }
+
+ private void uninstallBundles(Deque bundleIDsToUninstall) {
+ for (ID bundleIDToRemove : bundleIDsToUninstall) {
+ Bundle bundleForRemoval = installedBundlesByIdentifier.remove(bundleIDToRemove);
+ if (bundleForRemoval != null) {
+ try {
+ bundleForRemoval.uninstall();
+ } catch (BundleException e) {
+ LOG.warn(String.format("An error occurred uninstalling bundle %s", bundleIDToRemove), e);
+ }
+ }
+ }
+ }
+
+ private List getExistingBundlesSymbolicNames() {
+ List existingBundlesSymbolicNames = new ArrayList<>();
+
+ for (Bundle bundle : bundleContext.getBundles()) {
+ existingBundlesSymbolicNames.add(bundle.getSymbolicName());
+ }
+
+ return existingBundlesSymbolicNames;
+ }
+
+ private ArtifactRepository getDefaultLocalArtifactRepository() throws IOException {
+ return createRepository(defaultM2RepositoryPath);
+ }
+
+ private ArtifactRepository getDefaultRemoteArtifactRepository() throws IOException {
+ return createRepository(REMOTE_ARTIFACT_REPOSITORY_URI,
+ Map.of(REMOTE_ARTIFACT_REPOSITORY_NAME, DEFAULT_REMOTE_ARTIFACT_REPOSITORY_NAME,
+ LOCAL_ARTIFACT_REPOSITORY_PATH, defaultM2RepositoryPath.toString()));
+ }
+
+ private static Path getDefaultM2RepositoryPath() throws IOException {
+ File userHome = new File(System.getProperty("user.home"));
+
+ return Paths.get(userHome.getCanonicalPath(), ".m2", "repository");
+ }
}
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledBundleImpl.java b/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledBundleImpl.java
index 11f0bcc..6df398d 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledBundleImpl.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledBundleImpl.java
@@ -15,18 +15,33 @@
import java.util.Collection;
import java.util.List;
+import java.util.Objects;
import org.osgi.framework.Bundle;
import org.osgi.service.feature.ID;
import org.osgi.service.featurelauncher.runtime.InstalledBundle;
/**
- * TODO
+ * Implementation of {@link org.osgi.service.featurelauncher.runtime.InstalledBundle}
*
* @author Michael H. Siemaszko (mhs@into.software)
* @since Sep 15, 2024
*/
class InstalledBundleImpl implements InstalledBundle {
+ private final ID bundleId;
+ private final Collection aliases;
+ private final Bundle bundle;
+ private final int startLevel;
+ private final List owningFeatures;
+
+ public InstalledBundleImpl(ID bundleId, Collection aliases, Bundle bundle, int startLevel,
+ List owningFeatures) {
+ this.bundleId = bundleId;
+ this.aliases = aliases;
+ this.bundle = bundle;
+ this.startLevel = startLevel;
+ this.owningFeatures = owningFeatures;
+ }
/*
* (non-Javadoc)
@@ -34,8 +49,7 @@ class InstalledBundleImpl implements InstalledBundle {
*/
@Override
public ID getBundleId() {
- // TODO Auto-generated method stub
- return null;
+ return bundleId;
}
/*
@@ -44,8 +58,7 @@ public ID getBundleId() {
*/
@Override
public Collection getAliases() {
- // TODO Auto-generated method stub
- return null;
+ return aliases;
}
/*
@@ -54,8 +67,7 @@ public Collection getAliases() {
*/
@Override
public Bundle getBundle() {
- // TODO Auto-generated method stub
- return null;
+ return bundle;
}
/*
@@ -64,8 +76,7 @@ public Bundle getBundle() {
*/
@Override
public int getStartLevel() {
- // TODO Auto-generated method stub
- return 0;
+ return startLevel;
}
/*
@@ -74,8 +85,43 @@ public int getStartLevel() {
*/
@Override
public List getOwningFeatures() {
- // TODO Auto-generated method stub
- return null;
+ return owningFeatures;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(aliases, bundle, bundleId, owningFeatures, startLevel);
}
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ InstalledBundleImpl other = (InstalledBundleImpl) obj;
+ return Objects.equals(aliases, other.aliases) && Objects.equals(bundle, other.bundle)
+ && Objects.equals(bundleId, other.bundleId) && Objects.equals(owningFeatures, other.owningFeatures)
+ && startLevel == other.startLevel;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "InstalledBundleImpl [bundleId=" + bundleId + ", aliases=" + aliases + ", bundle=" + bundle
+ + ", startLevel=" + startLevel + ", owningFeatures=" + owningFeatures + "]";
+ }
}
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledConfigurationImpl.java b/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledConfigurationImpl.java
index 5e3253a..6b8e417 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledConfigurationImpl.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledConfigurationImpl.java
@@ -15,18 +15,31 @@
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import org.osgi.service.feature.ID;
import org.osgi.service.featurelauncher.runtime.InstalledConfiguration;
/**
- * TODO
+ * Implementation of {@link org.osgi.service.featurelauncher.runtime.InstalledConfiguration}
*
* @author Michael H. Siemaszko (mhs@into.software)
* @since Sep 15, 2024
*/
class InstalledConfigurationImpl implements InstalledConfiguration {
+ private final String pid;
+ private final Optional factoryPid;
+ private final Map properties;
+ private final List owningFeatures;
+
+ public InstalledConfigurationImpl(String pid, Optional factoryPid, Map properties,
+ List owningFeatures) {
+ this.pid = pid;
+ this.factoryPid = factoryPid;
+ this.properties = properties;
+ this.owningFeatures = owningFeatures;
+ }
/*
* (non-Javadoc)
@@ -34,8 +47,7 @@ class InstalledConfigurationImpl implements InstalledConfiguration {
*/
@Override
public String getPid() {
- // TODO Auto-generated method stub
- return null;
+ return pid;
}
/*
@@ -44,8 +56,7 @@ public String getPid() {
*/
@Override
public Optional getFactoryPid() {
- // TODO Auto-generated method stub
- return Optional.empty();
+ return factoryPid;
}
/*
@@ -54,8 +65,7 @@ public Optional getFactoryPid() {
*/
@Override
public Map getProperties() {
- // TODO Auto-generated method stub
- return null;
+ return properties;
}
/*
@@ -64,8 +74,42 @@ public Map getProperties() {
*/
@Override
public List getOwningFeatures() {
- // TODO Auto-generated method stub
- return null;
+ return owningFeatures;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(factoryPid, owningFeatures, pid, properties);
}
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ InstalledConfigurationImpl other = (InstalledConfigurationImpl) obj;
+ return Objects.equals(factoryPid, other.factoryPid) && Objects.equals(owningFeatures, other.owningFeatures)
+ && Objects.equals(pid, other.pid) && Objects.equals(properties, other.properties);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "InstalledConfigurationImpl [pid=" + pid + ", factoryPid=" + factoryPid + ", properties=" + properties
+ + ", owningFeatures=" + owningFeatures + "]";
+ }
}
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledFeatureImpl.java b/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledFeatureImpl.java
index 175758e..03c8fbe 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledFeatureImpl.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/runtime/InstalledFeatureImpl.java
@@ -14,6 +14,7 @@
package com.kentyou.featurelauncher.impl.runtime;
import java.util.List;
+import java.util.Objects;
import org.osgi.service.feature.ID;
import org.osgi.service.featurelauncher.runtime.InstalledBundle;
@@ -21,12 +22,24 @@
import org.osgi.service.featurelauncher.runtime.InstalledFeature;
/**
- * TODO
+ * Implementation of {@link org.osgi.service.featurelauncher.runtime.InstalledFeature}
*
* @author Michael H. Siemaszko (mhs@into.software)
* @since Sep 15, 2024
*/
class InstalledFeatureImpl implements InstalledFeature {
+ private final ID featureId;
+ private final boolean isInitialLaunch;
+ private final List installedBundles;
+ private final List installedConfigurations;
+
+ public InstalledFeatureImpl(ID featureId, boolean isInitialLaunch, List installedBundles,
+ List installedConfigurations) {
+ this.featureId = featureId;
+ this.isInitialLaunch = isInitialLaunch;
+ this.installedBundles = installedBundles;
+ this.installedConfigurations = installedConfigurations;
+ }
/*
* (non-Javadoc)
@@ -34,8 +47,7 @@ class InstalledFeatureImpl implements InstalledFeature {
*/
@Override
public ID getFeatureId() {
- // TODO Auto-generated method stub
- return null;
+ return featureId;
}
/*
@@ -44,8 +56,7 @@ public ID getFeatureId() {
*/
@Override
public boolean isInitialLaunch() {
- // TODO Auto-generated method stub
- return false;
+ return isInitialLaunch;
}
/*
@@ -54,8 +65,7 @@ public boolean isInitialLaunch() {
*/
@Override
public List getInstalledBundles() {
- // TODO Auto-generated method stub
- return null;
+ return installedBundles;
}
/*
@@ -64,8 +74,44 @@ public List getInstalledBundles() {
*/
@Override
public List getInstalledConfigurations() {
- // TODO Auto-generated method stub
- return null;
+ return installedConfigurations;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#hashCode()
+ */
+ @Override
+ public int hashCode() {
+ return Objects.hash(featureId, installedBundles, installedConfigurations, isInitialLaunch);
}
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#equals(java.lang.Object)
+ */
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ InstalledFeatureImpl other = (InstalledFeatureImpl) obj;
+ return Objects.equals(featureId, other.featureId) && Objects.equals(installedBundles, other.installedBundles)
+ && Objects.equals(installedConfigurations, other.installedConfigurations)
+ && isInitialLaunch == other.isInitialLaunch;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#toString()
+ */
+ @Override
+ public String toString() {
+ return "InstalledFeatureImpl [featureId=" + featureId + ", isInitialLaunch=" + isInitialLaunch
+ + ", installedBundles=" + installedBundles + ", installedConfigurations=" + installedConfigurations
+ + "]";
+ }
}
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/util/ConfigurationUtil.java b/src/main/java/com/kentyou/featurelauncher/impl/util/ConfigurationUtil.java
new file mode 100644
index 0000000..8fb9217
--- /dev/null
+++ b/src/main/java/com/kentyou/featurelauncher/impl/util/ConfigurationUtil.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2024 Kentyou and others.
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Kentyou - initial implementation
+ */
+package com.kentyou.featurelauncher.impl.util;
+
+/**
+ * Util for {@ org.osgi.service.cm.ConfigurationAdmin}
+ *
+ * @author Michael H. Siemaszko (mhs@into.software)
+ * @since Oct 5, 2024
+ */
+public class ConfigurationUtil {
+ public static final String CONFIGURATIONS_FILTER = ".featurelauncher.config";
+ public static final String CONFIGURATION_DEFAULT_LOCATION = "?";
+
+ public static String constructConfigurationsFilter() {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append("(");
+ sb.append(CONFIGURATIONS_FILTER);
+ sb.append("=");
+ sb.append(Boolean.TRUE);
+ sb.append(")");
+
+ return sb.toString();
+ }
+
+ public static String normalizePid(String rawPid) {
+ return rawPid.substring(rawPid.indexOf('~') + 1);
+ }
+}
diff --git a/src/main/java/com/kentyou/featurelauncher/impl/util/FileSystemUtil.java b/src/main/java/com/kentyou/featurelauncher/impl/util/FileSystemUtil.java
index f8cca5b..79eddfc 100644
--- a/src/main/java/com/kentyou/featurelauncher/impl/util/FileSystemUtil.java
+++ b/src/main/java/com/kentyou/featurelauncher/impl/util/FileSystemUtil.java
@@ -21,9 +21,9 @@
import java.nio.file.attribute.BasicFileAttributes;
/**
- * TODO
+ * Util for file system operations.
*
- * @author michal
+ * @author Michael H. Siemaszko (mhs@into.software)
* @since Sep 30, 2024
*/
public class FileSystemUtil {
diff --git a/src/test/java/com/kentyou/featurelauncher/impl/FeatureLauncherImplTest.java b/src/test/java/com/kentyou/featurelauncher/impl/FeatureLauncherImplTest.java
index e0ae4d9..2f7de69 100644
--- a/src/test/java/com/kentyou/featurelauncher/impl/FeatureLauncherImplTest.java
+++ b/src/test/java/com/kentyou/featurelauncher/impl/FeatureLauncherImplTest.java
@@ -13,11 +13,11 @@
*/
package com.kentyou.featurelauncher.impl;
-import static com.kentyou.featurelauncher.impl.FeatureLauncherImpl.FRAMEWORK_STORAGE_CLEAN_TESTONLY;
+import static com.kentyou.featurelauncher.impl.FeatureLauncherImplConstants.FRAMEWORK_STORAGE_CLEAN_TESTONLY;
import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.LOCAL_ARTIFACT_REPOSITORY_PATH;
import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.REMOTE_ARTIFACT_REPOSITORY_URI;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.osgi.service.featurelauncher.FeatureLauncherConstants.REMOTE_ARTIFACT_REPOSITORY_NAME;
import java.io.IOException;
@@ -29,8 +29,8 @@
import java.util.Optional;
import java.util.ServiceLoader;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.Constants;
@@ -54,7 +54,7 @@ public class FeatureLauncherImplTest {
Map frameworkProperties;
Path frameworkStorageTempDir;
- @Before
+ @BeforeEach
public void setUp() throws InterruptedException, IOException {
// Obtain path of dedicated local Maven repository
if (System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH) == null) {
@@ -80,12 +80,17 @@ public void setUp() throws InterruptedException, IOException {
}
@Test
- public void testLaunchFeatureWithNoConfigWithDefaultFrameworkWithLocalArtifactRepository()
+ public void testLaunchFeatureWithNoConfigWithDefaultFramework()
throws IOException, InterruptedException, URISyntaxException, BundleException {
- // Set up a repository
+ // Set up a repositories
ArtifactRepository localArtifactRepository = featureLauncher.createRepository(localM2RepositoryPath);
assertNotNull(localArtifactRepository);
+ ArtifactRepository remoteRepository = featureLauncher.createRepository(REMOTE_ARTIFACT_REPOSITORY_URI,
+ Map.of(REMOTE_ARTIFACT_REPOSITORY_NAME, "central", LOCAL_ARTIFACT_REPOSITORY_PATH,
+ localM2RepositoryPath.toString()));
+ assertNotNull(remoteRepository);
+
// Read Feature JSON
Path featureJSONPath = Paths.get(getClass().getResource("/features/gogo-console-feature.json").toURI());
@@ -93,6 +98,7 @@ public void testLaunchFeatureWithNoConfigWithDefaultFrameworkWithLocalArtifactRe
// @formatter:off
Framework osgiFramework = featureLauncher.launch(Files.newBufferedReader(featureJSONPath))
.withRepository(localArtifactRepository)
+ .withRepository(remoteRepository)
.withFrameworkProperties(frameworkProperties)
.launchFramework();
// @formatter:on
@@ -116,12 +122,17 @@ public void testLaunchFeatureWithNoConfigWithDefaultFrameworkWithLocalArtifactRe
}
@Test
- public void testLaunchFeatureWithConfigWithDefaultFrameworkWithLocalArtifactRepository()
+ public void testLaunchFeatureWithConfigWithDefaultFramework()
throws IOException, InterruptedException, URISyntaxException, BundleException {
- // Set up a repository
+ // Set up a repositories
ArtifactRepository localArtifactRepository = featureLauncher.createRepository(localM2RepositoryPath);
assertNotNull(localArtifactRepository);
+ ArtifactRepository remoteRepository = featureLauncher.createRepository(REMOTE_ARTIFACT_REPOSITORY_URI,
+ Map.of(REMOTE_ARTIFACT_REPOSITORY_NAME, "central", LOCAL_ARTIFACT_REPOSITORY_PATH,
+ localM2RepositoryPath.toString()));
+ assertNotNull(remoteRepository);
+
// Read Feature JSON
Path featureJSONPath = Paths.get(getClass().getResource("/features/console-webconsole-feature.json").toURI());
@@ -129,13 +140,14 @@ public void testLaunchFeatureWithConfigWithDefaultFrameworkWithLocalArtifactRepo
// @formatter:off
Framework osgiFramework = featureLauncher.launch(Files.newBufferedReader(featureJSONPath))
.withRepository(localArtifactRepository)
+ .withRepository(remoteRepository)
.withFrameworkProperties(frameworkProperties)
.launchFramework();
// @formatter:on
// Verify bundles defined in feature are installed and started
Bundle[] bundles = osgiFramework.getBundleContext().getBundles();
- assertEquals(10, bundles.length);
+ assertEquals(15, bundles.length);
assertEquals("org.apache.felix.configadmin", bundles[1].getSymbolicName());
assertEquals("ACTIVE", BundleStateUtil.getBundleStateString(bundles[1].getState()));
@@ -151,7 +163,10 @@ public void testLaunchFeatureWithConfigWithDefaultFrameworkWithLocalArtifactRepo
assertEquals("biz.aQute.gogo.commands.provider", bundles[5].getSymbolicName());
assertEquals("ACTIVE", BundleStateUtil.getBundleStateString(bundles[5].getState()));
-
+
+ assertEquals("org.apache.felix.webconsole", bundles[14].getSymbolicName());
+ assertEquals("ACTIVE", BundleStateUtil.getBundleStateString(bundles[14].getState()));
+
// Stop framework
osgiFramework.stop();
osgiFramework.waitForStop(0);
diff --git a/src/test/java/com/kentyou/featurelauncher/impl/repository/LocalArtifactRepositoryImplTest.java b/src/test/java/com/kentyou/featurelauncher/impl/repository/LocalArtifactRepositoryImplTest.java
index 357c097..0837941 100644
--- a/src/test/java/com/kentyou/featurelauncher/impl/repository/LocalArtifactRepositoryImplTest.java
+++ b/src/test/java/com/kentyou/featurelauncher/impl/repository/LocalArtifactRepositoryImplTest.java
@@ -14,10 +14,10 @@
package com.kentyou.featurelauncher.impl.repository;
import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.LOCAL_ARTIFACT_REPOSITORY_PATH;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.File;
import java.io.IOException;
@@ -31,9 +31,10 @@
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
-import org.apache.felix.feature.impl.IDImpl;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.osgi.service.feature.FeatureService;
+import org.osgi.service.feature.ID;
import org.osgi.service.featurelauncher.repository.ArtifactRepository;
import org.osgi.service.featurelauncher.repository.ArtifactRepositoryFactory;
@@ -50,9 +51,10 @@
*/
public class LocalArtifactRepositoryImplTest {
ArtifactRepositoryFactory artifactRepositoryFactory;
+ FeatureService featureService;
Path localM2RepositoryPath;
- @Before
+ @BeforeEach
public void setUp() {
// Obtain path of dedicated local Maven repository
if (System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH) == null) {
@@ -62,14 +64,24 @@ public void setUp() {
localM2RepositoryPath = Paths.get(System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH));
// Load the Artifact Repository Factory
- ServiceLoader loader = ServiceLoader.load(ArtifactRepositoryFactory.class);
- Optional artifactRepositoryFactoryOptional = loader.findFirst();
-
- if (artifactRepositoryFactoryOptional.isPresent()) {
- artifactRepositoryFactory = artifactRepositoryFactoryOptional.get();
+ ServiceLoader artifactRepositoryFactoryServiceLoader = ServiceLoader
+ .load(ArtifactRepositoryFactory.class);
+ Optional artifactRepositoryFactoryServiceLoaderOptional = artifactRepositoryFactoryServiceLoader
+ .findFirst();
+ if (artifactRepositoryFactoryServiceLoaderOptional.isPresent()) {
+ artifactRepositoryFactory = artifactRepositoryFactoryServiceLoaderOptional.get();
} else {
throw new IllegalStateException("Error loading artifact repository factory!");
}
+
+ // Load the Feature Service
+ ServiceLoader featureServiceLoader = ServiceLoader.load(FeatureService.class);
+ Optional featureServiceLoaderOptional = featureServiceLoader.findFirst();
+ if (featureServiceLoaderOptional.isPresent()) {
+ featureService = featureServiceLoaderOptional.get();
+ } else {
+ throw new IllegalStateException("Error loading feature service!");
+ }
}
@Test
@@ -110,7 +122,7 @@ public void testGetArtifactFromLocalArtifactRepository() throws IOException {
assertNotNull(localArtifactRepository);
assertTrue(localArtifactRepository instanceof LocalArtifactRepositoryImpl);
- IDImpl artifactId = IDImpl.fromMavenID("org.osgi:org.osgi.service.feature:1.0.0");
+ ID artifactId = featureService.getIDfromMavenCoordinates("org.osgi:org.osgi.service.feature:1.0.0");
assertNotNull(artifactId);
try (JarInputStream jarIs = new JarInputStream(localArtifactRepository.getArtifact(artifactId))) {
diff --git a/src/test/java/com/kentyou/featurelauncher/impl/repository/RemoteArtifactRepositoryImplTest.java b/src/test/java/com/kentyou/featurelauncher/impl/repository/RemoteArtifactRepositoryImplTest.java
index f1c6002..d6e49ef 100644
--- a/src/test/java/com/kentyou/featurelauncher/impl/repository/RemoteArtifactRepositoryImplTest.java
+++ b/src/test/java/com/kentyou/featurelauncher/impl/repository/RemoteArtifactRepositoryImplTest.java
@@ -15,10 +15,10 @@
import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.LOCAL_ARTIFACT_REPOSITORY_PATH;
import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.REMOTE_ARTIFACT_REPOSITORY_URI;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.osgi.service.featurelauncher.FeatureLauncherConstants.REMOTE_ARTIFACT_REPOSITORY_NAME;
import java.io.File;
@@ -35,9 +35,10 @@
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
-import org.apache.felix.feature.impl.IDImpl;
-import org.junit.Before;
-import org.junit.Test;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.osgi.service.feature.FeatureService;
+import org.osgi.service.feature.ID;
import org.osgi.service.featurelauncher.repository.ArtifactRepository;
import org.osgi.service.featurelauncher.repository.ArtifactRepositoryFactory;
@@ -54,9 +55,10 @@
*/
public class RemoteArtifactRepositoryImplTest {
ArtifactRepositoryFactory artifactRepositoryFactory;
+ FeatureService featureService;
Path localM2RepositoryPath;
- @Before
+ @BeforeEach
public void setUp() {
// Obtain path of dedicated local Maven repository
if (System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH) == null) {
@@ -66,14 +68,24 @@ public void setUp() {
localM2RepositoryPath = Paths.get(System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH));
// Load the Artifact Repository Factory
- ServiceLoader loader = ServiceLoader.load(ArtifactRepositoryFactory.class);
- Optional artifactRepositoryFactoryOptional = loader.findFirst();
-
- if (artifactRepositoryFactoryOptional.isPresent()) {
- artifactRepositoryFactory = artifactRepositoryFactoryOptional.get();
+ ServiceLoader artifactRepositoryFactoryServiceLoader = ServiceLoader
+ .load(ArtifactRepositoryFactory.class);
+ Optional artifactRepositoryFactoryServiceLoaderOptional = artifactRepositoryFactoryServiceLoader
+ .findFirst();
+ if (artifactRepositoryFactoryServiceLoaderOptional.isPresent()) {
+ artifactRepositoryFactory = artifactRepositoryFactoryServiceLoaderOptional.get();
} else {
throw new IllegalStateException("Error loading artifact repository factory!");
}
+
+ // Load the Feature Service
+ ServiceLoader featureServiceLoader = ServiceLoader.load(FeatureService.class);
+ Optional featureServiceLoaderOptional = featureServiceLoader.findFirst();
+ if (featureServiceLoaderOptional.isPresent()) {
+ featureService = featureServiceLoaderOptional.get();
+ } else {
+ throw new IllegalStateException("Error loading feature service!");
+ }
}
@Test
@@ -152,7 +164,7 @@ public void testGetArtifactFromRemoteArtifactRepository() throws IOException {
assertNotNull(remoteRepository);
assertTrue(remoteRepository instanceof RemoteArtifactRepositoryImpl);
- IDImpl artifactId = IDImpl.fromMavenID("org.apache.felix:org.apache.felix.webconsole:4.8.8");
+ ID artifactId = featureService.getIDfromMavenCoordinates("org.apache.felix:org.apache.felix.webconsole:4.8.8");
assertNotNull(artifactId);
try (JarInputStream jarIs = new JarInputStream(remoteRepository.getArtifact(artifactId))) {
diff --git a/src/test/java/com/kentyou/featurelauncher/impl/runtime/integration/FeatureRuntimeIntegrationTest.java b/src/test/java/com/kentyou/featurelauncher/impl/runtime/integration/FeatureRuntimeIntegrationTest.java
new file mode 100644
index 0000000..574f5d6
--- /dev/null
+++ b/src/test/java/com/kentyou/featurelauncher/impl/runtime/integration/FeatureRuntimeIntegrationTest.java
@@ -0,0 +1,456 @@
+/**
+ * Copyright (c) 2024 Kentyou and others.
+ * All rights reserved.
+ *
+ * This program and the accompanying materials are made
+ * available under the terms of the Eclipse Public License 2.0
+ * which is available at https://www.eclipse.org/legal/epl-2.0/
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Contributors:
+ * Kentyou - initial implementation
+ */
+package com.kentyou.featurelauncher.impl.runtime.integration;
+
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.DEFAULT_LOCAL_ARTIFACT_REPOSITORY_NAME;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.DEFAULT_REMOTE_ARTIFACT_REPOSITORY_NAME;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.LOCAL_ARTIFACT_REPOSITORY_PATH;
+import static com.kentyou.featurelauncher.impl.repository.ArtifactRepositoryConstants.REMOTE_ARTIFACT_REPOSITORY_URI;
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.CONFIGURATIONS_FILTER;
+import static com.kentyou.featurelauncher.impl.util.ConfigurationUtil.constructConfigurationsFilter;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.osgi.service.featurelauncher.FeatureLauncherConstants.REMOTE_ARTIFACT_REPOSITORY_NAME;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.nio.charset.Charset;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.feature.FeatureService;
+import org.osgi.service.feature.ID;
+import org.osgi.service.featurelauncher.repository.ArtifactRepository;
+import org.osgi.service.featurelauncher.runtime.FeatureRuntime;
+import org.osgi.service.featurelauncher.runtime.InstalledBundle;
+import org.osgi.service.featurelauncher.runtime.InstalledConfiguration;
+import org.osgi.service.featurelauncher.runtime.InstalledFeature;
+import org.osgi.test.common.annotation.InjectService;
+import org.osgi.test.common.service.ServiceAware;
+
+import com.kentyou.featurelauncher.impl.runtime.FeatureRuntimeConfigurationManager;
+
+/**
+ * Tests {@link com.kentyou.featurelauncher.impl.runtime.FeatureRuntimeImpl}
+ *
+ * As defined in: "160.5 The Feature Runtime Service"
+ *
+ * @author Michael H. Siemaszko (mhs@into.software)
+ * @since Oct 2, 2024
+ */
+public class FeatureRuntimeIntegrationTest {
+ Path localM2RepositoryPath;
+
+ @BeforeEach
+ public void setUp() throws InterruptedException, IOException {
+ // Obtain path of dedicated local Maven repository
+ if (System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH) == null) {
+ throw new IllegalStateException("Local Maven repository is not defined!");
+ }
+
+ localM2RepositoryPath = Paths.get(System.getProperty(LOCAL_ARTIFACT_REPOSITORY_PATH));
+ }
+
+ @Test
+ public void testServices(
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware configurationAdminServiceAware,
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeConfigurationManagerServiceAware,
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureServiceAware,
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeServiceAware) {
+
+ // ConfigurationAdmin
+ assertEquals(1, configurationAdminServiceAware.getServices().size());
+ ServiceReference configurationAdminServiceReference = configurationAdminServiceAware
+ .getServiceReference();
+ assertNotNull(configurationAdminServiceReference);
+
+ // FeatureRuntimeConfigurationManager
+ assertEquals(1, featureRuntimeConfigurationManagerServiceAware.getServices().size());
+ ServiceReference featureRuntimeConfigurationManagerServiceReference = featureRuntimeConfigurationManagerServiceAware
+ .getServiceReference();
+ assertNotNull(featureRuntimeConfigurationManagerServiceReference);
+
+ // FeatureService
+ assertEquals(1, featureServiceAware.getServices().size());
+ ServiceReference featureServiceReference = featureServiceAware.getServiceReference();
+ assertNotNull(featureServiceReference);
+
+ // FeatureRuntime
+ assertEquals(1, featureRuntimeServiceAware.getServices().size());
+ ServiceReference featureRuntimeServiceReference = featureRuntimeServiceAware
+ .getServiceReference();
+ assertNotNull(featureRuntimeServiceReference);
+ }
+
+ @Test
+ public void testGetDefaultRepositories(
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeServiceAware)
+ throws URISyntaxException, IOException {
+ assertEquals(1, featureRuntimeServiceAware.getServices().size());
+ FeatureRuntime featureRuntimeService = featureRuntimeServiceAware.getService();
+ assertNotNull(featureRuntimeService);
+
+ Map defaultArtifactRepositories = featureRuntimeService.getDefaultRepositories();
+ assertNotNull(defaultArtifactRepositories);
+ assertFalse(defaultArtifactRepositories.isEmpty());
+ assertEquals(2, defaultArtifactRepositories.size());
+ assertTrue(defaultArtifactRepositories.containsKey(DEFAULT_LOCAL_ARTIFACT_REPOSITORY_NAME));
+ assertTrue(defaultArtifactRepositories.containsKey(DEFAULT_REMOTE_ARTIFACT_REPOSITORY_NAME));
+ }
+
+ @Test
+ public void testInstallFeatureWithNoConfigWithDefaultRepositories(
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeServiceAware)
+ throws URISyntaxException, IOException {
+ assertEquals(1, featureRuntimeServiceAware.getServices().size());
+ FeatureRuntime featureRuntimeService = featureRuntimeServiceAware.getService();
+ assertNotNull(featureRuntimeService);
+
+ try (InputStream featureIs = getClass().getClassLoader()
+ .getResourceAsStream("/features/gogo-console-feature.json");
+ Reader featureReader = new BufferedReader(
+ new InputStreamReader(featureIs, Charset.forName("UTF-8").newDecoder()));) {
+
+ // Install Feature using default repositories
+ // @formatter:off
+ InstalledFeature installedFeature = featureRuntimeService.install(featureReader)
+ .useDefaultRepositories(true)
+ .install();
+ // @formatter:on
+ assertNotNull(installedFeature);
+ assertFalse(installedFeature.isInitialLaunch());
+
+ assertNotNull(installedFeature.getFeatureId());
+ assertEquals("com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ installedFeature.getFeatureId().toString());
+
+ assertNotNull(installedFeature.getInstalledBundles());
+ List installedBundles = installedFeature.getInstalledBundles();
+ assertEquals(3, installedBundles.size());
+
+ assertEquals("org.apache.felix.gogo.command", installedBundles.get(0).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(0).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.shell", installedBundles.get(1).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(1).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.runtime", installedBundles.get(2).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(2).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ // Verify also via installed features
+ List installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertFalse(installedFeatures.isEmpty());
+ assertEquals(1, installedFeatures.size());
+
+ // Remove feature
+ featureRuntimeService.remove(installedFeature.getFeatureId());
+
+ // Verify again via installed features
+ installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertTrue(installedFeatures.isEmpty());
+ assertEquals(0, installedFeatures.size());
+ }
+ }
+
+ @Test
+ public void testInstallFeatureWithNoConfigWithCustomRepositories(
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeServiceAware)
+ throws URISyntaxException, IOException {
+ assertEquals(1, featureRuntimeServiceAware.getServices().size());
+ FeatureRuntime featureRuntimeService = featureRuntimeServiceAware.getService();
+ assertNotNull(featureRuntimeService);
+
+ // Set up a repositories
+ ArtifactRepository localArtifactRepository = featureRuntimeService.createRepository(localM2RepositoryPath);
+ assertNotNull(localArtifactRepository);
+
+ ArtifactRepository remoteRepository = featureRuntimeService.createRepository(REMOTE_ARTIFACT_REPOSITORY_URI,
+ Map.of(REMOTE_ARTIFACT_REPOSITORY_NAME, "central", LOCAL_ARTIFACT_REPOSITORY_PATH,
+ localM2RepositoryPath.toString()));
+ assertNotNull(remoteRepository);
+
+ try (InputStream featureIs = getClass().getClassLoader()
+ .getResourceAsStream("/features/gogo-console-feature.json");
+ Reader featureReader = new BufferedReader(
+ new InputStreamReader(featureIs, Charset.forName("UTF-8").newDecoder()));) {
+
+ // Install Feature using default repositories
+ // @formatter:off
+ InstalledFeature installedFeature = featureRuntimeService.install(featureReader)
+ .useDefaultRepositories(false)
+ .addRepository("local", localArtifactRepository)
+ .addRepository("central", remoteRepository)
+ .install();
+ // @formatter:on
+ assertNotNull(installedFeature);
+ assertFalse(installedFeature.isInitialLaunch());
+
+ assertNotNull(installedFeature.getFeatureId());
+ assertEquals("com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ installedFeature.getFeatureId().toString());
+
+ assertNotNull(installedFeature.getInstalledBundles());
+ List installedBundles = installedFeature.getInstalledBundles();
+ assertEquals(3, installedBundles.size());
+
+ assertEquals("org.apache.felix.gogo.command", installedBundles.get(0).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(0).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.shell", installedBundles.get(1).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(1).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.runtime", installedBundles.get(2).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(2).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ // Verify also via installed features
+ List installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertFalse(installedFeatures.isEmpty());
+ assertEquals(1, installedFeatures.size());
+
+ // Remove feature
+ featureRuntimeService.remove(installedFeature.getFeatureId());
+
+ // Verify again via installed features
+ installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertTrue(installedFeatures.isEmpty());
+ assertEquals(0, installedFeatures.size());
+ }
+ }
+
+ @Test
+ public void testInstallFeatureWithConfigWithDefaultRepositories(
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeServiceAware,
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeConfigurationManagerServiceAware)
+ throws URISyntaxException, IOException {
+
+ assertEquals(1, featureRuntimeServiceAware.getServices().size());
+ FeatureRuntime featureRuntimeService = featureRuntimeServiceAware.getService();
+ assertNotNull(featureRuntimeService);
+
+ assertEquals(1, featureRuntimeConfigurationManagerServiceAware.getServices().size());
+ FeatureRuntimeConfigurationManager featureRuntimeConfigurationManagerService = featureRuntimeConfigurationManagerServiceAware
+ .getService();
+ assertNotNull(featureRuntimeConfigurationManagerService);
+
+ try (InputStream featureIs = getClass().getClassLoader()
+ .getResourceAsStream("/features/console-webconsole-feature.json");
+ Reader featureReader = new BufferedReader(
+ new InputStreamReader(featureIs, Charset.forName("UTF-8").newDecoder()));) {
+
+ // Install Feature using default repositories
+ // @formatter:off
+ InstalledFeature installedFeature = featureRuntimeService.install(featureReader)
+ .useDefaultRepositories(true)
+ .install();
+ // @formatter:on
+
+ assertNotNull(installedFeature);
+ assertFalse(installedFeature.isInitialLaunch());
+
+ assertNotNull(installedFeature.getFeatureId());
+ assertEquals("com.kentyou.featurelauncher:console-webconsole-feature:1.0",
+ installedFeature.getFeatureId().toString());
+
+ assertNotNull(installedFeature.getInstalledBundles());
+ List installedBundles = installedFeature.getInstalledBundles();
+ assertEquals(10, installedBundles.size());
+
+ assertEquals("org.apache.felix.gogo.command", installedBundles.get(0).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(0).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.shell", installedBundles.get(1).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(1).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.runtime", installedBundles.get(2).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(2).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("biz.aQute.gogo.commands.provider", installedBundles.get(3).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(3).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.webconsole", installedBundles.get(9).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(9).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ List installedConfigurations = installedFeature.getInstalledConfigurations();
+ assertFalse(installedConfigurations.isEmpty());
+ assertEquals(2, installedConfigurations.size());
+
+ assertEquals("org.apache.felix.http~httpFeatureLauncherTest", installedConfigurations.get(0).getPid());
+ assertTrue(installedConfigurations.get(0).getFactoryPid().isPresent());
+ assertEquals("org.apache.felix.http", installedConfigurations.get(0).getFactoryPid().get());
+
+ assertEquals("org.apache.felix.webconsole.internal.servlet.OsgiManager",
+ installedConfigurations.get(1).getPid());
+
+ List configurations = featureRuntimeConfigurationManagerService
+ .getConfigurations(constructConfigurationsFilter());
+ assertFalse(configurations.isEmpty());
+ assertEquals(2, configurations.size());
+
+ for (Configuration configuration : configurations) {
+ assertTrue(Boolean.valueOf(String.valueOf(configuration.getProperties().get(CONFIGURATIONS_FILTER)))
+ .booleanValue());
+ }
+
+ // Verify also via installed features
+ List installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertFalse(installedFeatures.isEmpty());
+ assertEquals(1, installedFeatures.size());
+
+ // Remove feature
+ featureRuntimeService.remove(installedFeature.getFeatureId());
+
+ // Verify again via installed features
+ installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertTrue(installedFeatures.isEmpty());
+ assertEquals(0, installedFeatures.size());
+ }
+ }
+
+ @Test
+ public void testUpdateFeatureWithConfigWithDefaultRepositories(
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeServiceAware,
+ @InjectService(cardinality = 1, timeout = 5000) ServiceAware featureRuntimeConfigurationManagerServiceAware)
+ throws URISyntaxException, IOException {
+
+ assertEquals(1, featureRuntimeServiceAware.getServices().size());
+ FeatureRuntime featureRuntimeService = featureRuntimeServiceAware.getService();
+ assertNotNull(featureRuntimeService);
+
+ assertEquals(1, featureRuntimeConfigurationManagerServiceAware.getServices().size());
+ FeatureRuntimeConfigurationManager featureRuntimeConfigurationManagerService = featureRuntimeConfigurationManagerServiceAware
+ .getService();
+ assertNotNull(featureRuntimeConfigurationManagerService);
+
+ // Install Feature using default repositories
+ try (InputStream featureIs = getClass().getClassLoader()
+ .getResourceAsStream("/features/gogo-console-feature.json");
+ Reader featureReader = new BufferedReader(
+ new InputStreamReader(featureIs, Charset.forName("UTF-8").newDecoder()));) {
+
+ // @formatter:off
+ InstalledFeature installedFeature = featureRuntimeService.install(featureReader)
+ .useDefaultRepositories(true)
+ .install();
+ // @formatter:on
+ assertNotNull(installedFeature);
+ assertFalse(installedFeature.isInitialLaunch());
+
+ assertNotNull(installedFeature.getFeatureId());
+ assertEquals("com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ installedFeature.getFeatureId().toString());
+
+ assertNotNull(installedFeature.getInstalledBundles());
+ List installedBundles = installedFeature.getInstalledBundles();
+ assertEquals(3, installedBundles.size());
+
+ assertEquals("org.apache.felix.gogo.command", installedBundles.get(0).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(0).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.shell", installedBundles.get(1).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(1).getOwningFeatures().contains(installedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.runtime", installedBundles.get(2).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(2).getOwningFeatures().contains(installedFeature.getFeatureId()));
+ }
+
+ // Verify via installed features
+ List installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertFalse(installedFeatures.isEmpty());
+ assertEquals(1, installedFeatures.size());
+
+ ID featureId = installedFeatures.get(0).getFeatureId();
+
+ // Update Feature with same ID with additional bundles
+ try (InputStream featureIs = getClass().getClassLoader()
+ .getResourceAsStream("/features/gogo-console-feature.update-with-webconsole.json");
+ Reader featureReader = new BufferedReader(
+ new InputStreamReader(featureIs, Charset.forName("UTF-8").newDecoder()));) {
+
+ // Update Feature using default repositories
+ // @formatter:off
+ InstalledFeature updatedFeature = featureRuntimeService.update(featureId, featureReader)
+ .useDefaultRepositories(true)
+ .update();
+ // @formatter:on
+
+ assertNotNull(updatedFeature);
+ assertFalse(updatedFeature.isInitialLaunch());
+
+ assertNotNull(updatedFeature.getFeatureId());
+ assertEquals("com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ updatedFeature.getFeatureId().toString());
+
+ assertNotNull(updatedFeature.getInstalledBundles());
+ List installedBundles = updatedFeature.getInstalledBundles();
+ assertEquals(10, installedBundles.size());
+
+ assertEquals("org.apache.felix.gogo.command", installedBundles.get(0).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(0).getOwningFeatures().contains(updatedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.shell", installedBundles.get(1).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(1).getOwningFeatures().contains(updatedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.gogo.runtime", installedBundles.get(2).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(2).getOwningFeatures().contains(updatedFeature.getFeatureId()));
+
+ assertEquals("biz.aQute.gogo.commands.provider", installedBundles.get(3).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(3).getOwningFeatures().contains(updatedFeature.getFeatureId()));
+
+ assertEquals("org.apache.felix.webconsole", installedBundles.get(9).getBundle().getSymbolicName());
+ assertTrue(installedBundles.get(9).getOwningFeatures().contains(updatedFeature.getFeatureId()));
+
+ List installedConfigurations = updatedFeature.getInstalledConfigurations();
+ assertFalse(installedConfigurations.isEmpty());
+ assertEquals(2, installedConfigurations.size());
+
+ assertEquals("org.apache.felix.http~httpFeatureLauncherTest", installedConfigurations.get(0).getPid());
+ assertTrue(installedConfigurations.get(0).getFactoryPid().isPresent());
+ assertEquals("org.apache.felix.http", installedConfigurations.get(0).getFactoryPid().get());
+
+ assertEquals("org.apache.felix.webconsole.internal.servlet.OsgiManager",
+ installedConfigurations.get(1).getPid());
+
+ List configurations = featureRuntimeConfigurationManagerService
+ .getConfigurations(constructConfigurationsFilter());
+ assertFalse(configurations.isEmpty());
+ assertEquals(2, configurations.size());
+
+ for (Configuration configuration : configurations) {
+ assertTrue(Boolean.valueOf(String.valueOf(configuration.getProperties().get(CONFIGURATIONS_FILTER)))
+ .booleanValue());
+ }
+
+ // Remove feature
+ featureRuntimeService.remove(updatedFeature.getFeatureId());
+
+ // Verify again via installed features
+ installedFeatures = featureRuntimeService.getInstalledFeatures();
+ assertTrue(installedFeatures.isEmpty());
+ assertEquals(0, installedFeatures.size());
+ }
+ }
+}
diff --git a/src/test/resources/features/console-webconsole-feature.json b/src/test/resources/features/console-webconsole-feature.json
index c34ee1c..a4c26e1 100644
--- a/src/test/resources/features/console-webconsole-feature.json
+++ b/src/test/resources/features/console-webconsole-feature.json
@@ -1,9 +1,8 @@
{
- "id": "com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ "id": "com.kentyou.featurelauncher:console-webconsole-feature:1.0",
"name": "Gogo console and Felix webconsole feature",
"description": "Gogo console and Felix webconsole feature",
- "complete": false,
- // TODO: add Felix Webconsole artifacts - core, plus dependencies and maybe plugins
+ "complete": true,
"bundles": [
{
"id": "org.apache.felix:org.apache.felix.configadmin:1.9.26"
@@ -31,6 +30,21 @@
},
{
"id": "org.slf4j:slf4j-simple:2.0.9"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.http.servlet-api:2.1.0"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.http.jetty:5.0.4"
+ },
+ {
+ "id": "commons-io:commons-io:2.6"
+ },
+ {
+ "id": "commons-fileupload:commons-fileupload:1.5"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.webconsole:4.8.8"
}
],
"configurations": {
diff --git a/src/test/resources/features/gogo-console-feature.update-with-webconsole.json b/src/test/resources/features/gogo-console-feature.update-with-webconsole.json
new file mode 100644
index 0000000..ff3c782
--- /dev/null
+++ b/src/test/resources/features/gogo-console-feature.update-with-webconsole.json
@@ -0,0 +1,64 @@
+{
+ "id": "com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ "name": "Gogo console feature updated with Felix webconsole",
+ "description": "Gogo console feature updated with Felix webconsole",
+ "complete": true,
+ "bundles": [
+ {
+ "id": "org.apache.felix:org.apache.felix.configadmin:1.9.26"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.gogo.command:1.1.2"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.gogo.shell:1.1.4"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.gogo.runtime:1.1.6"
+ },
+ {
+ "id": "biz.aQute:biz.aQute.gogo.commands.provider:1.9.0"
+ },
+ {
+ "id": "biz.aQute.bnd:biz.aQute.bnd.util:7.0.0"
+ },
+ {
+ "id": "org.apache.aries.spifly:org.apache.aries.spifly.dynamic.framework.extension:1.3.6"
+ },
+ {
+ "id": "org.slf4j:slf4j-api:2.0.9"
+ },
+ {
+ "id": "org.slf4j:slf4j-simple:2.0.9"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.http.servlet-api:2.1.0"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.http.jetty:5.0.4"
+ },
+ {
+ "id": "commons-io:commons-io:2.6"
+ },
+ {
+ "id": "commons-fileupload:commons-fileupload:1.5"
+ },
+ {
+ "id": "org.apache.felix:org.apache.felix.webconsole:4.8.8"
+ }
+ ],
+ "configurations": {
+ "org.apache.felix.http~httpFeatureLauncherTest": {
+ "org.osgi.service.http.port": "8088",
+ "org.osgi.service.http.host": "localhost",
+ "org.apache.felix.http.context_path": "/feature-launcher-test",
+ "org.apache.felix.http.name": "FeatureLauncherTestHTTP",
+ "org.apache.felix.http.runtime.init.id": "httpFeatureLauncherTest"
+ },
+ "org.apache.felix.webconsole.internal.servlet.OsgiManager": {
+ "username": "fcAdmin",
+ "password": "fcAdmin",
+ "http.service.filter": "id=httpFeatureLauncherTest"
+ }
+ }
+}
diff --git a/src/test/resources/features/gogo-console-launch-framework-extension-feature.json b/src/test/resources/features/gogo-console-launch-framework-extension-feature.json
index 24fd067..b05ea77 100644
--- a/src/test/resources/features/gogo-console-launch-framework-extension-feature.json
+++ b/src/test/resources/features/gogo-console-launch-framework-extension-feature.json
@@ -1,5 +1,5 @@
{
- "id": "com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ "id": "com.kentyou.featurelauncher:gogo-console-launch-framework-extension-feature:1.0",
"name": "Gogo console feature with extension",
"description": "Gogo console feature with launch framework extension",
"complete": true,
diff --git a/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-framework.json b/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-framework.json
index 3d0be19..3dc0a9b 100644
--- a/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-framework.json
+++ b/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-framework.json
@@ -1,5 +1,5 @@
{
- "id": "com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ "id": "com.kentyou.featurelauncher:gogo-console-launch-framework-extension-feature-non-framework:1.0",
"name": "Gogo console feature with extension",
"description": "Gogo console feature with launch framework extension containing non-framework artifacts",
"complete": true,
diff --git a/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-mandatory.json b/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-mandatory.json
index b69a4f2..5762b92 100644
--- a/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-mandatory.json
+++ b/src/test/resources/features/gogo-console-launch-framework-extension-feature.non-mandatory.json
@@ -1,5 +1,5 @@
{
- "id": "com.kentyou.featurelauncher:gogo-console-feature:1.0",
+ "id": "com.kentyou.featurelauncher:gogo-console-launch-framework-extension-feature-non-mandatory:1.0",
"name": "Gogo console feature with extension",
"description": "Gogo console feature with non-mandatory launch framework extension",
"complete": true,