You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gg...@apache.org on 2017/12/02 20:26:58 UTC

[karaf] 05/11: [KARAF-5376] Polish LocationPattern and FeaturePattern helpers for blacklist/override matching

This is an automated email from the ASF dual-hosted git repository.

ggrzybek pushed a commit to branch KARAF-5376-overrides_v2
in repository https://gitbox.apache.org/repos/asf/karaf.git

commit 37e211b070da61762f209ff5ff2fc5115c4c5146
Author: Grzegorz Grzybek <gr...@gmail.com>
AuthorDate: Wed Nov 15 15:27:34 2017 +0100

    [KARAF-5376] Polish LocationPattern and FeaturePattern helpers for blacklist/override matching
---
 .../org/apache/karaf/features/FeaturePattern.java  | 108 +++++++++++++
 .../{internal/service => }/LocationPattern.java    |  23 +--
 .../model/processing/BundleReplacements.java       |   4 +-
 .../model/processing/FeaturesProcessing.java       |   5 +-
 .../karaf/features/internal/service/Blacklist.java | 176 ++++++++-------------
 .../internal/service/FeaturesProcessorImpl.java    |  17 +-
 .../features/karaf-features-processing-1.0.0.xsd   |   2 +-
 .../internal/service/FeaturePatternTest.java       |  73 +++++++++
 .../internal/service/LocationPatternTest.java      |   1 +
 9 files changed, 267 insertions(+), 142 deletions(-)

diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
new file mode 100644
index 0000000..06db75a
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturePattern.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.karaf.features;
+
+import java.util.regex.Pattern;
+
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.version.VersionCleaner;
+import org.apache.felix.utils.version.VersionRange;
+import org.osgi.framework.Version;
+
+/**
+ * <p>Helper class to compare feature identifiers that may use globs and version ranges.</p>
+ *
+ * <p>Following feature identifiers are supported:<ul>
+ *     <li>name (simple name)</li>
+ *     <li>name/version (Karaf feature ID syntax)</li>
+ *     <li>name/version-range (Karaf feature ID syntax using version-range)</li>
+ *     <li>name;range=version (OSGi manifest header with <code>range</code> <em>attribute</em>)</li>
+ *     <li>name;range=version-range (OSGi manifest header with <code>range</code> <em>attribute</em>)</li>
+ * </ul></p>
+ */
+public class FeaturePattern {
+
+    public static final String RANGE = "range";
+
+    private String originalId;
+    private Pattern namePattern;
+    private String versionString;
+    private Version version;
+    private VersionRange versionRange;
+
+    public FeaturePattern(String featureId) throws IllegalArgumentException {
+        if (featureId == null) {
+            throw new IllegalArgumentException("Feature ID to match should not be null");
+        }
+        originalId = featureId;
+        String name = originalId;
+        if (name.indexOf("/") > 0) {
+            name = originalId.substring(0, originalId.indexOf("/"));
+            versionString = originalId.substring(originalId.indexOf("/") + 1);
+        } else if (name.contains(";")) {
+            Clause[] c = org.apache.felix.utils.manifest.Parser.parseClauses(new String[] { originalId });
+            name = c[0].getName();
+            versionString = c[0].getAttribute(RANGE);
+        }
+        namePattern = LocationPattern.toRegExp(name);
+
+        if (versionString != null && versionString.length() >= 1) {
+            try {
+                char first = versionString.charAt(0);
+                if (first == '[' || first == '(') {
+                    // range
+                    versionRange = new VersionRange(versionString, true, false);
+                } else {
+                    version = new Version(VersionCleaner.clean(versionString));
+                }
+            } catch (IllegalArgumentException e) {
+                throw new IllegalArgumentException("Can't parse version \"" + versionString + "\" as OSGi version object.", e);
+            }
+        } else {
+            versionRange = new VersionRange(Version.emptyVersion);
+        }
+    }
+
+    /**
+     * Returns <code>if this feature pattern</code> matches given feature/version
+     * @param featureName
+     * @param featureVersion
+     * @return
+     */
+    public boolean matches(String featureName, String featureVersion) {
+        if (featureName == null) {
+            return false;
+        }
+        boolean match = namePattern.matcher(featureName).matches();
+        if (!match) {
+            return false;
+        }
+        if (featureVersion == null) {
+            featureVersion = "0";
+        }
+        Version otherVersion = new Version(VersionCleaner.clean(featureVersion));
+        if (versionRange != null) {
+            match = versionRange.contains(otherVersion);
+        } else if (version != null) {
+            match = version.equals(otherVersion);
+        }
+        return match;
+    }
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/LocationPattern.java b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
similarity index 94%
rename from features/core/src/main/java/org/apache/karaf/features/internal/service/LocationPattern.java
rename to features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
index 7e55b9b..20390ca 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/LocationPattern.java
+++ b/features/core/src/main/java/org/apache/karaf/features/LocationPattern.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.karaf.features.internal.service;
+package org.apache.karaf.features;
 
 import java.net.MalformedURLException;
 import java.util.regex.Matcher;
@@ -30,12 +30,15 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * <p>Helper class to compare Maven URIs that may use globs and version ranges.</p>
+ * <p>Helper class to compare Maven URIs (and falling back to other URIs) that may use globs and version ranges.</p>
+ *
  * <p>Each Maven URI may contain these components: groupId, artifactId, optional version, optional type and optional
- * classifier. Concrete URIs do not use globs and use precise versions (we not consider <code>LATEST</code>
- * and <code>RELEASE</code> here).</p>
+ * classifier. Concrete URIs do not use globs and use precise versions (we do not consider <code>LATEST</code>
+ * and <code>RELEASE</code> Maven versions here).</p>
+ *
  * <p>When comparing two Maven URIs, we split them to components and may use RegExps and
  * {@link org.apache.felix.utils.version.VersionRange}s</p>
+ *
  * <p>When pattern URI doesn't use <code>mvn:</code> scheme, plain {@link String#equals(Object)} is used or
  * {@link Matcher#matches()} when pattern uses <code>*</code> glob.</p>
  */
@@ -110,7 +113,7 @@ public class LocationPattern {
      * @param value
      * @return
      */
-    private Pattern toRegExp(String value) {
+    static Pattern toRegExp(String value) {
         // TODO: escape all RegExp special chars that are valid path characters, only convert '*' into '.*'
         return Pattern.compile(value
                 .replaceAll("\\.", "\\\\\\.")
@@ -133,10 +136,6 @@ public class LocationPattern {
             // this pattern is not mvn:
             return originalPattern.matcher(otherUri).matches();
         }
-        if (!otherUri.startsWith("mvn:")) {
-            // other pattern is not mvn:
-            return originalUri.equals(otherUri);
-        }
 
         LocationPattern other;
         try {
@@ -145,6 +144,12 @@ public class LocationPattern {
             LOG.debug("Can't parse \"" + otherUri + "\" as Maven URI. Ignoring.");
             return false;
         }
+
+        if (other.originalPattern != null) {
+            // other pattern is not mvn:
+            return false;
+        }
+
         if (other.versionRange != null) {
             LOG.warn("Matched URI can't use version ranges: " + otherUri);
             return false;
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/BundleReplacements.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/BundleReplacements.java
index f51a8b9..ad309be 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/BundleReplacements.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/BundleReplacements.java
@@ -28,7 +28,7 @@ import javax.xml.bind.annotation.XmlEnumValue;
 import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
-import org.apache.karaf.features.internal.service.LocationPattern;
+import org.apache.karaf.features.LocationPattern;
 
 @XmlType(name = "bundleReplacements", propOrder = {
         "overrideBundles"
@@ -91,7 +91,7 @@ public class BundleReplacements {
         }
 
         /**
-         * Changes String for <code>originalUri</code> into {@link org.apache.karaf.features.internal.service.LocationPattern}
+         * Changes String for <code>originalUri</code> into {@link LocationPattern}
          */
         public void compile() throws MalformedURLException {
             originalUriPattern = new LocationPattern(originalUri);
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
index 142a16b..c2a0765 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
@@ -38,7 +38,7 @@ import org.apache.felix.utils.manifest.Parser;
 import org.apache.felix.utils.version.VersionCleaner;
 import org.apache.felix.utils.version.VersionRange;
 import org.apache.karaf.features.internal.service.Blacklist;
-import org.apache.karaf.features.internal.service.LocationPattern;
+import org.apache.karaf.features.LocationPattern;
 import org.osgi.framework.Version;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -90,6 +90,9 @@ public class FeaturesProcessing {
     private Blacklist blacklist;
 
     public FeaturesProcessing() {
+        overrideBundleDependency = new OverrideBundleDependency();
+        bundleReplacements = new BundleReplacements();
+        featureReplacements = new FeatureReplacements();
     }
 
     public List<String> getBlacklistedRepositories() {
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Blacklist.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Blacklist.java
index 8c6bfb9..046c4be 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/Blacklist.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Blacklist.java
@@ -25,33 +25,27 @@ import java.net.URL;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
+import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
-import java.util.regex.Pattern;
 
 import org.apache.felix.utils.manifest.Clause;
 import org.apache.felix.utils.manifest.Parser;
-import org.apache.felix.utils.version.VersionRange;
-import org.apache.felix.utils.version.VersionTable;
-import org.apache.karaf.features.internal.model.Bundle;
-import org.apache.karaf.features.internal.model.Conditional;
-import org.apache.karaf.features.internal.model.Feature;
+import org.apache.karaf.features.FeaturePattern;
+import org.apache.karaf.features.LocationPattern;
 import org.apache.karaf.features.internal.model.Features;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Helper class to deal with blacklisted features and bundles.
+ * Helper class to deal with blacklisted features and bundles. It doesn't process JAXB model at all - it only
+ * provides information about repository/feature/bundle being blacklisted.
  */
 public class Blacklist {
 
     public static Logger LOG = LoggerFactory.getLogger(Blacklist.class);
 
     public static final String BLACKLIST_URL = "url";
-    public static final String BLACKLIST_RANGE = "range";
     public static final String BLACKLIST_TYPE = "type"; // null -> "feature"
     public static final String TYPE_FEATURE = "feature";
     public static final String TYPE_BUNDLE = "bundle";
@@ -59,7 +53,10 @@ public class Blacklist {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Blacklist.class);
     private Clause[] clauses;
-    private Map<String, LocationPattern> bundleBlacklist = new LinkedHashMap<>();
+
+    private List<LocationPattern> repositoryBlacklist = new LinkedList<>();
+    private List<FeaturePattern> featureBlacklist = new LinkedList<>();
+    private List<LocationPattern> bundleBlacklist = new LinkedList<>();
 
     public Blacklist() {
         this(Collections.emptyList());
@@ -105,11 +102,33 @@ public class Blacklist {
                     type = TYPE_FEATURE;
                 }
             }
+            String location;
             switch (type) {
+                case TYPE_REPOSITORY:
+                    location = c.getName();
+                    if (c.getAttribute(BLACKLIST_URL) != null) {
+                        location = c.getAttribute(BLACKLIST_URL);
+                    }
+                    if (location == null) {
+                        // should not happen?
+                        LOG.warn("Repository blacklist URI is empty. Ignoring.");
+                    } else {
+                        try {
+                            repositoryBlacklist.add(new LocationPattern(location));
+                        } catch (MalformedURLException e) {
+                            LOG.warn("Problem parsing repository blacklist URI \"" + location + "\": " + e.getMessage() + ". Ignoring.");
+                        }
+                    }
+                    break;
                 case TYPE_FEATURE:
+                    try {
+                        featureBlacklist.add(new FeaturePattern(c.toString()));
+                    } catch (IllegalArgumentException e) {
+                        LOG.warn("Problem parsing blacklisted feature identifier \"" + c.toString() + "\": " + e.getMessage() + ". Ignoring.");
+                    }
                     break;
                 case TYPE_BUNDLE:
-                    String location = c.getName();
+                    location = c.getName();
                     if (c.getAttribute(BLACKLIST_URL) != null) {
                         location = c.getAttribute(BLACKLIST_URL);
                     }
@@ -118,74 +137,24 @@ public class Blacklist {
                         LOG.warn("Bundle blacklist URI is empty. Ignoring.");
                     } else {
                         try {
-                            bundleBlacklist.put(location, location.startsWith("mvn:") ? new LocationPattern(location) : null);
+                            bundleBlacklist.add(new LocationPattern(location));
                         } catch (MalformedURLException e) {
-                            LOG.warn("Problem parsing blacklist URI \"" + location + "\": " + e.getMessage() + ". Ignoring.");
+                            LOG.warn("Problem parsing bundle blacklist URI \"" + location + "\": " + e.getMessage() + ". Ignoring.");
                         }
                     }
                     break;
-                case TYPE_REPOSITORY:
             }
         }
     }
 
     /**
-     * TODO: set {@link Feature#setBlacklisted(boolean)} instead of removing from collection
-     * @param features
+     * Checks whether features XML repository URI is blacklisted.
+     * @param uri
+     * @return
      */
-    public void blacklist(Features features) {
-        features.getFeature().removeIf(this::blacklist);
-    }
-
-    public boolean blacklist(Feature feature) {
-        for (Clause clause : clauses) {
-            // Check feature name
-            if (clause.getName().equals(feature.getName())) {
-                // Check feature version
-                VersionRange range = VersionRange.ANY_VERSION;
-                String vr = clause.getAttribute(BLACKLIST_RANGE);
-                if (vr != null) {
-                    range = new VersionRange(vr, true);
-                }
-                if (range.contains(VersionTable.getVersion(feature.getVersion()))) {
-                    String type = clause.getAttribute(BLACKLIST_TYPE);
-                    if (type == null || TYPE_FEATURE.equals(type)) {
-                        return true;
-                    }
-                }
-            }
-            // Check bundles
-            blacklist(feature.getBundle());
-            // Check conditional bundles
-            for (Conditional cond : feature.getConditional()) {
-                blacklist(cond.getBundle());
-            }
-        }
-        return false;
-    }
-
-    private void blacklist(List<Bundle> bundles) {
-        for (Iterator<Bundle> iterator = bundles.iterator(); iterator.hasNext();) {
-            Bundle info = iterator.next();
-            for (Clause clause : clauses) {
-                String url = clause.getName();
-                if (clause.getAttribute(BLACKLIST_URL) != null) {
-                    url = clause.getAttribute(BLACKLIST_URL);
-                }
-                if (info.getLocation().equals(url)) {
-                    String type = clause.getAttribute(BLACKLIST_TYPE);
-                    if (type == null || TYPE_BUNDLE.equals(type)) {
-                        iterator.remove();
-                        break;
-                    }
-                }
-            }
-        }
-    }
-
-    public boolean isBundleBlacklisted(String uri) {
-        for (Map.Entry<String, LocationPattern> clause : bundleBlacklist.entrySet()) {
-            if (mavenMatches(clause.getKey(), clause.getValue(), uri)) {
+    public boolean isRepositoryBlacklisted(String uri) {
+        for (LocationPattern pattern : repositoryBlacklist) {
+            if (pattern.matches(uri)) {
                 return true;
             }
         }
@@ -193,56 +162,30 @@ public class Blacklist {
     }
 
     /**
-     * Checks whether given <code>uri</code> matches Maven artifact pattern (group, artifact, optional type/classifier, version
-     * range, globs).
-     * @param blacklistedUri
-     * @param compiledUri
-     * @param uri
+     * Checks whether the feature is blacklisted according to configured rules by name
+     * (possibly with wildcards) and optional version (possibly specified as version range)
+     * @param name
+     * @param version
      * @return
      */
-    private boolean mavenMatches(String blacklistedUri, LocationPattern compiledUri, String uri) {
-        if (compiledUri == null) {
-            // non maven URI - we can't be smart
-            return blacklistedUri.equals(uri);
-        } else {
-            return compiledUri.matches(uri);
-        }
-    }
-
     public boolean isFeatureBlacklisted(String name, String version) {
-        for (Clause clause : clauses) {
-            String type = clause.getAttribute(BLACKLIST_TYPE);
-            if (type != null && !TYPE_FEATURE.equals(type)) {
-                continue;
-            }
-            if (Pattern.matches(clause.getName().replaceAll("\\*", ".*"), name)) {
-                // Check feature version
-                VersionRange range = VersionRange.ANY_VERSION;
-                String vr = clause.getAttribute(BLACKLIST_RANGE);
-                if (vr != null) {
-                    range = new VersionRange(vr, true);
-                }
-                if (range.contains(VersionTable.getVersion(version))) {
-                    if (type == null || TYPE_FEATURE.equals(type)) {
-                        return true;
-                    }
-                }
+        for (FeaturePattern pattern : featureBlacklist) {
+            if (pattern.matches(name, version)) {
+                return true;
             }
         }
         return false;
     }
 
-    public boolean isRepositoryBlacklisted(String uri) {
-        for (Clause clause : clauses) {
-            String url = clause.getName();
-            if (clause.getAttribute(BLACKLIST_URL) != null) {
-                url = clause.getAttribute(BLACKLIST_URL);
-            }
-            if (uri.equals(url)) {
-                String type = clause.getAttribute(BLACKLIST_TYPE);
-                if (type == null || TYPE_REPOSITORY.equals(type)) {
-                    return true;
-                }
+    /**
+     * Checks whether the bundle URI is blacklisted according to configured rules
+     * @param uri
+     * @return
+     */
+    public boolean isBundleBlacklisted(String uri) {
+        for (LocationPattern pattern : bundleBlacklist) {
+            if (pattern.matches(uri)) {
+                return true;
             }
         }
         return false;
@@ -262,7 +205,9 @@ public class Blacklist {
             System.arraycopy(others.clauses, ours.length, this.clauses, 0, others.clauses.length);
         }
         if (others != null) {
-            this.bundleBlacklist.putAll(others.bundleBlacklist);
+            this.repositoryBlacklist.addAll(others.repositoryBlacklist);
+            this.featureBlacklist.addAll(others.featureBlacklist);
+            this.bundleBlacklist.addAll(others.bundleBlacklist);
         }
     }
 
@@ -270,4 +215,7 @@ public class Blacklist {
         return clauses;
     }
 
+    public void blacklist(Features featuresModel) {
+    }
+
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
index 03dadea..c14c559 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
@@ -29,15 +29,14 @@ import javax.xml.bind.JAXBException;
 import javax.xml.bind.Unmarshaller;
 
 import org.apache.karaf.features.BundleInfo;
+import org.apache.karaf.features.LocationPattern;
 import org.apache.karaf.features.internal.model.Bundle;
 import org.apache.karaf.features.internal.model.Conditional;
 import org.apache.karaf.features.internal.model.Feature;
 import org.apache.karaf.features.internal.model.Features;
 import org.apache.karaf.features.internal.model.processing.BundleReplacements;
-import org.apache.karaf.features.internal.model.processing.FeatureReplacements;
 import org.apache.karaf.features.internal.model.processing.FeaturesProcessing;
 import org.apache.karaf.features.internal.model.processing.ObjectFactory;
-import org.apache.karaf.features.internal.model.processing.OverrideBundleDependency;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -55,7 +54,7 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
     public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessorImpl.class);
     private static final JAXBContext FEATURES_PROCESSING_CONTEXT;
 
-    private FeaturesProcessing processing;
+    private FeaturesProcessing processing = new FeaturesProcessing();
 
     static {
         try {
@@ -96,18 +95,6 @@ public class FeaturesProcessorImpl implements FeaturesProcessor {
             }
         }
 
-        if (processing == null) {
-            processing = new FeaturesProcessing();
-        }
-        if (processing.getBundleReplacements() == null) {
-            processing.setBundleReplacements(new BundleReplacements());
-        }
-        if (processing.getFeatureReplacements() == null) {
-            processing.setFeatureReplacements(new FeatureReplacements());
-        }
-        if (processing.getOverrideBundleDependency() == null) {
-            processing.setOverrideBundleDependency(new OverrideBundleDependency());
-        }
         processing.postUnmarshall(blacklist, overrides);
     }
 
diff --git a/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd b/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
index d0e5d48..b82bf08 100644
--- a/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
+++ b/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
@@ -83,7 +83,7 @@ features, "range" manifest header attribute should be specified in "version" XML
             <xs:documentation><![CDATA[Blacklisted feature name may use '*' character as glob. "version" attribute
 MAY specify a version (or range) of features to blacklist, e.g.,:
  * version="[1,2)" - feature with versions 1, 1.1, 1.4.3, 1.9.99, ... will be blacklisted
- * version="[2,*)" - features with all versions above 2.0.0 will be blacklisted
+ * version="[2,*)" - features with all versions above and including 2.0.0 will be blacklisted
  * version="3.0" - feature with version 3.0 only will be blacklisted
 ]]></xs:documentation>
         </xs:annotation>
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturePatternTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturePatternTest.java
new file mode 100644
index 0000000..c462b37
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturePatternTest.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.karaf.features.internal.service;
+
+import org.apache.karaf.features.FeaturePattern;
+import org.junit.Test;
+import org.osgi.framework.Version;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class FeaturePatternTest {
+
+    @Test
+    public void matchingFeatureIds() {
+        assertTrue(new FeaturePattern("spring").matches("spring", null));
+        assertTrue(new FeaturePattern("spring").matches("spring", "0.0.0"));
+        assertTrue(new FeaturePattern("spring").matches("spring", "1.0.0"));
+        assertFalse(new FeaturePattern("spring").matches("springish", "1.0.0"));
+        assertFalse(new FeaturePattern("commons/1").matches("commons", "0.0.0"));
+        assertFalse(new FeaturePattern("commons/1").matches("commons", null));
+        assertTrue(new FeaturePattern("commons/1").matches("commons", "1"));
+        assertFalse(new FeaturePattern("commons/1").matches("commons", "1.0.0.1"));
+        assertFalse(new FeaturePattern("space/[3,4]").matches("space", "1"));
+        assertTrue(new FeaturePattern("space/[3,4]").matches("space", "3"));
+        assertTrue(new FeaturePattern("space/[3,4]").matches("space", "3.1"));
+        assertFalse(new FeaturePattern("space/[3,4]").matches("x-space", "3.1"));
+        assertTrue(new FeaturePattern("space/[3,4]").matches("space", "4.0.0"));
+        assertFalse(new FeaturePattern("space/[3,4]").matches("space", "4.0.0.0")); // last ".0" is qualifier
+        assertFalse(new FeaturePattern("space/[3,4]").matches("space", "4.0.1"));
+        assertTrue(new FeaturePattern("special;range=1").matches("special", "1"));
+        assertTrue(new FeaturePattern("special;range=1").matches("special", "1.0"));
+        assertTrue(new FeaturePattern("special;range=1").matches("special", "1.0.0"));
+        assertFalse(new FeaturePattern("special;range=1").matches("special2", "1.0.0"));
+        assertFalse(new FeaturePattern("special;range=1").matches("special", "1.0.1"));
+        assertTrue(new FeaturePattern("universal;range=[3,4)").matches("universal", "3"));
+        assertTrue(new FeaturePattern("universal;range=[3,4)").matches("universal", "3.0"));
+        assertTrue(new FeaturePattern("universal;range=[3,4)").matches("universal", "3.0.0"));
+        assertTrue(new FeaturePattern("universal;range=[3,4)").matches("universal", "3.4.2"));
+        assertTrue(new FeaturePattern("universal;range=[3,4)").matches("universal", "3.9.9.GA"));
+        assertFalse(new FeaturePattern("universal;range=[3,4)").matches("universalis", "3.9.9.GA"));
+        assertFalse(new FeaturePattern("universal;range=[3,4)").matches("universal", "4.0.0"));
+        assertTrue(new FeaturePattern("a*").matches("alphabet", null));
+        assertTrue(new FeaturePattern("a*").matches("alphabet", "0"));
+        assertTrue(new FeaturePattern("a*").matches("alphabet", "999"));
+        assertFalse(new FeaturePattern("a*").matches("_alphabet", "999"));
+        assertTrue(new FeaturePattern("*b/[3,4)").matches("b", "3.5"));
+        assertTrue(new FeaturePattern("*b/[3,4)").matches("cb", "3.5"));
+        assertFalse(new FeaturePattern("*b/[3,4)").matches("bc", "3.5"));
+        assertFalse(new FeaturePattern("*b/[3,4)").matches("cb", "4.0.0"));
+        assertFalse(new FeaturePattern("*b/[3,4)").matches("cb", null));
+        assertFalse(new FeaturePattern("*b/[3,4)").matches("cb", "0"));
+        assertFalse(new FeaturePattern("*b/[3,4)").matches("cb", org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION));
+        assertFalse(new FeaturePattern("*b/[3,4)").matches("cb", Version.emptyVersion.toString()));
+    }
+
+}
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/LocationPatternTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/LocationPatternTest.java
index c0a7b31..3c0a808 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/LocationPatternTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/LocationPatternTest.java
@@ -20,6 +20,7 @@ package org.apache.karaf.features.internal.service;
 
 import java.net.MalformedURLException;
 
+import org.apache.karaf.features.LocationPattern;
 import org.junit.Test;
 
 import static org.junit.Assert.assertFalse;

-- 
To stop receiving notification emails like this one, please contact
"commits@karaf.apache.org" <co...@karaf.apache.org>.