FELIX-1457: refactor features management layer
git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@801925 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/features/core/pom.xml b/karaf/features/core/pom.xml
new file mode 100644
index 0000000..e7e03e7
--- /dev/null
+++ b/karaf/features/core/pom.xml
@@ -0,0 +1,110 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+ <!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>org.apache.felix.karaf.features</groupId>
+ <artifactId>features</artifactId>
+ <version>1.2.0-SNAPSHOT</version>
+ </parent>
+
+ <groupId>org.apache.felix.karaf.features</groupId>
+ <artifactId>org.apache.felix.karaf.features.core</artifactId>
+ <packaging>bundle</packaging>
+ <version>1.2.0-SNAPSHOT</version>
+ <name>Apache Felix Karaf :: Features Core</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.apache.felix.bundlerepository</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.felix.karaf.gshell</groupId>
+ <artifactId>org.apache.felix.karaf.gshell.console</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.felix.karaf.gshell</groupId>
+ <artifactId>org.apache.felix.karaf.gshell.obr</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>org.springframework.osgi</groupId>
+ <artifactId>spring-osgi-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicemix.bundles</groupId>
+ <artifactId>org.apache.servicemix.bundles.junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easymock</groupId>
+ <artifactId>easymock</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-jdk14</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <configuration>
+ <instructions>
+ <Bundle-SymbolicName>${artifactId}</Bundle-SymbolicName>
+ <Export-Package>org.apache.felix.karaf.features;version=${project.version}</Export-Package>
+ <Import-Package>
+ javax.management,
+ javax.management.loading,
+ org.osgi.service.command,
+ org.apache.felix.gogo.commands,
+ org.apache.felix.karaf.gshell.console,
+ *
+ </Import-Package>
+ <Private-Package>org.apache.felix.karaf.features.internal</Private-Package>
+ <_versionpolicy>${bnd.version.policy}</_versionpolicy>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/Feature.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/Feature.java
new file mode 100644
index 0000000..6fc7f23
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/Feature.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A feature is a list of bundles associated identified by its name.
+ */
+public interface Feature {
+
+ String getId();
+
+ String getName();
+
+ String getVersion();
+
+ List<Feature> getDependencies();
+
+ List<String> getBundles();
+
+ Map<String, Map<String, String>> getConfigurations();
+
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeatureEvent.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeatureEvent.java
new file mode 100644
index 0000000..f368ff8
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeatureEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.util.EventObject;
+
+public class FeatureEvent extends EventObject {
+
+ public static enum EventType {
+ FeatureInstalled,
+ FeatureUninstalled
+ }
+
+ private final EventType type;
+ private final Feature feature;
+ private final boolean replay;
+
+ public FeatureEvent(Feature feature, EventType type, boolean replay) {
+ super(feature);
+ this.type = type;
+ this.feature = feature;
+ this.replay = replay;
+ }
+
+ public EventType getType() {
+ return type;
+ }
+
+ public Feature getFeature() {
+ return feature;
+ }
+
+ public boolean isReplay() {
+ return replay;
+ }
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesListener.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesListener.java
new file mode 100644
index 0000000..1b54ed9
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesListener.java
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+public interface FeaturesListener {
+
+ void featureEvent(FeatureEvent event);
+
+ void repositoryEvent(RepositoryEvent event);
+
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
new file mode 100644
index 0000000..de7c36a
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.net.URI;
+
+/**
+ * The service managing features repositories.
+ */
+public interface FeaturesService {
+
+ void addRepository(URI url) throws Exception;
+
+ void removeRepository(URI url);
+
+ Repository[] listRepositories();
+
+ void installFeature(String name) throws Exception;
+
+ void installFeature(String name, String version) throws Exception;
+
+ void uninstallFeature(String name) throws Exception;
+
+ void uninstallFeature(String name, String version) throws Exception;
+
+ Feature[] listFeatures() throws Exception;
+
+ Feature[] listInstalledFeatures();
+
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/Repository.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/Repository.java
new file mode 100644
index 0000000..25ffbad
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/Repository.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.net.URI;
+
+/**
+ * A repository of features.
+ */
+public interface Repository {
+
+ URI getURI();
+
+ URI[] getRepositories() throws Exception;
+
+ Feature[] getFeatures() throws Exception;
+
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/RepositoryEvent.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/RepositoryEvent.java
new file mode 100644
index 0000000..f62076a
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/RepositoryEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.util.EventObject;
+
+public class RepositoryEvent extends EventObject {
+
+ public static enum EventType {
+ RepositoryAdded,
+ RepositoryRemoved,
+ }
+
+ private final EventType type;
+ private final Repository repository;
+ private final boolean replay;
+
+ public RepositoryEvent(Repository repository, EventType type, boolean replay) {
+ super(repository);
+ this.type = type;
+ this.repository = repository;
+ this.replay = replay;
+ }
+
+ public EventType getType() {
+ return type;
+ }
+
+ public Repository getRepository() {
+ return repository;
+ }
+
+ public boolean isReplay() {
+ return replay;
+ }
+}
\ No newline at end of file
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeatureImpl.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeatureImpl.java
new file mode 100644
index 0000000..0ae7c6b
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeatureImpl.java
@@ -0,0 +1,125 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features.internal;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.karaf.features.Feature;
+
+/**
+ * A feature
+ */
+public class FeatureImpl implements Feature {
+
+ private String id;
+ private String name;
+ private String version;
+ private List<Feature> dependencies = new ArrayList<Feature>();
+ private List<String> bundles = new ArrayList<String>();
+ private Map<String, Map<String,String>> configs = new HashMap<String, Map<String,String>>();
+ public static String SPLIT_FOR_NAME_AND_VERSION = "_split_for_name_and_version_";
+ public static String DEFAULT_VERSION = "0.0.0";
+
+ public FeatureImpl(String name) {
+ this(name, DEFAULT_VERSION);
+ }
+
+ public FeatureImpl(String name, String version) {
+ this.name = name;
+ this.version = version;
+ this.id = name + "-" + version;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public List<Feature> getDependencies() {
+ return dependencies;
+ }
+
+ public List<String> getBundles() {
+ return bundles;
+ }
+
+ public Map<String, Map<String, String>> getConfigurations() {
+ return configs;
+ }
+
+ public void addDependency(Feature dependency) {
+ dependencies.add(dependency);
+ }
+
+ public void addBundle(String bundle) {
+ bundles.add(bundle);
+ }
+
+ public void addConfig(String name, Map<String,String> properties) {
+ configs.put(name, properties);
+ }
+
+ public String toString() {
+ String ret = getName() + SPLIT_FOR_NAME_AND_VERSION + getVersion();
+ return ret;
+ }
+
+ public static Feature valueOf(String str) {
+ if (str.indexOf(SPLIT_FOR_NAME_AND_VERSION) >= 0) {
+ String strName = str.substring(0, str.indexOf(SPLIT_FOR_NAME_AND_VERSION));
+ String strVersion = str.substring(str.indexOf(SPLIT_FOR_NAME_AND_VERSION)
+ + SPLIT_FOR_NAME_AND_VERSION.length(), str.length());
+ return new FeatureImpl(strName, strVersion);
+ } else {
+ return new FeatureImpl(str);
+ }
+
+
+ }
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FeatureImpl feature = (FeatureImpl) o;
+
+ if (!name.equals(feature.name)) return false;
+ if (!version.equals(feature.version)) return false;
+
+ return true;
+ }
+
+ public int hashCode() {
+ int result = name.hashCode();
+ result = 31 * result + version.hashCode();
+ return result;
+ }
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
new file mode 100644
index 0000000..39a5e90
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
@@ -0,0 +1,602 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features.internal;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.felix.karaf.features.FeaturesService;
+import org.apache.felix.karaf.features.Feature;
+import org.apache.felix.karaf.features.Repository;
+import org.apache.felix.karaf.features.FeaturesListener;
+import org.apache.felix.karaf.features.FeatureEvent;
+import org.apache.felix.karaf.features.RepositoryEvent;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.Version;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.prefs.BackingStoreException;
+import org.osgi.service.prefs.Preferences;
+import org.osgi.service.prefs.PreferencesService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The Features service implementation.
+ * Adding a repository url will load the features contained in this repository and
+ * create dummy sub shells. When invoked, these commands will prompt the user for
+ * installing the needed bundles.
+ *
+ */
+public class FeaturesServiceImpl implements FeaturesService {
+
+ private static final String ALIAS_KEY = "_alias_factory_pid";
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(FeaturesServiceImpl.class);
+
+ private BundleContext bundleContext;
+ private ConfigurationAdmin configAdmin;
+ private PreferencesService preferences;
+ private Set<URI> uris;
+ private Map<URI, RepositoryImpl> repositories = new HashMap<URI, RepositoryImpl>();
+ private Map<String, Map<String, Feature>> features;
+ private Map<Feature, Set<Long>> installed = new HashMap<Feature, Set<Long>>();
+ private String boot;
+ private boolean bootFeaturesInstalled;
+ private List<FeaturesListener> listeners = new CopyOnWriteArrayList<FeaturesListener>();
+
+ public BundleContext getBundleContext() {
+ return bundleContext;
+ }
+
+ public void setBundleContext(BundleContext bundleContext) {
+ this.bundleContext = bundleContext;
+ }
+
+ public ConfigurationAdmin getConfigAdmin() {
+ return configAdmin;
+ }
+
+ public void setConfigAdmin(ConfigurationAdmin configAdmin) {
+ this.configAdmin = configAdmin;
+ }
+
+ public PreferencesService getPreferences() {
+ return preferences;
+ }
+
+ public void setPreferences(PreferencesService preferences) {
+ this.preferences = preferences;
+ }
+
+ public void registerListener(FeaturesListener listener) {
+ listeners.add(listener);
+ for (Repository repository : listRepositories()) {
+ listener.repositoryEvent(new RepositoryEvent(repository, RepositoryEvent.EventType.RepositoryAdded, true));
+ }
+ for (Feature feature : listInstalledFeatures()) {
+ listener.featureEvent(new FeatureEvent(feature, FeatureEvent.EventType.FeatureInstalled, true));
+ }
+ }
+
+ public void unregisterListener(FeaturesListener listener) {
+ listeners.remove(listener);
+ }
+
+ public void setUrls(String uris) throws URISyntaxException {
+ String[] s = uris.split(",");
+ this.uris = new HashSet<URI>();
+ for (String value : s) {
+ this.uris.add(new URI(value));
+ }
+ }
+
+ public void setBoot(String boot) {
+ this.boot = boot;
+ }
+
+ public void addRepository(URI uri) throws Exception {
+ if (!repositories.containsKey(uri)) {
+ internalAddRepository(uri);
+ saveState();
+ }
+ }
+
+ protected RepositoryImpl internalAddRepository(URI uri) throws Exception {
+ RepositoryImpl repo = new RepositoryImpl(uri);
+ repositories.put(uri, repo);
+ callListeners(new RepositoryEvent(repo, RepositoryEvent.EventType.RepositoryAdded, false));
+ features = null;
+ return repo;
+ }
+
+ public void removeRepository(URI uri) {
+ if (repositories.containsKey(uri)) {
+ internalRemoveRepository(uri);
+ saveState();
+ }
+ }
+
+ public void internalRemoveRepository(URI uri) {
+ Repository repo = repositories.remove(uri);
+ callListeners(new RepositoryEvent(repo, RepositoryEvent.EventType.RepositoryRemoved, false));
+ features = null;
+ }
+
+ public Repository[] listRepositories() {
+ Collection<RepositoryImpl> repos = repositories.values();
+ return repos.toArray(new Repository[repos.size()]);
+ }
+
+ public void installAllFeatures(URI uri) throws Exception {
+ RepositoryImpl repo = internalAddRepository(uri);
+ for (Feature f : repo.getFeatures()) {
+ installFeature(f.getName(), f.getVersion());
+ }
+ internalRemoveRepository(uri);
+ }
+
+ public void uninstallAllFeatures(URI uri) throws Exception {
+ RepositoryImpl repo = internalAddRepository(uri);
+ for (Feature f : repo.getFeatures()) {
+ uninstallFeature(f.getName(), f.getVersion());
+ }
+ internalRemoveRepository(uri);
+ }
+
+ public void installFeature(String name) throws Exception {
+ installFeature(name, FeatureImpl.DEFAULT_VERSION);
+ }
+
+ public void installFeature(String name, String version) throws Exception {
+ Feature f = getFeature(name, version);
+ if (f == null) {
+ throw new Exception("No feature named '" + name
+ + "' with version '" + version + "' available");
+ }
+ for (Feature dependency : f.getDependencies()) {
+ installFeature(dependency.getName(), dependency.getVersion());
+ }
+ for (String config : f.getConfigurations().keySet()) {
+ Dictionary<String,String> props = new Hashtable<String, String>(f.getConfigurations().get(config));
+ String[] pid = parsePid(config);
+ if (pid[1] != null) {
+ props.put(ALIAS_KEY, pid[1]);
+ }
+ Configuration cfg = getConfiguration(configAdmin, pid[0], pid[1]);
+ if (cfg.getBundleLocation() != null) {
+ cfg.setBundleLocation(null);
+ }
+ cfg.update(props);
+ }
+ Set<Long> bundles = new HashSet<Long>();
+ for (String bundleLocation : f.getBundles()) {
+ Bundle b = installBundleIfNeeded(bundleLocation);
+ bundles.add(b.getBundleId());
+ }
+ for (long id : bundles) {
+ bundleContext.getBundle(id).start();
+ }
+
+ callListeners(new FeatureEvent(f, FeatureEvent.EventType.FeatureInstalled, false));
+ installed.put(f, bundles);
+ saveState();
+ }
+ protected Bundle installBundleIfNeeded(String bundleLocation) throws IOException, BundleException {
+ LOGGER.debug("Checking " + bundleLocation);
+ InputStream is;
+ try {
+ is = new BufferedInputStream(new URL(bundleLocation).openStream());
+ } catch (RuntimeException e) {
+ LOGGER.error(e.getMessage());
+ throw e;
+ }
+ try {
+ is.mark(256 * 1024);
+ JarInputStream jar = new JarInputStream(is);
+ Manifest m = jar.getManifest();
+ String sn = m.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+ String vStr = m.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+ Version v = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
+ for (Bundle b : bundleContext.getBundles()) {
+ if (b.getSymbolicName() != null && b.getSymbolicName().equals(sn)) {
+ vStr = (String) b.getHeaders().get(Constants.BUNDLE_VERSION);
+ Version bv = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
+ if (v.equals(bv)) {
+ LOGGER.debug(" found installed bundle: " + b);
+ return b;
+ }
+ }
+ }
+ try {
+ is.reset();
+ } catch (IOException e) {
+ is.close();
+ is = new BufferedInputStream(new URL(bundleLocation).openStream());
+ }
+ LOGGER.debug("Installing bundle " + bundleLocation);
+ return getBundleContext().installBundle(bundleLocation, is);
+ } finally {
+ is.close();
+ }
+ }
+
+ public void uninstallFeature(String name) throws Exception {
+ List<String> versions = new ArrayList<String>();
+ for (Feature f : installed.keySet()) {
+ if (name.equals(f.getName())) {
+ versions.add(f.getVersion());
+ }
+ }
+ if (versions.size() == 0) {
+ throw new Exception("Feature named '" + name + "' is not installed");
+ } else if (versions.size() > 1) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Feature named '").append(name).append("' has multiple versions installed (");
+ for (int i = 0; i < versions.size(); i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(versions.get(i));
+ }
+ sb.append("). Please specify the version to uninstall.");
+ throw new Exception(sb.toString());
+ }
+ uninstallFeature(name, versions.get(0));
+ }
+
+ public void uninstallFeature(String name, String version) throws Exception {
+ Feature feature = getFeature(name, version);
+ if (feature == null || !installed.containsKey(feature)) {
+ throw new Exception("Feature named '" + name
+ + "' with version '" + version + "' is not installed");
+ }
+ // Grab all the bundles installed by this feature
+ // and remove all those who will still be in use.
+ // This gives this list of bundles to uninstall.
+ Set<Long> bundles = installed.remove(feature);
+ for (Set<Long> b : installed.values()) {
+ bundles.removeAll(b);
+ }
+ for (long bundleId : bundles) {
+ getBundleContext().getBundle(bundleId).uninstall();
+ }
+ callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureInstalled, false));
+ saveState();
+ }
+
+ public Feature[] listFeatures() throws Exception {
+ Collection<Feature> features = new ArrayList<Feature>();
+ for (Map<String, Feature> featureWithDifferentVersion : getFeatures().values()) {
+ for (Feature f : featureWithDifferentVersion.values()) {
+ features.add(f);
+ }
+ }
+ return features.toArray(new Feature[features.size()]);
+ }
+
+ public Feature[] listInstalledFeatures() {
+ Set<Feature> result = installed.keySet();
+ return result.toArray(new Feature[result.size()]);
+ }
+
+ protected Feature getFeature(String name, String version) throws Exception {
+ if (version != null) {
+ version = version.trim();
+ }
+ Map<String, Feature> versions = getFeatures().get(name);
+ if (versions == null || versions.isEmpty()) {
+ return null;
+ } else {
+ Feature feature = versions.get(version);
+ if (feature == null && FeatureImpl.DEFAULT_VERSION.equals(version)) {
+ Version latest = new Version(cleanupVersion(version));
+ for (String available : versions.keySet()) {
+ Version availableVersion = new Version(cleanupVersion(available));
+ if (availableVersion.compareTo(latest) > 0) {
+ feature = versions.get(available);
+ latest = availableVersion;
+ }
+ }
+ }
+ return feature;
+ }
+ }
+
+ protected Map<String, Map<String, Feature>> getFeatures() throws Exception {
+ if (features == null) {
+ //the outer map's key is feature name, the inner map's key is feature version
+ Map<String, Map<String, Feature>> map = new HashMap<String, Map<String, Feature>>();
+ // Two phase load:
+ // * first load dependent repositories
+ for (;;) {
+ boolean newRepo = false;
+ for (Repository repo : listRepositories()) {
+ for (URI uri : repo.getRepositories()) {
+ if (!repositories.containsKey(uri)) {
+ internalAddRepository(uri);
+ newRepo = true;
+ }
+ }
+ }
+ if (!newRepo) {
+ break;
+ }
+ }
+ // * then load all features
+ for (Repository repo : repositories.values()) {
+ for (Feature f : repo.getFeatures()) {
+ if (map.get(f.getName()) == null) {
+ Map<String, Feature> versionMap = new HashMap<String, Feature>();
+ versionMap.put(f.getVersion(), f);
+ map.put(f.getName(), versionMap);
+ } else {
+ map.get(f.getName()).put(f.getVersion(), f);
+ }
+ }
+ }
+ features = map;
+ }
+ return features;
+ }
+
+ public void start() throws Exception {
+ if (!loadState()) {
+ if (uris != null) {
+ for (URI uri : uris) {
+ internalAddRepository(uri);
+ }
+ }
+ saveState();
+ }
+ if (boot != null && !bootFeaturesInstalled) {
+ new Thread() {
+ public void run() {
+ String[] list = boot.split(",");
+ for (String f : list) {
+ if (f.length() > 0) {
+ try {
+ installFeature(f);
+ } catch (Exception e) {
+ LOGGER.error("Error installing boot feature " + f, e);
+ }
+ }
+ }
+ bootFeaturesInstalled = true;
+ saveState();
+ }
+ }.start();
+ }
+ }
+
+ public void stop() throws Exception {
+ uris = new HashSet<URI>(repositories.keySet());
+ while (!repositories.isEmpty()) {
+ internalRemoveRepository(repositories.keySet().iterator().next());
+ }
+ }
+
+ protected String[] parsePid(String pid) {
+ int n = pid.indexOf('-');
+ if (n > 0) {
+ String factoryPid = pid.substring(n + 1);
+ pid = pid.substring(0, n);
+ return new String[]{pid, factoryPid};
+ } else {
+ return new String[]{pid, null};
+ }
+ }
+
+ protected Configuration getConfiguration(ConfigurationAdmin configurationAdmin,
+ String pid, String factoryPid) throws IOException, InvalidSyntaxException {
+ if (factoryPid != null) {
+ Configuration[] configs = configurationAdmin.listConfigurations("(|(" + ALIAS_KEY + "=" + pid + ")(.alias_factory_pid=" + factoryPid + "))");
+ if (configs == null || configs.length == 0) {
+ return configurationAdmin.createFactoryConfiguration(pid, null);
+ } else {
+ return configs[0];
+ }
+ } else {
+ return configurationAdmin.getConfiguration(pid, null);
+ }
+ }
+
+ protected void saveState() {
+ try {
+ Preferences prefs = preferences.getUserPreferences("FeaturesServiceState");
+ saveSet(prefs.node("repositories"), repositories.keySet());
+ saveMap(prefs.node("features"), installed);
+ prefs.putBoolean("bootFeaturesInstalled", bootFeaturesInstalled);
+ prefs.flush();
+ } catch (Exception e) {
+ LOGGER.error("Error persisting FeaturesService state", e);
+ }
+ }
+
+ protected boolean loadState() {
+ try {
+ Preferences prefs = preferences.getUserPreferences("FeaturesServiceState");
+ if (prefs.nodeExists("repositories")) {
+ Set<URI> repositories = loadSet(prefs.node("repositories"));
+ for (URI repo : repositories) {
+ internalAddRepository(repo);
+ }
+ installed = loadMap(prefs.node("features"));
+ for (Feature f : installed.keySet()) {
+ callListeners(new FeatureEvent(f, FeatureEvent.EventType.FeatureInstalled, true));
+ }
+ bootFeaturesInstalled = prefs.getBoolean("bootFeaturesInstalled", false);
+ return true;
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error loading FeaturesService state", e);
+ }
+ return false;
+ }
+
+ protected void saveSet(Preferences node, Set<URI> set) throws BackingStoreException {
+ List<URI> l = new ArrayList<URI>(set);
+ node.clear();
+ node.putInt("count", l.size());
+ for (int i = 0; i < l.size(); i++) {
+ node.put("item." + i, l.get(i).toString());
+ }
+ }
+
+ protected Set<URI> loadSet(Preferences node) {
+ Set<URI> l = new HashSet<URI>();
+ int count = node.getInt("count", 0);
+ for (int i = 0; i < count; i++) {
+ l.add(URI.create(node.get("item." + i, null)));
+ }
+ return l;
+ }
+
+ protected void saveMap(Preferences node, Map<Feature, Set<Long>> map) throws BackingStoreException {
+ node.clear();
+ for (Map.Entry<Feature, Set<Long>> entry : map.entrySet()) {
+ Feature key = entry.getKey();
+ String val = createValue(entry.getValue());
+ node.put(key.toString(), val);
+ }
+ }
+
+ protected Map<Feature, Set<Long>> loadMap(Preferences node) throws BackingStoreException {
+ Map<Feature, Set<Long>> map = new HashMap<Feature, Set<Long>>();
+ for (String key : node.keys()) {
+ String val = node.get(key, null);
+ Set<Long> set = readValue(val);
+ map.put(FeatureImpl.valueOf(key), set);
+ }
+ return map;
+ }
+
+ protected String createValue(Set<Long> set) {
+ StringBuilder sb = new StringBuilder();
+ for (long i : set) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(i);
+ }
+ return sb.toString();
+ }
+
+ protected Set<Long> readValue(String val) {
+ Set<Long> set = new HashSet<Long>();
+ for (String str : val.split(",")) {
+ set.add(Long.parseLong(str));
+ }
+ return set;
+ }
+
+ protected void callListeners(FeatureEvent event) {
+ for (FeaturesListener listener : listeners) {
+ listener.featureEvent(event);
+ }
+ }
+
+ protected void callListeners(RepositoryEvent event) {
+ for (FeaturesListener listener : listeners) {
+ listener.repositoryEvent(event);
+ }
+ }
+
+ static Pattern fuzzyVersion = Pattern.compile("(\\d+)(\\.(\\d+)(\\.(\\d+))?)?([^a-zA-Z0-9](.*))?",
+ Pattern.DOTALL);
+ static Pattern fuzzyModifier = Pattern.compile("(\\d+[.-])*(.*)",
+ Pattern.DOTALL);
+
+ /**
+ * Clean up version parameters. Other builders use more fuzzy definitions of
+ * the version syntax. This method cleans up such a version to match an OSGi
+ * version.
+ *
+ * @param version
+ * @return
+ */
+ static public String cleanupVersion(String version) {
+ Matcher m = fuzzyVersion.matcher(version);
+ if (m.matches()) {
+ StringBuffer result = new StringBuffer();
+ String d1 = m.group(1);
+ String d2 = m.group(3);
+ String d3 = m.group(5);
+ String qualifier = m.group(7);
+
+ if (d1 != null) {
+ result.append(d1);
+ if (d2 != null) {
+ result.append(".");
+ result.append(d2);
+ if (d3 != null) {
+ result.append(".");
+ result.append(d3);
+ if (qualifier != null) {
+ result.append(".");
+ cleanupModifier(result, qualifier);
+ }
+ } else if (qualifier != null) {
+ result.append(".0.");
+ cleanupModifier(result, qualifier);
+ }
+ } else if (qualifier != null) {
+ result.append(".0.0.");
+ cleanupModifier(result, qualifier);
+ }
+ return result.toString();
+ }
+ }
+ return version;
+ }
+
+ static void cleanupModifier(StringBuffer result, String modifier) {
+ Matcher m = fuzzyModifier.matcher(modifier);
+ if (m.matches())
+ modifier = m.group(2);
+
+ for (int i = 0; i < modifier.length(); i++) {
+ char c = modifier.charAt(i);
+ if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z') || c == '_' || c == '-')
+ result.append(c);
+ }
+ }
+
+}
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/RepositoryImpl.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/RepositoryImpl.java
new file mode 100644
index 0000000..0d413c3
--- /dev/null
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/RepositoryImpl.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features.internal;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import org.apache.felix.karaf.features.Repository;
+import org.apache.felix.karaf.features.Feature;
+import org.xml.sax.SAXException;
+
+/**
+ * The repository implementation.
+ */
+public class RepositoryImpl implements Repository {
+
+ private URI uri;
+ private List<Feature> features;
+ private List<URI> repositories;
+
+ public RepositoryImpl(URI uri) {
+ this.uri = uri;
+ }
+
+ public URI getURI() {
+ return uri;
+ }
+
+ public URI[] getRepositories() throws Exception {
+ if (repositories == null) {
+ load();
+ }
+ return repositories.toArray(new URI[repositories.size()]);
+ }
+
+ public Feature[] getFeatures() throws Exception {
+ if (features == null) {
+ load();
+ }
+ return features.toArray(new Feature[features.size()]);
+ }
+
+ public void load() throws IOException {
+ try {
+ repositories = new ArrayList<URI>();
+ features = new ArrayList<Feature>();
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ URLConnection conn = uri.toURL().openConnection();
+ conn.setDefaultUseCaches(false);
+ Document doc = factory.newDocumentBuilder().parse(conn.getInputStream());
+
+ NodeList nodes = doc.getDocumentElement().getChildNodes();
+ for (int i = 0; i < nodes.getLength(); i++) {
+ Node node = nodes.item(i);
+ if (!(node instanceof Element)) {
+ continue;
+ }
+ if ("repository".equals(node.getNodeName())) {
+ Element e = (Element) nodes.item(i);
+ repositories.add(new URI(e.getTextContent()));
+ } else if ("feature".equals(node.getNodeName())) {
+ Element e = (Element) nodes.item(i);
+ String name = e.getAttribute("name");
+ String version = e.getAttribute("version");
+ FeatureImpl f;
+ if (version != null && version.length() > 0) {
+ f = new FeatureImpl(name, version);
+ } else {
+ f = new FeatureImpl(name);
+ }
+
+ NodeList featureNodes = e.getElementsByTagName("feature");
+ for (int j = 0; j < featureNodes.getLength(); j++) {
+ Element b = (Element) featureNodes.item(j);
+ String dependencyFeatureVersion = b.getAttribute("version");
+ if (dependencyFeatureVersion != null && dependencyFeatureVersion.length() > 0) {
+ f.addDependency(new FeatureImpl(b.getTextContent(), dependencyFeatureVersion));
+ } else {
+ f.addDependency(new FeatureImpl(b.getTextContent()));
+ }
+ }
+ NodeList configNodes = e.getElementsByTagName("config");
+ for (int j = 0; j < configNodes.getLength(); j++) {
+ Element c = (Element) configNodes.item(j);
+ String cfgName = c.getAttribute("name");
+ String data = c.getTextContent();
+ Properties properties = new Properties();
+ properties.load(new ByteArrayInputStream(data.getBytes()));
+ interpolation(properties);
+ Map<String, String> hashtable = new Hashtable<String, String>();
+ for (Object key : properties.keySet()) {
+ String n = key.toString();
+ hashtable.put(n, properties.getProperty(n));
+ }
+ f.addConfig(cfgName, hashtable);
+ }
+ NodeList bundleNodes = e.getElementsByTagName("bundle");
+ for (int j = 0; j < bundleNodes.getLength(); j++) {
+ Element b = (Element) bundleNodes.item(j);
+ f.addBundle(b.getTextContent());
+ }
+ features.add(f);
+ }
+ }
+ } catch (SAXException e) {
+ throw (IOException) new IOException().initCause(e);
+ } catch (ParserConfigurationException e) {
+ throw (IOException) new IOException().initCause(e);
+ } catch (URISyntaxException e) {
+ throw (IOException) new IOException(e.getMessage() + " : " + uri).initCause(e);
+ } catch (IllegalArgumentException e) {
+ throw (IOException) new IOException(e.getMessage() + " : " + uri).initCause(e);
+ }
+ }
+
+ protected void interpolation(Properties properties) {
+ for (Enumeration e = properties.propertyNames(); e.hasMoreElements();) {
+ String key = (String)e.nextElement();
+ String val = properties.getProperty(key);
+ Matcher matcher = Pattern.compile("\\$\\{([^}]+)\\}").matcher(val);
+ while (matcher.find()) {
+ String rep = System.getProperty(matcher.group(1));
+ if (rep != null) {
+ val = val.replace(matcher.group(0), rep);
+ matcher.reset(val);
+ }
+ }
+ properties.put(key, val);
+ }
+ }
+
+}
diff --git a/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml b/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml
new file mode 100644
index 0000000..0d7e2c4
--- /dev/null
+++ b/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+-->
+<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
+ xmlns:ext="http://geronimo.apache.org/blueprint/xmlns/blueprint-ext/v1.0.0">
+
+ <ext:property-placeholder placeholder-prefix="$(" placeholder-suffix=")"/>
+
+ <ext:property-placeholder placeholder-prefix="$[" placeholder-suffix="]" ignore-missing-locations="true">
+ <ext:default-properties>
+ <ext:property name="featuresRepositories" value=""/>
+ <ext:property name="featuresBoot" value=""/>
+ </ext:default-properties>
+ <ext:location>file:$(karaf.home)/etc/org.apache.felix.karaf.features.cfg</ext:location>
+ </ext:property-placeholder>
+
+ <bean id="featuresService" class="org.apache.felix.karaf.features.internal.FeaturesServiceImpl" init-method="start" destroy-method="stop">
+ <property name="urls" value="$[featuresRepositories]" />
+ <property name="boot" value="$[featuresBoot]" />
+ <property name="configAdmin" ref="configAdmin" />
+ <property name="preferences" ref="preferences" />
+ <property name="bundleContext" ref="blueprintBundleContext" />
+ </bean>
+
+ <reference-list id="featuresListeners" interface="org.apache.felix.karaf.features.FeaturesListener" availability="optional">
+ <reference-listener ref="featuresService"
+ bind-method="registerListener"
+ unbind-method="unregisterListener" />
+ </reference-list>
+
+ <reference id="configAdmin" interface="org.osgi.service.cm.ConfigurationAdmin" />
+
+ <reference id="preferences" interface="org.osgi.service.prefs.PreferencesService" availability="optional"/>
+
+ <service ref="featuresService" interface="org.apache.felix.karaf.features.FeaturesService" />
+
+</blueprint>
diff --git a/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeatureTest.java b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeatureTest.java
new file mode 100644
index 0000000..bd88118
--- /dev/null
+++ b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeatureTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import junit.framework.TestCase;
+import org.apache.felix.karaf.features.internal.FeatureImpl;
+
+public class FeatureTest extends TestCase {
+
+ public void testValueOf() {
+ Feature feature = FeatureImpl.valueOf("name" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "version");
+ assertEquals(feature.getName(), "name");
+ assertEquals(feature.getVersion(), "version");
+ feature = FeatureImpl.valueOf("name");
+ assertEquals(feature.getName(), "name");
+ assertEquals(feature.getVersion(), FeatureImpl.DEFAULT_VERSION);
+ }
+
+}
diff --git a/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java
new file mode 100644
index 0000000..0fcec78
--- /dev/null
+++ b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java
@@ -0,0 +1,685 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.net.URI;
+
+import junit.framework.TestCase;
+import org.apache.felix.karaf.features.internal.FeaturesServiceImpl;
+import org.apache.felix.karaf.features.internal.FeatureImpl;
+import org.easymock.EasyMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.isA;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.reset;
+import static org.easymock.EasyMock.verify;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.prefs.Preferences;
+import org.osgi.service.prefs.PreferencesService;
+import org.springframework.context.ApplicationContext;
+
+public class FeaturesServiceTest extends TestCase {
+
+ public void testInstallFeature() throws Exception {
+
+ String name = ApplicationContext.class.getName();
+ name = name.replace(".", "/") + ".class";
+ name = getClass().getClassLoader().getResource(name).toString();
+ name = name.substring("jar:".length(), name.indexOf('!'));
+
+ File tmp = File.createTempFile("smx", ".feature");
+ PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+ pw.println("<features>");
+ pw.println(" <feature name=\"f1\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println("</features>");
+ pw.close();
+
+ URI uri = tmp.toURI();
+
+ Preferences prefs = EasyMock.createMock(Preferences.class);
+ PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+ Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+ Preferences featuresNode = EasyMock.createMock(Preferences.class);
+ BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+ Bundle installedBundle = EasyMock.createMock(Bundle.class);
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ FeaturesServiceImpl svc = new FeaturesServiceImpl();
+ svc.setPreferences(preferencesService);
+ svc.setBundleContext(bundleContext);
+ svc.addRepository(uri);
+
+ Repository[] repositories = svc.listRepositories();
+ assertNotNull(repositories);
+ assertEquals(1, repositories.length);
+ assertNotNull(repositories[0]);
+ Feature[] features = repositories[0].getFeatures();
+ assertNotNull(features);
+ assertEquals(1, features.length);
+ assertNotNull(features[0]);
+ assertEquals("f1", features[0].getName());
+ assertNotNull(features[0].getDependencies());
+ assertEquals(0, features[0].getDependencies().size());
+ assertNotNull(features[0].getBundles());
+ assertEquals(1, features[0].getBundles().size());
+ assertEquals(name, features[0].getBundles().get(0));
+
+ verify(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ reset(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(12345L);
+ expect(bundleContext.getBundle(12345L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + FeatureImpl.DEFAULT_VERSION, "12345");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ svc.installFeature("f1");
+
+ Feature[] installed = svc.listInstalledFeatures();
+ assertEquals(1, installed.length);
+ assertEquals("f1", installed[0].getName());
+ }
+
+ public void testUninstallFeature() throws Exception {
+
+ String name = ApplicationContext.class.getName();
+ name = name.replace(".", "/") + ".class";
+ name = getClass().getClassLoader().getResource(name).toString();
+ name = name.substring("jar:".length(), name.indexOf('!'));
+
+ File tmp = File.createTempFile("smx", ".feature");
+ PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+ pw.println("<features>");
+ pw.println(" <feature name=\"f1\" version=\"0.1\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println(" <feature name=\"f1\" version=\"0.2\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println("</features>");
+ pw.close();
+
+ URI uri = tmp.toURI();
+
+ Preferences prefs = EasyMock.createMock(Preferences.class);
+ PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+ Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+ Preferences featuresNode = EasyMock.createMock(Preferences.class);
+ BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+ Bundle installedBundle = EasyMock.createMock(Bundle.class);
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ FeaturesServiceImpl svc = new FeaturesServiceImpl();
+ svc.setPreferences(preferencesService);
+ svc.setBundleContext(bundleContext);
+ svc.addRepository(uri);
+
+ verify(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ reset(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ // Installs f1 and 0.1
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(12345L);
+ expect(bundleContext.getBundle(12345L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // Installs f1 and 0.2
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(123456L);
+ expect(bundleContext.getBundle(123456L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // UnInstalls f1 and 0.1
+ expect(bundleContext.getBundle(12345)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // UnInstalls f1 and 0.2
+ expect(bundleContext.getBundle(123456)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ try {
+ svc.uninstallFeature("f1");
+ fail("Uninstall should have failed as feature is not installed");
+ } catch (Exception e) {
+ // ok
+ }
+
+ svc.installFeature("f1", "0.1");
+ svc.installFeature("f1", "0.2");
+
+ try {
+ svc.uninstallFeature("f1");
+ fail("Uninstall should have failed as feature is installed in multiple versions");
+ } catch (Exception e) {
+ // ok
+ }
+
+ svc.uninstallFeature("f1", "0.1");
+ svc.uninstallFeature("f1");
+ }
+
+ // Tests Add and Remove Repository
+ public void testAddAndRemoveRepository() throws Exception {
+
+ String name = ApplicationContext.class.getName();
+ name = name.replace(".", "/") + ".class";
+ name = getClass().getClassLoader().getResource(name).toString();
+ name = name.substring("jar:".length(), name.indexOf('!'));
+
+ File tmp = File.createTempFile("smx", ".feature");
+ PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+ pw.println("<features>");
+ pw.println(" <feature name=\"f1\" version=\"0.1\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println(" <feature name=\"f1\" version=\"0.2\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println(" <feature name=\"f2\" version=\"0.2\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println("</features>");
+ pw.close();
+
+ URI uri = tmp.toURI();
+
+ // loads the state
+ Preferences prefs = EasyMock.createMock(Preferences.class);
+ PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+ Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+ Preferences featuresNode = EasyMock.createMock(Preferences.class);
+ BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+ Bundle installedBundle = EasyMock.createMock(Bundle.class);
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // SaveState for addRepository
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // SaveState for removeRepository
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 0);
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ FeaturesServiceImpl svc = new FeaturesServiceImpl();
+ svc.setPreferences(preferencesService);
+ svc.setBundleContext(bundleContext);
+
+ // Adds Repository
+ svc.addRepository(uri);
+
+ // Removes Repository
+ svc.removeRepository(uri);
+ }
+
+ // Tests installing all features in a repo and uninstalling
+ // all features in a repo
+ public void testInstallUninstallAllFeatures() throws Exception {
+
+ String name = ApplicationContext.class.getName();
+ name = name.replace(".", "/") + ".class";
+ name = getClass().getClassLoader().getResource(name).toString();
+ name = name.substring("jar:".length(), name.indexOf('!'));
+
+ File tmp = File.createTempFile("smx", ".feature");
+ PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+ pw.println("<features>");
+ pw.println(" <feature name=\"f1\" version=\"0.1\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println(" <feature name=\"f1\" version=\"0.2\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println(" <feature name=\"f2\" version=\"0.2\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println("</features>");
+ pw.close();
+
+ URI uri = tmp.toURI();
+
+ // loads the state
+ Preferences prefs = EasyMock.createMock(Preferences.class);
+ PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+ Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+ Preferences featuresNode = EasyMock.createMock(Preferences.class);
+ BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+ Bundle installedBundle = EasyMock.createMock(Bundle.class);
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // Installs first feature name = f1, version = 0.1
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(12345L);
+ expect(bundleContext.getBundle(12345L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // Installs second feature name = f1, version = 0.2
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(123456L);
+ expect(bundleContext.getBundle(123456L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // Installs third feature name = f2, version = 0.2
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(1234567L);
+ expect(bundleContext.getBundle(1234567L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "1234567");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 0);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "1234567");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // uninstallAllFeatures
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "1234567");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // uninstalls first feature name = f1, version = 0.1
+ expect(bundleContext.getBundle(12345)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "1234567");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // uninstalls third feature name = f2, version = 0.2
+ expect(bundleContext.getBundle(1234567)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.2", "123456");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // uninstalls second feature name = f1, version = 0.2
+ expect(bundleContext.getBundle(123456)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 0);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ FeaturesServiceImpl svc = new FeaturesServiceImpl();
+ svc.setPreferences(preferencesService);
+ svc.setBundleContext(bundleContext);
+ svc.installAllFeatures(uri);
+
+ // Uninstalls features with versions.
+ svc.uninstallAllFeatures(uri);
+ }
+
+
+ // Tests install of a Repository that includes a feature
+ // with a feature dependency
+ // The dependant feature is in the same repository
+ // Tests uninstall of features
+ public void testInstallFeatureWithDependantFeatures() throws Exception {
+
+ String name = ApplicationContext.class.getName();
+ name = name.replace(".", "/") + ".class";
+ name = getClass().getClassLoader().getResource(name).toString();
+ name = name.substring("jar:".length(), name.indexOf('!'));
+
+ File tmp = File.createTempFile("smx", ".feature");
+ PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+ pw.println("<features>");
+ pw.println(" <feature name=\"f1\" version=\"0.1\">");
+ pw.println(" <feature version=\"0.1\">f2</feature>");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println(" <feature name=\"f2\" version=\"0.1\">");
+ pw.println(" <bundle>" + name + "</bundle>");
+ pw.println(" </feature>");
+ pw.println("</features>");
+ pw.close();
+
+ URI uri = tmp.toURI();
+
+ // loads the state
+ Preferences prefs = EasyMock.createMock(Preferences.class);
+ PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+ Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+ Preferences repositoriesAvailableNode = EasyMock.createMock(Preferences.class);
+ Preferences featuresNode = EasyMock.createMock(Preferences.class);
+ BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+ Bundle installedBundle = EasyMock.createMock(Bundle.class);
+
+ // savestate from addRepository
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // Installs feature f1 with dependency on f2
+ // so will install f2 first
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(12345L);
+ expect(bundleContext.getBundle(12345L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // Then installs f1
+ expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+ expect(bundleContext.installBundle(isA(String.class),
+ isA(InputStream.class))).andReturn(installedBundle);
+ expect(installedBundle.getBundleId()).andReturn(1234L);
+ expect(bundleContext.getBundle(1234L)).andReturn(installedBundle);
+ installedBundle.start();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ featuresNode.put("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "1234");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // uninstalls first feature name = f1, version = 0.1
+ expect(bundleContext.getBundle(1234)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.1", "12345");
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ // uninstalls first feature name = f2, version = 0.1
+ expect(bundleContext.getBundle(12345)).andReturn(installedBundle);
+ installedBundle.uninstall();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 1);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+ expect(prefs.node("repositories")).andReturn(repositoriesNode);
+ repositoriesNode.clear();
+ repositoriesNode.putInt("count", 0);
+ repositoriesNode.put("item.0", uri.toString());
+ expect(prefs.node("features")).andReturn(featuresNode);
+ featuresNode.clear();
+ prefs.putBoolean("bootFeaturesInstalled", false);
+ prefs.flush();
+
+ replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
+
+ FeaturesServiceImpl svc = new FeaturesServiceImpl();
+ svc.setPreferences(preferencesService);
+ svc.setBundleContext(bundleContext);
+ svc.addRepository(uri);
+
+ svc.installFeature("f1", "0.1");
+
+ // Uninstall repository
+ svc.uninstallFeature("f1", "0.1");
+ svc.uninstallFeature("f2", "0.1");
+
+ }
+
+}
diff --git a/karaf/features/core/src/test/java/org/apache/felix/karaf/features/RepositoryTest.java b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/RepositoryTest.java
new file mode 100644
index 0000000..737acaf
--- /dev/null
+++ b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/RepositoryTest.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features;
+
+import java.net.URI;
+
+import junit.framework.TestCase;
+import org.apache.felix.karaf.features.internal.RepositoryImpl;
+import org.apache.felix.karaf.features.internal.FeatureImpl;
+
+
+public class RepositoryTest extends TestCase {
+
+ public void testLoad() throws Exception {
+ RepositoryImpl r = new RepositoryImpl(getClass().getResource("repo1.xml").toURI());
+ // Check repo
+ URI[] repos = r.getRepositories();
+ assertNotNull(repos);
+ assertEquals(1, repos.length);
+ assertEquals(URI.create("urn:r1"), repos[0]);
+ // Check features
+ Feature[] features = r.getFeatures();
+ assertNotNull(features);
+ assertEquals(2, features.length);
+ assertNotNull(features[0]);
+ assertEquals("f1", features[0].getName());
+ assertNotNull(features[0].getConfigurations());
+ assertEquals(1, features[0].getConfigurations().size());
+ assertNotNull(features[0].getConfigurations().get("c1"));
+ assertEquals(1, features[0].getConfigurations().get("c1").size());
+ assertEquals("v", features[0].getConfigurations().get("c1").get("k"));
+ assertNotNull(features[0].getDependencies());
+ assertEquals(0, features[0].getDependencies().size());
+ assertNotNull(features[0].getBundles());
+ assertEquals(2, features[0].getBundles().size());
+ assertEquals("b1", features[0].getBundles().get(0));
+ assertEquals("b2", features[0].getBundles().get(1));
+ assertNotNull(features[1]);
+ assertEquals("f2", features[1].getName());
+ assertNotNull(features[1].getConfigurations());
+ assertEquals(0, features[1].getConfigurations().size());
+ assertNotNull(features[1].getDependencies());
+ assertEquals(1, features[1].getDependencies().size());
+ assertEquals("f1" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + FeatureImpl.DEFAULT_VERSION, features[1].getDependencies().get(0).toString());
+ assertNotNull(features[1].getBundles());
+ assertEquals(1, features[1].getBundles().size());
+ assertEquals("b3", features[1].getBundles().get(0));
+ }
+
+ public void testShowWrongUriInException() throws Exception {
+ String uri = "src/test/resources/org/apache/felix/karaf/gshell/features/repo1.xml";
+ RepositoryImpl r = new RepositoryImpl(new URI(uri));
+ try {
+ r.load();
+ } catch (Exception e) {
+ assertTrue(e.getMessage().contains(uri));
+ }
+ }
+}
diff --git a/karaf/features/core/src/test/java/org/apache/felix/karaf/features/internal/FeaturesServiceImplTest.java b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/internal/FeaturesServiceImplTest.java
new file mode 100644
index 0000000..8c8cb10
--- /dev/null
+++ b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/internal/FeaturesServiceImplTest.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.felix.karaf.features.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+import org.apache.felix.karaf.features.Feature;
+
+/**
+ * Test cases for {@link FeaturesServiceImpl}
+ */
+public class FeaturesServiceImplTest extends TestCase {
+
+ public void testGetFeature() throws Exception {
+ final Map<String, Map<String, Feature>> features = new HashMap<String, Map<String,Feature>>();
+ Map<String, Feature> versions = new HashMap<String, Feature>();
+ FeatureImpl feature = new FeatureImpl("transaction");
+ versions.put("1.0.0", feature);
+ features.put("transaction", versions);
+ final FeaturesServiceImpl impl = new FeaturesServiceImpl() {
+ protected Map<String,Map<String,Feature>> getFeatures() throws Exception {
+ return features;
+ };
+ };
+ assertNotNull(impl.getFeature("transaction", FeatureImpl.DEFAULT_VERSION));
+ assertSame(feature, impl.getFeature("transaction", FeatureImpl.DEFAULT_VERSION));
+ }
+
+ public void testGetFeatureStripVersion() throws Exception {
+ final Map<String, Map<String, Feature>> features = new HashMap<String, Map<String,Feature>>();
+ Map<String, Feature> versions = new HashMap<String, Feature>();
+ FeatureImpl feature = new FeatureImpl("transaction");
+ versions.put("1.0.0", feature);
+ features.put("transaction", versions);
+ final FeaturesServiceImpl impl = new FeaturesServiceImpl() {
+ protected Map<String,Map<String,Feature>> getFeatures() throws Exception {
+ return features;
+ };
+ };
+ assertNotNull(impl.getFeature("transaction", " 1.0.0 "));
+ assertSame(feature, impl.getFeature("transaction", " 1.0.0 "));
+ }
+
+ public void testGetFeatureNotAvailable() throws Exception {
+ final Map<String, Map<String, Feature>> features = new HashMap<String, Map<String,Feature>>();
+ Map<String, Feature> versions = new HashMap<String, Feature>();
+ versions.put("1.0.0", new FeatureImpl("transaction"));
+ features.put("transaction", versions);
+ final FeaturesServiceImpl impl = new FeaturesServiceImpl() {
+ protected Map<String,Map<String,Feature>> getFeatures() throws Exception {
+ return features;
+ };
+ };
+ assertNull(impl.getFeature("activemq", FeatureImpl.DEFAULT_VERSION));
+ }
+
+ public void testGetFeatureHighestAvailable() throws Exception {
+ final Map<String, Map<String, Feature>> features = new HashMap<String, Map<String,Feature>>();
+ Map<String, Feature> versions = new HashMap<String, Feature>();
+ versions.put("1.0.0", new FeatureImpl("transaction", "1.0.0"));
+ versions.put("2.0.0", new FeatureImpl("transaction", "2.0.0"));
+ features.put("transaction", versions);
+ final FeaturesServiceImpl impl = new FeaturesServiceImpl() {
+ protected Map<String,Map<String,Feature>> getFeatures() throws Exception {
+ return features;
+ };
+ };
+ assertNotNull(impl.getFeature("transaction", FeatureImpl.DEFAULT_VERSION));
+ assertSame("2.0.0", impl.getFeature("transaction", FeatureImpl.DEFAULT_VERSION).getVersion());
+ }
+
+}
diff --git a/karaf/features/core/src/test/resources/org/apache/felix/karaf/features/repo1.xml b/karaf/features/core/src/test/resources/org/apache/felix/karaf/features/repo1.xml
new file mode 100644
index 0000000..3284661
--- /dev/null
+++ b/karaf/features/core/src/test/resources/org/apache/felix/karaf/features/repo1.xml
@@ -0,0 +1,31 @@
+<!--
+
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<features>
+ <repository>urn:r1</repository>
+ <feature name="f1">
+ <config name="c1">
+ k=v
+ </config>
+ <bundle>b1</bundle>
+ <bundle>b2</bundle>
+ </feature>
+ <feature name="f2">
+ <feature>f1</feature>
+ <bundle>b3</bundle>
+ </feature>
+</features>