karaf-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ggrzy...@apache.org
Subject [karaf] 01/07: [KARAF-5376] Moving overrides and blacklist mechanisms to "features processor" service
Date Wed, 15 Nov 2017 15:29:26 GMT
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 46620727ce5b7daa1da3dd193e3fd7cf7a851ef9
Author: Grzegorz Grzybek <gr.grzybek@gmail.com>
AuthorDate: Mon Nov 6 10:26:26 2017 +0100

    [KARAF-5376] Moving overrides and blacklist mechanisms to "features processor" service
---
 features/core/pom.xml                              |   2 +-
 .../org/apache/karaf/features/Blacklisting.java    |  34 +++
 .../java/org/apache/karaf/features/BundleInfo.java |   6 +-
 .../java/org/apache/karaf/features/Feature.java    |   2 +-
 .../java/org/apache/karaf/features/Repository.java |  43 ++-
 .../karaf/features/internal/model/Bundle.java      |  36 ++-
 .../karaf/features/internal/model/Feature.java     |  12 +
 .../model/processing/BundleReplacements.java       | 101 +++++++
 .../model/processing/FeatureReplacements.java      |  81 ++++++
 .../model/processing/FeaturesProcessing.java       | 296 +++++++++++++++++++++
 .../internal/model/processing/ObjectFactory.java   |  30 +++
 .../model/processing/OverrideBundleDependency.java | 111 ++++++++
 .../internal/model/processing/package-info.java    |  36 +++
 .../karaf/features/internal/osgi/Activator.java    |   9 +-
 .../karaf/features/internal/region/Subsystem.java  |  48 +++-
 .../karaf/features/internal/service/Blacklist.java | 128 ++++++++-
 .../karaf/features/internal/service/Deployer.java  |  17 ++
 .../internal/service/FeaturesProcessor.java        |  44 +++
 .../internal/service/FeaturesProcessorImpl.java    | 197 ++++++++++++++
 .../internal/service/FeaturesServiceConfig.java    |  15 +-
 .../internal/service/FeaturesServiceImpl.java      |   3 +-
 .../features/internal/service/LocationPattern.java | 198 ++++++++++++++
 .../karaf/features/internal/service/Overrides.java |   5 +-
 .../features/internal/service/RepositoryCache.java | 147 +++++-----
 ...positoryCache.java => RepositoryCacheImpl.java} |  41 ++-
 .../features/internal/service/RepositoryImpl.java  |  43 ++-
 .../karaf/features/internal/service/State.java     |  22 +-
 .../features/karaf-features-processing-1.0.0.xsd   | 236 ++++++++++++++++
 .../features/internal/service/BlacklistTest.java   |  49 ++--
 .../internal/service/FeaturesProcessorTest.java    | 200 ++++++++++++++
 .../internal/service/FeaturesValidationTest.java   |   2 +-
 .../internal/service/LocationPatternTest.java      | 162 +++++++++++
 .../features/internal/service/OverridesTest.java   |   4 +-
 .../internal/service/RepositoryCacheTest.java      |  70 +++++
 .../features/internal/service/urn/Handler.java     |  35 +++
 features/core/src/test/resources/log4j.properties  |  35 +++
 ...verrides.properties => blacklisted2.properties} |   9 +-
 .../karaf/features/internal/service/fp01.xml       |  26 ++
 .../karaf/features/internal/service/fp02.xml       |  28 ++
 .../karaf/features/internal/service/fp03.xml       |  34 +++
 .../karaf/features/internal/service/fpi01.xml      |  41 +++
 .../karaf/features/internal/service/fpi02.xml      |  29 ++
 .../internal/service/org.apache.karaf.features.xml |  98 +++++++
 .../features/internal/service/overrides.properties |   4 +-
 .../internal/service/overrides2.properties         |  37 +++
 .../resources/org/apache/karaf/features/r1.xml     |  20 ++
 .../org/apache/karaf/profile/assembly/Builder.java |   2 +-
 .../java/org/apache/karaf/util/maven/Parser.java   |  66 +++++
 .../java/org/apache/karaf/util/ParserTest.java     |  37 ++-
 .../karaf/webconsole/features/ExtendedFeature.java |   6 +
 50 files changed, 2741 insertions(+), 196 deletions(-)

diff --git a/features/core/pom.xml b/features/core/pom.xml
index 9e5a272..028de45 100644
--- a/features/core/pom.xml
+++ b/features/core/pom.xml
@@ -89,7 +89,7 @@
 
         <dependency>
             <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-jdk14</artifactId>
+            <artifactId>slf4j-log4j12</artifactId>
             <scope>test</scope>
         </dependency>
 
diff --git a/features/core/src/main/java/org/apache/karaf/features/Blacklisting.java b/features/core/src/main/java/org/apache/karaf/features/Blacklisting.java
new file mode 100644
index 0000000..39a3ce0
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/Blacklisting.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+/**
+ * <p>Indication that given object may be <em>blacklisted</em>.</p>
+ * <p>Blacklisted item is available for query, but can't be used to alter system (e.g., blacklisted feature
+ * can't be installed)</p>
+ */
+public interface Blacklisting {
+
+    /**
+     * Returns <code>true</code> if this item is <em>blacklisted</em>.
+     * @return
+     */
+    boolean isBlacklisted();
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/BundleInfo.java b/features/core/src/main/java/org/apache/karaf/features/BundleInfo.java
index c1a4c05..306d8ea 100644
--- a/features/core/src/main/java/org/apache/karaf/features/BundleInfo.java
+++ b/features/core/src/main/java/org/apache/karaf/features/BundleInfo.java
@@ -19,14 +19,18 @@ package org.apache.karaf.features;
 /**
  * A bundle info holds info about a Bundle.
  */
-public interface BundleInfo {
+public interface BundleInfo extends Blacklisting {
 
     String getLocation();
 
+    String getOriginalLocation();
+
     int getStartLevel();
 
     boolean isStart();
 
     boolean isDependency();
 
+    boolean isOverriden();
+
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/Feature.java b/features/core/src/main/java/org/apache/karaf/features/Feature.java
index 93450e8..ed4d077 100644
--- a/features/core/src/main/java/org/apache/karaf/features/Feature.java
+++ b/features/core/src/main/java/org/apache/karaf/features/Feature.java
@@ -21,7 +21,7 @@ import java.util.List;
 /**
  * A feature is a list of bundles associated identified by its name.
  */
-public interface Feature {
+public interface Feature extends Blacklisting {
 
     String DEFAULT_INSTALL_MODE = "auto";
 
diff --git a/features/core/src/main/java/org/apache/karaf/features/Repository.java b/features/core/src/main/java/org/apache/karaf/features/Repository.java
index fd4ef85..adbd1a4 100644
--- a/features/core/src/main/java/org/apache/karaf/features/Repository.java
+++ b/features/core/src/main/java/org/apache/karaf/features/Repository.java
@@ -16,22 +16,43 @@
  */
 package org.apache.karaf.features;
 
-import java.io.IOException;
 import java.net.URI;
 
 /**
- * A repository of features.
+ * <p>A repository of features. A runtime representation of JAXB model read from feature XML files.</p>
+ *
+ * <p>Original model may be subject to further processing (e.g., blacklisting)</p>
  */
-public interface Repository {
-
-    String getName() throws IOException;
-
+public interface Repository extends Blacklisting {
+
+    /**
+     * Logical name of the {@link Repository}
+     * @return
+     */
+    String getName();
+
+    /**
+     * Original URI of the {@link Repository}, where feature declarations were loaded from
+     * @return
+     */
     URI getURI();
 
-    URI[] getRepositories() throws Exception;
-
-    URI[] getResourceRepositories() throws Exception;
-
-    Feature[] getFeatures() throws Exception;
+    /**
+     * An array of referenced repository URIs (<code>/features/repository</code>)
+     * @return
+     */
+    URI[] getRepositories();
+
+    /**
+     * An array of referenced resource repository URIs (<code>/features/resource-repository</code>)
+     * @return
+     */
+    URI[] getResourceRepositories();
+
+    /**
+     * An array of {@link Feature features} in this {@link Repository} after possible processing.
+     * @return
+     */
+    Feature[] getFeatures();
 
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/Bundle.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/Bundle.java
index 8afa5ff..d0ba74f 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/Bundle.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/Bundle.java
@@ -20,6 +20,7 @@ import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlAttribute;
 import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 import javax.xml.bind.annotation.XmlValue;
 
@@ -53,13 +54,19 @@ public class Bundle implements BundleInfo {
     @XmlValue
     @XmlSchemaType(name = "anyURI")
     protected String value;
+    /** Original value may be queried if {@link #isOverriden()} is <code>true</code> */
+    @XmlTransient
+    protected String originalValue;
     @XmlAttribute(name = "start-level")
     protected Integer startLevel;
     @XmlAttribute
     protected Boolean start; // = true;
     @XmlAttribute
     protected Boolean dependency;
-
+    @XmlTransient
+    private boolean blacklisted = false;
+    @XmlTransient
+    private boolean overriden = false;
 
     public Bundle() {
     }
@@ -149,6 +156,33 @@ public class Bundle implements BundleInfo {
     }
 
     @Override
+    public boolean isBlacklisted() {
+        return blacklisted;
+    }
+
+    public void setBlacklisted(boolean blacklisted) {
+        this.blacklisted = blacklisted;
+    }
+
+    public boolean isOverriden() {
+        return overriden;
+    }
+
+    public void setOverriden(boolean overriden) {
+        this.overriden = overriden;
+
+    }
+
+    @Override
+    public String getOriginalLocation() {
+        return originalValue;
+    }
+
+    public void setOriginalLocation(String originalLocation) {
+        this.originalValue = originalLocation;
+    }
+
+    @Override
     public boolean equals(Object o) {
         if (this == o) {
             return true;
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
index 54a739b..44216e4 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
@@ -113,6 +113,8 @@ public class Feature extends Content implements org.apache.karaf.features.Featur
     protected List<String> resourceRepositories;
     @XmlTransient
     protected String repositoryUrl;
+    @XmlTransient
+    private boolean blacklisted;
 
     public Feature() {
     }
@@ -470,4 +472,14 @@ public class Feature extends Content implements org.apache.karaf.features.Featur
     public void setRepositoryUrl(String repositoryUrl) {
         this.repositoryUrl = repositoryUrl;
     }
+
+    @Override
+    public boolean isBlacklisted() {
+        return blacklisted;
+    }
+
+    public void setBlacklisted(boolean blacklisted) {
+        this.blacklisted = blacklisted;
+    }
+
 }
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
new file mode 100644
index 0000000..f51a8b9
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/BundleReplacements.java
@@ -0,0 +1,101 @@
+/*
+ * 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.model.processing;
+
+import java.net.MalformedURLException;
+import java.util.LinkedList;
+import java.util.List;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlEnum;
+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;
+
+@XmlType(name = "bundleReplacements", propOrder = {
+        "overrideBundles"
+})
+public class BundleReplacements {
+
+    @XmlElement(name = "bundle")
+    private List<OverrideBundle> overrideBundles = new LinkedList<>();
+
+    public List<OverrideBundle> getOverrideBundles() {
+        return overrideBundles;
+    }
+
+    @XmlType(name = "bundleOverrideMode")
+    @XmlEnum
+    public enum BundleOverrideMode {
+        @XmlEnumValue("osgi")
+        OSGI,
+        @XmlEnumValue("maven")
+        MAVEN
+    }
+
+    @XmlType(name = "overrideBundle")
+    public static class OverrideBundle {
+        @XmlAttribute
+        private String originalUri;
+        @XmlTransient
+        private LocationPattern originalUriPattern;
+        @XmlAttribute
+        private String replacement;
+        @XmlAttribute
+        private BundleOverrideMode mode = BundleOverrideMode.OSGI;
+
+        public String getOriginalUri() {
+            return originalUri;
+        }
+
+        public void setOriginalUri(String originalUri) {
+            this.originalUri = originalUri;
+        }
+
+        public String getReplacement() {
+            return replacement;
+        }
+
+        public void setReplacement(String replacement) {
+            this.replacement = replacement;
+        }
+
+        public BundleOverrideMode getMode() {
+            return mode;
+        }
+
+        public void setMode(BundleOverrideMode mode) {
+            this.mode = mode;
+        }
+
+        public LocationPattern getOriginalUriPattern() {
+            return originalUriPattern;
+        }
+
+        /**
+         * Changes String for <code>originalUri</code> into {@link org.apache.karaf.features.internal.service.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/FeatureReplacements.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeatureReplacements.java
new file mode 100644
index 0000000..2f36dd2
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeatureReplacements.java
@@ -0,0 +1,81 @@
+/*
+ * 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.model.processing;
+
+import java.util.LinkedList;
+import java.util.List;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlEnum;
+import javax.xml.bind.annotation.XmlEnumValue;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.karaf.features.FeaturesNamespaces;
+import org.apache.karaf.features.internal.model.Feature;
+
+@XmlType(name = "featureReplacements", propOrder = {
+        "replacements"
+})
+public class FeatureReplacements {
+
+    @XmlElement(name = "replacement")
+    private List<OverrideFeature> replacements = new LinkedList<>();
+
+    public List<OverrideFeature> getReplacements() {
+        return replacements;
+    }
+
+    @XmlType(name = "featureOverrideMode")
+    @XmlEnum
+    public enum FeatureOverrideMode {
+        @XmlEnumValue("replace")
+        REPLACE,
+        @XmlEnumValue("merge")
+        MERGE,
+        @XmlEnumValue("remove")
+        REMOVE
+    }
+
+    @XmlType(name = "overrideFeature", propOrder = {
+            "feature"
+    })
+    public static class OverrideFeature {
+        @XmlAttribute
+        private FeatureOverrideMode mode = FeatureOverrideMode.REPLACE;
+        @XmlElement
+        private Feature feature;
+
+        public FeatureOverrideMode getMode() {
+            return mode;
+        }
+
+        public void setMode(FeatureOverrideMode mode) {
+            this.mode = mode;
+        }
+
+        public Feature getFeature() {
+            return feature;
+        }
+
+        public void setFeature(Feature feature) {
+            this.feature = feature;
+        }
+    }
+
+}
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
new file mode 100644
index 0000000..142a16b
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/FeaturesProcessing.java
@@ -0,0 +1,296 @@
+/*
+ * 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.model.processing;
+
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlTransient;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.bind.annotation.XmlValue;
+
+import org.apache.felix.utils.manifest.Clause;
+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.osgi.framework.Version;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.karaf.features.internal.service.Overrides.OVERRIDE_RANGE;
+
+/**
+ * A set of instructions to process {@link org.apache.karaf.features.internal.model.Features} model. The actual
+ * use of these instructions is moved to {@link org.apache.karaf.features.internal.service.FeaturesProcessorImpl}
+ */
+@XmlRootElement(name = "featuresProcessing", namespace = FeaturesProcessing.FEATURES_PROCESSING_NS)
+@XmlType(name = "featuresProcessing", propOrder = {
+        "blacklistedRepositories",
+        "blacklistedFeatures",
+        "blacklistedBundles",
+        "overrideBundleDependency",
+        "bundleReplacements",
+        "featureReplacements"
+})
+public class FeaturesProcessing {
+
+    public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessing.class);
+    public static final String FEATURES_PROCESSING_NS = "http://karaf.apache.org/xmlns/features-processing/v1.0.0";
+
+    @XmlElementWrapper(name = "blacklistedRepositories")
+    @XmlElement(name = "repository")
+    private List<String> blacklistedRepositories = new LinkedList<>();
+    @XmlTransient
+    private List<LocationPattern> blacklistedRepositoryLocationPatterns = new LinkedList<>();
+
+    @XmlElementWrapper(name = "blacklistedFeatures")
+    @XmlElement(name = "feature")
+    private List<BlacklistedFeature> blacklistedFeatures = new LinkedList<>();
+
+    @XmlElementWrapper(name = "blacklistedBundles")
+    @XmlElement(name = "bundle")
+    private List<String> blacklistedBundles = new LinkedList<>();
+
+    @XmlElement
+    private OverrideBundleDependency overrideBundleDependency;
+
+    @XmlElement
+    private BundleReplacements bundleReplacements;
+
+    @XmlElement
+    private FeatureReplacements featureReplacements;
+
+    @XmlTransient
+    private Blacklist blacklist;
+
+    public FeaturesProcessing() {
+    }
+
+    public List<String> getBlacklistedRepositories() {
+        return blacklistedRepositories;
+    }
+
+    public List<LocationPattern> getBlacklistedRepositoryLocationPatterns() {
+        return blacklistedRepositoryLocationPatterns;
+    }
+
+    public List<BlacklistedFeature> getBlacklistedFeatures() {
+        return blacklistedFeatures;
+    }
+
+    public List<String> getBlacklistedBundles() {
+        return blacklistedBundles;
+    }
+
+    public OverrideBundleDependency getOverrideBundleDependency() {
+        return overrideBundleDependency;
+    }
+
+    public void setOverrideBundleDependency(OverrideBundleDependency overrideBundleDependency) {
+        this.overrideBundleDependency = overrideBundleDependency;
+    }
+
+    public BundleReplacements getBundleReplacements() {
+        return bundleReplacements;
+    }
+
+    public void setBundleReplacements(BundleReplacements bundleReplacements) {
+        this.bundleReplacements = bundleReplacements;
+    }
+
+    public FeatureReplacements getFeatureReplacements() {
+        return featureReplacements;
+    }
+
+    public void setFeatureReplacements(FeatureReplacements featureReplacements) {
+        this.featureReplacements = featureReplacements;
+    }
+
+    public Blacklist getBlacklist() {
+        return blacklist;
+    }
+
+    /**
+     * Perform <em>compilation</em> of rules declared in feature processing XML file.
+     * @param blacklist additional {@link Blacklist} definition with lower priority
+     * @param overrides additional overrides definition with lower priority
+     */
+    public void postUnmarshall(Blacklist blacklist, Set<String> overrides) {
+        // compile blacklisted repository URIs
+        for (String repositoryURI : this.getBlacklistedRepositories()) {
+            try {
+                blacklistedRepositoryLocationPatterns.add(new LocationPattern(repositoryURI));
+            } catch (MalformedURLException e) {
+                LOG.warn("Can't parse blacklisted repository location pattern: " + repositoryURI + ". Ignoring.");
+            }
+        }
+
+        // verify bundle override definitions
+        for (Iterator<BundleReplacements.OverrideBundle> iterator = this.bundleReplacements.getOverrideBundles().iterator(); iterator.hasNext(); ) {
+            BundleReplacements.OverrideBundle overrideBundle = iterator.next();
+            if (overrideBundle.getOriginalUri() == null) {
+                // we have to derive it from replacement - as with etc/overrides.properties entry
+                if (overrideBundle.getMode() == BundleReplacements.BundleOverrideMode.MAVEN) {
+                    LOG.warn("Can't override bundle in maven mode without explicit original URL. Switching to osgi mode.");
+                    overrideBundle.setMode(BundleReplacements.BundleOverrideMode.OSGI);
+                }
+                String originalUri = calculateOverridenURI(overrideBundle.getReplacement(), null);
+                if (originalUri != null) {
+                    overrideBundle.setOriginalUri(originalUri);
+                } else {
+                    iterator.remove();
+                    continue;
+                }
+            }
+            try {
+                overrideBundle.compile();
+            } catch (MalformedURLException e) {
+                LOG.warn("Can't parse override URL location pattern: " + overrideBundle.getOriginalUri() + ". Ignoring.");
+                iterator.remove();
+            }
+        }
+
+        // etc/blacklisted.properties
+        // blacklisted bundle from XML to instruction for Blacklist class
+        List<String> blacklisted = new LinkedList<>();
+        for (String bl : this.getBlacklistedBundles()) {
+            blacklisted.add(bl + ";type=bundle");
+        }
+        // blacklisted features - XML type to String instruction for Blacklist class
+        blacklisted.addAll(this.getBlacklistedFeatures().stream()
+                .map(bf -> bf.getName() + ";type=feature" + (bf.getVersion() == null ? "" : ";range=\"" + bf.getVersion() + "\""))
+                .collect(Collectors.toList()));
+
+        this.blacklist = new Blacklist(blacklisted);
+        this.blacklist.merge(blacklist);
+
+        // etc/overrides.properties (mvn: URIs)
+        for (Clause clause : Parser.parseClauses(overrides.toArray(new String[overrides.size()]))) {
+            // name of the clause will become a bundle replacement
+            String mvnURI = clause.getName();
+            URI uri = URI.create(mvnURI);
+            if (!"mvn".equals(uri.getScheme())) {
+                LOG.warn("Override URI \"" + mvnURI + "\" should use mvn: scheme. Ignoring.");
+                continue;
+            }
+            BundleReplacements.OverrideBundle override = new BundleReplacements.OverrideBundle();
+            override.setMode(BundleReplacements.BundleOverrideMode.OSGI);
+            override.setReplacement(mvnURI);
+            String originalUri = calculateOverridenURI(mvnURI, clause.getAttribute(OVERRIDE_RANGE));
+            if (originalUri != null) {
+                override.setOriginalUri(originalUri);
+                try {
+                    override.compile();
+                    bundleReplacements.getOverrideBundles().add(override);
+                } catch (MalformedURLException e) {
+                    LOG.warn("Can't parse override URL location pattern: " + originalUri + ". Ignoring.");
+                }
+            }
+        }
+    }
+
+    /**
+     * For <code>etc/overrides.properties</code>, we know what is the target URI for bundles we should use. We need
+     * a pattern of original bundle URIs that are candidates for replacement
+     * @param replacement
+     * @param range
+     * @return
+     */
+    private String calculateOverridenURI(String replacement, String range) {
+        try {
+            org.apache.karaf.util.maven.Parser parser = new org.apache.karaf.util.maven.Parser(replacement);
+            if (parser.getVersion() != null
+                    && (parser.getVersion().startsWith("[") || parser.getVersion().startsWith("("))) {
+                // replacement URI should not contain ranges
+                throw new MalformedURLException("Override URI should use single version.");
+            }
+            if (range != null) {
+                // explicit range determines originalUri
+                VersionRange vr = new VersionRange(range, true);
+                if (vr.isOpenCeiling() && vr.getCeiling() == VersionRange.INFINITE_VERSION) {
+                    // toString() will give only floor version
+                    parser.setVersion(String.format("%s%s,*)",
+                            vr.isOpenFloor() ? "(" : "[",
+                            vr.getFloor()));
+                } else {
+                    parser.setVersion(vr.toString());
+                }
+            } else {
+                // no range: originalUri based on replacemenet URI with range deducted using default rules
+                // assume version in override URI is NOT a range
+                Version v;
+                try {
+                    v = new Version(VersionCleaner.clean(parser.getVersion()));
+                } catch (IllegalArgumentException e) {
+                    LOG.warn("Problem parsing override URI \"" + replacement + "\": " + e.getMessage() + ". Version ranges are not handled. Ignoring.");
+                    return null;
+                }
+                Version vfloor = new Version(v.getMajor(), v.getMinor(), 0, null);
+                parser.setVersion(new VersionRange(false, vfloor, v, true).toString());
+            }
+            return parser.toMvnURI();
+        } catch (MalformedURLException e) {
+            LOG.warn("Problem parsing override URI \"" + replacement + "\": " + e.getMessage() + ". Ignoring.");
+            return null;
+        }
+    }
+
+    @XmlType(name = "blacklistedFeature")
+    public static class BlacklistedFeature {
+        @XmlValue
+        private String name;
+        @XmlAttribute
+        private String version;
+
+        public BlacklistedFeature() {
+        }
+
+        public BlacklistedFeature(String name, String version) {
+            this.name = name;
+            this.version = version;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getVersion() {
+            return version;
+        }
+
+        public void setVersion(String version) {
+            this.version = version;
+        }
+    }
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/ObjectFactory.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/ObjectFactory.java
new file mode 100644
index 0000000..e1bdede
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/ObjectFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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.model.processing;
+
+import javax.xml.bind.annotation.XmlRegistry;
+
+@XmlRegistry
+public class ObjectFactory {
+
+    public FeaturesProcessing createFeaturesProcessing() {
+        return new FeaturesProcessing();
+    }
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/OverrideBundleDependency.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/OverrideBundleDependency.java
new file mode 100644
index 0000000..398e0c2
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/OverrideBundleDependency.java
@@ -0,0 +1,111 @@
+/*
+ * 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.model.processing;
+
+import java.util.LinkedList;
+import java.util.List;
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlType;
+
+@XmlType(name = "overrideBundleDependency", propOrder = {
+        "repositories",
+        "features",
+        "bundles"
+})
+public class OverrideBundleDependency {
+
+    @XmlElement(name = "repository")
+    private List<OverrideDependency> repositories = new LinkedList<>();
+    @XmlElement(name = "feature")
+    private List<OverrideFeatureDependency> features = new LinkedList<>();
+    @XmlElement(name = "bundle")
+    private List<OverrideDependency> bundles = new LinkedList<>();
+
+    public List<OverrideDependency> getRepositories() {
+        return repositories;
+    }
+
+    public List<OverrideFeatureDependency> getFeatures() {
+        return features;
+    }
+
+    public List<OverrideDependency> getBundles() {
+        return bundles;
+    }
+
+    @XmlType(name = "overrideDependency")
+    public static class OverrideDependency {
+        @XmlAttribute
+        private String uri;
+        @XmlAttribute
+        private boolean dependency = false;
+
+        public String getUri() {
+            return uri;
+        }
+
+        public void setUri(String uri) {
+            this.uri = uri;
+        }
+
+        public boolean isDependency() {
+            return dependency;
+        }
+
+        public void setDependency(boolean dependency) {
+            this.dependency = dependency;
+        }
+    }
+
+    @XmlType(name = "overrideFeatureDependency")
+    public static class OverrideFeatureDependency {
+        @XmlAttribute
+        private String name;
+        @XmlAttribute
+        private String version;
+        @XmlAttribute
+        private boolean dependency = false;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+
+        public String getVersion() {
+            return version;
+        }
+
+        public void setVersion(String version) {
+            this.version = version;
+        }
+
+        public boolean isDependency() {
+            return dependency;
+        }
+
+        public void setDependency(boolean dependency) {
+            this.dependency = dependency;
+        }
+    }
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/package-info.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/package-info.java
new file mode 100644
index 0000000..8086150
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/processing/package-info.java
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+@XmlSchema(namespace = "http://karaf.apache.org/xmlns/features-processing/v1.0.0",
+        elementFormDefault = XmlNsForm.QUALIFIED, attributeFormDefault = XmlNsForm.UNQUALIFIED,
+        xmlns = {
+                @XmlNs(prefix = "", namespaceURI = FEATURES_PROCESSING_NS
+                ),
+                @XmlNs(prefix = "f", namespaceURI = FeaturesNamespaces.URI_CURRENT)
+        }
+)
+@XmlAccessorType(XmlAccessType.FIELD)
+package org.apache.karaf.features.internal.model.processing;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlNs;
+import javax.xml.bind.annotation.XmlNsForm;
+import javax.xml.bind.annotation.XmlSchema;
+
+import org.apache.karaf.features.FeaturesNamespaces;
+
+import static org.apache.karaf.features.internal.model.processing.FeaturesProcessing.FEATURES_PROCESSING_NS;
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
index 6881d47..ad6d940 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
@@ -28,9 +28,7 @@ import java.util.Dictionary;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.Executor;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadPoolExecutor;
 import java.util.concurrent.TimeUnit;
@@ -93,6 +91,7 @@ import org.slf4j.LoggerFactory;
 public class Activator extends BaseActivator {
 
     public static final String FEATURES_SERVICE_CONFIG_FILE = "org.apache.karaf.features.cfg";
+    public static final String FEATURES_SERVICE_PROCESSING_FILE = "org.apache.karaf.features.xml";
 
     private static final String STATE_FILE = "state.json";
 
@@ -228,15 +227,17 @@ public class Activator extends BaseActivator {
     }
 
     private FeaturesServiceConfig getConfig() {
+        String karafEtc = System.getProperty("karaf.etc");
         return new FeaturesServiceConfig(
-            getString("overrides", new File(System.getProperty("karaf.etc"), "overrides.properties").toURI().toString()),
+            getString("overrides", new File(karafEtc, "overrides.properties").toURI().toString()),
             getString("featureResolutionRange", FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE),
             getString("bundleUpdateRange", FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE),
             getString("updateSnapshots", FeaturesService.DEFAULT_UPDATE_SNAPSHOTS),
             getInt("downloadThreads", FeaturesService.DEFAULT_DOWNLOAD_THREADS),
             getLong("scheduleDelay", FeaturesService.DEFAULT_SCHEDULE_DELAY),
             getInt("scheduleMaxRun", FeaturesService.DEFAULT_SCHEDULE_MAX_RUN),
-            getString("blacklisted", new File(System.getProperty("karaf.etc"), "blacklisted.properties").toURI().toString()),
+            getString("blacklisted", new File(karafEtc, "blacklisted.properties").toURI().toString()),
+            getString("featureProcessing", new File(karafEtc, FEATURES_SERVICE_PROCESSING_FILE).toURI().toString()),
             getString("serviceRequirements", FeaturesService.SERVICE_REQUIREMENTS_DEFAULT));
     }
 
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/region/Subsystem.java b/features/core/src/main/java/org/apache/karaf/features/internal/region/Subsystem.java
index e55e60d..cfd295e 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/region/Subsystem.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/region/Subsystem.java
@@ -451,23 +451,23 @@ public class Subsystem extends ResourceImpl {
                 }
                 boolean mandatory = !bi.isDependency() && cond == null;
                 if (bi.isDependency()) {
-                    addDependency(res, mandatory, bi.isStart(), sl);
+                    addDependency(res, mandatory, bi.isStart(), sl, bi.isBlacklisted());
                 } else {
-                    doAddDependency(res, mandatory, bi.isStart(), sl);
+                    doAddDependency(res, mandatory, bi.isStart(), sl, bi.isBlacklisted());
                 }
             }
             for (Library library : feature.getLibraries()) {
                 if (library.isExport()) {
                     final String loc = library.getLocation();
                     ResourceImpl res = bundles.get(loc);
-                    addDependency(res, false, false, 0);
+                    addDependency(res, false, false, 0, false);
                 }
             }
             for (String uri : feature.getResourceRepositories()) {
                 BaseRepository repo = repos.getRepository(feature.getRepositoryUrl(), uri);
                 for (Resource resource : repo.getResources()) {
                     ResourceImpl res = cloneResource(resource);
-                    addDependency(res, false, true, 0);
+                    addDependency(res, false, true, 0, false);
                 }
             }
         }
@@ -475,6 +475,7 @@ public class Subsystem extends ResourceImpl {
             final String loc = bundle.getName();
             boolean dependency = Boolean.parseBoolean(bundle.getAttribute("dependency"));
             boolean start = bundle.getAttribute("start") == null || Boolean.parseBoolean(bundle.getAttribute("start"));
+            boolean blacklisted = bundle.getAttribute("blacklisted") != null && Boolean.parseBoolean(bundle.getAttribute("blacklisted"));
             int startLevel = 0;
             try {
                 startLevel = Integer.parseInt(bundle.getAttribute("start-level"));
@@ -482,9 +483,9 @@ public class Subsystem extends ResourceImpl {
                 // Ignore
             }
             if (dependency) {
-                addDependency(bundles.get(loc), false, start, startLevel);
+                addDependency(bundles.get(loc), false, start, startLevel, blacklisted);
             } else {
-                doAddDependency(bundles.get(loc), true, start, startLevel);
+                doAddDependency(bundles.get(loc), true, start, startLevel, blacklisted);
                 addIdentityRequirement(this, bundles.get(loc));
             }
         }
@@ -535,17 +536,17 @@ public class Subsystem extends ResourceImpl {
         throw new IllegalArgumentException("Resource " + provider.getUrl() + " does not contain a manifest");
     }
 
-    void addDependency(ResourceImpl resource, boolean mandatory, boolean start, int startLevel) {
+    void addDependency(ResourceImpl resource, boolean mandatory, boolean start, int startLevel, boolean blacklisted) {
         if (isAcceptDependencies()) {
-            doAddDependency(resource, mandatory, start, startLevel);
+            doAddDependency(resource, mandatory, start, startLevel, blacklisted);
         } else {
-            parent.addDependency(resource, mandatory, start, startLevel);
+            parent.addDependency(resource, mandatory, start, startLevel, blacklisted);
         }
     }
 
-    private void doAddDependency(ResourceImpl resource, boolean mandatory, boolean start, int startLevel) {
+    private void doAddDependency(ResourceImpl resource, boolean mandatory, boolean start, int startLevel, boolean blacklisted) {
         String id = ResolverUtil.getSymbolicName(resource) + "|" + ResolverUtil.getVersion(resource);
-        DependencyInfo info = new DependencyInfo(resource, mandatory, start, startLevel);
+        DependencyInfo info = new DependencyInfo(resource, mandatory, start, startLevel, blacklisted);
         dependencies.merge(id, info, this::merge);
     }
 
@@ -589,15 +590,18 @@ public class Subsystem extends ResourceImpl {
         boolean mandatory;
         boolean start;
         int startLevel;
+        boolean blacklisted;
+        boolean overriden;
 
         public DependencyInfo() {
         }
 
-        public DependencyInfo(ResourceImpl resource, boolean mandatory, boolean start, int startLevel) {
+        public DependencyInfo(ResourceImpl resource, boolean mandatory, boolean start, int startLevel, boolean blacklisted) {
             this.resource = resource;
             this.mandatory = mandatory;
             this.start = start;
             this.startLevel = startLevel;
+            this.blacklisted = blacklisted;
         }
 
         @Override
@@ -616,11 +620,31 @@ public class Subsystem extends ResourceImpl {
         }
 
         @Override
+        public String getOriginalLocation() {
+            // resource is already overriden
+            return getUri(resource);
+        }
+
+        @Override
         public boolean isDependency() {
             return !mandatory;
         }
 
         @Override
+        public boolean isBlacklisted() {
+            return blacklisted;
+        }
+
+        @Override
+        public boolean isOverriden() {
+            return overriden;
+        }
+
+        public void setOverriden(boolean overriden) {
+            this.overriden = overriden;
+        }
+
+        @Override
         public String toString() {
             return "DependencyInfo{" +
                     "resource=" + resource +
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 ac971fc..8c6bfb9 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
@@ -16,18 +16,21 @@
  */
 package org.apache.karaf.features.internal.service;
 
-import static java.util.stream.Collectors.toSet;
-
 import java.io.BufferedReader;
 import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.net.MalformedURLException;
 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.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;
@@ -45,24 +48,28 @@ import org.slf4j.LoggerFactory;
  */
 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";
+    public static final String BLACKLIST_TYPE = "type"; // null -> "feature"
     public static final String TYPE_FEATURE = "feature";
     public static final String TYPE_BUNDLE = "bundle";
     public static final String TYPE_REPOSITORY = "repository";
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Blacklist.class);
     private Clause[] clauses;
-    
+    private Map<String, LocationPattern> bundleBlacklist = new LinkedHashMap<>();
+
     public Blacklist() {
         this(Collections.emptyList());
     }
 
     public Blacklist(List<String> blacklist) {
-        this.clauses = org.apache.felix.utils.manifest.Parser.parseClauses(blacklist.toArray(new String[blacklist.size()]));
+        this.clauses = Parser.parseClauses(blacklist.toArray(new String[blacklist.size()]));
+        compileClauses();
     }
-    
+
     public Blacklist(String blacklistUrl) {
         Set<String> blacklist = new HashSet<>();
         if (blacklistUrl != null) {
@@ -79,8 +86,53 @@ public class Blacklist {
             }
         }
         this.clauses = Parser.parseClauses(blacklist.toArray(new String[blacklist.size()]));
+        compileClauses();
     }
 
+    /**
+     * Extracts blacklisting clauses related to bundles, features and repositories and changes them to more
+     * usable form.
+     */
+    private void compileClauses() {
+        for (Clause c : clauses) {
+            String type = c.getAttribute(BLACKLIST_TYPE);
+            if (type == null) {
+                String url = c.getAttribute(BLACKLIST_URL);
+                if (url != null || c.getName().startsWith("mvn:")) {
+                    // some special rules from etc/blacklisted.properties
+                    type = TYPE_BUNDLE;
+                } else {
+                    type = TYPE_FEATURE;
+                }
+            }
+            switch (type) {
+                case TYPE_FEATURE:
+                    break;
+                case TYPE_BUNDLE:
+                    String location = c.getName();
+                    if (c.getAttribute(BLACKLIST_URL) != null) {
+                        location = c.getAttribute(BLACKLIST_URL);
+                    }
+                    if (location == null) {
+                        // should not happen?
+                        LOG.warn("Bundle blacklist URI is empty. Ignoring.");
+                    } else {
+                        try {
+                            bundleBlacklist.put(location, location.startsWith("mvn:") ? new LocationPattern(location) : null);
+                        } catch (MalformedURLException e) {
+                            LOG.warn("Problem parsing blacklist URI \"" + location + "\": " + e.getMessage() + ". Ignoring.");
+                        }
+                    }
+                    break;
+                case TYPE_REPOSITORY:
+            }
+        }
+    }
+
+    /**
+     * TODO: set {@link Feature#setBlacklisted(boolean)} instead of removing from collection
+     * @param features
+     */
     public void blacklist(Features features) {
         features.getFeature().removeIf(this::blacklist);
     }
@@ -131,9 +183,39 @@ public class Blacklist {
         }
     }
 
+    public boolean isBundleBlacklisted(String uri) {
+        for (Map.Entry<String, LocationPattern> clause : bundleBlacklist.entrySet()) {
+            if (mavenMatches(clause.getKey(), clause.getValue(), uri)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Checks whether given <code>uri</code> matches Maven artifact pattern (group, artifact, optional type/classifier, version
+     * range, globs).
+     * @param blacklistedUri
+     * @param compiledUri
+     * @param uri
+     * @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) {
-            if (clause.getName().equals(name)) {
+            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);
@@ -141,7 +223,6 @@ public class Blacklist {
                     range = new VersionRange(vr, true);
                 }
                 if (range.contains(VersionTable.getVersion(version))) {
-                    String type = clause.getAttribute(BLACKLIST_TYPE);
                     if (type == null || TYPE_FEATURE.equals(type)) {
                         return true;
                     }
@@ -151,11 +232,7 @@ public class Blacklist {
         return false;
     }
 
-    public boolean isBundleBlacklisted(String uri) {
-        return isBlacklisted(uri, TYPE_BUNDLE);
-    }
-
-    public boolean isBlacklisted(String uri, String btype) {
+    public boolean isRepositoryBlacklisted(String uri) {
         for (Clause clause : clauses) {
             String url = clause.getName();
             if (clause.getAttribute(BLACKLIST_URL) != null) {
@@ -163,11 +240,34 @@ public class Blacklist {
             }
             if (uri.equals(url)) {
                 String type = clause.getAttribute(BLACKLIST_TYPE);
-                if (type == null || btype.equals(type)) {
+                if (type == null || TYPE_REPOSITORY.equals(type)) {
                     return true;
                 }
             }
         }
         return false;
     }
+
+    /**
+     * Merge clauses from another {@link Blacklist} into this object
+     * @param others
+     */
+    public void merge(Blacklist others) {
+        Clause[] ours = this.clauses;
+        if (ours == null) {
+            this.clauses = Arrays.copyOf(others.clauses, others.clauses.length);
+        } else if (others != null && others.clauses.length > 0) {
+            this.clauses = new Clause[ours.length + others.clauses.length];
+            System.arraycopy(ours, 0, this.clauses, 0, ours.length);
+            System.arraycopy(others.clauses, ours.length, this.clauses, 0, others.clauses.length);
+        }
+        if (others != null) {
+            this.bundleBlacklist.putAll(others.bundleBlacklist);
+        }
+    }
+
+    public Clause[] getClauses() {
+        return clauses;
+    }
+
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
index 48eed92..61244b6 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
@@ -160,17 +160,34 @@ public class Deployer {
         }
     }
 
+    /**
+     * <p>Representation of the state of system from the point of view of <em>bundles</em> and <em>features</em></p>
+     */
     public static class DeploymentState {
+        /** Current {@link State} of system */
         public State state;
+        /** A {@link Bundle} providing {@link FeaturesService} */
         public Bundle serviceBundle;
+        /** {@link org.osgi.framework.startlevel.FrameworkStartLevel#getInitialBundleStartLevel()} */
         public int initialBundleStartLevel;
+        /** {@link org.osgi.framework.startlevel.FrameworkStartLevel#getStartLevel()} */
         public int currentStartLevel;
+        /** bundle-id -&gt; bundle for all currently installed bundles */
         public Map<Long, Bundle> bundles;
+        /** feature-name/feature-id -&gt; feature for all available features (not only installed) */
         public Map<String, Feature> features;
+        /** region-name -&gt; ids for bundles installed in region */
         public Map<String, Set<Long>> bundlesPerRegion;
+        /** region-name -&gt; connected, filtered, region-name -&gt; filter-namespace -&gt; filters */
         public Map<String, Map<String, Map<String, Set<String>>>> filtersPerRegion;
     }
 
+    /**
+     * <p>A request to change current {@link State state} of system</p>
+     * <p>{@link #requirements} specify target set of system requirements. If new features are installed,
+     * requirements should include currently installed features and new ones. If features are being uninstalled,
+     * requirements should include currently installed features minus the ones that are removed.</p>
+     */
     public static class DeploymentRequest {
         public Set<String> overrides;
         public String featureResolutionRange;
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessor.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessor.java
new file mode 100644
index 0000000..28e805d
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessor.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.karaf.features.internal.service;
+
+import java.net.URI;
+
+import org.apache.karaf.features.Repository;
+import org.apache.karaf.features.internal.model.Features;
+
+/**
+ * Service that can process (enhance, modify, trim, ...) a set of features read from {@link Repository}.
+ */
+public interface FeaturesProcessor {
+
+    /**
+     * Checks whether given repository URI is <em>blacklisted</em>
+     * @param uri
+     * @return
+     */
+    boolean isRepositoryBlacklisted(URI uri);
+
+    /**
+     * Processes original {@link Features JAXB model of features}
+     * @param features
+     */
+    void process(Features features);
+
+}
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
new file mode 100644
index 0000000..03dadea
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesProcessorImpl.java
@@ -0,0 +1,197 @@
+/*
+ * 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 java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
+import java.util.List;
+import java.util.Set;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.Unmarshaller;
+
+import org.apache.karaf.features.BundleInfo;
+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;
+
+/**
+ * <p>Configurable {@link FeaturesProcessor}, controlled by several files from <code>etc/</code> directory:<ul>
+ *     <li><code>etc/overrides.properties</code>: may alter bundle versions in features</li>
+ *     <li><code>etc/blacklisted.properties</code>: may filter out some features/bundles</li>
+ *     <li><code>etc/org.apache.karaf.features.xml</code> (<strong>new!</strong>): incorporates two above files
+ *     and may define additional processing (changing G/A/V, adding bundles to features, changing <code>dependency</code>
+ *     attributes, ...)</li>
+ * </ul></p>
+ */
+public class FeaturesProcessorImpl implements FeaturesProcessor {
+
+    public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessorImpl.class);
+    private static final JAXBContext FEATURES_PROCESSING_CONTEXT;
+
+    private FeaturesProcessing processing;
+
+    static {
+        try {
+            FEATURES_PROCESSING_CONTEXT = JAXBContext.newInstance(ObjectFactory.class);
+        } catch (JAXBException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * <p>Creates instance of features processor using {@link FeaturesServiceConfig configuration object} where
+     * three files may be specified: overrides.properties, blacklisted.properties and org.apache.karaf.features.xml.</p>
+     * @param configuration
+     */
+    public FeaturesProcessorImpl(FeaturesServiceConfig configuration) {
+        // org.apache.karaf.features.xml - highest priority
+        String featureModificationsURI = configuration.featureModifications;
+        // blacklisted.properties - if available, adds to main configuration of feature processing
+        String blacklistedURI = configuration.blacklisted;
+        // overrides.properties - if available, adds to main configuration of feature processing
+        String overridesURI = configuration.overrides;
+
+        // these two are not changed - they still may be used, but if etc/org.apache.karaf.features.xml is available
+        // both of the below are merged into single processing configuration
+        Blacklist blacklist = new Blacklist(blacklistedURI);
+        Set<String> overrides = Overrides.loadOverrides(overridesURI);
+
+        if (featureModificationsURI != null) {
+            try {
+                try (InputStream stream = new URL(featureModificationsURI).openStream()) {
+                    Unmarshaller unmarshaller = FEATURES_PROCESSING_CONTEXT.createUnmarshaller();
+                    processing = (FeaturesProcessing) unmarshaller.unmarshal(stream);
+                }
+            } catch (FileNotFoundException e) {
+                LOG.warn("Can't find feature processing file (" + featureModificationsURI + ")");
+            } catch (Exception e) {
+                LOG.warn("Can't initialize feature processor: " + e.getMessage());
+            }
+        }
+
+        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);
+    }
+
+    public FeaturesProcessing getInstructions() {
+        return processing;
+    }
+
+    @Override
+    public void process(Features features) {
+        // blacklisting features
+        for (Feature feature : features.getFeature()) {
+            feature.setBlacklisted(isFeatureBlacklisted(feature));
+            // blacklisting bundles
+            processBundles(feature.getBundle());
+            for (Conditional c : feature.getConditional()) {
+                processBundles(c.getBundle());
+            }
+        }
+
+        // TODO: changing "dependency" flag of features
+        // TODO: changing "dependency" flag of bundles
+        // TODO: overriding features
+    }
+
+    private void processBundles(List<Bundle> bundles) {
+        for (Bundle bundle : bundles) {
+            boolean bundleBlacklisted = isBundleBlacklisted(bundle.getLocation());
+            if (bundleBlacklisted) {
+                // blacklisting has higher priority
+                bundle.setBlacklisted(true);
+            } else {
+                // if not blacklisted, it may be overriden
+                staticOverrideBundle(bundle);
+            }
+        }
+    }
+
+    /**
+     * Processes {@link Bundle bundle definition} and (according to override instructions) maybe sets different target
+     * location and {@link BundleInfo#isOverriden()} flag
+     * @param bundle
+     */
+    private void staticOverrideBundle(Bundle bundle) {
+        for (BundleReplacements.OverrideBundle override : this.getInstructions().getBundleReplacements().getOverrideBundles()) {
+            String originalLocation = bundle.getLocation();
+            if (override.getOriginalUriPattern().matches(originalLocation)) {
+                LOG.debug("Overriding bundle location \"" + originalLocation + "\" with \"" + override.getReplacement() + "\"");
+                bundle.setOriginalLocation(originalLocation);
+                bundle.setOverriden(true);
+                bundle.setLocation(override.getReplacement());
+                // last rule wins - no break!!!
+                //break;
+            }
+        }
+
+    }
+
+    @Override
+    public boolean isRepositoryBlacklisted(URI uri) {
+        for (LocationPattern lp : processing.getBlacklistedRepositoryLocationPatterns()) {
+            if (lp.matches(uri.toString())) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Matching name and version of given feature, checks whether this feature is blacklisted
+     * @param feature
+     * @return
+     */
+    private boolean isFeatureBlacklisted(Feature feature) {
+        return getInstructions().getBlacklist().isFeatureBlacklisted(feature.getName(), feature.getVersion());
+    }
+
+    /**
+     * Matching location of the bundle, checks whether this bundle is blacklisted
+     * @param location
+     * @return
+     */
+    private boolean isBundleBlacklisted(String location) {
+        return getInstructions().getBlacklist().isBundleBlacklisted(location);
+    }
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceConfig.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceConfig.java
index f6fe033..1f1fdfd 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceConfig.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceConfig.java
@@ -20,8 +20,6 @@ import org.apache.karaf.features.FeaturesService;
 
 public class FeaturesServiceConfig {
 
-    public final String overrides;
-    
     /**
      * Range to use when a version is specified on a feature dependency.
      * The default is {@link org.apache.karaf.features.FeaturesService#DEFAULT_FEATURE_RESOLUTION_RANGE}
@@ -54,14 +52,20 @@ public class FeaturesServiceConfig {
      * Service requirements enforcement
      */
     public final String serviceRequirements;
-    
+
     public final String blacklisted;
+    public final String featureModifications;
+    public final String overrides;
 
     public FeaturesServiceConfig() {
-        this(null, FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE, FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE, null, 1, 0, 0, null, null);
+        this(null, null, null);
+    }
+
+    public FeaturesServiceConfig(String overrides, String blacklisted, String featureModifications) {
+        this(overrides, FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE, FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE, null, 1, 0, 0, blacklisted, featureModifications, null);
     }
 
-    public FeaturesServiceConfig(String overrides, String featureResolutionRange, String bundleUpdateRange, String updateSnapshots, int downloadThreads, long scheduleDelay, int scheduleMaxRun, String blacklisted, String serviceRequirements) {
+    public FeaturesServiceConfig(String overrides, String featureResolutionRange, String bundleUpdateRange, String updateSnapshots, int downloadThreads, long scheduleDelay, int scheduleMaxRun, String blacklisted, String featureModifications, String serviceRequirements) {
         this.overrides = overrides;
         this.featureResolutionRange = featureResolutionRange;
         this.bundleUpdateRange = bundleUpdateRange;
@@ -70,6 +74,7 @@ public class FeaturesServiceConfig {
         this.scheduleDelay = scheduleDelay;
         this.scheduleMaxRun = scheduleMaxRun;
         this.blacklisted = blacklisted;
+        this.featureModifications = featureModifications;
         this.serviceRequirements = serviceRequirements;
     }
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
index 1478385..5a6d788 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
@@ -146,8 +146,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         this.resolver = resolver;
         this.installSupport = installSupport;
         this.globalRepository = globalRepository;
-        Blacklist blacklist = new Blacklist(cfg.blacklisted);
-        this.repositories = new RepositoryCache(blacklist);
+        this.repositories = new RepositoryCacheImpl(new FeaturesProcessorImpl(cfg));
         this.cfg = cfg;
         this.executor = Executors.newSingleThreadExecutor(ThreadUtils.namedThreadFactory("features"));
         loadState();
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/internal/service/LocationPattern.java
new file mode 100644
index 0000000..7e55b9b
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/LocationPattern.java
@@ -0,0 +1,198 @@
+/*
+ * 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 java.net.MalformedURLException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.felix.utils.version.VersionCleaner;
+import org.apache.felix.utils.version.VersionRange;
+import org.apache.karaf.util.maven.Parser;
+import org.osgi.framework.Version;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <p>Helper class to compare Maven 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>
+ * <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>
+ */
+public class LocationPattern {
+
+    public static Logger LOG = LoggerFactory.getLogger(LocationPattern.class);
+
+    private String originalUri;
+    private Pattern originalPattern;
+    private String groupId;
+    private Pattern groupIdPattern;
+    private String artifactId;
+    private Pattern artifactIdPattern;
+    private String versionString;
+    private Version version;
+    private VersionRange versionRange;
+    private String type;
+    private Pattern typePattern;
+    private String classifier;
+    private Pattern classifierPattern;
+
+    public LocationPattern(String uri) throws MalformedURLException {
+        if (uri == null) {
+            throw new IllegalArgumentException("URI to match should not be null");
+        }
+        originalUri = uri;
+        if (!originalUri.startsWith("mvn:")) {
+            originalPattern = toRegExp(originalUri);
+        } else {
+            uri = uri.substring(4);
+            Parser parser = new Parser(uri);
+            if (Parser.VERSION_LATEST.equals(parser.getVersion())) {
+                parser.setVersion(null);
+            }
+            groupId = parser.getGroup();
+            if (groupId.contains("*")) {
+                groupIdPattern = toRegExp(groupId);
+            }
+            artifactId = parser.getArtifact();
+            if (artifactId.contains("*")) {
+                artifactIdPattern = toRegExp(artifactId);
+            }
+            versionString = parser.getVersion();
+            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) {
+                    MalformedURLException mue = new MalformedURLException("Can't parse version \"" + versionString + "\" as OSGi version object.");
+                    mue.initCause(e);
+                    throw mue;
+                }
+            }
+            type = parser.getType();
+            if (type != null && type.contains("*")) {
+                typePattern = toRegExp(type);
+            }
+            classifier = parser.getClassifier();
+            if (classifier != null && classifier.contains("*")) {
+                classifierPattern = toRegExp(classifier);
+            }
+        }
+    }
+
+    /**
+     * Converts a String with one special character (<code>*</code>) into working {@link Pattern}
+     * @param value
+     * @return
+     */
+    private Pattern toRegExp(String value) {
+        // TODO: escape all RegExp special chars that are valid path characters, only convert '*' into '.*'
+        return Pattern.compile(value
+                .replaceAll("\\.", "\\\\\\.")
+                .replaceAll("\\$", "\\\\\\$")
+                .replaceAll("\\^", "\\\\\\^")
+                .replaceAll("\\*", ".*")
+        );
+    }
+
+    /**
+     * Returns <code>true</code> if this location pattern matches other pattern.
+     * @param otherUri
+     * @return
+     */
+    public boolean matches(String otherUri) {
+        if (otherUri == null) {
+            return false;
+        }
+        if (originalPattern != null) {
+            // 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 {
+            other = new LocationPattern(otherUri);
+        } catch (MalformedURLException e) {
+            LOG.debug("Can't parse \"" + otherUri + "\" as Maven URI. Ignoring.");
+            return false;
+        }
+        if (other.versionRange != null) {
+            LOG.warn("Matched URI can't use version ranges: " + otherUri);
+            return false;
+        }
+
+        boolean match;
+
+        if (groupIdPattern == null) {
+            match = groupId.equals(other.groupId);
+        } else {
+            match = groupIdPattern.matcher(other.groupId).matches();
+        }
+        if (!match) {
+            return false;
+        }
+        if (artifactIdPattern == null) {
+            match = artifactId.equals(other.artifactId);
+        } else {
+            match = artifactIdPattern.matcher(other.artifactId).matches();
+        }
+        if (!match) {
+            return false;
+        }
+        if (versionRange != null && other.version != null) {
+            match = versionRange.contains(other.version);
+        } else {
+            match = version == null || version.equals(other.version);
+        }
+        if (!match) {
+            return false;
+        }
+        if (typePattern != null) {
+            match = typePattern.matcher(other.type == null ? "jar" : other.type).matches();
+        } else {
+            match = versionString == null || type.equals(other.type);
+        }
+        if (!match) {
+            return false;
+        }
+        if (classifierPattern != null) {
+            match = classifierPattern.matcher(other.classifier == null ? "" : other.classifier).matches();
+        } else if (classifier != null) {
+            match = classifier.equals(other.classifier);
+        } else {
+            match = other.classifierPattern == null;
+        }
+
+        return match;
+    }
+
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java
index 5e48bdf..7d6f3e2 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java
@@ -24,6 +24,7 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -43,7 +44,7 @@ import static org.apache.karaf.features.internal.resolver.ResolverUtil.getVersio
  */
 public final class Overrides {
 
-    protected static final String OVERRIDE_RANGE = "range";
+    public static final String OVERRIDE_RANGE = "range";
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Overrides.class);
 
@@ -110,7 +111,7 @@ public final class Overrides {
     }
 
     public static Set<String> loadOverrides(String overridesUrl) {
-        Set<String> overrides = new HashSet<>();
+        Set<String> overrides = new LinkedHashSet<>();
         try {
             if (overridesUrl != null) {
                 try (
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java
index 801ffeb..4f684a5 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java
@@ -1,103 +1,90 @@
 /*
- * 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
+ * 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
+ *   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.
+ * 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 java.net.URI;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 import org.apache.karaf.features.Repository;
 
-public class RepositoryCache {
-
-    private final Map<String, Repository> repositoryCache = new HashMap<>();
-    private final Blacklist blacklist;
-    
-    public RepositoryCache(Blacklist blacklist) {
-        this.blacklist = blacklist;
-    }
+/**
+ * <p>An interface for accessing repository/features information. Simple implementations
+ * may just map feature XMLs directly to JAXB model
+ * (see: {@link org.apache.karaf.features.internal.model.Features}).</p>
+ *
+ * <p>In more complex cases, additional processing (blacklisting, overrides, patching)
+ * may be performed.</p>
+ */
+public interface RepositoryCache {
 
-    public Repository create(URI uri, boolean validate) throws Exception {
-        return new RepositoryImpl(uri, blacklist, validate);
-    }
+    /**
+     * Creates {@link Repository} without adding it to cache
+     * @param uri an URI (e.g., <code>mvn:groupId/artifactId/version/xml/features</code> of repository
+     * @param validate whether to perform XML Schema validation of loaded features XML
+     * @return a {@link Repository} that may be inspected or added to cache
+     */
+    Repository create(URI uri, boolean validate);
 
-    public void addRepository(Repository repository) throws Exception {
-        String repoUriSt = repository.getURI().toString();
-        repositoryCache.put(repoUriSt, repository);
-    }
+    /**
+     * Adds existing {@link Repository} to be tracked/managed by this cache and later be available e.g., via
+     * {@link #getRepository(String)}
+     * @param repository existing repository to add to cache
+     */
+    void addRepository(Repository repository);
 
-    public void removeRepository(URI repositoryUri) throws Exception {
-        List<String> toRemove = new ArrayList<>();
-        toRemove.add(repositoryUri.toString());
-        while (!toRemove.isEmpty()) {
-            Repository rep = repositoryCache.remove(toRemove.remove(0));
-            if (rep != null) {
-                for (URI u : rep.getRepositories()) {
-                    toRemove.add(u.toString());
-                }
-            }
-        }
-    }
+    /**
+     * Removes existing {@link Repository} by its {@link URI}
+     * @param repositoryUri {@link URI} of the {@link Repository} to remove
+     */
+    void removeRepository(URI repositoryUri);
 
-    public Repository[] listRepositories() {
-        return repositoryCache.values().toArray(new Repository[repositoryCache.size()]);
-    }
+    /**
+     * Gets {@link Repository} by its {@link URI}
+     * @param uri {@link URI} of the repository
+     * @return {@link Repository} as it's stored inside the cache
+     */
+    Repository getRepository(String uri);
 
-    public Repository[] listMatchingRepositories(Set<String> uris) throws Exception {
-        return repositoryCache.values().stream()
-                .filter(r -> uris.contains(r.getURI().toString()))
-                .toArray(Repository[]::new);
-    }
+    /**
+     * Gets {@link Repository} by its name
+     * @param name Name of the repository
+     * @return {@link Repository} as it's stored inside the cache
+     */
+    Repository getRepositoryByName(String name);
 
-    public Repository getRepositoryByName(String name) throws Exception {
-        for (Repository repo : this.repositoryCache.values()) {
-            if (name.equals(repo.getName())) {
-                return repo;
-            }
-        }
-        return null;
-    }
+    /**
+     * Returns an array of all cached {@link Repository repositories}
+     * @return list of all {@link Repository repositories}
+     */
+    Repository[] listRepositories();
 
-    public Repository getRepository(String uri) {
-        return repositoryCache.get(uri);
-    }
+    /**
+     * Returns an array of cached {@link Repository repositories} for a set of {@link URI repository URIs}
+     * @return list of matched {@link Repository repositories}
+     */
+    Repository[] listMatchingRepositories(Set<String> uris);
 
     /**
-     * Returns a set containing the given repository and all its dependencies recursively
+     * Returns a set of {@link Repository repositories} including passed repository and all referenced repositories.
+     * @param repo A {@link Repository}, that possibly references other feature repositories.
+     * @return A closure of {@link Repository repositories}
      */
-    public Set<Repository> getRepositoryClosure(Repository repo) throws Exception {
-        Set<Repository> closure = new HashSet<>();
-        Deque<Repository> remaining = new ArrayDeque<>(Collections.singleton(repo));
-        while (!remaining.isEmpty()) {
-            Repository rep = remaining.removeFirst();
-            if (closure.add(rep)) {
-                for (URI uri : rep.getRepositories()) {
-                    remaining.add(getRepository(uri.toString()));
-                }
-            }
-        }
-        return closure;
-    }
+    Set<Repository> getRepositoryClosure(Repository repo);
 
 }
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
similarity index 74%
copy from features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java
copy to features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
index 801ffeb..d33b39d 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCache.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryCacheImpl.java
@@ -29,25 +29,37 @@ import java.util.Set;
 
 import org.apache.karaf.features.Repository;
 
-public class RepositoryCache {
+/**
+ * Implementation of {@link RepositoryCache} that makes use of {@link FeaturesProcessor} to alter feature
+ * definitions after reading them from XML file.
+ */
+public class RepositoryCacheImpl implements RepositoryCache {
 
     private final Map<String, Repository> repositoryCache = new HashMap<>();
-    private final Blacklist blacklist;
-    
-    public RepositoryCache(Blacklist blacklist) {
-        this.blacklist = blacklist;
+    private final FeaturesProcessor featuresProcessor;
+
+    public RepositoryCacheImpl(FeaturesProcessor featuresProcessor) {
+        this.featuresProcessor = featuresProcessor;
     }
 
-    public Repository create(URI uri, boolean validate) throws Exception {
-        return new RepositoryImpl(uri, blacklist, validate);
+    @Override
+    public Repository create(URI uri, boolean validate) {
+        RepositoryImpl repository = new RepositoryImpl(uri, validate);
+        if (featuresProcessor != null) {
+            repository.setBlacklisted(featuresProcessor.isRepositoryBlacklisted(uri));
+            repository.processFeatures(featuresProcessor);
+        }
+        return repository;
     }
 
-    public void addRepository(Repository repository) throws Exception {
+    @Override
+    public void addRepository(Repository repository) {
         String repoUriSt = repository.getURI().toString();
         repositoryCache.put(repoUriSt, repository);
     }
 
-    public void removeRepository(URI repositoryUri) throws Exception {
+    @Override
+    public void removeRepository(URI repositoryUri) {
         List<String> toRemove = new ArrayList<>();
         toRemove.add(repositoryUri.toString());
         while (!toRemove.isEmpty()) {
@@ -60,17 +72,20 @@ public class RepositoryCache {
         }
     }
 
+    @Override
     public Repository[] listRepositories() {
         return repositoryCache.values().toArray(new Repository[repositoryCache.size()]);
     }
 
-    public Repository[] listMatchingRepositories(Set<String> uris) throws Exception {
+    @Override
+    public Repository[] listMatchingRepositories(Set<String> uris) {
         return repositoryCache.values().stream()
                 .filter(r -> uris.contains(r.getURI().toString()))
                 .toArray(Repository[]::new);
     }
 
-    public Repository getRepositoryByName(String name) throws Exception {
+    @Override
+    public Repository getRepositoryByName(String name) {
         for (Repository repo : this.repositoryCache.values()) {
             if (name.equals(repo.getName())) {
                 return repo;
@@ -79,6 +94,7 @@ public class RepositoryCache {
         return null;
     }
 
+    @Override
     public Repository getRepository(String uri) {
         return repositoryCache.get(uri);
     }
@@ -86,7 +102,8 @@ public class RepositoryCache {
     /**
      * Returns a set containing the given repository and all its dependencies recursively
      */
-    public Set<Repository> getRepositoryClosure(Repository repo) throws Exception {
+    @Override
+    public Set<Repository> getRepositoryClosure(Repository repo) {
         Set<Repository> closure = new HashSet<>();
         Deque<Repository> remaining = new ArrayDeque<>(Collections.singleton(repo));
         while (!remaining.isEmpty()) {
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryImpl.java
index 0e9d703..31aadaf 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/RepositoryImpl.java
@@ -33,28 +33,34 @@ import org.apache.karaf.features.internal.model.JaxbUtil;
  */
 public class RepositoryImpl implements Repository {
 
+    /** {@link URI original URI} of the resource where feature declarations were loaded from */
     private final URI uri;
-    private final Blacklist blacklist;
+
+    /** Transformed {@link Features model} of the repository */
     private Features features;
-    
+
+    private boolean blacklisted;
+
     public RepositoryImpl(URI uri) {
-        this(uri, null, false);
+        this(uri, false);
     }
 
-    public RepositoryImpl(URI uri, Blacklist blacklist, boolean validate) {
+    public RepositoryImpl(URI uri, boolean validate) {
         this.uri = uri;
-        this.blacklist = blacklist;
         load(validate);
     }
 
+    @Override
     public URI getURI() {
         return uri;
     }
 
+    @Override
     public String getName() {
         return features.getName();
     }
 
+    @Override
     public URI[] getRepositories() {
         return features.getRepository().stream()
                 .map(String::trim)
@@ -62,6 +68,7 @@ public class RepositoryImpl implements Repository {
                 .toArray(URI[]::new);
     }
 
+    @Override
     public URI[] getResourceRepositories() {
         return features.getResourceRepository().stream()
                 .map(String::trim)
@@ -69,27 +76,39 @@ public class RepositoryImpl implements Repository {
                 .toArray(URI[]::new);
     }
 
+    @Override
     public Feature[] getFeatures() {
         return features.getFeature()
                 .toArray(new Feature[features.getFeature().size()]);
     }
 
+    @Override
+    public boolean isBlacklisted() {
+        return blacklisted;
+    }
+
+    public void setBlacklisted(boolean blacklisted) {
+        this.blacklisted = blacklisted;
+    }
 
     private void load(boolean validate) {
         if (features == null) {
-            try (
-                    InputStream inputStream = new InterruptibleInputStream(uri.toURL().openStream())
-            ) {
+            try (InputStream inputStream = new InterruptibleInputStream(uri.toURL().openStream())) {
                 features = JaxbUtil.unmarshal(uri.toASCIIString(), inputStream, validate);
-                if (blacklist != null) {
-                    blacklist.blacklist(features);
-                }
             } catch (Exception e) {
                 throw new RuntimeException(e.getMessage() + " : " + uri, e);
             }
         }
     }
 
+    /**
+     * An extension point to alter {@link Features JAXB model of features}
+     * @param processor
+     */
+    public void processFeatures(FeaturesProcessor processor) {
+        processor.process(features);
+    }
+
     static class InterruptibleInputStream extends FilterInputStream {
         InterruptibleInputStream(InputStream in) {
             super(in);
@@ -121,5 +140,5 @@ public class RepositoryImpl implements Repository {
     public String toString() {
         return getURI().toString();
     }
-}
 
+}
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
index f8f477c..b89947b 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
@@ -24,19 +24,35 @@ import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.karaf.features.internal.util.MapUtils;
 
+/**
+ * <p>Representation of the state of system from the point of view of <em>requirements</em>.
+ * It's a collection of:<ul>
+ *     <li>used repositories</li>
+ *     <li>region -&gt; requirements</li>
+ *     <li>region -&gt; installed features</li>
+ *     <li>region -&gt; installed features -&gt; state of feature installation</li>
+ *     <li>region -&gt; bundle ids</li>
+ *     <li>bundle id -&gt; checksum</li>
+ * </ul></p>
+ * <p>State is replaced (swapped) after uninstalling/updating/installing all the bundles as requested, but
+ * before resolving/refreshing them.</p>
+ */
 public class State {
 
     public final AtomicBoolean bootDone = new AtomicBoolean();
     public final Set<String> repositories = new TreeSet<>();
     
-    // Map from region name to Set of feature requirements (name/version range)
+    /** Map from region name to Set of feature requirements (name/version range) */
     public final Map<String, Set<String>> requirements = new HashMap<>();
-    // Map from region name to Set of feature id (name/version)
+    /** Map from region name to Set of feature id (name/version) */
     public final Map<String, Set<String>> installedFeatures = new HashMap<>();
     
-    // State of features by region and feature id (name/version)
+    /** State of features by region and feature id (name/version) */
     public final Map<String, Map<String, String>> stateFeatures = new HashMap<>();
+
+    /** Map from region name to Set of installed bundle ids */
     public final Map<String, Set<Long>> managedBundles = new HashMap<>();
+    /** Map from bundle id to bundle's java.util.zip.CRC32 */
     public final Map<Long, Long> bundleChecksums = new HashMap<>();
 
     public State copy() {
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
new file mode 100644
index 0000000..d0e5d48
--- /dev/null
+++ b/features/core/src/main/resources/org/apache/karaf/features/karaf-features-processing-1.0.0.xsd
@@ -0,0 +1,236 @@
+<?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.
+-->
+<xs:schema elementFormDefault="qualified"
+        targetNamespace="http://karaf.apache.org/xmlns/features-processing/v1.0.0"
+        xmlns:tns="http://karaf.apache.org/xmlns/features-processing/v1.0.0"
+        xmlns:features="http://karaf.apache.org/xmlns/features/v1.5.0"
+        xmlns:xs="http://www.w3.org/2001/XMLSchema">
+
+    <xs:import namespace="http://karaf.apache.org/xmlns/features/v1.5.0" />
+
+    <xs:element name="featuresProcessing" type="tns:featuresProcessing" />
+
+    <xs:complexType name="featuresProcessing">
+        <xs:annotation>
+            <xs:documentation><![CDATA[Configuration of FeaturesProcessor that may modify feature definitions
+after reading them from features XML file and before using them by FeaturesService.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:choice minOccurs="0" maxOccurs="unbounded">
+            <xs:element name="blacklistedRepositories" type="tns:blacklistedRepositories" />
+            <xs:element name="blacklistedFeatures" type="tns:blacklistedFeatures" />
+            <xs:element name="blacklistedBundles" type="tns:blacklistedBundles" />
+            <xs:element name="overrideBundleDependency" type="tns:overrideBundleDependency" />
+            <xs:element name="bundleReplacements" type="tns:bundleReplacements" />
+            <xs:element name="featureReplacements" type="tns:featureReplacements" />
+        </xs:choice>
+    </xs:complexType>
+
+    <xs:complexType name="blacklistedRepositories">
+        <xs:annotation>
+            <xs:documentation><![CDATA[A list of feature repository URIs (e.g., mvn:group/artifact/version/xml/features)
+that should be blacklisted - they can't be added to FeaturesService and can't be searched for features to install.
+
+Repository URI should use 'mvn:' scheme, may use '*' glob (not RegExp) for all components except versions and
+version ranges as maven versions, e.g., "mvn:group/artifact/[3,5)/xml/*features*". At least groupId and artifactId
+should be specified. In most abstract case, "mvn:*/*" describes all maven URIs.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="repository" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="blacklistedFeatures">
+        <xs:annotation>
+            <xs:documentation><![CDATA[A list of feature names (and related OSGi clause attributes) that should be
+blacklisted.
+
+Attempt to install such feature will be prohibited, but such feature is still available in `feature:list`
+output and marked as blacklisted. When custom Karaf distribution is assembled, blacklisted features' bundles are not
+taken into account (are not declared in "etc/startup.properties" and "etc/org.apache.karaf.features.cfg" and are not
+copied to "system/" directory).
+
+Feature names may use '*' glob (not RegExp) in names, "range" attribute is used in "etc/blacklisted.properties" file
+and may be used to specify version range, e.g., "*jclouds*;range=[1,3)". When using XML to configure blacklisted
+features, "range" manifest header attribute should be specified in "version" XML attribute.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="feature" type="tns:blacklistedFeature" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="blacklistedFeature">
+        <xs:annotation>
+            <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="3.0" - feature with version 3.0 only will be blacklisted
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:simpleContent>
+            <xs:extension base="xs:string">
+                <xs:attribute name="version" type="xs:string" />
+            </xs:extension>
+        </xs:simpleContent>
+    </xs:complexType>
+
+    <xs:complexType name="blacklistedBundles">
+        <xs:annotation>
+            <xs:documentation><![CDATA[A list of bundle URIs that should be blacklisted.
+When feature is installed, all blacklisted bundles are skipped.
+
+When custom Karaf distribution is assembled, blacklisted bundles are not taken into account (are not declared in
+"etc/startup.properties" and are not copied to "system/" directory).
+
+Bundle URIs may use '*' globs (not RegExp), e.g., "*jclouds*;url=mvn:...". Both header-like format may be used
+(as in etc/blacklisted.properties, triggered, when ';' is used) or plain URI format for bundle locations.
+
+Bundle URIs should use 'mvn:' scheme, may use '*' glob (not RegExp) for all components except versions and
+version ranges may be specified as maven versions, e.g., "mvn:group/artifact/[3,5)/classifier". At least
+groupId and artifactId should be specified. In most abstract case, "mvn:*/*" describes all maven URIs.
+
+This element may be used instead of "etc/blacklisted.properties" file for bundle blacklisting.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="bundle" type="xs:anyURI" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="overrideBundleDependency">
+        <xs:annotation>
+            <xs:documentation><![CDATA[FeaturesService configuration uses "dependency" flags for bundles and features
+declared in features XML file. This flag instructs the service to consider such "dependency" item only if current
+state of system doesn't provide required capabilities. When such flag is chosen wisely it greatly improves consistency
+of installed features and bundles. However many external features XML files (which are out of control, or simply are no
+longer maintained) do not use this flag. With "overrideBundleDependency" element it's possible to externally
+override this flag for any repository, feature or particular bundles.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="repository" type="tns:overrideDependency" minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="feature" type="tns:overrideFeatureDependency" minOccurs="0" maxOccurs="unbounded" />
+            <xs:element name="bundle" type="tns:overrideDependency" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="overrideDependency">
+        <xs:annotation>
+            <xs:documentation><![CDATA[An URI of depedency (bundle or repository) should use 'mvn:' scheme.
+Maven schemes allow parsing version/version ranges.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:attribute name="uri" type="xs:anyURI" />
+        <xs:attribute name="dependency" type="xs:boolean" default="false" />
+    </xs:complexType>
+
+    <xs:complexType name="overrideFeatureDependency">
+        <xs:attribute name="name" type="xs:string" />
+        <xs:attribute name="version" type="xs:string" />
+        <xs:attribute name="dependency" type="xs:boolean" default="false" />
+    </xs:complexType>
+
+    <xs:complexType name="bundleReplacements">
+        <xs:annotation>
+            <xs:documentation><![CDATA[When feature is loaded from features XML file, "etc/overrides.properties" may
+be used to change the version of one/more of the bundles from given feature. Override is used only when symbolic
+names match. With bundleReplacements element, its possible to do the same and more. It's possible to replace a bundle
+with totally different bundle (in terms of OSGi's Symbolic-Name and Maven groupId/artifactId/version). That's useful
+especially with JavaEE API bundles, where single API may be provided by different bundles (ServiceMix, Geronimo,
+JBoss, javax.*, ...).
+
+Bundle replacement doesn't use globs - just version ranges to match replacement candidates
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="bundle" type="tns:overrideBundle" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="overrideBundle">
+        <xs:annotation>
+            <xs:documentation><![CDATA[For each bundle of any feature we can point bundle's URI to different location.
+
+URIs should use 'mvn:' scheme, to allow version/version ranges parsing. Replacement URI should use concrete version
+- no range. Original URI may use version ranges to indicate which version is eligible for replacement. E.g,:
+ * mvn:groupId/artifactId/[1,2) - bundle location will be overriden for versions 1, 1.1, 1.4.3, 1.9.99, ...
+ * mvn:groupId/artifactId/[2,*) - bundle location will be overriden for version 2.0, 3.1, 4.2, ...
+ * mvn:groupId/artifactId/3 - bundle location will be overriden for version 3 (3.0, 3.0.0) only (i.e., [3.0.0,3.0.0]
+URIs can't use globs for Maven components (groupId, artifactId, ...).
+
+mode="osgi" is used for etc/overrides.properties compatibility - symbolic name should be equal (requires
+resource's/bundle's manifest parsing) and target/replacement bundle's version shold be lower to perform replacement.
+When reading etc/overrides.properties, groupId and artifactId will match anyway, but if configured in XML, they do
+not have to match.
+
+with mode="maven", only location matching is done and target version is not compared - this is useful
+to replace bundle using different groupId/artifactId. The most common case is e.g.,
+"org.eclipse.jetty.orbit/javax.servlet/[3,4)" -> "org.apache.geronimo.specs/geronimo-servlet_3.0_spec/1.0" replacement.
+This is new mode (comparing to etc/overrides.properties).
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:attribute name="originalUri" type="xs:anyURI" />
+        <xs:attribute name="replacement" type="xs:anyURI" />
+        <xs:attribute name="mode" type="tns:bundleOverrideMode" default="osgi" />
+    </xs:complexType>
+
+    <xs:simpleType name="bundleOverrideMode">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="osgi" />
+            <xs:enumeration value="maven" />
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:complexType name="featureReplacements">
+        <xs:annotation>
+            <xs:documentation><![CDATA[This element may be used to completely "rewrite" any feature (by name and
+version). Depending on mode of modification (replace, merge) it's possible to replace any feature definition, or just
+add/remove some items (usually bundles) to/from original feature.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="replacement" type="tns:overrideFeature" minOccurs="0" maxOccurs="unbounded" />
+        </xs:sequence>
+    </xs:complexType>
+
+    <xs:complexType name="overrideFeature">
+        <xs:annotation>
+            <xs:documentation><![CDATA[Any feature may be "overriden" simply by including its changed definition
+according to http://karaf.apache.org/xmlns/features/v1.5.0 XML schema.
+]]></xs:documentation>
+        </xs:annotation>
+        <xs:sequence>
+            <xs:element name="feature" type="features:feature" />
+        </xs:sequence>
+        <xs:attribute name="mode" type="tns:featureOverrideMode" default="replace" />
+    </xs:complexType>
+
+    <xs:simpleType name="featureOverrideMode">
+        <xs:restriction base="xs:string">
+            <xs:enumeration value="replace" />
+            <xs:enumeration value="merge" />
+            <xs:enumeration value="remove" />
+        </xs:restriction>
+    </xs:simpleType>
+
+</xs:schema>
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/BlacklistTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/BlacklistTest.java
index 8e835ea..6c5d7e2 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/BlacklistTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/BlacklistTest.java
@@ -16,63 +16,76 @@
  */
 package org.apache.karaf.features.internal.service;
 
-import static org.junit.Assert.assertTrue;
-
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.util.Arrays;
-import java.util.Collections;
+import java.util.HashSet;
 import java.util.stream.Stream;
 
 import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
 import org.junit.Test;
 
+import static org.junit.Assert.assertTrue;
+
 public class BlacklistTest {
 
     @Test
-    public void testBlacklistFeatureWithRange() {
+    public void testBlacklistFeatureWithRange() throws IOException {
         Stream<Feature> features = blacklistWith("spring;range=\"[2,3)\"");
-        assertTrue(features.noneMatch(f -> f.getId().equals("spring/2.5.6.SEC02")));
+        assertTrue(features.noneMatch(f -> f.getId().equals("spring/2.5.6.SEC02") && !f.isBlacklisted()));
     }
 
     @Test
-    public void testBlacklistFeatureWithVersion() {
+    public void testBlacklistFeatureWithVersion() throws IOException {
         Stream<Feature> features = blacklistWith("spring;range=2.5.6.SEC02");
-        assertTrue(features.noneMatch(f -> f.getId().equals("spring/2.5.6.SEC02")));
+        assertTrue(features.noneMatch(f -> f.getId().equals("spring/2.5.6.SEC02") && !f.isBlacklisted()));
     }
 
     @Test
-    public void testBlacklistFeatureWithoutVersion() {
+    public void testBlacklistFeatureWithoutVersion() throws IOException {
         Stream<Feature> features = blacklistWith("spring");
-        assertTrue(features.noneMatch(f -> f.getId().startsWith("spring/")));
+        assertTrue(features.noneMatch(f -> f.getId().startsWith("spring/") && !f.isBlacklisted()));
     }
 
     @Test
-    public void testBlacklistBundle() {
+    public void testBlacklistBundle() throws IOException {
         String blacklisted = "mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jasypt/1.7_1";
         Stream<Feature> features = blacklistWith(blacklisted);
         Stream<BundleInfo> bundles = features.flatMap(f -> f.getBundles().stream());
-        assertTrue(bundles.noneMatch(b -> b.getLocation().equals(blacklisted)));
+        assertTrue(bundles.noneMatch(b -> b.getLocation().equals(blacklisted) && !b.isBlacklisted()));
     }
 
     @Test
     public void testBlacklistLoad() throws URISyntaxException {
         Blacklist blacklist = new Blacklist(getClass().getResource("blacklist.txt").toExternalForm());
-        RepositoryImpl repo = new RepositoryImpl(getClass().getResource("f02.xml").toURI(), blacklist, true);
-        Stream<Feature> features = Arrays.asList(repo.getFeatures()).stream();
-        assertTrue(features.noneMatch(f -> f.getId().equals("spring/2.5.6.SEC02")));
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource("f02.xml").toURI(), true);
+        Stream<Feature> features = Arrays.stream(repo.getFeatures());
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig());
+        processor.getInstructions().postUnmarshall(blacklist, new HashSet<>());
+        repo.processFeatures(processor);
+        assertTrue(features.noneMatch(f -> f.getId().equals("spring/2.5.6.SEC02") && !f.isBlacklisted()));
     }
 
-    private Stream<org.apache.karaf.features.Feature> blacklistWith(String blacklistClause) {
+    private Stream<org.apache.karaf.features.Feature> blacklistWith(String blacklistClause) throws IOException {
         URI uri;
         try {
             uri = getClass().getResource("f02.xml").toURI();
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
-        Blacklist blacklist = new Blacklist(Collections.singletonList(blacklistClause));
-        RepositoryImpl features = new RepositoryImpl(uri, blacklist, true);
-        return Arrays.asList(features.getFeatures()).stream();
+        File blacklistedProperties = File.createTempFile("blacklisted-", ".properties", new File("target"));
+        try (FileOutputStream fos = new FileOutputStream(blacklistedProperties)) {
+            fos.write(blacklistClause.getBytes("UTF-8"));
+        }
+        RepositoryImpl features = new RepositoryImpl(uri, true);
+        FeaturesServiceConfig config = new FeaturesServiceConfig(null, FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE, FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE, null, 1, 0, 0, blacklistedProperties.toURI().toString(), null, null);
+        features.processFeatures(new FeaturesProcessorImpl(config));
+        return Arrays.stream(features.getFeatures());
     }
+
 }
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesProcessorTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesProcessorTest.java
new file mode 100644
index 0000000..ce96722
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesProcessorTest.java
@@ -0,0 +1,200 @@
+/*
+ * 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 java.net.URI;
+import javax.xml.bind.JAXBContext;
+import javax.xml.bind.Marshaller;
+
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.version.VersionRange;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.internal.model.processing.BundleReplacements;
+import org.apache.karaf.features.internal.model.processing.FeaturesProcessing;
+import org.apache.karaf.features.internal.model.processing.ObjectFactory;
+import org.apache.karaf.util.maven.Parser;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+public class FeaturesProcessorTest {
+
+    public static Logger LOG = LoggerFactory.getLogger(FeaturesProcessorTest.class);
+
+    @Test
+    public void jaxbModelForProcessor() throws Exception {
+        JAXBContext jaxb = JAXBContext.newInstance(ObjectFactory.class);
+        FeaturesProcessing fp = (FeaturesProcessing) jaxb.createUnmarshaller().unmarshal(getClass().getResourceAsStream("/org/apache/karaf/features/internal/service/org.apache.karaf.features.xml"));
+        assertThat(fp.getFeatureReplacements().getReplacements().get(0).getFeature().getName(), equalTo("pax-jsf-resources-support"));
+
+        Marshaller marshaller = jaxb.createMarshaller();
+        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
+        marshaller.marshal(fp, System.out);
+    }
+
+    @Test
+    public void versionRanges() {
+        LOG.info(new VersionRange("1", false).toString());
+        LOG.info(new VersionRange("[2,3)", true).toString());
+    }
+
+    @Test
+    public void mavenURIs() throws Exception {
+        Parser p = new Parser("group/artifact/[1,2)/xml/features*");
+        assertThat(p.getVersion(), equalTo("[1,2)"));
+        assertThat(p.getClassifier(), equalTo("features*"));
+
+        p = new Parser("org.springframework*/*cloud*/*");
+        assertThat(p.getVersion(), equalTo("*"));
+        assertThat(p.getArtifact(), equalTo("*cloud*"));
+        assertThat(p.getGroup(), equalTo("org.springframework*"));
+        assertThat(p.getType(), equalTo("jar"));
+        assertThat(p.getClassifier(), nullValue());
+
+        p = new Parser("org.ops4j/org.ops4j*/*//uber");
+        assertThat(p.getVersion(), equalTo("*"));
+        assertThat(p.getArtifact(), equalTo("org.ops4j*"));
+        assertThat(p.getGroup(), equalTo("org.ops4j"));
+        assertThat(p.getType(), equalTo("jar"));
+        assertThat(p.getClassifier(), equalTo("uber"));
+    }
+
+    @Test
+    public void readingLegacyOverrides() {
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig(
+                "file:src/test/resources/org/apache/karaf/features/internal/service/overrides2.properties",
+                null, null));
+
+        FeaturesProcessing instructions = processor.getInstructions();
+        BundleReplacements bundleReplacements = instructions.getBundleReplacements();
+        assertThat(bundleReplacements.getOverrideBundles().size(), equalTo(5));
+        BundleReplacements.OverrideBundle o1 = bundleReplacements.getOverrideBundles().get(0);
+        assertThat(o1.getOriginalUri(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/[2.3.0,2.3.0.61033X)"));
+        assertThat(o1.getReplacement(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.61033X"));
+        BundleReplacements.OverrideBundle o2 = bundleReplacements.getOverrideBundles().get(1);
+        assertThat(o2.getOriginalUri(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/[2.2.0,2.4.0)"));
+        assertThat(o2.getReplacement(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.61033X"));
+        BundleReplacements.OverrideBundle o3 = bundleReplacements.getOverrideBundles().get(2);
+        assertThat(o3.getOriginalUri(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.resources/[2.3.0,2.3.14)"));
+        assertThat(o3.getReplacement(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.resources/2.3.14"));
+        BundleReplacements.OverrideBundle o4 = bundleReplacements.getOverrideBundles().get(3);
+        assertThat(o4.getOriginalUri(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.kernel/[2.0.0,2.0.0]"));
+        assertThat(o4.getReplacement(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.kernel/2.3.14"));
+        BundleReplacements.OverrideBundle o5 = bundleReplacements.getOverrideBundles().get(4);
+        assertThat(o5.getOriginalUri(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.infinity/[1.0.0,*)"));
+        assertThat(o5.getReplacement(), equalTo("mvn:org.apache.karaf.admin/org.apache.karaf.admin.infinity/2.3.14"));
+    }
+
+    @Test
+    public void readingLegacyBlacklist() {
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig(
+                null,
+                "file:src/test/resources/org/apache/karaf/features/internal/service/blacklisted2.properties",
+                null));
+
+        FeaturesProcessing instructions = processor.getInstructions();
+        Blacklist blacklist = instructions.getBlacklist();
+        Clause[] clauses = blacklist.getClauses();
+        assertThat(clauses.length, equalTo(4));
+        assertTrue(blacklist.isFeatureBlacklisted("spring", "2.5.6.SEC02"));
+        assertFalse(blacklist.isFeatureBlacklisted("spring", "2.5.7.SEC02"));
+        assertTrue(blacklist.isFeatureBlacklisted("jclouds", "42"));
+
+        assertTrue(blacklist.isBundleBlacklisted("mvn:org.spring/spring-infinity/42"));
+        assertFalse(blacklist.isBundleBlacklisted("mvn:org.spring/spring-infinity/41"));
+        assertTrue(blacklist.isBundleBlacklisted("mvn:org.spring/spring-eternity/42"));
+        assertTrue(blacklist.isBundleBlacklisted("mvn:jclouds/jclouds/1"));
+    }
+
+    @Test
+    public void blacklistingRepositories() {
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig(
+                null, null,
+                "file:src/test/resources/org/apache/karaf/features/internal/service/fpi01.xml"));
+        URI uri = URI.create("file:src/test/resources/org/apache/karaf/features/internal/service/fp01.xml");
+        RepositoryImpl repo = (RepositoryImpl) new RepositoryCacheImpl(processor).create(uri, true);
+        assertThat(repo.getRepositories().length, equalTo(3));
+        assertFalse(repo.isBlacklisted());
+        assertFalse(processor.isRepositoryBlacklisted(repo.getRepositories()[0]));
+        assertTrue(processor.isRepositoryBlacklisted(repo.getRepositories()[1]));
+        assertFalse(processor.isRepositoryBlacklisted(repo.getRepositories()[2]));
+    }
+
+    @Test
+    public void blacklistingFeatures() {
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig(
+                null, null,
+                "file:src/test/resources/org/apache/karaf/features/internal/service/fpi01.xml"));
+        URI uri = URI.create("file:src/test/resources/org/apache/karaf/features/internal/service/fp02.xml");
+        RepositoryImpl repo = (RepositoryImpl) new RepositoryCacheImpl(processor).create(uri, true);
+
+        Feature[] features = repo.getFeatures();
+        assertTrue(features[0].isBlacklisted());
+        assertFalse(features[1].isBlacklisted());
+        assertTrue(features[2].isBlacklisted());
+        assertTrue(features[3].isBlacklisted());
+        assertFalse(features[4].isBlacklisted());
+    }
+
+    @Test
+    public void blacklistingBundles() {
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig(
+                null, null,
+                "file:src/test/resources/org/apache/karaf/features/internal/service/fpi01.xml"));
+        URI uri = URI.create("file:src/test/resources/org/apache/karaf/features/internal/service/fp03.xml");
+        RepositoryImpl repo = (RepositoryImpl) new RepositoryCacheImpl(processor).create(uri, true);
+
+        Feature f1 = repo.getFeatures()[0];
+        assertFalse(f1.getBundles().get(0).isBlacklisted());
+        assertFalse(f1.getBundles().get(1).isBlacklisted());
+        assertTrue(f1.getBundles().get(2).isBlacklisted());
+        assertTrue(f1.getBundles().get(3).isBlacklisted());
+        assertTrue(f1.getConditional().get(0).getBundles().get(0).isBlacklisted());
+    }
+
+    @Test
+    public void overridingBundles() {
+        FeaturesProcessorImpl processor = new FeaturesProcessorImpl(new FeaturesServiceConfig(
+                null, null,
+                "file:src/test/resources/org/apache/karaf/features/internal/service/fpi02.xml"));
+        URI uri = URI.create("file:src/test/resources/org/apache/karaf/features/internal/service/fp03.xml");
+        RepositoryImpl repo = (RepositoryImpl) new RepositoryCacheImpl(processor).create(uri, true);
+
+        Feature f1 = repo.getFeatures()[0];
+        assertFalse(f1.getBundles().get(0).isOverriden());
+        assertTrue(f1.getBundles().get(1).isOverriden());
+        assertThat(f1.getBundles().get(1).getLocation(), equalTo("mvn:commons-io/commons-io/1.3.5"));
+        assertThat(f1.getBundles().get(1).getOriginalLocation(), equalTo("mvn:commons-io/commons-io/1.3"));
+        assertTrue(f1.getBundles().get(2).isOverriden());
+        assertThat(f1.getBundles().get(2).getLocation(), equalTo("mvn:commons-codec/commons-codec/1.4.2"));
+        assertThat(f1.getBundles().get(2).getOriginalLocation(), equalTo("mvn:commons-codec/commons-codec/0.4"));
+        assertFalse(f1.getBundles().get(3).isOverriden());
+        assertTrue(f1.getConditional().get(0).getBundles().get(0).isOverriden());
+        assertThat(f1.getConditional().get(0).getBundles().get(0).getLocation(), equalTo("mvn:org.glassfish/something-strangest/4.3.1"));
+        assertThat(f1.getConditional().get(0).getBundles().get(0).getOriginalLocation(), equalTo("mvn:org.glassfish/something-strangest/4.3.0"));
+        assertFalse(f1.getConditional().get(0).getBundles().get(1).isOverriden());
+    }
+
+}
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
index e21c854..feee89d 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
@@ -84,7 +84,7 @@ public class FeaturesValidationTest {
 
     private Repository unmarshalAndValidate(String path) throws Exception {
         URI uri = getClass().getResource(path).toURI();
-        return new RepositoryImpl(uri, null, true);
+        return new RepositoryImpl(uri, true);
     }
 
 }
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
new file mode 100644
index 0000000..c0a7b31
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/LocationPatternTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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 java.net.MalformedURLException;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class LocationPatternTest {
+
+    @Test
+    public void matchingNonMavenUris() throws MalformedURLException {
+        assertTrue(new LocationPattern("file:1").matches("file:1"));
+        assertFalse(new LocationPattern("file:1").matches("file:2"));
+        assertFalse(new LocationPattern("file:*").matches(null));
+        assertTrue(new LocationPattern("http://*").matches("http://a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p/q.txt"));
+        assertTrue(new LocationPattern("file:/tmp/x*.txt").matches("file:/tmp/x1.txt"));
+        assertTrue(new LocationPattern("file:/tmp/x$2*.txt").matches("file:/tmp/x$24.txt"));
+        assertTrue(new LocationPattern("file:/tmp/x^2*.txt").matches("file:/tmp/x^24.txt"));
+    }
+
+    @Test
+    public void correctMvnLocationPatterns() {
+        boolean exception = false;
+        for (String p : new String[] {
+                "mvn:groupId/artifactId",
+                "mvn:groupId/artifactId/1",
+                "mvn:groupId/artifactId/1/t",
+                "mvn:groupId/artifactId/1/t/c",
+                "mvn:groupId/*",
+                "mvn:*/*",
+                "mvn:*/*/[0,*)/*/*",
+                "mvn:g/a/[1,2)",
+                "mvn:g/a/[1,*)",
+                "mvn:groupId/artifactId/[1.0.0,1.0.0.0)",
+                "mvn:groupId/artifactId/[1.0,1.0.0.0)",
+                "mvn:groupId/artifactId/[1,1.0.0.0)",
+        }) {
+            try {
+                new LocationPattern(p);
+            } catch (MalformedURLException ignored) {
+                exception |= true;
+            }
+        }
+        assertFalse("We should not fail for correct mvn: URIs", exception);
+    }
+
+    @Test
+    public void incorrectMvnLocationPatterns() {
+        boolean exception = true;
+        for (String p : new String[] {
+                "mvn:onlyGroupId",
+//                "mvn:groupId/artifactId/wrongVersion",
+                "mvn:groupId/artifactId/[1.2,2",
+                "mvn:groupId/artifactId/[1.2,",
+                "mvn:groupId/artifactId/[1.2",
+                "mvn:groupId/artifactId/[",
+//                "mvn:groupId/artifactId/*",
+                "mvn:groupId/artifactId/[wrongRange,wrongRange]",
+                "mvn:groupId/artifactId/(wrongRange,wrongRange]",
+                "mvn:groupId/artifactId/(wrongRange,3]",
+                "mvn:groupId/artifactId/[1,wrongRange)",
+                "mvn:groupId/artifactId/[1,1.2.3.4.5)",
+                "mvn:groupId/artifactId/[1,1.2.a)",
+                "mvn:groupId/artifactId/[1,1.a)",
+                "mvn:groupId/artifactId/[1,1)",
+                "mvn:groupId/artifactId/[1.0,1)",
+                "mvn:groupId/artifactId/[1.0.0,1)",
+                "mvn:groupId/artifactId/[1.0.0.0,1)",
+                "mvn:groupId/artifactId/[1.0.0.0,1.0)",
+                "mvn:groupId/artifactId/[1.0.0.0,1.0.0)",
+                "mvn:groupId/artifactId/[1.0.0.0,1.0.0.0)"
+        }) {
+            try {
+                new LocationPattern(p);
+                exception &= false;
+            } catch (MalformedURLException ignored) {
+            }
+        }
+        assertTrue("We should fail for all broken mvn: URIs", exception);
+    }
+
+    @Test
+    public void matchingMavenUrisWithoutPatterns() throws MalformedURLException {
+        assertTrue(new LocationPattern("mvn:g/a/1/t/c").matches("mvn:g/a/1/t/c"));
+        assertTrue(new LocationPattern("mvn:g/a/1//c").matches("mvn:g/a/1/jar/c"));
+        assertTrue(new LocationPattern("mvn:g/a/1").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:g/a").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:g/a").matches("mvn:g/a/1/jar"));
+        assertTrue("Special case - when there's no version, we don't match to \"jar\" type, but to all types",
+                new LocationPattern("mvn:g/a").matches("mvn:g/a/1/j"));
+        assertTrue(new LocationPattern("mvn:g/a").matches("mvn:g/a/1/t/c"));
+        assertTrue(new LocationPattern("mvn:g/a/1").matches("mvn:g/a/1/jar"));
+        assertFalse(new LocationPattern("mvn:g/a/1").matches("mvn:g/a/1/war"));
+        assertTrue(new LocationPattern("mvn:g/a/1").matches("mvn:g/a/1/jar/c"));
+        assertTrue(new LocationPattern("mvn:g/a/1").matches("mvn:g/a/1//c"));
+    }
+
+    @Test
+    public void matchingMavenUrisWithVersionRangesInPattern() throws MalformedURLException {
+        assertTrue(new LocationPattern("mvn:g/a/[1,1]").matches("mvn:g/a"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,1]").matches("mvn:g/a/1"));
+        assertFalse(new LocationPattern("mvn:g/a/[1,1.1)").matches("mvn:g/a/1.1"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,2)").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,2)").matches("mvn:g/a/1.1"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,2)").matches("mvn:g/a/1.9"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,2)").matches("mvn:g/a/1.9.9.BUILD-SNAPSHOT"));
+        assertFalse(new LocationPattern("mvn:g/a/[1,2)").matches("mvn:g/a/2.0"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,*)").matches("mvn:g/a/2.0"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,*)").matches("mvn:g/a/42.0"));
+        assertTrue(new LocationPattern("mvn:g/a/[1,*)").matches("mvn:g/a/9999.9999.9999.9999"));
+    }
+
+    @Test
+    public void matchingMavenUrisWithPatterns() throws MalformedURLException {
+        assertTrue(new LocationPattern("mvn:*/a/1").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:g/*/1").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:*/*/1").matches("mvn:g/a/1"));
+        assertFalse(new LocationPattern("mvn:*/*/[1,*)/jar").matches("mvn:g/a/1/war/c"));
+        assertFalse(new LocationPattern("mvn:*/*/[1,*)/jar").matches("mvn:g/a/1/war"));
+        assertTrue(new LocationPattern("mvn:*/*/[1,*)/jar").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:*/*/[1,*)/jar").matches("mvn:g/a/1//c"));
+        assertFalse(new LocationPattern("mvn:*/*/[1,*)/jar/c*").matches("mvn:g/a/1//d"));
+        assertTrue(new LocationPattern("mvn:*/*/[1,*)/jar/d*").matches("mvn:g/a/1//d"));
+        assertTrue(new LocationPattern("mvn:*/*").matches("mvn:g/a/1//c"));
+        assertTrue(new LocationPattern("mvn:*/*").matches("mvn:g/a/1/t/c"));
+        assertTrue(new LocationPattern("mvn:*/*").matches("mvn:g/a/1/t"));
+        assertTrue(new LocationPattern("mvn:*/*").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:*/*").matches("mvn:g/a"));
+        assertFalse(new LocationPattern("mvn:*/*/2/*/*").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:*/*/[1,2)/*/*").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:*/*/1/*/*").matches("mvn:g/a/1"));
+        assertTrue(new LocationPattern("mvn:*/*/1/*/*").matches("mvn:g/a/1//c"));
+        assertTrue(new LocationPattern("mvn:*/*/1/*/*").matches("mvn:g/a/1/jar/c"));
+        assertTrue(new LocationPattern("mvn:*/*/1/*/*").matches("mvn:g/a/1/t/c"));
+    }
+
+    @Test
+    public void matchingMavenUrisWithVersionRangesInUri() throws MalformedURLException {
+        assertFalse(new LocationPattern("mvn:g/a/[1,1]").matches("mvn:g/a/[1,1]"));
+    }
+
+}
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/OverridesTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/OverridesTest.java
index 033ac7c..d64685d 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/OverridesTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/OverridesTest.java
@@ -134,10 +134,10 @@ public class OverridesTest {
         Clause karafAdminCommand = null;
         Clause karafAdminCore = null;
         for (Clause clause : Parser.parseClauses(overrides.toArray(new String[overrides.size()]))) {
-            if (clause.getName().equals("mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.redhat-61033X")) {
+            if (clause.getName().equals("mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.61033X")) {
                 karafAdminCommand = clause;
             }
-            if (clause.getName().equals("mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.redhat-61033X")) {
+            if (clause.getName().equals("mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.61033X")) {
                 karafAdminCore = clause;
             }
         }
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/RepositoryCacheTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/RepositoryCacheTest.java
new file mode 100644
index 0000000..e8e9fd0
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/RepositoryCacheTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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 java.net.URI;
+
+import org.apache.karaf.features.Repository;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+
+public class RepositoryCacheTest {
+
+    private String pkgs;
+
+    @Before
+    public void init() {
+        String _pkgs = pkgs = System.getProperty("java.protocol.handler.pkgs");
+        if (_pkgs == null || "".equals(_pkgs.trim())) {
+            _pkgs = "";
+        } else {
+            _pkgs += "|";
+        }
+        _pkgs += this.getClass().getPackage().getName();
+        System.setProperty("java.protocol.handler.pkgs", _pkgs);
+    }
+
+    @After
+    public void cleanup() {
+        if (pkgs != null) {
+            System.setProperty("java.protocol.handler.pkgs", pkgs);
+        }
+    }
+
+    @Test
+    @Ignore("Ignoring to check if it's real problem")
+    public void refCountForIncludedRepositories() throws Exception {
+        RepositoryCacheImpl cache = new RepositoryCacheImpl(null);
+        Repository repo1 = cache.create(getClass().getResource("/org/apache/karaf/features/repo1.xml").toURI(), false);
+        Repository repo2 = cache.create(getClass().getResource("/org/apache/karaf/features/repo2.xml").toURI(), false);
+        cache.addRepository(repo1);
+        cache.addRepository(repo2);
+        cache.addRepository(new RepositoryImpl(URI.create("urn:r1"), false));
+        assertNotNull(cache.getRepository("urn:r1"));
+
+        cache.removeRepository(repo2.getURI());
+        assertNotNull("Repository referenced from two different repositories should not be cascade-removed",
+                cache.getRepository("urn:r1"));
+    }
+
+}
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/urn/Handler.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/urn/Handler.java
new file mode 100644
index 0000000..7d366de
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/urn/Handler.java
@@ -0,0 +1,35 @@
+/*
+ * 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.urn;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+
+public class Handler extends URLStreamHandler {
+
+    @Override
+    protected URLConnection openConnection(URL u) throws IOException {
+        String name = new File(u.getPath()).getName();
+        return getClass().getResource("/org/apache/karaf/features/" + name + ".xml").openConnection();
+    }
+
+}
diff --git a/features/core/src/test/resources/log4j.properties b/features/core/src/test/resources/log4j.properties
new file mode 100644
index 0000000..696136e
--- /dev/null
+++ b/features/core/src/test/resources/log4j.properties
@@ -0,0 +1,35 @@
+## ---------------------------------------------------------------------------
+## 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.
+## ---------------------------------------------------------------------------
+
+#
+# The logging properties used during tests..
+#
+log4j.rootLogger=DEBUG, console, file
+log4j.logger.org.apache.karaf.features.internal.service=TRACE
+
+# Console will only display warnnings
+log4j.appender.console=org.apache.log4j.ConsoleAppender
+log4j.appender.console.layout=org.apache.log4j.PatternLayout
+log4j.appender.console.layout.ConversionPattern=%d{ISO8601} [%-5.5p] {%t} %c{1} (%F:%L) : %m%n
+#log4j.appender.console.threshold=WARN
+
+# File appender will contain all info messages
+log4j.appender.file=org.apache.log4j.FileAppender
+log4j.appender.file.layout=org.apache.log4j.PatternLayout
+log4j.appender.file.layout.ConversionPattern=%d | %-5p | %m | %c | %t%n
+log4j.appender.file.file=target/test.log
+log4j.appender.file.append=true
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties b/features/core/src/test/resources/org/apache/karaf/features/internal/service/blacklisted2.properties
similarity index 80%
copy from features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties
copy to features/core/src/test/resources/org/apache/karaf/features/internal/service/blacklisted2.properties
index d34fa7e..e4247ac 100644
--- a/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/blacklisted2.properties
@@ -18,6 +18,9 @@
 #
 ################################################################################
 
-# Sample etc/overrides.properties file for testing purposes
-mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.redhat-61033X
-mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.redhat-61033X;range=[2.3.0,2.5)
+spring;range=2.5.6.SEC02
+
+jclouds;url=mvn:jclouds/jclouds/1
+
+spring;type=bundle;url=mvn:org.spring/spring-infinity/42
+mvn:org.spring/spring-eternity/42
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp01.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp01.xml
new file mode 100644
index 0000000..99407ac
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp01.xml
@@ -0,0 +1,26 @@
+<?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.
+-->
+<features name="fp01" xmlns="http://karaf.apache.org/xmlns/features/v1.5.0">
+
+    <repository>mvn:org.hibernate/hibernate-validator-osgi-karaf-features/4.1/xml/features</repository>
+    <repository>mvn:org.hibernate/hibernate-validator-osgi-karaf-features/5.2/xml/features</repository>
+    <repository>mvn:org.hibernate/hibernate-validator-osgi-karaf-features/5.3/xml/features</repository>
+
+</features>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp02.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp02.xml
new file mode 100644
index 0000000..94d01cf
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp02.xml
@@ -0,0 +1,28 @@
+<?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.
+-->
+<features name="fp02" xmlns="http://karaf.apache.org/xmlns/features/v1.5.0">
+
+    <feature name="f1" />
+    <feature name="f2a" version="42" />
+    <feature name="f3a" version="1.3" />
+    <feature name="f4" version="4" />
+    <feature name="f4" version="5" />
+
+</features>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp03.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp03.xml
new file mode 100644
index 0000000..ad621e3
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fp03.xml
@@ -0,0 +1,34 @@
+<?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.
+-->
+<features name="fp02" xmlns="http://karaf.apache.org/xmlns/features/v1.5.0">
+
+    <feature name="f5" version="4.2">
+        <bundle>mvn:commons-io/commons-io/1.2</bundle>
+        <bundle>mvn:commons-io/commons-io/1.3</bundle>
+        <bundle>mvn:commons-codec/commons-codec/0.4</bundle>
+        <bundle>mvn:commons-codec/commons-codec/1.2</bundle>
+        <conditional>
+            <condition>x=y</condition>
+            <bundle>mvn:org.glassfish/something-strangest/4.3.0</bundle>
+            <bundle>mvn:org.glassfish/something-strangest/4.3.2</bundle>
+        </conditional>
+    </feature>
+
+</features>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/fpi01.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fpi01.xml
new file mode 100644
index 0000000..6f299e0
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fpi01.xml
@@ -0,0 +1,41 @@
+<?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.
+-->
+<featuresProcessing xmlns="http://karaf.apache.org/xmlns/features-processing/v1.0.0">
+
+    <blacklistedRepositories>
+        <repository>mvn:org.hibernate/hibernate-validator-osgi-karaf-features/[5,5.3)/xml/features</repository>
+    </blacklistedRepositories>
+
+    <blacklistedFeatures>
+        <feature>f1</feature>
+        <feature version="[1,2)">f*a</feature>
+        <feature version="[4,5)">f4</feature>
+    </blacklistedFeatures>
+
+    <blacklistedBundles>
+        <bundle>mvn:commons-io/commons-io/[0,1.1)</bundle>
+        <bundle>mvn:commons-codec/commons-codec/[0,1.5)</bundle>
+        <bundle>mvn:org.glassfish/*</bundle>
+        <bundle>mvn:org.apache.karaf/exception/[0,*)</bundle>
+        <!-- same as: -->
+        <!--<bundle>mvn:org.apache.karaf/exception</bundle>-->
+    </blacklistedBundles>
+
+</featuresProcessing>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/fpi02.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fpi02.xml
new file mode 100644
index 0000000..e67a0be
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/fpi02.xml
@@ -0,0 +1,29 @@
+<?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.
+-->
+<featuresProcessing xmlns="http://karaf.apache.org/xmlns/features-processing/v1.0.0">
+
+    <bundleReplacements>
+        <bundle replacement="mvn:commons-io/commons-io/1.3.5" />
+        <bundle originalUri="mvn:commons-codec/commons-codec/[0,1.2)"
+                replacement="mvn:commons-codec/commons-codec/1.4.2" mode="maven" />
+        <bundle replacement="mvn:org.glassfish/something-strangest/4.3.1" />
+    </bundleReplacements>
+
+</featuresProcessing>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/org.apache.karaf.features.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/org.apache.karaf.features.xml
new file mode 100644
index 0000000..3691319
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/org.apache.karaf.features.xml
@@ -0,0 +1,98 @@
+<?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.
+-->
+<featuresProcessing xmlns="http://karaf.apache.org/xmlns/features-processing/v1.0.0"
+        xmlns:f="http://karaf.apache.org/xmlns/features/v1.5.0">
+
+    <!--
+        org.apache.karaf.features.internal.service.RepositoryCache will refuse to add/track a repository URI if it's blacklisted,
+        either if added explicitly or as referenced features repository URI
+    -->
+    <blacklistedRepositories>
+        <repository>mvn:org.hibernate/hibernate-validator-osgi-karaf-features/[5,*)/xml/features</repository>
+        <!-- ... -->
+    </blacklistedRepositories>
+
+    <!--
+        org.apache.karaf.features.internal.service.RepositoryCache will transform added/tracked repository by removing blacklisted features
+    -->
+    <blacklistedFeatures>
+        <feature>*jetty*</feature>
+        <feature version="[2,3)">*jclouds*</feature>
+        <!-- ... -->
+    </blacklistedFeatures>
+
+    <!--
+        org.apache.karaf.features.internal.service.RepositoryCache will transform added/tracked repository and remove all blacklisted
+        bundles from all the features in the repository
+    -->
+    <blacklistedBundles>
+        <bundle>mvn:commons-logging/*</bundle>
+        <!-- ... -->
+    </blacklistedBundles>
+
+    <!--
+        We can configure RepositoryCache to change 'dependency="false|true"' flag on given bundles, features,
+        repositories or globally
+    -->
+    <overrideBundleDependency>
+        <!-- Override "dependency" flag for all bundles of all features for given repository URI(s) -->
+        <repository uri="mvn:org.ops4j.pax.cdi/pax-cdi-features/*/xml/features" dependency="true" />
+        <repository uri="mvn:*/xml/features" dependency="true" />
+        <!-- Override "dependency" flag for all bundles of given feature(s) -->
+        <feature name="jclouds*" version="[1,3)" dependency="true" />
+        <!-- Override "dependency" flag for given bundle(s) -->
+        <bundle uri="mvn:javax.annotation/javax.annotation-api/*" dependency="true" />
+    </overrideBundleDependency>
+
+    <!--
+        Knowing there are multiple bundles containing the same classes (usually APIs), we can "translate"
+        bundle location to completely different bundles
+    -->
+    <bundleReplacements>
+        <bundle originalUri="mvn:commons-beanutils/commons-beanutils/[1.9,2)"
+                replacement="mvn:commons-beanutils/commons-beanutils/1.9.3" />
+        <!--
+            An example of direct etc/overrides.properties equivalent - originalUri will be derived from replacement
+             - candidate must have version lower than replacement
+             - candidate must be in eligible range for update: [3.2.0, 3.2.2)
+        -->
+        <bundle replacement="mvn:commons-collections/commons-collections/3.2.2" />
+        <bundle originalUri="mvn:org.eclipse.jetty.orbit/javax.servlet/[3,4)"
+                replacement="mvn:org.apache.geronimo.specs/geronimo-servlet_3.0_spec/1.0" mode="maven" />
+        <!-- ... -->
+    </bundleReplacements>
+
+    <!--
+        We can completely rewrite any feature deifnition, which may be useful for features beyond our control or
+        which are no longer maintained. This is expert setting and has to be configured with care.
+        We can add, remove and change all the items inside feature definition
+    -->
+    <featureReplacements>
+        <replacement mode="replace">
+            <feature name="pax-jsf-resources-support" description="Provide sharing of resources according to Servlet 3.0 for OSGi bundles and JSF" version="6.0.7">
+                <f:feature version="[6.0,6.1)">pax-jsf-support</f:feature>
+                <f:bundle dependency="true">mvn:org.ops4j.pax.web/pax-web-resources-extender/6.0.7</f:bundle>
+                <f:bundle>mvn:org.ops4j.pax.web/pax-web-resources-jsf/6.0.7</f:bundle>
+            </feature>
+        </replacement>
+        <!-- ... -->
+    </featureReplacements>
+
+</featuresProcessing>
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties b/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties
index d34fa7e..84f070e 100644
--- a/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides.properties
@@ -19,5 +19,5 @@
 ################################################################################
 
 # Sample etc/overrides.properties file for testing purposes
-mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.redhat-61033X
-mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.redhat-61033X;range=[2.3.0,2.5)
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.61033X
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.61033X;range=[2.3.0,2.5)
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides2.properties b/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides2.properties
new file mode 100644
index 0000000..d112d43
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/overrides2.properties
@@ -0,0 +1,37 @@
+
+################################################################################
+#
+#    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.
+#
+################################################################################
+
+# Each line is a location of a bundle that may override any feature bundle with matching symbolic name and versions
+# (according to implicit or explicit rules)
+
+# 2.3.0.61033X will be used instead of any org.apache.karaf.admin.command with version in range [2.3.0, 2.4.0) and
+# lower than 2.3.0.61033X, i.e., for version in range [2.3.0, 2.3.0.61033X) (conjunction).
+# version 2.2.x can be upgraded only up to and exluding 2.3.0, so it won't be overriden
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.command/2.3.0.61033X
+# explicit range. can be used to declare "bigger" override, which is not normally allowed (e.g., 2.2.0 -> 2.3.0)
+# normally, we can upgrade to version different at micro (3rd) digit (e.g., 2.3.1 -> 2.3.4)
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.core/2.3.0.61033X;range=[2.2.0,2.4)
+# invalid override - exact version should be specified
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.invalid/[2.3,3)
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.resources/2.3.14
+# range specified as single version is a short-hand of [2.0.0,2.0.0]
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.kernel/2.3.14;range=2.0
+# range with '*' as open ceiling (it makes no sense to use "[v1,*]") is just wider range of bundle versions to consider
+mvn:org.apache.karaf.admin/org.apache.karaf.admin.infinity/2.3.14;range=[1.0,*)
diff --git a/features/core/src/test/resources/org/apache/karaf/features/r1.xml b/features/core/src/test/resources/org/apache/karaf/features/r1.xml
new file mode 100644
index 0000000..5c136b6
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/r1.xml
@@ -0,0 +1,20 @@
+<?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.
+-->
+<features name="r1" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0">
+    <feature name="f1" />
+</features>
diff --git a/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java b/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java
index 01f9a4a..d9e156d 100644
--- a/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java
+++ b/profile/src/main/java/org/apache/karaf/profile/assembly/Builder.java
@@ -1141,7 +1141,7 @@ public class Builder {
                 @Override
                 public void downloaded(final StreamProvider provider) throws Exception {
                     String url = provider.getUrl();
-                    if (repoBlacklist.isBlacklisted(url, TYPE_REPOSITORY)) {
+                    if (repoBlacklist.isRepositoryBlacklisted(url)) {
                         LOGGER.info("      feature repository " + url + " is blacklisted");
                         return;
                     }
diff --git a/util/src/main/java/org/apache/karaf/util/maven/Parser.java b/util/src/main/java/org/apache/karaf/util/maven/Parser.java
index 96b9f88..b39d856 100644
--- a/util/src/main/java/org/apache/karaf/util/maven/Parser.java
+++ b/util/src/main/java/org/apache/karaf/util/maven/Parser.java
@@ -285,6 +285,27 @@ public class Parser
     }
 
     /**
+     * Prints parsed mvn: URI (after possible change of any component)
+     * @return
+     */
+    public String toMvnURI()
+    {
+        StringBuilder sb = new StringBuilder();
+        sb.append(m_group).append(ARTIFACT_SEPARATOR).append(m_artifact).append(ARTIFACT_SEPARATOR).append(m_version);
+        if (!TYPE_JAR.equals(m_type)) {
+            sb.append(ARTIFACT_SEPARATOR).append(m_type);
+        }
+        if (m_classifier != null && !"".equals(m_classifier)) {
+            if (TYPE_JAR.equals(m_type)) {
+                sb.append(ARTIFACT_SEPARATOR).append(m_type);
+            }
+            sb.append(ARTIFACT_SEPARATOR).append(m_classifier);
+        }
+
+        return sb.toString();
+    }
+
+    /**
      * Return the group id of the artifact.
      *
      * @return group ID.
@@ -335,6 +356,51 @@ public class Parser
     }
 
     /**
+     * Changes parsed group - to allow printing mvn: URI with changed groupId
+     * @param m_group
+     */
+    public void setGroup(String m_group)
+    {
+        this.m_group = m_group;
+    }
+
+    /**
+     * Changes parsed artifact - to allow printing mvn: URI with changed artifactId
+     * @param m_artifact
+     */
+    public void setArtifact(String m_artifact)
+    {
+        this.m_artifact = m_artifact;
+    }
+
+    /**
+     * Changes parsed version - to allow printing mvn: URI with changed version
+     * @param m_version
+     */
+    public void setVersion(String m_version)
+    {
+        this.m_version = m_version;
+    }
+
+    /**
+     * Changes parsed type - to allow printing mvn: URI with changed type
+     * @param m_type
+     */
+    public void setType(String m_type)
+    {
+        this.m_type = m_type;
+    }
+
+    /**
+     * Changes parsed classifier - to allow printing mvn: URI with changed classifier
+     * @param m_classifier
+     */
+    public void setClassifier(String m_classifier)
+    {
+        this.m_classifier = m_classifier;
+    }
+
+    /**
      * Return the complete path to artifact as stated by Maven 2 repository layout.
      *
      * @return artifact path.
diff --git a/util/src/test/java/org/apache/karaf/util/ParserTest.java b/util/src/test/java/org/apache/karaf/util/ParserTest.java
index d15487a..0f3c246 100644
--- a/util/src/test/java/org/apache/karaf/util/ParserTest.java
+++ b/util/src/test/java/org/apache/karaf/util/ParserTest.java
@@ -16,12 +16,17 @@
  */
 package org.apache.karaf.util;
 
-import junit.framework.Assert;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Map;
+
 import org.apache.karaf.util.maven.Parser;
 import org.junit.Test;
 
-import java.util.HashMap;
-import java.util.Map;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 public class ParserTest {
 
@@ -29,14 +34,30 @@ public class ParserTest {
     private final static String PATH_WITHOUT_CLASSIFIER = "org/apache/karaf/test/1.0-SNAPSHOT/test-1.0-SNAPSHOT.xml";
 
     @Test
-    public void parserTest() throws Exception {
+    public void parserTest() {
         Map parts = new HashMap();
         String uri = Parser.pathToMaven(PATH_WITH_CLASSIFIER, parts);
-        Assert.assertEquals("mvn:org.apache.karaf/test/1.0-SNAPSHOT/xml/feature", uri);
-        Assert.assertEquals("feature", parts.get("classifier"));
+        assertEquals("mvn:org.apache.karaf/test/1.0-SNAPSHOT/xml/feature", uri);
+        assertEquals("feature", parts.get("classifier"));
         uri = Parser.pathToMaven(PATH_WITHOUT_CLASSIFIER, parts);
-        Assert.assertEquals("mvn:org.apache.karaf/test/1.0-SNAPSHOT/xml", uri);
-        Assert.assertNull(parts.get("classifier"));
+        assertEquals("mvn:org.apache.karaf/test/1.0-SNAPSHOT/xml", uri);
+        assertNull(parts.get("classifier"));
+    }
+
+    @Test
+    public void unparserTest() throws MalformedURLException {
+        Parser p1 = new Parser("org.apache/karaf/1/xml/features");
+        assertThat(p1.toMvnURI(), equalTo("org.apache/karaf/1/xml/features"));
+        Parser p2 = new Parser("org.apache/karaf/1/xml");
+        assertThat(p2.toMvnURI(), equalTo("org.apache/karaf/1/xml"));
+        Parser p3 = new Parser("org.apache/karaf/1/jar/uber");
+        assertThat(p3.toMvnURI(), equalTo("org.apache/karaf/1/jar/uber"));
+        Parser p4 = new Parser("org.apache/karaf/1//uber");
+        assertThat(p4.toMvnURI(), equalTo("org.apache/karaf/1/jar/uber"));
+        Parser p5 = new Parser("org.apache/karaf/1/jar");
+        assertThat(p5.toMvnURI(), equalTo("org.apache/karaf/1"));
+        Parser p6 = new Parser("org.apache/karaf/1");
+        assertThat(p6.toMvnURI(), equalTo("org.apache/karaf/1"));
     }
 
 }
diff --git a/webconsole/features/src/main/java/org/apache/karaf/webconsole/features/ExtendedFeature.java b/webconsole/features/src/main/java/org/apache/karaf/webconsole/features/ExtendedFeature.java
index 61738f6..a601cf8 100644
--- a/webconsole/features/src/main/java/org/apache/karaf/webconsole/features/ExtendedFeature.java
+++ b/webconsole/features/src/main/java/org/apache/karaf/webconsole/features/ExtendedFeature.java
@@ -169,4 +169,10 @@ public class ExtendedFeature implements Feature {
     public String getRepositoryUrl() {
         return feature.getRepositoryUrl();
     }
+
+    @Override
+    public boolean isBlacklisted() {
+        return feature.isBlacklisted();
+    }
+
 }

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

Mime
View raw message