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,