You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2009/10/13 00:55:30 UTC

svn commit: r824529 - in /felix/trunk/karaf/features: command/src/main/java/org/apache/felix/karaf/features/command/ core/src/main/java/org/apache/felix/karaf/features/ core/src/main/java/org/apache/felix/karaf/features/internal/ core/src/main/resource...

Author: gnodet
Date: Mon Oct 12 22:55:29 2009
New Revision: 824529

URL: http://svn.apache.org/viewvc?rev=824529&view=rev
Log:
FELIX-1689: When deploying a feature, we need to refresh some bundles to cope with newly resolved optional dependencies
FELIX-1682: fix problem with bundles not being started after an installation failure

Added:
    felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/HeaderParser.java
    felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/VersionRange.java
Modified:
    felix/trunk/karaf/features/command/src/main/java/org/apache/felix/karaf/features/command/InstallFeatureCommand.java
    felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
    felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
    felix/trunk/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml
    felix/trunk/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java

Modified: felix/trunk/karaf/features/command/src/main/java/org/apache/felix/karaf/features/command/InstallFeatureCommand.java
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/command/src/main/java/org/apache/felix/karaf/features/command/InstallFeatureCommand.java?rev=824529&r1=824528&r2=824529&view=diff
==============================================================================
--- felix/trunk/karaf/features/command/src/main/java/org/apache/felix/karaf/features/command/InstallFeatureCommand.java (original)
+++ felix/trunk/karaf/features/command/src/main/java/org/apache/felix/karaf/features/command/InstallFeatureCommand.java Mon Oct 12 22:55:29 2009
@@ -16,6 +16,8 @@
  */
 package org.apache.felix.karaf.features.command;
 
+import java.util.EnumSet;
+
 import org.apache.felix.gogo.commands.Option;
 import org.apache.felix.karaf.features.FeaturesService;
 import org.apache.felix.gogo.commands.Argument;
@@ -30,13 +32,22 @@
     String name;
     @Argument(index = 1, name = "version", description = "The version of the feature", required = false, multiValued = false)
     String version;
-    @Option(name = "-n", aliases = "--no-clean", description = "Do not uninstall bundles on failure", required = false, multiValued = false)
+    @Option(name = "-c", aliases = "--no-clean", description = "Do not uninstall bundles on failure", required = false, multiValued = false)
     boolean noClean;
+    @Option(name = "-r", aliases = "--no-auto-refresh", description = "Do not automatically refresh bundles", required = false, multiValued = false)
+    boolean noRefresh;
 
     protected void doExecute(FeaturesService admin) throws Exception {
     	if (version == null || version.length() == 0) {
             version = DEFAULT_VERSION;
     	}
-        admin.installFeature(name, version, !noClean);
+        EnumSet<FeaturesService.Option> options = EnumSet.of(FeaturesService.Option.PrintBundlesToRefresh);
+        if (noRefresh) {
+            options.add(FeaturesService.Option.NoAutoRefreshBundles);
+        }
+        if (noClean) {
+            options.add(FeaturesService.Option.NoCleanIfFailure);
+        }
+        admin.installFeature(name, version, options);
     }
 }

Modified: felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java?rev=824529&r1=824528&r2=824529&view=diff
==============================================================================
--- felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java (original)
+++ felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java Mon Oct 12 22:55:29 2009
@@ -17,12 +17,19 @@
 package org.apache.felix.karaf.features;
 
 import java.net.URI;
+import java.util.EnumSet;
 
 /**
  * The service managing features repositories.
  */
 public interface FeaturesService {
 
+    enum Option {
+        NoCleanIfFailure,
+        PrintBundlesToRefresh,
+        NoAutoRefreshBundles,
+    }
+
     void addRepository(URI url) throws Exception;
 
     void removeRepository(URI url);
@@ -33,7 +40,7 @@
     
     void installFeature(String name, String version) throws Exception;
 
-    void installFeature(String name, String version, boolean cleanIfFailure) throws Exception;
+    void installFeature(String name, String version, EnumSet<Option> options) throws Exception;
 
     void uninstallFeature(String name) throws Exception;
     

Modified: felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java?rev=824529&r1=824528&r2=824529&view=diff
==============================================================================
--- felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java (original)
+++ felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java Mon Oct 12 22:55:29 2009
@@ -25,9 +25,11 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Dictionary;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Hashtable;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -55,6 +57,7 @@
 import org.osgi.service.prefs.Preferences;
 import org.osgi.service.prefs.PreferencesService;
 import org.osgi.service.packageadmin.PackageAdmin;
+import org.osgi.service.startlevel.StartLevel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -74,6 +77,7 @@
     private BundleContext bundleContext;
     private ConfigurationAdmin configAdmin;
     private PackageAdmin packageAdmin;
+    private StartLevel startLevel;
     private PreferencesService preferences;
     private Set<URI> uris;
     private Map<URI, RepositoryImpl> repositories = new HashMap<URI, RepositoryImpl>();
@@ -115,6 +119,14 @@
         this.preferences = preferences;
     }
 
+    public StartLevel getStartLevel() {
+        return startLevel;
+    }
+
+    public void setStartLevel(StartLevel startLevel) {
+        this.startLevel = startLevel;
+    }
+
     public void registerListener(FeaturesListener listener) {
         listeners.add(listener);
         for (Repository repository : listRepositories()) {
@@ -196,10 +208,10 @@
     }
 
     public void installFeature(String name, String version) throws Exception {
-        installFeature(name, version, true);
+        installFeature(name, version, EnumSet.noneOf(Option.class));
     }
 
-    public void installFeature(String name, String version, boolean cleanIfFailure) throws Exception {
+    public void installFeature(String name, String version, EnumSet<Option> options) throws Exception {
         InstallationState state = new InstallationState();
         Feature f = getFeature(name, version);
         if (f == null) {
@@ -209,20 +221,66 @@
         try {
             // Install everything
             doInstallFeature(state, f);
+            // Find bundles to refresh
+            boolean print = options.contains(Option.PrintBundlesToRefresh);
+            boolean refresh = !options.contains(Option.NoAutoRefreshBundles);
+            if (print || refresh) {
+                Set<Bundle> bundlesToRefresh = findBundlesToRefresh(state);
+                StringBuilder sb = new StringBuilder();
+                for (Bundle b : bundlesToRefresh) {
+                    if (sb.length() > 0) {
+                        sb.append(", ");
+                    }
+                    sb.append(b.getSymbolicName()).append(" (").append(b.getBundleId()).append(")");
+                }
+                LOGGER.info("Bundles to refresh: {}", sb.toString());
+                if (!bundlesToRefresh.isEmpty()) {
+                    if (print) {
+                        if (refresh) {
+                            System.out.println("Refreshing bundles " + sb.toString());
+                        } else {
+                            System.out.println("The following bundles may need to be refreshed: " + sb.toString());
+                        }
+                    }
+                    if (refresh) {
+                        LOGGER.info("Refreshing bundles: {}", sb.toString());
+                        getPackageAdmin().refreshPackages(bundlesToRefresh.toArray(new Bundle[bundlesToRefresh.size()]));
+                    }
+                }
+            }
             // Start all bundles
             for (Bundle b : state.bundles) {
-                // do not start fragment bundles.
+                // do not start fragment bundles
                 Dictionary d = b.getHeaders();
                 String fragmentHostHeader = (String) d.get(Constants.FRAGMENT_HOST);
                 if (fragmentHostHeader == null || fragmentHostHeader.trim().length() == 0) {
-                    b.start();
+                    // do not start bundles that are persistently stopped
+                    if (state.installed.contains(b)
+                            || (b.getState() != Bundle.STARTING && b.getState() != Bundle.ACTIVE
+                                    && getStartLevel().isBundlePersistentlyStarted(b))) {
+                        b.start();
+                    }
                 }
             }
         } catch (Exception e) {
-            // uninstall everything
-            if (cleanIfFailure) {
-                for (Bundle b : state.bundles) {
-                    b.uninstall();
+            // cleanup on error
+            if (!options.contains(Option.NoCleanIfFailure)) {
+                // Uninstall everything
+                for (Bundle b : state.installed) {
+                    try {
+                        b.uninstall();
+                    } catch (Exception e2) {
+                        // Ignore
+                    }
+                }
+            } else {
+                // Force start of bundles so that they are flagged as persistently started
+                for (Bundle b : state.installed) {
+                    try {
+                        b.start();
+                    } catch (Exception e2) {
+                        // Ignore
+                    }
                 }
             }
             // rethrow exception
@@ -236,6 +294,7 @@
     }
 
     protected static class InstallationState {
+        final Set<Bundle> installed = new HashSet<Bundle>();
         final Set<Bundle> bundles = new HashSet<Bundle>();
         final Map<Feature, Set<Long>> features = new HashMap<Feature, Set<Long>>();
     }
@@ -262,18 +321,91 @@
         }
         Set<Long> bundles = new HashSet<Long>();
         for (String bundleLocation : feature.getBundles()) {
-            try {
-                Bundle b = installBundleIfNeeded(bundleLocation);
-                state.bundles.add(b);
-                bundles.add(b.getBundleId());
-            } catch (BundleAlreadyInstalledException e) {
-                bundles.add(e.getBundle().getBundleId());
-            }
+            Bundle b = installBundleIfNeeded(state, bundleLocation);
+            bundles.add(b.getBundleId());
         }
         state.features.put(feature, bundles);
     }
 
-    protected Bundle installBundleIfNeeded(String bundleLocation) throws IOException, BundleException, BundleAlreadyInstalledException {
+    protected Set<Bundle> findBundlesToRefresh(InstallationState state) {
+        // First pass: include all bundles contained in these features
+        Set<Bundle> bundles = new HashSet<Bundle>(state.bundles);
+        bundles.removeAll(state.installed);
+        if (bundles.isEmpty()) {
+            return bundles;
+        }
+        // Second pass: for each bundle, check if there is any unresolved optional package that could be resolved
+        Map<Bundle, List<HeaderParser.PathElement>> imports = new HashMap<Bundle, List<HeaderParser.PathElement>>();
+        for (Iterator<Bundle> it = bundles.iterator(); it.hasNext();) {
+            Bundle b = it.next();
+            String importsStr = (String) b.getHeaders().get(Constants.IMPORT_PACKAGE);
+            if (importsStr == null) {
+                it.remove();
+            } else {
+                List<HeaderParser.PathElement> importsList = HeaderParser.parseHeader(importsStr);
+                for (Iterator<HeaderParser.PathElement> itp = importsList.iterator(); itp.hasNext();) {
+                    HeaderParser.PathElement p = itp.next();
+                    String resolution = p.getDirective(Constants.RESOLUTION_DIRECTIVE);
+                    if (!Constants.RESOLUTION_OPTIONAL.equals(resolution)) {
+                        itp.remove();
+                    }
+                }
+                if (importsList.isEmpty()) {
+                    it.remove();
+                } else {
+                    imports.put(b, importsList);
+                }
+            }
+        }
+        if (bundles.isEmpty()) {
+            return bundles;
+        }
+        // Third pass: compute a list of packages that are exported by our bundles and see if
+        //             some exported packages can be wired to the optional imports
+        List<HeaderParser.PathElement> exports = new ArrayList<HeaderParser.PathElement>();
+        for (Bundle b : state.installed) {
+            String exportsStr = (String) b.getHeaders().get(Constants.EXPORT_PACKAGE);
+            if (exportsStr != null) {
+                List<HeaderParser.PathElement> exportsList = HeaderParser.parseHeader(exportsStr);
+                exports.addAll(exportsList);
+            }
+        }
+        for (Iterator<Bundle> it = bundles.iterator(); it.hasNext();) {
+            Bundle b = it.next();
+            List<HeaderParser.PathElement> importsList = imports.get(b);
+            for (Iterator<HeaderParser.PathElement> itpi = importsList.iterator(); itpi.hasNext();) {
+                HeaderParser.PathElement pi = itpi.next();
+                boolean matching = false;
+                for (HeaderParser.PathElement pe : exports) {
+                    if (pi.getName().equals(pe.getName())) {
+                        String evStr = pe.getAttribute(Constants.VERSION_ATTRIBUTE);
+                        String ivStr = pi.getAttribute(Constants.VERSION_ATTRIBUTE);
+                        Version exported = evStr != null ? Version.parseVersion(evStr) : Version.emptyVersion;
+                        VersionRange imported = ivStr != null ? VersionRange.parse(ivStr) : VersionRange.infiniteRange;
+                        if (imported.isInRange(exported)) {
+                            matching = true;
+                            break;
+                        }
+                    }
+                }
+                if (!matching) {
+                    itpi.remove();
+                }
+            }
+            if (importsList.isEmpty()) {
+                it.remove();
+            } else {
+                LOGGER.debug("Refeshing bundle {} ({}) to solve the following optional imports", b.getSymbolicName(), b.getBundleId());
+                for (HeaderParser.PathElement p : importsList) {
+                    LOGGER.debug("    {}", p);
+                }
+
+            }
+        }
+        return bundles;
+    }
+
+    protected Bundle installBundleIfNeeded(InstallationState state, String bundleLocation) throws IOException, BundleException {
         LOGGER.debug("Checking " + bundleLocation);
         InputStream is;
         try {
@@ -295,7 +427,8 @@
                     Version bv = vStr == null ? Version.emptyVersion : Version.parseVersion(vStr);
                     if (v.equals(bv)) {
                         LOGGER.debug("  found installed bundle: " + b);
-                        throw new BundleAlreadyInstalledException(b);
+                        state.bundles.add(b);
+                        return b;
                     }
                 }
             }
@@ -306,24 +439,15 @@
                 is = new BufferedInputStream(new URL(bundleLocation).openStream());
             }
             LOGGER.debug("Installing bundle " + bundleLocation);
-            return getBundleContext().installBundle(bundleLocation, is);
+            Bundle b = getBundleContext().installBundle(bundleLocation, is);
+            state.bundles.add(b);
+            state.installed.add(b);
+            return b;
         } finally {
             is.close();
         }
     }
 
-    protected static class BundleAlreadyInstalledException extends Exception {
-        private final Bundle bundle;
-
-        public BundleAlreadyInstalledException(Bundle bundle) {
-            this.bundle = bundle;
-        }
-
-        public Bundle getBundle() {
-            return bundle;
-        }
-    }
-
     public void uninstallFeature(String name) throws Exception {
         List<String> versions = new ArrayList<String>();
         for (Feature f : installed.keySet()) {

Added: felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/HeaderParser.java
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/HeaderParser.java?rev=824529&view=auto
==============================================================================
--- felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/HeaderParser.java (added)
+++ felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/HeaderParser.java Mon Oct 12 22:55:29 2009
@@ -0,0 +1,204 @@
+/**
+ * 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;
+
+/**
+ * Utility class to parse a standard OSGi header with paths.
+ *
+ * @author <a href="mailto:dev@geronimo.apache.org">Apache Geronimo Project</a>
+ * @version $Rev: 786132 $, $Date: 2009-06-18 17:47:58 +0200 (Thu, 18 Jun 2009) $
+ */
+public final class HeaderParser  {
+
+    // Private constructor for static final class
+    private HeaderParser() {
+    }
+
+    /**
+     * Parse a given OSGi header into a list of paths
+     *
+     * @param header the OSGi header to parse
+     * @return the list of paths extracted from this header
+     */
+    public static List<PathElement> parseHeader(String header) {
+        List<PathElement> elements = new ArrayList<PathElement>();
+        if (header == null || header.trim().length() == 0) {
+            return elements;
+        }
+        String[] clauses = parseDelimitedString(header, ",");
+        for (String clause : clauses) {
+            String[] tokens = clause.split(";");
+            if (tokens.length < 1) {
+                throw new IllegalArgumentException("Invalid header clause: " + clause);
+            }
+            PathElement elem = new PathElement(tokens[0].trim());
+            elements.add(elem);
+            for (int i = 1; i < tokens.length; i++) {
+                int pos = tokens[i].indexOf('=');
+                if (pos != -1) {
+                    if (pos > 0 && tokens[i].charAt(pos - 1) == ':') {
+                        String name = tokens[i].substring(0, pos - 1).trim();
+                        String value = tokens[i].substring(pos + 1).trim();
+                        if (value.startsWith("\"") && value.endsWith("\"")) {
+                            value = value.substring(1, value.length() - 1);
+                        }
+                        elem.addDirective(name, value);
+                    } else {
+                        String name = tokens[i].substring(0, pos).trim();
+                        String value = tokens[i].substring(pos + 1).trim();
+                        if (value.startsWith("\"") && value.endsWith("\"")) {
+                            value = value.substring(1, value.length() - 1);
+                        }
+                        elem.addAttribute(name, value);
+                    }
+                } else {
+                    elem = new PathElement(tokens[i].trim());
+                    elements.add(elem);
+                }
+            }
+        }
+        return elements;
+    }
+
+    /**
+     * Parses delimited string and returns an array containing the tokens. This
+     * parser obeys quotes, so the delimiter character will be ignored if it is
+     * inside of a quote. This method assumes that the quote character is not
+     * included in the set of delimiter characters.
+     * @param value the delimited string to parse.
+     * @param delim the characters delimiting the tokens.
+     * @return an array of string tokens or null if there were no tokens.
+    **/
+    public static String[] parseDelimitedString(String value, String delim)
+    {
+        if (value == null)
+        {
+           value = "";
+        }
+
+        List list = new ArrayList();
+
+        int CHAR = 1;
+        int DELIMITER = 2;
+        int STARTQUOTE = 4;
+        int ENDQUOTE = 8;
+
+        StringBuffer sb = new StringBuffer();
+
+        int expecting = (CHAR | DELIMITER | STARTQUOTE);
+
+        for (int i = 0; i < value.length(); i++)
+        {
+            char c = value.charAt(i);
+
+            boolean isDelimiter = (delim.indexOf(c) >= 0);
+            boolean isQuote = (c == '"');
+
+            if (isDelimiter && ((expecting & DELIMITER) > 0))
+            {
+                list.add(sb.toString().trim());
+                sb.delete(0, sb.length());
+                expecting = (CHAR | DELIMITER | STARTQUOTE);
+            }
+            else if (isQuote && ((expecting & STARTQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = CHAR | ENDQUOTE;
+            }
+            else if (isQuote && ((expecting & ENDQUOTE) > 0))
+            {
+                sb.append(c);
+                expecting = (CHAR | STARTQUOTE | DELIMITER);
+            }
+            else if ((expecting & CHAR) > 0)
+            {
+                sb.append(c);
+            }
+            else
+            {
+                throw new IllegalArgumentException("Invalid delimited string: " + value);
+            }
+        }
+
+        if (sb.length() > 0)
+        {
+            list.add(sb.toString().trim());
+        }
+
+        return (String[]) list.toArray(new String[list.size()]);
+    }
+
+    public static class PathElement {
+
+        private String path;
+        private Map<String, String> attributes;
+        private Map<String, String> directives;
+
+        public PathElement(String path) {
+            this.path = path;
+            this.attributes = new HashMap<String, String>();
+            this.directives = new HashMap<String, String>();
+        }
+
+        public String getName() {
+            return this.path;
+        }
+
+        public Map<String, String> getAttributes() {
+            return attributes;
+        }
+
+        public String getAttribute(String name) {
+            return attributes.get(name);
+        }
+
+        public void addAttribute(String name, String value) {
+            attributes.put(name, value);
+        }
+
+        public Map<String, String> getDirectives() {
+            return directives;
+        }
+
+        public String getDirective(String name) {
+            return directives.get(name);
+        }
+
+        public void addDirective(String name, String value) {
+            directives.put(name, value);
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder(this.path);
+            for (Map.Entry<String,String> directive : this.directives.entrySet()) {
+                sb.append(";").append(directive.getKey()).append(":=").append(directive.getValue());
+            }
+            for (Map.Entry<String,String> attribute : this.attributes.entrySet()) {
+                sb.append(";").append(attribute.getKey()).append("=").append(attribute.getValue());
+            }
+            return sb.toString(); 
+        }
+
+    }
+}

Added: felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/VersionRange.java
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/VersionRange.java?rev=824529&view=auto
==============================================================================
--- felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/VersionRange.java (added)
+++ felix/trunk/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/VersionRange.java Mon Oct 12 22:55:29 2009
@@ -0,0 +1,158 @@
+/*
+ * 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 org.osgi.framework.Version;
+
+public class VersionRange
+{
+    private final Version m_low;
+    private final boolean m_isLowInclusive;
+    private final Version m_high;
+    private final boolean m_isHighInclusive;
+    public static final VersionRange infiniteRange = new VersionRange(Version.emptyVersion, true, null, true);
+
+    public VersionRange(
+        Version low, boolean isLowInclusive,
+        Version high, boolean isHighInclusive)
+    {
+        m_low = low;
+        m_isLowInclusive = isLowInclusive;
+        m_high = high;
+        m_isHighInclusive = isHighInclusive;
+    }
+
+    public Version getLow()
+    {
+        return m_low;
+    }
+
+    public boolean isLowInclusive()
+    {
+        return m_isLowInclusive;
+    }
+
+    public Version getHigh()
+    {
+        return m_high;
+    }
+
+    public boolean isHighInclusive()
+    {
+        return m_isHighInclusive;
+    }
+
+    public boolean isInRange(Version version)
+    {
+        // We might not have an upper end to the range.
+        if (m_high == null)
+        {
+            return (version.compareTo(m_low) >= 0);
+        }
+        else if (isLowInclusive() && isHighInclusive())
+        {
+            return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) <= 0);
+        }
+        else if (isHighInclusive())
+        {
+            return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) <= 0);
+        }
+        else if (isLowInclusive())
+        {
+            return (version.compareTo(m_low) >= 0) && (version.compareTo(m_high) < 0);
+        }
+        return (version.compareTo(m_low) > 0) && (version.compareTo(m_high) < 0);
+    }
+
+    public static VersionRange parse(String range)
+    {
+        // Check if the version is an interval.
+        if (range.indexOf(',') >= 0)
+        {
+            String s = range.substring(1, range.length() - 1);
+            String vlo = s.substring(0, s.indexOf(',')).trim();
+            String vhi = s.substring(s.indexOf(',') + 1, s.length()).trim();
+            return new VersionRange (
+                new Version(vlo), (range.charAt(0) == '['),
+                new Version(vhi), (range.charAt(range.length() - 1) == ']'));
+        }
+        else
+        {
+            return new VersionRange(new Version(range), true, null, false);
+        }
+    }
+
+    public boolean equals(Object obj)
+    {
+        if (obj == null)
+        {
+            return false;
+        }
+        if (getClass() != obj.getClass())
+        {
+            return false;
+        }
+        final VersionRange other = (VersionRange) obj;
+        if (m_low != other.m_low && (m_low == null || !m_low.equals(other.m_low)))
+        {
+            return false;
+        }
+        if (m_isLowInclusive != other.m_isLowInclusive)
+        {
+            return false;
+        }
+        if (m_high != other.m_high && (m_high == null || !m_high.equals(other.m_high)))
+        {
+            return false;
+        }
+        if (m_isHighInclusive != other.m_isHighInclusive)
+        {
+            return false;
+        }
+        return true;
+    }
+
+    public int hashCode()
+    {
+        int hash = 5;
+        hash = 97 * hash + (m_low != null ? m_low.hashCode() : 0);
+        hash = 97 * hash + (m_isLowInclusive ? 1 : 0);
+        hash = 97 * hash + (m_high != null ? m_high.hashCode() : 0);
+        hash = 97 * hash + (m_isHighInclusive ? 1 : 0);
+        return hash;
+    }
+
+    public String toString()
+    {
+        if (m_high != null)
+        {
+            StringBuffer sb = new StringBuffer();
+            sb.append(m_isLowInclusive ? '[' : '(');
+            sb.append(m_low.toString());
+            sb.append(',');
+            sb.append(m_high.toString());
+            sb.append(m_isHighInclusive ? ']' : ')');
+            return sb.toString();
+        }
+        else
+        {
+            return m_low.toString();
+        }
+    }
+}
\ No newline at end of file

Modified: felix/trunk/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml?rev=824529&r1=824528&r2=824529&view=diff
==============================================================================
--- felix/trunk/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml (original)
+++ felix/trunk/karaf/features/core/src/main/resources/OSGI-INF/blueprint/gshell-features.xml Mon Oct 12 22:55:29 2009
@@ -36,6 +36,7 @@
         <property name="configAdmin" ref="configAdmin" />
         <property name="packageAdmin" ref="packageAdmin" />
         <property name="preferences" ref="preferences" />
+        <property name="startLevel" ref="startLevel" />
         <property name="bundleContext" ref="blueprintBundleContext" />
     </bean>
 
@@ -51,6 +52,8 @@
 
     <reference id="packageAdmin" interface="org.osgi.service.packageadmin.PackageAdmin" />
 
+    <reference id="startLevel" interface="org.osgi.service.startlevel.StartLevel" />
+    
     <service ref="featuresService" interface="org.apache.felix.karaf.features.FeaturesService" />
 
 </blueprint>

Modified: felix/trunk/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java?rev=824529&r1=824528&r2=824529&view=diff
==============================================================================
--- felix/trunk/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java (original)
+++ felix/trunk/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java Mon Oct 12 22:55:29 2009
@@ -21,6 +21,7 @@
 import java.io.InputStream;
 import java.io.PrintWriter;
 import java.net.URI;
+import java.util.EnumSet;
 import java.util.Hashtable;
 
 import junit.framework.TestCase;
@@ -121,7 +122,7 @@
 
         replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
 
-        svc.installFeature("f1");
+        svc.installFeature("f1", FeatureImpl.DEFAULT_VERSION, EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));
         
         Feature[] installed = svc.listInstalledFeatures();
         assertEquals(1, installed.length);
@@ -256,8 +257,8 @@
             // ok
         }
 
-        svc.installFeature("f1", "0.1");
-        svc.installFeature("f1", "0.2");
+        svc.installFeature("f1", "0.1", EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));
+        svc.installFeature("f1", "0.2", EnumSet.of(FeaturesService.Option.NoAutoRefreshBundles));
 
         try {
             svc.uninstallFeature("f1");
@@ -397,6 +398,7 @@
         expect(installedBundle.getBundleId()).andReturn(12345L);
         expect(bundleContext.getBundle(12345L)).andReturn(installedBundle);
         expect(installedBundle.getHeaders()).andReturn(new Hashtable());
+
         installedBundle.start();
         
         expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
@@ -466,6 +468,8 @@
         prefs.putBoolean("bootFeaturesInstalled", false);
         prefs.flush();
 
+        expect(installedBundle.getHeaders()).andReturn(new Hashtable()).anyTimes();
+
         // uninstallAllFeatures 
         
         expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);